Instalación de los paquetes necesarios para el análisis predictivo y modelación.
install.packages("devtools")
devtools::install_github("centromagis/paqueteMODELOS", force = TRUE)
install.packages(c("tidyverse", "plotly", "leaflet", "car", "lmtest",
"DT", "broom", "GGally", "gridExtra"))
Cargamos las librerías necesarias en R [8]: paqueteMODELOS para los datos, tidyverse [9] para manipulación y gráficos base, plotly [14] para gráficos interactivos, leaflet [10] para mapas, car [11] y lmtest [12] para las pruebas de supuestos del modelo, broom [13] para presentar resultados de regresión en formato tabular, y gridExtra para organizar los gráficos de diagnóstico.También definimos, en este mismo chunk, una función auxiliar (graficos_diagnostico) que reconstruye los 4 gráficos clásicos de diagnóstico del modelo lineal.
library(paqueteMODELOS)
library(tidyverse)
library(plotly)
library(leaflet)
library(car) # vif()
library(lmtest) # bptest(), dwtest()
library(DT)
library(broom)
library(GGally)
library(gridExtra)
# Función para graficar los 4 diagnósticos del modelo.
graficos_diagnostico <- function(modelo, titulo = "") {
df <- augment(modelo)
p1 <- ggplot(df, aes(.fitted, .resid)) +
geom_point(shape = 1) +
geom_hline(yintercept = 0, linetype = "dashed", color = "grey50") +
geom_smooth(se = FALSE, color = "red", linewidth = 0.6, method = "loess") +
labs(title = "Residuos vs. Ajustados", x = "Valores ajustados", y = "Residuos") +
theme_minimal()
p2 <- ggplot(df, aes(sample = .std.resid)) +
stat_qq(shape = 1) +
stat_qq_line(color = "grey50", linetype = "dashed") +
labs(title = "Q-Q de residuos", x = "Cuantiles teóricos", y = "Residuos estandarizados") +
theme_minimal()
p3 <- ggplot(df, aes(.fitted, sqrt(abs(.std.resid)))) +
geom_point(shape = 1) +
geom_smooth(se = FALSE, color = "red", linewidth = 0.6, method = "loess") +
labs(title = "Escala-Ubicación", x = "Valores ajustados",
y = "Raíz de |residuos estandarizados|") +
theme_minimal()
p4 <- ggplot(df, aes(.hat, .std.resid)) +
geom_point(shape = 1) +
geom_hline(yintercept = 0, linetype = "dashed", color = "grey50") +
geom_smooth(se = FALSE, color = "red", linewidth = 0.6, method = "loess") +
labs(title = "Residuos vs. Apalancamiento", x = "Apalancamiento (leverage)",
y = "Residuos estandarizados") +
theme_minimal()
grid.arrange(p1, p2, p3, p4, ncol = 2, top = titulo)
}
Cargamos el dataset vivienda del paquete paqueteMODELOS, que contiene las ofertas de vivienda de los últimos tres meses en Cali. glimpse() nos permite revisar rápidamente el tipo de cada variable y algunos valores de ejemplo, como primer chequeo de que los datos se cargaron correctamente.
data("vivienda")
vivienda <- as_tibble(vivienda)
glimpse(vivienda)
## Rows: 8,322
## Columns: 13
## $ id <dbl> 1147, 1169, 1350, 5992, 1212, 1724, 2326, 4386, 1209, 159…
## $ zona <chr> "Zona Oriente", "Zona Oriente", "Zona Oriente", "Zona Sur…
## $ piso <chr> NA, NA, NA, "02", "01", "01", "01", "01", "02", "02", "02…
## $ estrato <dbl> 3, 3, 3, 4, 5, 5, 4, 5, 5, 5, 6, 4, 5, 6, 4, 5, 5, 4, 5, …
## $ preciom <dbl> 250, 320, 350, 400, 260, 240, 220, 310, 320, 780, 750, 62…
## $ areaconst <dbl> 70, 120, 220, 280, 90, 87, 52, 137, 150, 380, 445, 355, 2…
## $ parqueaderos <dbl> 1, 1, 2, 3, 1, 1, 2, 2, 2, 2, NA, 3, 2, 2, 1, 4, 2, 2, 2,…
## $ banios <dbl> 3, 2, 2, 5, 2, 3, 2, 3, 4, 3, 7, 5, 6, 2, 4, 4, 4, 3, 2, …
## $ habitaciones <dbl> 6, 3, 4, 3, 3, 3, 3, 4, 6, 3, 6, 5, 6, 2, 5, 5, 4, 3, 3, …
## $ tipo <chr> "Casa", "Casa", "Casa", "Casa", "Apartamento", "Apartamen…
## $ barrio <chr> "20 de julio", "20 de julio", "20 de julio", "3 de julio"…
## $ longitud <dbl> -76.51168, -76.51237, -76.51537, -76.54000, -76.51350, -7…
## $ latitud <dbl> 3.43382, 3.43369, 3.43566, 3.43500, 3.45891, 3.36971, 3.4…
El análisis de este caso se desarrolla en dos partes independientes, una por cada solicitud de vivienda recibida por María. En la Parte A se trabaja la Solicitud 1 (Vivienda 1: casa, zona norte, crédito preaprobado de 350 millones de pesos), siguiendo la secuencia completa de los pasos solicitados:
En la Parte B se repite exactamente la misma secuencia de pasos, pero aplicada a la Solicitud 2 (Vivienda 2: apartamento, zona sur, crédito preaprobado de 850 millones de pesos). Separar el análisis de esta forma permite comparar directamente ambos casos y, al final, consolidar las conclusiones y recomendaciones para cada una de las dos viviendas en el informe ejecutivo dirigido a María.
Antes de aplicar el filtro, verificaremos los valores exactos que existen en las variables tipo y zona, para asegurarnos de que las categorías usadas en filter() coincidan exactamente (mayúsculas, espacios, tildes) con las que tiene la base. Después de crear base1, comprobamos que el número de filas obtenido coincide con un conteo manual independiente.
# Ver los valores exactos que existen en la base para tipo y zona
unique(vivienda$tipo)
## [1] "Casa" "Apartamento" NA
unique(vivienda$zona)
## [1] "Zona Oriente" "Zona Sur" "Zona Norte" "Zona Oeste" "Zona Centro"
## [6] NA
Ahora construimos “base1”, el subconjunto de la base que corresponde a la primera solicitud: solo casas ubicadas en la zona norte. Este filtro delimita el universo de comparación relevante para estimar y ofertar la Vivienda 1. Adicionalmente presentamos los primeros 3 registros y una tabla de conteo por tipo y zona sobre la base completa, que sirve para comprobar que el número de filas de “base1” coincide con lo esperado.
base1 <- vivienda %>%
filter(tipo == "Casa", zona == "Zona Norte")
# primeros 3 registros
head(base1, 3)
## # A tibble: 3 × 13
## id zona piso estrato preciom areaconst parqueaderos banios habitaciones
## <dbl> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 1209 Zona N… 02 5 320 150 2 4 6
## 2 1592 Zona N… 02 5 780 380 2 3 3
## 3 4057 Zona N… 02 6 750 445 NA 7 6
## # ℹ 4 more variables: tipo <chr>, barrio <chr>, longitud <dbl>, latitud <dbl>
# tabla de comprobación: conteo por tipo y zona antes/después del filtro
vivienda %>% count(tipo, zona) %>% datatable()
Verificamos que la base1 no haya quedado vacía
# Verificar que base1 no quedó vacía ni sospechosamente pequeña
nrow(base1)
## [1] 722
# Doble chequeo: el conteo de base1 debe coincidir con un filtro manual independiente
sum(vivienda$tipo == "Casa" & vivienda$zona == "Zona Norte", na.rm = TRUE)
## [1] 722
# Deben ser iguales; si no, hay un problema de coincidencia de texto
nrow(base1) == sum(vivienda$tipo == "Casa" & vivienda$zona == "Zona Norte", na.rm = TRUE)
## [1] TRUE
El filtro produjo base1 con 722 registros, cifra que coincide exactamente con un conteo manual independiente (sum(vivienda\(tipo == "Casa" & vivienda\)zona == “Zona Norte”, na.rm = TRUE)), lo que valida que el filtro está correctamente aplicado.
Cabe anotar que tanto tipo como zona presentan valores NA en la base completa: esas filas no entran en base1 porque dplyr::filter() descarta automáticamente cualquier fila donde la condición no se evalúe como estrictamente verdadera (un valor faltante nunca cumple == “Casa”), lo cual es el comportamiento deseado en este caso, pero vale la pena tenerlo presente como una limitación de calidad de datos: existen registros en la base completa cuyo tipo o zona no fue capturado, y por tanto quedan fuera de cualquier segmento que se filtre por esas variables.
El mapa ubica geográficamente cada vivienda de base1 usando sus coordenadas de longitud y latitud. Esto permite validar visualmente si el filtro por zona == “Zona Norte” refleja la ubicación real de las viviendas, o si existen puntos que caen fuera del área esperada, lo cual señalaría posibles errores de captura de datos (zona mal etiquetada, coordenadas incorrectas) o viviendas en zonas límite.
leaflet(base1) %>%
addTiles() %>%
addCircleMarkers(~longitud, ~latitud,
radius = 4, color = "steelblue",
popup = ~paste0("Precio: ", preciom, "M<br>",
"Barrio: ", barrio)) %>%
fitBounds(lng1 = min(base1$longitud, na.rm = TRUE),
lat1 = min(base1$latitud, na.rm = TRUE),
lng2 = max(base1$longitud, na.rm = TRUE),
lat2 = max(base1$latitud, na.rm = TRUE))
Al graficar los 722 registros de base1 sobre el mapa de Cali, los puntos aparecen concentrados en una única zona contigua de la ciudad, ubicada entre el sector de Pichindé y el norte/centro, cerca de la vía a Palmira. No se observan puntos dispersos hacia sectores claramente asociados a otras zonas de la ciudad (sur profundo, oriente, Jamundí o Candelaria), lo que respalda que el filtro zona == “Zona Norte” capturó un conjunto geográficamente coherente y no incluyó registros mal etiquetados o atípicos.
Ahora calculamos la matriz de correlación entre el precio (preciom) y las variables numéricas que se usarán en el modelo (areaconst, estrato, banios, habitaciones). Esto da una primera idea de qué tan fuerte y en qué dirección se relaciona cada variable con el precio, antes de ajustar cualquier modelo formal.
num_vars <- vivienda %>%
select(preciom, areaconst, estrato, banios, habitaciones)
# matriz de correlación numérica
cor(num_vars, use = "complete.obs") %>% round(2)
## preciom areaconst estrato banios habitaciones
## preciom 1.00 0.69 0.61 0.67 0.26
## areaconst 0.69 1.00 0.27 0.65 0.52
## estrato 0.61 0.27 1.00 0.42 -0.07
## banios 0.67 0.65 0.42 1.00 0.59
## habitaciones 0.26 0.52 -0.07 0.59 1.00
La matriz de correlación muestra que el precio (preciom) se relaciona más fuertemente con el área construida (0.69) y el número de baños (0.67), seguido del estrato (0.61). La correlación con el número de habitaciones es notablemente más débil (0.26).
Esto sugiere que, en este mercado, el tamaño de la vivienda y el número de baños explican mejor el precio que el simple conteo de habitaciones. Es decir, importa más “cuánto espacio y cuántos baños” que “cuántos cuartos”.
Un hallazgo relevante para el modelo es que areaconst y banios están correlacionadas entre sí en 0.65, lo cual anticipa un posible problema de multicolinealidad en la regresión múltiple (dos predictores que se mueven juntos pueden dificultar aislar el efecto individual de cada uno sobre el precio. Por lo tanto, esto habrá que confirmarlo con el VIF en el Paso 4.
También llama la atención que estrato y habitaciones prácticamente no están correlacionados (-0.07): tener más estrato no implica tener más habitaciones, lo cual tiene sentido si los estratos altos corresponden a viviendas más compactas pero mejor ubicadas o con mejores acabados, no necesariamente más grandes en número de cuartos.
p1 <- plot_ly(vivienda, x = ~areaconst, y = ~preciom,
color = ~zona, type = "scatter", mode = "markers",
text = ~paste("Barrio:", barrio, "<br>Estrato:", estrato)) %>%
layout(title = "Precio vs. Área construida por zona")
p1
Este gráfico interactivo muestra la relación entre el área construida y el precio, diferenciando por zona con color. Permite explorar visualmente si la relación es aproximadamente lineal y si existen diferencias claras de nivel de precio entre zonas para un área similar.
La relación entre área construida y precio es claramente positiva, como es de esperarse, pero la dispersión de precios aumenta a medida que crece el área (heterocedasticidad visual), lo que ya anticipa un posible incumplimiento del supuesto de varianza constante que se evaluará formalmente en el Paso 4.
El gráfico también revela que la zona actúa como un factor adicional más allá del área: los puntos de Zona Oeste (morado) se concentran en la parte alta del gráfico incluso para áreas moderadas, es decir, esas viviendas alcanzan precios altos con menos metros cuadrados que otras zonas, lo cuál es un indicio de que la ubicación aporta valor independientemente del tamaño.
En contraste, se observan algunos puntos de Zona Norte y Zona Oriente con áreas grandes (por encima de 1.500 m²) pero precios comparativamente bajos, lo cual vale la pena señalar como posible valor atípico o caso particular que convendría revisar antes de confiar plenamente en el modelo lineal para esos rangos extremos de área.
p2 <- plot_ly(vivienda, x = ~factor(estrato), y = ~preciom,
type = "box", color = ~factor(estrato)) %>%
layout(title = "Precio por estrato", xaxis = list(title = "Estrato"))
p2
El diagrama de cajas compara la distribución de precios según el estrato socioeconómico. Se espera que a mayor estrato, mayor sea el precio mediano y probablemente mayor la dispersión, dado que los estratos altos suelen tener mercados más heterogéneos (acabados, ubicación exacta, etc.).
Se confirma una relación monótona y clara entre estrato y precio: la mediana del precio aumenta de forma consistente del estrato 3 al 6 (aproximadamente de 180 a 700 millones). Sin embargo, la dispersión también crece con el estrato. El rango intercuartílico del estrato 6 es mucho más amplio que el del estrato 3, lo que indica que el mercado de vivienda de estratos altos es más heterogéneo: factores como acabados, vista, antigüedad o ubicación exacta dentro del estrato generan mayor variabilidad de precios que en los estratos más bajos, donde las viviendas tienden a ser más homogéneas.
Esto refuerza al estrato como un predictor importante, pero también sugiere que, por sí solo, no captura toda la variabilidad del precio en los segmentos más costosos.
p3 <- plot_ly(vivienda, x = ~zona, y = ~preciom, type = "box", color = ~zona) %>%
layout(title = "Precio por zona")
p3
Este gráfico confirma que la zona sí marca diferencias relevantes de precio, y con una jerarquía clara: Zona Oeste tiene la mediana más alta y la mayor dispersión, seguida de Zona Sur; Zona Centro y Zona Norte se ubican en un rango intermedio similar. Zona Oriente presenta la mediana más baja y la distribución más compacta, es decir, es el mercado más económico y más homogéneo de la ciudad.
Para el caso de María, esto es directamente relevante: la Vivienda 1 (zona norte) se ubica en un segmento de precio medio, mientras que la Vivienda 2 (zona sur) corresponde a una zona de precios más altos y con mayor variabilidad. Esto es coherente con que su crédito preaprobado (850 millones) sea más de dos veces el de la Vivienda 1 (350 millones).
En este paso ajustamos un modelo de regresión lineal múltiple [1] donde el precio de la vivienda se explica en función del área construida, el estrato, el número de habitaciones, parqueaderos y baños [3]. El objetivo es cuantificar cuánto aporta cada característica al precio, manteniendo las demás constantes, y usar ese modelo posteriormente para predecir el precio de las viviendas solicitadas.
\[preciom = \beta_0 + \beta_1\,areaconst + \beta_2\,estrato + \beta_3\,habitaciones + \beta_4\,parqueaderos + \beta_5\,banios + \varepsilon\]
modelo1 <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios,
data = vivienda)
summary(modelo1)
##
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos +
## banios, data = vivienda)
##
## Residuals:
## Min 1Q Median 3Q Max
## -1412.57 -88.36 -17.55 56.24 1101.38
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -380.7629 14.3638 -26.51 <2e-16 ***
## areaconst 0.8510 0.0222 38.33 <2e-16 ***
## estrato 97.9107 2.8342 34.55 <2e-16 ***
## habitaciones -31.2744 2.2613 -13.83 <2e-16 ***
## parqueaderos 74.4922 2.5900 28.76 <2e-16 ***
## banios 61.0554 2.6271 23.24 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 177.2 on 6711 degrees of freedom
## (1605 observations deleted due to missingness)
## Multiple R-squared: 0.7206, Adjusted R-squared: 0.7204
## F-statistic: 3462 on 5 and 6711 DF, p-value: < 2.2e-16
tidy(modelo1, conf.int = TRUE)
## # A tibble: 6 × 7
## term estimate std.error statistic p.value conf.low conf.high
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 (Intercept) -381. 14.4 -26.5 2.41e-147 -409. -353.
## 2 areaconst 0.851 0.0222 38.3 7.61e-291 0.807 0.895
## 3 estrato 97.9 2.83 34.5 7.59e-241 92.4 103.
## 4 habitaciones -31.3 2.26 -13.8 6.49e- 43 -35.7 -26.8
## 5 parqueaderos 74.5 2.59 28.8 1.21e-171 69.4 79.6
## 6 banios 61.1 2.63 23.2 5.57e-115 55.9 66.2
glance(modelo1) %>% select(r.squared, adj.r.squared, sigma, statistic, p.value)
## # A tibble: 1 × 5
## r.squared adj.r.squared sigma statistic p.value
## <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 0.721 0.720 177. 3462. 0
Los cinco predictores resultaron estadísticamente significativos (p < 0.001 en todos los casos), lo que indica que ninguno de ellos puede descartarse del modelo por falta de evidencia estadística:
1. Área construida: (areaconst, β = 0.851): por cada m² adicional, el precio esperado aumenta en 0.851 millones de pesos (~851 mil pesos/m²). Es un efecto lógico y de magnitud razonable para el mercado inmobiliario de Cali.
2. Estrato: (β = 97.91): pasar de un estrato al siguiente incrementa el precio esperado en cerca de 98 millones de pesos, manteniendo el área y las demás características fijas. Es el segundo efecto más grande del modelo y confirma al estrato como un fuerte proxy de ubicación/calidad de la zona.
3. Parqueaderos: (β = 74.49): cada parqueadero adicional suma en promedio 74.5 millones al precio. Tiene sentido: el parqueadero es un atributo escaso y valorado, especialmente en estratos altos.
4. Baños: Baños (β = 61.06): cada baño adicional aporta cerca de 61 millones. También es coherente con la intuición del mercado.
5. Habitaciones: este es el resultado que más merece discusión. El coeficiente es negativo y significativo (β = −31.27): a igualdad de área construida, estrato, parqueaderos y baños, tener una habitación adicional está asociado con una reducción de ~31 millones en el precio.
Esto no es ilógico si se interpreta correctamente: como el área ya está controlada en el modelo, “más habitaciones para la misma área” implica necesariamente cuartos más pequeños o una distribución más fragmentada del espacio, lo cual el mercado tiende a penalizar frente a espacios más amplios y menos subdivididos (por ejemplo, alcobas más grandes o áreas sociales más generosas).
Es un hallazgo económicamente interesante: el número de habitaciones por sí solo no agrega valor si no viene acompañado de más área.
El R² es 0.7206 y el R² ajustado 0.7204 (prácticamente idénticos, lo cual es esperable dado el alto número de observaciones frente a solo 5 predictores). Esto significa que el modelo explica cerca del 72% de la variabilidad del precio de la vivienda, un ajuste bueno para un modelo de precios inmobiliarios con solo características físicas de la vivienda.
El 28% restante de la variabilidad no explicada probablemente corresponde a factores que el modelo no incluye: barrio específico (más fino que la zona), antigüedad de la construcción, calidad de acabados, vista, cercanía a vías principales o centros comerciales, y condiciones particulares de negociación.
Para mejorar el modelo se podría incorporar el barrio como variable categórica, interacciones entre área y zona, o variables de ubicación más precisas (distancia a puntos de interés).
Antes de confiar en las interpretaciones y predicciones del modelo, verificamos que cumpla los supuestos clásicos de la regresión lineal [2]: linealidad y homocedasticidad (gráfico de residuos vs. ajustados y prueba de Breusch-Pagan [5]), normalidad de los residuos (Q-Q plot y prueba de Shapiro-Wilk [4]), independencia de los errores (prueba de Durbin-Watson [6]) y ausencia de multicolinealidad entre predictores (factor de inflación de varianza, VIF [7]). Si estos supuestos no se cumplen razonablemente, los p-valores y errores estándar del modelo pueden no ser confiables.
graficos_diagnostico(modelo1, "Diagnóstico del modelo - Vivienda 1")
# Normalidad de residuos
# shapiro.test() solo admite muestras entre 3 y 5000 observaciones;
# si el modelo tiene más datos, se evalúa sobre una muestra aleatoria.
res1 <- residuals(modelo1)
set.seed(123)
res1_muestra <- if (length(res1) > 5000) sample(res1, 5000) else res1
shapiro.test(res1_muestra)
##
## Shapiro-Wilk normality test
##
## data: res1_muestra
## W = 0.86303, p-value < 2.2e-16
# Homocedasticidad
bptest(modelo1)
##
## studentized Breusch-Pagan test
##
## data: modelo1
## BP = 1212.2, df = 5, p-value < 2.2e-16
# Independencia de errores
dwtest(modelo1)
##
## Durbin-Watson test
##
## data: modelo1
## DW = 1.6081, p-value < 2.2e-16
## alternative hypothesis: true autocorrelation is greater than 0
# Multicolinealidad
vif(modelo1)
## areaconst estrato habitaciones parqueaderos banios
## 2.190532 1.549549 2.036909 1.816620 2.813537
Normalidad: la prueba de Shapiro-Wilk (sobre una muestra de los residuos, dado que modelo1 supera las 5.000 observaciones que admite la prueba) arroja W = 0.863, p < 2.2e-16, rechazando contundentemente la hipótesis de normalidad. Esto confirma lo observado en el Q-Q plot: los residuos tienen colas más pesadas que una distribución normal, es decir, hay más valores extremos de los que un modelo lineal clásico esperaría.
Homocedasticidad: la prueba de Breusch-Pagan (BP = 1212.2, df = 5, p < 2.2e-16) rechaza también la hipótesis de varianza constante. Esto confirma numéricamente el patrón visual de los gráficos de Residuos vs. Ajustados y Escala-Ubicación: el error del modelo crece a medida que aumenta el precio predicho, es decir, el modelo es menos preciso (en términos absolutos) para las viviendas más costosas.
Independencia (Durbin-Watson): DW = 1.608, p < 2.2e-16, con hipótesis alternativa de autocorrelación positiva. Aunque el resultado es estadísticamente significativo, vale la pena una aclaración importante: la prueba de Durbin-Watson fue diseñada para datos ordenados en el tiempo (series temporales), y estos datos de vivienda son de corte transversal, sin una secuencia temporal real.
Lo que probablemente está capturando esta prueba no es “autocorrelación temporal” en sentido estricto, sino que las filas de la base están ordenadas de alguna forma sistemática (por ejemplo, agrupadas por barrio o zona), lo que hace que observaciones consecutivas en la tabla tiendan a parecerse entre sí.
Esto es más bien un indicio de dependencia espacial (viviendas cercanas geográficamente tienen precios correlacionados) que el modelo actual no está capturando explícitamente, más que una violación clásica de independencia en el sentido de series de tiempo.
Multicolinealidad (VIF): los valores de VIF son todos bajos (areaconst = 2.19, estrato = 1.55, habitaciones = 2.04, parqueaderos = 1.82, banios = 2.81), muy por debajo del umbral de preocupación (5 o 10). Esto es una buena noticia: a pesar de que areaconst y banios mostraban una correlación relativamente alta en el EDA (0.65), no llega a generar un problema real de multicolinealidad en el modelo. Es decir, los coeficientes estimados pueden interpretarse con relativa confianza en cuanto a estabilidad.
De acuerdo con los resultados, de los cuatro supuestos evaluados, dos se cumplen razonablemente bien (independencia, en el sentido de que la “autocorrelación” detectada es más un artefacto de orden espacial/de agrupación que un problema real de series de tiempo y ausencia de multicolinealidad grave), mientras que normalidad y homocedasticidad se incumplen de forma estadísticamente significativa.
Esto no invalida el modelo para fines exploratorios y de estimación puntual (el R² de 0.72 sigue siendo informativo), pero sí implica que los intervalos de confianza y los p-valores reportados podrían ser optimistas (más estrechos o más significativos de lo que realmente son).
Ahora bien, respecto al diagnóstico del modelo y, de acuerdo con los resultados, podemos realizar las siguientes interpretaciones:
Residuos vs. Ajustados: la línea de tendencia (roja) no es plana ni cercana a cero en todo el rango. Es decir, se mantiene cerca de cero para valores ajustados bajos, pero se curva marcadamente hacia abajo a partir de valores ajustados por encima de ~1.000 millones, llegando a residuos de -1.000 o más en la zona de precios altos.
Esto es evidencia de que el supuesto de linealidad no se cumple completamente: el modelo tiende a sobreestimar el precio de las viviendas más costosas (el residuo negativo grande significa que el precio real fue menor al predicho). Adicionalmente, la dispersión vertical de los puntos también aumenta con los valores ajustados, lo cual es un primer indicio visual de heterocedasticidad (varianza no constante).
Q-Q de residuos: los puntos se desvían claramente de la línea de referencia en ambos extremos, formando una especie de “S” pronunciada. En la cola inferior los residuos caen mucho más abajo de lo que la normal predeciría (hasta -8 desviaciones estándar), y en la cola superior se observan valores por encima de lo esperado. Esto indica que los residuos no siguen una distribución normal: tienen colas más pesadas que una normal (hay más valores extremos de los que se esperarían). Esto es consistente con lo que ya habíamos visto en el resumen del modelo: un rango de residuos muy amplio (de -1.412 a 1.101 millones).
Escala-Ubicación: la raíz de los residuos estandarizados aumenta claramente a medida que crecen los valores ajustados (hasta estabilizarse cerca de los 1.000-1.500 millones), confirmando visualmente lo que ya se intuía en el primer gráfico: la varianza de los errores no es constante, sino que crece con el nivel de precio. Esto típicamente ocurre en variables de precio, donde los errores absolutos tienden a ser proporcionalmente más grandes en los segmentos más caros del mercado.
Residuos vs. Apalancamiento: la mayoría de las observaciones se concentran en valores de apalancamiento (leverage) bajos, cerca de cero, pero existen algunas pocas observaciones con apalancamiento notablemente más alto (hasta ~0.045) que además presentan residuos muy negativos (cercanos a -8). Estas observaciones son candidatas a ser puntos influyentes: por su combinación de posición atípica en los predictores y mal ajuste del modelo, podrían estar afectando de forma desproporcionada la estimación de los coeficientes. Valdría la pena identificarlas puntualmente (por ejemplo, con augment(modelo1) %>% arrange(desc(.cooksd)) %>% head()) para decidir si corresponden a errores de captura o a casos legítimos pero atípicos del mercado.
En este Paso 5 usamos el modelo ajustado para predecir el precio esperado de una vivienda con las características exactas solicitadas por el cliente para la Vivienda 1 (200 m² construidos, 4 habitaciones, 1 parqueadero, 2 baños). El intervalo de predicción (interval = “prediction”) nos da, además del valor puntual, un rango donde es probable que caiga el precio real de una vivienda con esas características.
vivienda1_datos <- data.frame(
areaconst = 200,
estrato = 5, # usar 4 o 5 según disponibilidad; documentar el supuesto
habitaciones = 4,
parqueaderos = 1,
banios = 2
)
pred1 <- predict(modelo1, newdata = vivienda1_datos, interval = "prediction")
pred1
## fit lwr upr
## 1 350.4967 3.086741 697.9067
De acuerdo con los resultados de la predicción, para una casa con las características solicitadas por el cliente (200 m² construidos, estrato 5, 4 habitaciones, 1 parqueadero, 2 baños), el modelo estima un precio puntual de 350.5 millones de pesos, cifra que coincide casi exactamente con el crédito preaprobado de la empresa (350 millones). Esto es un hallazgo clave para el informe: según el modelo, una vivienda con exactamente esas características está en el límite justo de lo que el cliente puede financiar, sin margen de holgura.
Sin embargo, el intervalo de predicción al 95% es extremadamente amplio: va de 3.1 millones a 697.9 millones, es decir, un rango de casi 695 millones de pesos alrededor de la estimación puntual. Un intervalo de predicción tan disperso, que en su límite inferior sugiere un precio casi nulo, algo sin sentido económico para una casa de 200 m² en zona norte, es exactamente la consecuencia que anticipamos al validar los supuestos en el Paso 4: como los residuos del modelo no son normales y presentan heterocedasticidad (mayor varianza en el rango de precios donde cae esta predicción, cercano a los 350-1000 millones), el intervalo de predicción calculado bajo el supuesto de errores normales y de varianza constante resulta poco informativo y probablemente más ancho e inestable de lo que un modelo mejor especificado produciría. Por ende, no debe presentarse como una garantía estadística confiable de rango de precios.
Al revisar las ofertas inicialmente seleccionadas para la Vivienda 1, se identificó que una de ellas (barrio “acopi”) presenta una inconsistencia entre la zona registrada en la base (“Zona Norte”) y su ubicación geográfica real: Acopi es un sector reconocido del suroccidente de Cali, cercano al corredor industrial Cali-Yumbo, no de la zona norte.
Esta discrepancia sugiere un posible error de captura en la variable zona para ese registro específico, y no un problema generalizado del filtro ya validado previamente. Por precaución, y para no comprometer la calidad de la recomendación entregada a María, se excluye esta oferta del listado final de recomendaciones y se reemplaza por la siguiente disponible dentro del presupuesto aprobado.
ofertas1 <- vivienda %>%
filter(tipo == "Casa", zona == "Zona Norte", preciom <= 350,
barrio != "acopi") %>%
arrange(desc(preciom)) %>%
slice_head(n = 5)
ofertas1 %>% select(barrio, preciom, areaconst, estrato, habitaciones, banios, parqueaderos)
## # A tibble: 5 × 7
## barrio preciom areaconst estrato habitaciones banios parqueaderos
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 chipichape 350 98 4 4 3 2
## 2 el bosque 350 200 5 4 3 3
## 3 el bosque 350 300 5 6 5 3
## 4 el bosque 350 240 5 6 3 2
## 5 el bosque 350 203 5 5 2 2
leaflet(ofertas1) %>%
addTiles() %>%
addCircleMarkers(~longitud, ~latitud, radius = 6, color = "darkgreen",
popup = ~paste0(barrio, "<br>$", preciom, "M"))
Tras excluir la oferta con inconsistencia geográfica (Acopi), las 5 ofertas dentro del presupuesto de 350 millones se concentran mayoritariamente en el barrio El Bosque (4 de las 5 opciones), con una oferta adicional en Chipichape. El mapa confirma que ahora todos los puntos caen en un mismo clúster geográfico coherente, sin valores atípicos evidentes — la corrección aplicada resolvió el problema detectado.
La opción de El Bosque con 200 m² es la que mejor se ajusta al perfil solicitado: coincide exactamente en área construida (200 m²) y número de habitaciones (4), y además ofrece un baño y dos parqueaderos adicionales a los mínimos pedidos por el cliente, sin superar el presupuesto — es decir, entrega más de lo solicitado al mismo precio máximo aprobado. Esta sería la recomendación principal del presente análisis para María.
Como alternativa, la oferta de El Bosque con 203 m² iguala exactamente el número de baños solicitado (2) y tiene un área prácticamente idéntica a la pedida, aunque con una habitación adicional (5 en vez de 4), una opción válida si el cliente valora más el número exacto de baños que el de habitaciones.
La oferta de Chipichape, aunque cumple con estrato y habitaciones, ofrece considerablemente menos área construida (98 m² frente a los 200 m² solicitados) por el mismo precio máximo, lo que la convierte en la opción de menor relación espacio-precio del grupo. No la descartaría por completo, pero no sería la primera recomendación.
Siguiendo con la misma secuencia que la Parte A, construimos “base2”, el subconjunto de la base que corresponde a la segunda solicitud: solo apartamentos ubicados en la zona sur. Este filtro delimita el universo de comparación relevante para estimar y ofertar la Vivienda 2. Al igual que con base1, presentamos los primeros 3 registros y verificamos que el filtro se haya aplicado correctamente antes de continuar.
base2 <- vivienda %>%
filter(tipo == "Apartamento", zona == "Zona Sur")
# primeros 3 registros
head(base2, 3)
## # A tibble: 3 × 13
## id zona piso estrato preciom areaconst parqueaderos banios habitaciones
## <dbl> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 5098 Zona S… 05 4 290 96 1 2 3
## 2 698 Zona S… 02 3 78 40 1 1 2
## 3 8199 Zona S… <NA> 6 875 194 2 5 3
## # ℹ 4 more variables: tipo <chr>, barrio <chr>, longitud <dbl>, latitud <dbl>
# tabla de comprobación: conteo por tipo y zona sobre la base completa
vivienda %>% count(tipo, zona) %>% datatable()
# Verificar que base2 no quedó vacía ni sospechosamente pequeña
nrow(base2)
## [1] 2787
# Doble chequeo: el conteo de base2 debe coincidir con un filtro manual independiente
sum(vivienda$tipo == "Apartamento" & vivienda$zona == "Zona Sur", na.rm = TRUE)
## [1] 2787
# Deben ser iguales; si no, hay un problema de coincidencia de texto
nrow(base2) == sum(vivienda$tipo == "Apartamento" & vivienda$zona == "Zona Sur", na.rm = TRUE)
## [1] TRUE
El filtro produjo base2 con 2.787 registros, cifra que coincide exactamente con el conteo manual independiente (sum(vivienda\(tipo == "Apartamento" & vivienda\)zona == “Zona Sur”, na.rm = TRUE)), confirmando que el filtro está correctamente aplicado.
Al revisar los primeros 3 registros se observa además que la variable piso tiene un valor faltante (NA) en uno de ellos (id 8199), lo cual no afecta la validez del filtro por tipo y zona, pero es una limitación de calidad de datos a tener presente si más adelante se decide incorporar el piso como variable relevante en el análisis (por ejemplo, para apartamentos, donde el piso suele influir en el precio por vista o accesibilidad).
leaflet(base2) %>%
addTiles() %>%
addCircleMarkers(~longitud, ~latitud, radius = 4, color = "tomato",
popup = ~paste0("Precio: ", preciom, "M<br>Barrio: ", barrio)) %>%
fitBounds(lng1 = min(base2$longitud, na.rm = TRUE),
lat1 = min(base2$latitud, na.rm = TRUE),
lng2 = max(base2$longitud, na.rm = TRUE),
lat2 = max(base2$latitud, na.rm = TRUE))
La nube de puntos de base2 (apartamentos, zona sur) es bastante amplia y parece extenderse desde el sector norte de la ciudad (cerca del cruce hacia el aeropuerto y la vía a Palmira, en la parte superior del mapa) hasta el sur, cerca de la vía a Jamundí. Eso en sí mismo tiene sentido si “zona sur” abarca un área geográfica grande de la ciudad.
Lo que llama la atención es que la parte alta de este clúster ocupa prácticamente la misma zona geográfica donde vimos concentrados los puntos de base1 (casas, zona norte) en el mapa anterior.
Eso podría significar dos cosas muy distintas, que son importantes y, por eso, las señalaremos:
Que las zonas “Norte” y “Sur” de este dataset, tal como fueron definidas por quien capturó los datos, sí se superponen legítimamente en cierta franja de la ciudad (por ejemplo, si la división no es estrictamente por coordenada, sino por comuna/sector administrativo con formas irregulares).
Que hay un subconjunto de registros mal etiquetados en zona (algunos apartamentos realmente ubicados en el norte quedaron marcados como “Zona Sur”, o viceversa).
Por ende, antes de sacar una conclusión es mejor comprobarlo. Comparemos la latitud de ambas bases:
# Rango de latitud de las casas en zona norte (base1)
range(base1$latitud, na.rm = TRUE)
## [1] 3.33308 3.49584
# Rango de latitud de los apartamentos en zona sur (base2)
range(base2$latitud, na.rm = TRUE)
## [1] 3.33367 3.49684
# ¿Cuántos puntos de "zona sur" están más al norte que la mediana de "zona norte"?
sum(base2$latitud > median(base1$latitud, na.rm = TRUE), na.rm = TRUE)
## [1] 44
El rango de latitud de base1 (casas, zona norte) es [3.333, 3.496], y el de base2 (apartamentos, zona sur) es [3.334, 3.497], lo que significa que son prácticamente idénticos en sus extremos, explicando por qué visualmente el clúster de base2 parecía ocupar la misma región que el de base1 en la parte norte del mapa.
Sin embargo, al cuantificar cuántos registros de base2 están realmente más al norte que la mediana de latitud de base1, el resultado es de solo 44 registros sobre 2.787 (~1.6% de la base de apartamentos en zona sur). Es decir, la inmensa mayoría (98.4%) de los apartamentos etiquetados como “Zona Sur” sí están geográficamente por debajo del punto medio de la zona norte, y el solapamiento visible en el mapa corresponde a una fracción pequeña y probablemente esperable de la base.
A diferencia de la Parte A, aquí calculamos las correlaciones (Matriz de correlación) y gráficos exploratorios específicamente sobre base2 (apartamentos en zona sur), en lugar de sobre la base completa. Esto permite ver si el comportamiento del precio dentro de este segmento particular del mercado (apartamentos, zona sur) es similar o distinto al patrón general observado en la Vivienda 1.
num_vars2 <- base2 %>%
select(preciom, areaconst, estrato, banios, habitaciones, parqueaderos)
# matriz de correlación numérica, específica para apartamentos en zona sur
cor(num_vars2, use = "complete.obs") %>% round(2)
## preciom areaconst estrato banios habitaciones parqueaderos
## preciom 1.00 0.74 0.65 0.71 0.30 0.69
## areaconst 0.74 1.00 0.45 0.66 0.41 0.58
## estrato 0.65 0.45 1.00 0.53 0.18 0.49
## banios 0.71 0.66 0.53 1.00 0.52 0.56
## habitaciones 0.30 0.41 0.18 0.52 1.00 0.24
## parqueaderos 0.69 0.58 0.49 0.56 0.24 1.00
De acuerdo con los resultados, dentro de este segmento el precio se correlaciona más fuertemente con el área construida (0.74) y el número de baños (0.71), seguido de cerca por los parqueaderos (0.69) y el estrato (0.65). Nuevamente, el número de habitaciones muestra la correlación más débil con el precio (0.30).
Comparado con el patrón general observado en la Parte A (área 0.69, baños 0.67, estrato 0.61, habitaciones 0.26), las correlaciones aquí son consistentemente más altas en casi todas las variables, lo que sugiere que el precio de los apartamentos en zona sur está mejor explicado por estas características físicas que el de las casas en general. Es decir, es probable que un modelo de regresión ajuste mejor (mayor R²) para este segmento específico.
Un hallazgo propio de este segmento es la fuerte relación entre precio y parqueaderos (0.69), superando incluso a estrato. Esto tiene sentido en el contexto de apartamentos: a diferencia de una casa, donde el parqueadero suele estar incluido en el lote, en edificios de apartamentos el parqueadero es un activo escaso y frecuentemente vendido o valorado por separado, por lo que su presencia (y cantidad) impacta más directamente el precio.
p1b <- plot_ly(base2, x = ~areaconst, y = ~preciom,
color = ~factor(estrato), type = "scatter", mode = "markers",
text = ~paste("Barrio:", barrio, "<br>Piso:", piso)) %>%
layout(title = "Vivienda 2: Precio vs. Área construida por estrato (Zona Sur)")
p1b
Sobre la relación Precio vs. Área construida por estrato, el gráfico confirma la relación positiva esperada entre área construida y precio, pero lo más revelador es la clara estratificación por color: los apartamentos de estrato 6 (rosado) se ubican sistemáticamente en la parte alta del gráfico incluso para áreas moderadas (por ejemplo, con solo 250-350 m² alcanzan precios de 1.000 a 1.800 millones), mientras que los de estrato 3 (verde) se concentran compactamente en la esquina inferior izquierda, con áreas y precios bajos y poca dispersión. Esto refuerza lo que ya vimos en la matriz de correlación: el estrato aporta un efecto de precio independiente del área. Es decir, la ubicación/estrato paga por sí sola, no solo el tamaño.
También se aprecia que la dispersión de precios crece con el área (más pronunciado en estratos 5 y 6), anticipando nuevamente heterocedasticidad, tal como ocurrió con el modelo de la Vivienda 1.
Vale la pena señalar un punto atípico notorio: un apartamento de estrato 5 con un área excepcionalmente grande (cerca de 950 m², muy por encima del resto de la muestra que rara vez supera los 400-600 m²) pero con un precio comparativamente bajo (~300 millones). Este caso rompe claramente el patrón general y sería recomendable revisarlo antes de confiar en el modelo para rangos de área tan extremos. Podría tratarse de un error de captura (quizás el área está mal registrada, o corresponde a una propiedad atípica como una bodega o lote mixto clasificado como apartamento).
p2b <- plot_ly(base2, x = ~factor(estrato), y = ~preciom,
type = "box", color = ~factor(estrato)) %>%
layout(title = "Vivienda 2: Precio por estrato (Zona Sur)", xaxis = list(title = "Estrato"))
p2b
Sobre la variable Precio por estrato (Zona Sur) el patrón es idéntico en forma al observado en la Parte A: la mediana del precio aumenta de manera clara y monótona con el estrato (de ~130-150 millones en estrato 3 a ~560 millones en estrato 6), y la dispersión también crece marcadamente. El estrato 6 muestra un rango intercuartílico mucho más amplio (aproximadamente 420 a 700 millones) y una cantidad considerable de valores atípicos que llegan hasta 1.800 millones, mientras que el estrato 3 es compacto y homogéneo.
Esto confirma, con datos específicos de este segmento, que el estrato sigue siendo un predictor fuerte y consistente del precio, y que el mercado de apartamentos de estrato alto en zona sur es notablemente más heterogéneo (mayor variabilidad de acabados, vista, ubicación exacta) que el de estratos bajos.
p3b <- plot_ly(base2, x = ~piso, y = ~preciom, type = "box", color = ~piso) %>%
layout(title = "Vivienda 2: Precio por piso (Zona Sur)", xaxis = list(title = "Piso"))
p3b
Finalmente, sobre la variable Precio por piso (Zona Sur), a diferencia del estrato, el piso no muestra una relación tan clara ni monótona con el precio: las medianas de los pisos 1 al 10 son bastante similares entre sí (rondando los 220-290 millones), con abundantes valores atípicos dispersos en casi todos los pisos (hasta 1.600-1.800 millones en pisos bajos como el 2 y 3, lo cual sugiere que esos outliers probablemente corresponden más bien a apartamentos de estrato alto que a un efecto del piso en sí).
Se observa una leve tendencia al alza en los pisos 11 y 12, con medianas más altas (~350 y ~400 millones respectivamente) y cajas visiblemente más angostas. Esto último indica que hay muy pocas observaciones en esos pisos, por lo que esa aparente “prima por piso alto” debe interpretarse con cautela: podría ser un efecto real (apartamentos tipo penthouse) o simplemente ruido estadístico por el tamaño de muestra reducido en esos niveles.
En conjunto, el piso parece ser un predictor mucho más débil que el estrato para este segmento, y no se recomendaría priorizarlo como variable explicativa central del modelo sin antes verificar el número de observaciones por piso.
A diferencia del enfoque genérico planteado inicialmente, y con base en lo que encontramos en el Paso 2 (correlaciones más fuertes y un rol más relevante de parqueaderos específicamente en el segmento de apartamentos de zona sur), se decide ajustar un modelo propio para base2 en lugar de reutilizar modelo1 (que fue entrenado con toda la base, mezclando casas y apartamentos de todas las zonas). Esto permite que el modelo capture la dinámica de precios específica de este segmento del mercado, en vez de generalizar a partir de un modelo entrenado sobre un universo distinto de propiedades.
modelo2 <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios,
data = base2)
summary(modelo2)
##
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos +
## banios, data = base2)
##
## Residuals:
## Min 1Q Median 3Q Max
## -1092.02 -42.28 -1.33 40.58 926.56
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -261.62501 15.63220 -16.736 < 2e-16 ***
## areaconst 1.28505 0.05403 23.785 < 2e-16 ***
## estrato 60.89709 3.08408 19.746 < 2e-16 ***
## habitaciones -24.83693 3.89229 -6.381 2.11e-10 ***
## parqueaderos 72.91468 3.95797 18.422 < 2e-16 ***
## banios 50.69675 3.39637 14.927 < 2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 98.02 on 2375 degrees of freedom
## (406 observations deleted due to missingness)
## Multiple R-squared: 0.7485, Adjusted R-squared: 0.748
## F-statistic: 1414 on 5 and 2375 DF, p-value: < 2.2e-16
tidy(modelo2, conf.int = TRUE)
## # A tibble: 6 × 7
## term estimate std.error statistic p.value conf.low conf.high
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 (Intercept) -262. 15.6 -16.7 1.60e- 59 -292. -231.
## 2 areaconst 1.29 0.0540 23.8 2.39e-112 1.18 1.39
## 3 estrato 60.9 3.08 19.7 1.76e- 80 54.8 66.9
## 4 habitaciones -24.8 3.89 -6.38 2.11e- 10 -32.5 -17.2
## 5 parqueaderos 72.9 3.96 18.4 6.04e- 71 65.2 80.7
## 6 banios 50.7 3.40 14.9 3.16e- 48 44.0 57.4
glance(modelo2) %>% select(r.squared, adj.r.squared, sigma, statistic, p.value)
## # A tibble: 1 × 5
## r.squared adj.r.squared sigma statistic p.value
## <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 0.749 0.748 98.0 1414. 0
Los cinco predictores resultaron nuevamente significativos (p < 0.001 en todos los casos). Manteniendo las demás variables constantes. Podemos concluir, con base en esto, que:
1. Área construida (β = 1.285): cada m² adicional aumenta el precio esperado en 1.285 millones de pesos, considerablemente más que en el modelo de casas (0.851), lo que sugiere que en este segmento (apartamentos, zona sur) el metro cuadrado tiene un valor de mercado más alto, posiblemente porque estos apartamentos tienden a estar en zonas más consolidadas o de mayor plusvalía por unidad de área que las casas de zona norte.
2. Estrato (β = 60.90): cada estrato adicional suma cerca de 61 millones, un efecto más moderado que en el modelo de la Vivienda 1 (97.91). Esto es coherente con la matriz de correlación del Paso 2, donde el estrato tenía un peso relativamente menor frente a área y parqueaderos en este segmento.
Parqueaderos (β = 72.91): confirma lo que ya anticipaba el EDA: el parqueadero es un fuerte determinante del precio en apartamentos (72.9 millones por unidad adicional), con una magnitud muy similar a la del modelo de casas (74.49), pese a que en el EDA de zona sur su correlación con el precio era proporcionalmente más alta que la del estrato.
Baños (β = 50.70): cada baño adicional aporta cerca de 50.7 millones, un poco menos que en el modelo de casas (61.06).
Habitaciones (β = −24.84): se repite el patrón contraintuitivo-pero-explicable ya visto en la Vivienda 1: a igualdad de área, más habitaciones se asocia con un precio menor (~25 millones menos por habitación adicional), reforzando la interpretación de que subdividir el mismo espacio en más cuartos reduce el valor percibido frente a espacios más amplios y menos fragmentados.
El R² es 0.7485 (R² ajustado 0.7480), superior al 0.7206 obtenido en el modelo de la Vivienda 1. Esto confirma la expectativa que planteamos en el Paso 2: al ajustar el modelo específicamente sobre el segmento de apartamentos en zona sur (en lugar de usar el modelo general entrenado con toda la base), se logra un mejor ajuste. Es decir, el modelo explica cerca del 75% de la variabilidad del precio en este segmento.
El error estándar residual también es notablemente menor (98.02 frente a 177.2 en el modelo de casas), lo que indica predicciones más precisas en términos absolutos, aunque parte de esto se debe a que los precios de este segmento son en general más bajos que los de la Vivienda 1. Se excluyeron 406 observaciones por datos faltantes (14.6% de base2), una proporción a tener en cuenta como limitación.
Al igual que con el modelo de la Vivienda 1, verificamos que modelo2 cumpla los supuestos clásicos de la regresión lineal: linealidad y homocedasticidad (gráfico de residuos vs. ajustados y prueba de Breusch-Pagan), normalidad de los residuos (Q-Q plot y prueba de Shapiro-Wilk), independencia de los errores (prueba de Durbin-Watson) y ausencia de multicolinealidad entre predictores (factor de inflación de varianza, VIF).
Dado que este modelo fue ajustado específicamente sobre el segmento de apartamentos en zona sur, es interesante comparar si presenta los mismos problemas de supuestos que el modelo de casas, o si el ajuste más específico también se traduce en un mejor comportamiento de los residuos.
graficos_diagnostico(modelo2, "Diagnóstico del modelo - Vivienda 2")
res2 <- residuals(modelo2)
set.seed(123)
res2_muestra <- if (length(res2) > 5000) sample(res2, 5000) else res2
shapiro.test(res2_muestra)
##
## Shapiro-Wilk normality test
##
## data: res2_muestra
## W = 0.79118, p-value < 2.2e-16
bptest(modelo2)
##
## studentized Breusch-Pagan test
##
## data: modelo2
## BP = 754.81, df = 5, p-value < 2.2e-16
dwtest(modelo2)
##
## Durbin-Watson test
##
## data: modelo2
## DW = 1.5333, p-value < 2.2e-16
## alternative hypothesis: true autocorrelation is greater than 0
vif(modelo2)
## areaconst estrato habitaciones parqueaderos banios
## 2.066518 1.545162 1.429280 1.737878 2.529494
Residuos vs. Ajustados: la tendencia se mantiene cerca de cero para valores ajustados bajos y medios, pero se curva hacia abajo en el extremo superior (valores ajustados por encima de ~800-1000), llegando a residuos de -300 a -500. El patrón es similar al de modelo1, aunque algo menos pronunciado en la mayor parte del rango, sugiriendo una leve mejora en linealidad, consistente con el mejor R² obtenido en el Paso 3.
Q-Q de residuos: aquí la desviación de la normalidad es, si acaso, más severa que en modelo1. Las colas llegan hasta -12/-14 y +10 desviaciones estándar (frente a -8/+5 en el modelo de casas), formando una “S” muy pronunciada. Esto se confirma numéricamente: Shapiro-Wilk da W = 0.791 (más bajo que el 0.863 de modelo1), indicando una desviación de la normalidad incluso mayor en este segmento.
Escala-Ubicación: la dispersión de residuos crece de forma marcada con los valores ajustados, con un punto extremo cerca de la raíz de residuo estandarizado = 10 en el rango de precios más altos. La prueba de Breusch-Pagan (BP = 754.81, df = 5, p < 2.2e-16) confirma heterocedasticidad significativa, igual que en modelo1.
Residuos vs. Apalancamiento: este es el hallazgo más notorio de este modelo. Hay observaciones con apalancamiento mucho más alto que en modelo1 (hasta ~0.21, frente a un máximo de ~0.045 en el modelo de casas), y esas mismas observaciones presentan residuos muy negativos (hasta -12).
Esto señala la presencia de puntos potencialmente muy influyentes en el modelo de apartamentos. Es decir, es probable que el valor atípico que detectamos en el EDA (el apartamento de ~950 m² con precio bajo) sea justamente una de estas observaciones de alto apalancamiento. Valdría la pena identificarlo puntualmente con augment(modelo2) %>% arrange(desc(.cooksd)) %>% head().
Durbin-Watson: DW = 1.533 (p < 2.2e-16), un poco más alejado de 2 que en modelo1 (1.608), sugiriendo una dependencia similar o levemente mayor entre observaciones. Aplica la misma aclaración que hicimos antes: esto probablemente refleja agrupamiento espacial/de orden en la base más que autocorrelación temporal real.
VIF: areaconst = 2.07, estrato = 1.55, habitaciones = 1.43, parqueaderos = 1.74, banios = 2.53. Todos bajos, sin evidencia de multicolinealidad, resultado prácticamente idéntico al de modelo1.
Aunque modelo2 tiene mejor ajuste general (R² más alto, error estándar residual más bajo), no tiene mejores supuestos que modelo1. De hecho, la normalidad es peor y hay observaciones más influyentes. Esto es un matiz importante para el informe: un mejor R² no necesariamente implica un modelo estadísticamente “más sano”.
En este caso, el ajuste superior probablemente se debe a que el segmento es más homogéneo (todos apartamentos, una sola zona), pero la presencia de puntos de alto apalancamiento sugiere que unos pocos casos atípicos podrían estar teniendo un peso desproporcionado en las estimaciones.
Como sugerencia y/o anotación importante (sin necesidad de implementarla), sería razonable identificar y revisar puntualmente esas observaciones de alto apalancamiento antes de usar el modelo para decisiones de alto impacto, además de las mismas recomendaciones generales que para modelo1 (transformación logarítmica del precio, errores estándar robustos, e incorporación de variables de ubicación más finas).
Usamos modelo2 para predecir el precio esperado de un apartamento con las características exactas solicitadas por el cliente para la Vivienda 2 (300 m² construidos, estrato 6, 5 habitaciones, 3 parqueaderos, 3 baños).
Al igual que con la Vivienda 1, el intervalo de predicción debe interpretarse con cautela: en el Paso 4 confirmamos que modelo2 incumple los supuestos de normalidad y homocedasticidad. De hecho, de forma más pronunciada que modelo1, por lo que el rango del intervalo probablemente sea menos confiable de lo que su cálculo formal sugiere.
vivienda2_datos <- data.frame(
areaconst = 300,
estrato = 6, # usar 5 o 6 según disponibilidad; documentar el supuesto
habitaciones = 5,
parqueaderos = 3,
banios = 3
)
pred2 <- predict(modelo2, newdata = vivienda2_datos, interval = "prediction")
pred2
## fit lwr upr
## 1 735.9218 542.3141 929.5296
Los resultados son bastante reveladores. Para un apartamento con las características solicitadas (300 m² construidos, estrato 6, 5 habitaciones, 3 parqueaderos, 3 baños), el modelo estima un precio puntual de 735.9 millones de pesos, una cifra que deja un margen razonable de aproximadamente 114 millones por debajo del crédito preaprobado de 850 millones. A diferencia de la Vivienda 1 (donde el precio estimado coincidía casi exactamente con el techo del crédito), aquí el cliente tendría cierto colchón financiero.
El intervalo de predicción al 95% va de 542.3 a 929.5 millones, un rango de 387.2 millones. Proporcionalmente, este intervalo es más angosto respecto a su valor puntual que el obtenido para la Vivienda 1 (donde el rango representaba casi el doble del valor estimado, frente a poco más de la mitad aquí), lo cual es coherente con el mejor ajuste general de modelo2 (R² más alto, menor error estándar residual).
Sin embargo, conviene no perder de vista la advertencia del Paso 4: modelo2 mostró una violación de normalidad más severa que modelo1, además de observaciones de alto apalancamiento que podrían estar distorsionando la estimación, por lo que, aunque el intervalo luzca más “razonable” en apariencia, su confiabilidad estadística formal sigue siendo cuestionable.
El hallazgo más relevante para el informe: el límite superior del intervalo (929.5 millones) supera el crédito preaprobado de 850 millones. Esto significa que, si bien la estimación puntual es tranquilizadora, existe una probabilidad no despreciable (dada la incertidumbre del modelo) de que una vivienda con estas características específicas termine costando más de lo que el cliente puede financiar.
Esto es un punto importante a comunicarle a María: aunque el valor central sugiere que el presupuesto alcanza con margen, la variabilidad del mercado (y las limitaciones del modelo) no permiten descartar por completo un escenario donde el precio real exceda el crédito aprobado.
Por tanto, la recomendación puntual para el informe debería ser: usar el valor puntual (735.9 millones) como referencia de negociación, pero advertir a María que busque opciones con cierto margen adicional bajo el techo de 850 millones (no exactamente en el límite), precisamente para absorber la incertidumbre que el propio modelo reconoce en su intervalo de predicción.
Buscamos apartamentos en zona sur cuyo precio no supere el crédito preaprobado de 850 millones, seleccionando los 5 más costosos dentro de ese límite. Adicionalmente, y a raíz del hallazgo del Paso 1 (44 registros de “Zona Sur” con latitud más al norte que la mediana de “Zona Norte”), se incluye una bandera de verificación (zona_limite) que señala si alguna de las ofertas seleccionadas cae en ese grupo geográficamente ambiguo, aplicando el mismo criterio de precaución que usamos con “Acopi” en la Vivienda 1.
ofertas2 <- vivienda %>%
filter(tipo == "Apartamento", zona == "Zona Sur", preciom <= 850) %>%
mutate(zona_limite = latitud > median(base1$latitud, na.rm = TRUE)) %>%
arrange(desc(preciom)) %>%
slice_head(n = 5)
ofertas2 %>% select(barrio, preciom, areaconst, estrato, habitaciones, banios, parqueaderos, zona_limite)
## # A tibble: 5 × 8
## barrio preciom areaconst estrato habitaciones banios parqueaderos zona_limite
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <lgl>
## 1 ciudad… 850 187 6 4 5 3 FALSE
## 2 ciudad… 850 168 6 4 5 3 FALSE
## 3 ciudad… 850 192 6 3 5 2 FALSE
## 4 ciudad… 850 192. 6 4 5 2 FALSE
## 5 ciudad… 850 186 6 4 5 3 FALSE
Las 5 ofertas seleccionadas se ubican todas en el barrio Ciudad Jardín, todas en estrato 6, y todas exactamente en el techo del crédito preaprobado (850 millones), un patrón similar al observado con “El Bosque” en la Vivienda 1, que sugiere que Ciudad Jardín es el sector premium por excelencia de la zona sur (coherente con lo visto en el EDA, donde el estrato 6 mostraba los precios más altos y dispersos).
Sin embargo, al comparar estas ofertas contra el perfil solicitado por el cliente (300 m², estrato 6, 5 habitaciones, 3 parqueaderos, 3 baños), aparece una discrepancia significativa en área: ninguna de las 5 ofertas se acerca a los 300 m² pedidos. El rango real es de apenas 168 a 192 m², es decir, entre un 36% y un 44% más pequeñas que lo solicitado. En cuanto a habitaciones, las ofertas tienen 3 o 4, por debajo de las 5 pedidas. En baños, las 5 ofertas superan lo pedido (5 baños frente a 3 solicitados) y, en parqueaderos, están en línea o ligeramente por debajo (2-3 frente a 3 solicitados).
Esto sugiere que, en Ciudad Jardín (el sector más exclusivo dentro de la zona sur), el precio por metro cuadrado es tan alto que el presupuesto de 850 millones no alcanza para un apartamento de 300 m² como el que pide el cliente. En su lugar, ese presupuesto rinde para apartamentos considerablemente más pequeños pero con muchos baños, reflejando un mercado orientado a lujo compacto más que a gran metraje.
Esto es consistente con la advertencia que hicimos en el Paso 5: el límite superior del intervalo de predicción (929.5 millones) superaba el crédito aprobado, y ahora el mercado real confirma que un apartamento de exactamente esas características (300 m², 5 habitaciones) probablemente no está disponible dentro del presupuesto en este submercado específico.
Ninguna de las 5 ofertas cumple estrictamente el requisito de área y número de habitaciones solicitado, aunque todas superan las expectativas en baños.
Se recomienda comunicarle al cliente que, dentro del presupuesto aprobado y en el sector más exclusivo de la zona sur, deberá flexibilizar su expectativa de área (aceptar un apartamento más compacto, de 170-190 m² en lugar de 300 m²), o bien considerar otros barrios de zona sur menos premium donde el precio por metro cuadrado sea menor y sí permita alcanzar los 300 m² deseados dentro del mismo presupuesto.
Dado que el segmento de apartamentos en zona sur mostró un comportamiento de precios distinto al de las casas (mayor peso de parqueaderos y área, y correlaciones más fuertes en general), se ajustó un modelo específico para este segmento, logrando un mejor ajuste (R² = 0.75).
El modelo estima que un apartamento con las características solicitadas (300 m², 5 habitaciones, 3 parqueaderos, 3 baños) costaría 735.9 millones, dejando en principio un margen de 114 millones frente al crédito aprobado. Sin embargo, el mercado real revela una limitación importante: las mejores ofertas disponibles dentro del presupuesto se concentran en el barrio Ciudad Jardín (el sector más exclusivo de la zona), pero con áreas de apenas 168 a 192 m², muy por debajo de los 300 m² solicitados, aunque con más baños de los pedidos.
Esto sugiere que, en el submercado más premium de la zona sur, el presupuesto aprobado alcanza para un apartamento de lujo compacto, no para uno del tamaño exacto solicitado, por lo que se recomienda a la empresa flexibilizar el requisito de área o considerar sectores de la zona sur con precio por metro cuadrado más moderado.
[1] M. H. Kutner, C. J. Nachtsheim, J. Neter, and W. Li, Applied Linear Statistical Models, 5th ed. New York, NY, USA: McGraw-Hill/Irwin, 2005.
[2] D. N. Gujarati and D. C. Porter, Basic Econometrics, 5th ed. New York, NY, USA: McGraw-Hill, 2009.
[3] S. Rosen, “Hedonic prices and implicit markets: Product differentiation in pure competition,” Journal of Political Economy, vol. 82, no. 1, pp. 34–55, 1974.
Pruebas de validación de supuestos
[4] S. S. Shapiro and M. B. Wilk, “An analysis of variance test for normality (complete samples),” Biometrika, vol. 52, no. 3/4, pp. 591–611, 1965.
[5] T. S. Breusch and A. R. Pagan, “A simple test for heteroscedasticity and random coefficient variation,” Econometrica, vol. 47, no. 5, pp. 1287–1294, 1979.
[6] J. Durbin and G. S. Watson, “Testing for serial correlation in least squares regression, I,” Biometrika, vol. 37, no. 3/4, pp. 409–428, 1950.
[7] R. M. O’Brien, “A caution regarding rules of thumb for variance inflation factors,” Quality & Quantity, vol. 41, no. 5, pp. 673–690, 2007.
Software y paquetes de R utilizados
[8] R Core Team, R: A Language and Environment for Statistical Computing, R Foundation for Statistical Computing, Vienna, Austria, 2024. [Online]. Available: https://www.R-project.org/
[9] H. Wickham et al., “Welcome to the tidyverse,” Journal of Open Source Software, vol. 4, no. 43, p. 1686, 2019.
[10] J. Cheng, B. Karambelkar, and Y. Xie, leaflet: Create Interactive Web Maps with the JavaScript ‘Leaflet’ Library, R package, 2023. [Online]. Available: https://CRAN.R-project.org/package=leaflet
[11] J. Fox and S. Weisberg, An R Companion to Applied Regression, 3rd ed. Thousand Oaks, CA, USA: Sage, 2019.
[12] A. Zeileis and T. Hothorn, “Diagnostic checking in regression relationships,” R News, vol. 2, no. 3, pp. 7–10, 2002.
[13] D. Robinson, A. Hayes, and S. Couch, broom: Convert Statistical Objects into Tidy Tibbles, R package, 2023. [Online]. Available: https://CRAN.R-project.org/package=broom
[14] C. Sievert, Interactive Web-Based Data Visualization with R, plotly, and shiny. Boca Raton, FL, USA: CRC Press, 2020.