La compañía C&A recibió una solicitud de asesoría inmobiliaria por parte de una empresa internacional interesada en ubicar a dos familias en Cali. Para responder de manera objetiva, se construyeron modelos de regresión lineal múltiple a partir de las ofertas del mercado de los últimos tres meses, segmentadas según las condiciones de cada solicitud (tipo de inmueble y zona). Estos modelos permiten estimar el precio de mercado esperado para cada vivienda solicitada y, a partir de ahí, identificar ofertas reales que se ajusten al presupuesto pre-aprobado de cada familia.
Los resultados detallados, las validaciones estadísticas y la comparación de los dos modelos se presentan en la sección de Anexos. A continuación se resume la recomendación para cada solicitud; la síntesis ejecutiva completa se retoma al final del documento, una vez expuesta la evidencia que la soporta.
El análisis se enmarca en la teoría de precios hedónicos, según la cual el precio de un bien heterogéneo como una vivienda puede descomponerse como una función de sus atributos estructurales (área, número de habitaciones, baños, parqueaderos) y de localización (zona, estrato como proxy del entorno socioeconómico). Bajo este marco, un modelo de regresión múltiple sobre el precio permite recuperar el “precio implícito” de cada atributo, manteniendo los demás constantes.
Dado que los precios de vivienda suelen presentar asimetría a la derecha y varianza creciente con el nivel del precio, se contrastan dos especificaciones para cada solicitud:
preciom ~ areaconst + estrato + habitaciones + parqueaderos + banioslog(preciom) ~ areaconst + estrato + habitaciones + parqueaderos + banios,
donde los coeficientes se interpretan como cambios porcentuales
aproximados en el precio ante cambios marginales en cada atributo.La selección del modelo final para cada solicitud no se basa únicamente en el ajuste dentro de muestra (R²), sino en una validación fuera de muestra (partición 80/20 entrenamiento-prueba) que compara el error de predicción de ambas especificaciones en la escala original de precios (millones de pesos), corrigiendo el sesgo de retransformación del modelo log-lineal mediante el estimador de smearing de Duan (1983): \(\hat{y} = \exp(\hat{\beta}'x) \cdot \overline{\exp(\hat{e})}\), donde \(\overline{\exp(\hat{e})}\) es el promedio de los residuos exponenciados del modelo de entrenamiento.
Adicionalmente, dado que los datos tienen coordenadas geográficas, se revisa si el estadístico de Durbin-Watson sugiere posible dependencia entre observaciones, como indicio indirecto de agrupamiento espacial (submercados o efectos de barrio) que el modelo estructural no captura.
Se filtra la base se incluye únicamente ofertas de casas ubicadas en la Zona Norte, que es el segmento relevante para la primera solicitud.
base1 <- vivienda %>% filter(tipo == "Casa", zona == "Zona Norte")
cat("Número de observaciones en base1:", nrow(base1), "\n")
## Número de observaciones en base1: 435
kable(head(base1, 3), caption = "Primeros 3 registros de base1 (Casas, Zona Norte)")
| id | zona | piso | estrato | preciom | areaconst | parqueaderos | banios | habitaciones | tipo | barrio | longitud | latitud |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1209 | Zona Norte | 02 | 5 | 320 | 150 | 2 | 4 | 6 | Casa | acopi | -76.51341 | 3.47968 |
| 1592 | Zona Norte | 02 | 5 | 780 | 380 | 2 | 3 | 3 | Casa | acopi | -76.51674 | 3.48721 |
| 4460 | Zona Norte | 02 | 4 | 625 | 355 | 3 | 5 | 5 | Casa | acopi | -76.53179 | 3.40590 |
kable(base1 %>%
summarise(n = n(), precio_prom = mean(preciom), area_prom = mean(areaconst),
estrato_prom = round(mean(estrato), 1)),
caption = "Resumen de base1")
| n | precio_prom | area_prom | estrato_prom |
|---|---|---|---|
| 435 | 479.7724 | 292.7177 | 4.5 |
Para verificar la consistencia geográfica del filtro, se ubican los
puntos de base1 en el mapa y se comparan contra la
distribución de todas las zonas de la ciudad.
set.seed(123)
muestra_zonas <- vivienda %>% sample_n(min(1500, nrow(vivienda)))
plot_ly(muestra_zonas, lat = ~latitud, lon = ~longitud, type = "scattermapbox",
mode = "markers", color = ~zona,
marker = list(size = 6, opacity = 0.6),
text = ~paste("Zona:", zona, "<br>Barrio:", barrio)) %>%
layout(mapbox = list(style = "open-street-map", zoom = 10.5,
center = list(lat = 3.43, lon = -76.53)),
title = "Distribución espacial de todas las zonas (muestra)")
rangos <- vivienda %>% group_by(zona) %>%
summarise(lat_min = min(latitud), lat_max = max(latitud),
lon_min = min(longitud), lon_max = max(longitud))
kable(rangos, caption = "Rango de coordenadas por zona")
| zona | lat_min | lat_max | lon_min | lon_max |
|---|---|---|---|---|
| Zona Centro | 3.39841 | 3.48700 | -76.54400 | -76.49700 |
| Zona Norte | 3.33308 | 3.49770 | -76.58915 | -76.46745 |
| Zona Oeste | 3.35973 | 3.49415 | -76.58744 | -76.46400 |
| Zona Oriente | 3.34400 | 3.47300 | -76.56113 | -76.46570 |
| Zona Sur | 3.33300 | 3.49684 | -76.56706 | -76.46300 |
Discusión: los rangos de latitud y longitud de las
cinco zonas se traslapan de manera considerable (todas comparten
prácticamente el mismo rango de longitud, entre -76.6° y -76.5°, y
rangos de latitud muy similares). Esto indica que la variable
zona no corresponde a una partición geográfica estricta por
cuadrantes de coordenadas, sino a una clasificación administrativa o
comercial (probablemente por comuna o sector catastral) que no se
traduce en fronteras limpias sobre el mapa. Por esta razón, al graficar
los puntos de base1 se observan casas etiquetadas como
“Zona Norte” geográficamente cercanas a puntos de otras zonas, esto no
es un error del filtro, sino una característica de cómo fue construida
la variable en la fuente original. En consecuencia, el filtro por la
variable zona (no por coordenadas) es el criterio correcto
y consistente con la forma en que el mercado y la inmobiliaria
clasifican los sectores.
plot_ly(base1, lat = ~latitud, lon = ~longitud, type = "scattermapbox",
mode = "markers", marker = list(size = 7, color = "#2c7fb8", opacity = 0.7),
text = ~paste("Barrio:", barrio, "<br>Precio:", preciom, "M")) %>%
layout(mapbox = list(style = "open-street-map", zoom = 11,
center = list(lat = mean(base1$latitud), lon = mean(base1$longitud))),
title = "Ofertas de casas en Zona Norte (base1)")
Se analiza la relación entre el precio (preciom) y las
variables área construida, estrato, número de baños, número de
habitaciones y zona (esta última ya fijada en “Zona Norte” para
base1).
num_vars <- base1 %>% select(preciom, areaconst, estrato, banios, habitaciones)
kable(round(cor(num_vars), 2), caption = "Matriz de correlación - base1")
| preciom | areaconst | estrato | banios | habitaciones | |
|---|---|---|---|---|---|
| preciom | 1.00 | 0.69 | 0.53 | 0.51 | 0.37 |
| areaconst | 0.69 | 1.00 | 0.35 | 0.46 | 0.42 |
| estrato | 0.53 | 0.35 | 1.00 | 0.35 | 0.06 |
| banios | 0.51 | 0.46 | 0.35 | 1.00 | 0.59 |
| habitaciones | 0.37 | 0.42 | 0.06 | 0.59 | 1.00 |
p1 <- ggplot(base1, aes(x = areaconst, y = preciom, color = factor(estrato),
text = paste("Barrio:", barrio))) +
geom_point(alpha = 0.7) +
labs(title = "Precio vs. Área construida (coloreado por estrato)",
x = "Área construida (m²)", y = "Precio (millones $)", color = "Estrato") +
theme_minimal()
ggplotly(p1, tooltip = c("x", "y", "text", "colour"))
p2 <- ggplot(base1, aes(x = factor(estrato), y = preciom)) +
geom_boxplot(fill = "#74a9cf") +
labs(title = "Precio por estrato", x = "Estrato", y = "Precio (millones $)") +
theme_minimal()
ggplotly(p2)
p3 <- ggplot(base1, aes(x = factor(habitaciones), y = preciom)) +
geom_boxplot(fill = "#a1d99b") +
labs(title = "Precio por número de habitaciones", x = "Habitaciones", y = "Precio (millones $)") +
theme_minimal()
ggplotly(p3)
p4 <- ggplot(base1, aes(x = factor(banios), y = preciom)) +
geom_boxplot(fill = "#fdae6b") +
labs(title = "Precio por número de baños", x = "Baños", y = "Precio (millones $)") +
theme_minimal()
ggplotly(p4)
Interpretación: el área construida presenta la correlación más fuerte con el precio, seguida del estrato; ambas relaciones son positivas y consistentes con la lógica del mercado inmobiliario (a mayor área y mejor estrato, mayor precio). El número de habitaciones y de baños muestran una asociación positiva más moderada, y se observa dispersión considerable dentro de cada categoría, lo que sugiere que estas variables por sí solas no explican completamente el precio y que su efecto probablemente se solape con el del área construida (viviendas más grandes tienden a tener más habitaciones y baños).
Siguiendo la estrategia metodológica descrita, se estiman dos
especificaciones sobre base1: un modelo lineal y un modelo
log-lineal.
modelo1 <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = base1)
modelo1_log <- lm(log(preciom) ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = base1)
summary(modelo1)
##
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos +
## banios, data = base1)
##
## Residuals:
## Min 1Q Median 3Q Max
## -784.29 -77.56 -16.03 47.67 978.61
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -238.17090 44.40551 -5.364 1.34e-07 ***
## areaconst 0.67673 0.05281 12.814 < 2e-16 ***
## estrato 80.63495 9.82632 8.206 2.70e-15 ***
## habitaciones 7.64511 5.65873 1.351 0.177
## parqueaderos 24.00598 5.86889 4.090 5.14e-05 ***
## banios 18.89938 7.48800 2.524 0.012 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 155.1 on 429 degrees of freedom
## Multiple R-squared: 0.6041, Adjusted R-squared: 0.5995
## F-statistic: 130.9 on 5 and 429 DF, p-value: < 2.2e-16
kable(tidy(modelo1, conf.int = TRUE), digits = 3, caption = "Coeficientes del modelo lineal (Casas, Zona Norte)")
| term | estimate | std.error | statistic | p.value | conf.low | conf.high |
|---|---|---|---|---|---|---|
| (Intercept) | -238.171 | 44.406 | -5.364 | 0.000 | -325.450 | -150.891 |
| areaconst | 0.677 | 0.053 | 12.814 | 0.000 | 0.573 | 0.781 |
| estrato | 80.635 | 9.826 | 8.206 | 0.000 | 61.321 | 99.949 |
| habitaciones | 7.645 | 5.659 | 1.351 | 0.177 | -3.477 | 18.767 |
| parqueaderos | 24.006 | 5.869 | 4.090 | 0.000 | 12.471 | 35.541 |
| banios | 18.899 | 7.488 | 2.524 | 0.012 | 4.182 | 33.617 |
kable(glance(modelo1)[, c("r.squared", "adj.r.squared", "sigma", "statistic", "p.value")],
digits = 3, caption = "Bondad de ajuste - modelo lineal")
| r.squared | adj.r.squared | sigma | statistic | p.value |
|---|---|---|---|---|
| 0.604 | 0.599 | 155.115 | 130.919 | 0 |
Interpretación de los coeficientes (modelo lineal):
kable(tidy(coeftest(modelo1, vcov = vcovHC(modelo1, type = "HC3"))), digits = 4,
caption = "Coeficientes con errores estándar robustos (HC3) - modelo lineal, base1")
| term | estimate | std.error | statistic | p.value |
|---|---|---|---|---|
| (Intercept) | -238.1709 | 45.7844 | -5.2020 | 0.0000 |
| areaconst | 0.6767 | 0.1479 | 4.5754 | 0.0000 |
| estrato | 80.6349 | 12.1550 | 6.6339 | 0.0000 |
| habitaciones | 7.6451 | 6.9791 | 1.0954 | 0.2739 |
| parqueaderos | 24.0060 | 7.0696 | 3.3957 | 0.0007 |
| banios | 18.8994 | 10.4622 | 1.8065 | 0.0715 |
Bajo HC3, el coeficiente de banios pierde significancia
al 5% (p ≈ 0.07), lo que indica que su significancia bajo el supuesto de
varianza constante era, al menos en parte, artificial. Esto ilustra por
qué la validación de supuestos del Paso 4 es indispensable antes de
fijar conclusiones sobre significancia estadística.
Especificación alternativa (log-lineal):
kable(tidy(modelo1_log, conf.int = TRUE), digits = 4, caption = "Coeficientes del modelo log-lineal (Casas, Zona Norte)")
| term | estimate | std.error | statistic | p.value | conf.low | conf.high |
|---|---|---|---|---|---|---|
| (Intercept) | 4.4148 | 0.0742 | 59.5164 | 0.0000 | 4.2690 | 4.5606 |
| areaconst | 0.0011 | 0.0001 | 12.8891 | 0.0000 | 0.0010 | 0.0013 |
| estrato | 0.2152 | 0.0164 | 13.1135 | 0.0000 | 0.1830 | 0.2475 |
| habitaciones | 0.0165 | 0.0095 | 1.7416 | 0.0823 | -0.0021 | 0.0350 |
| parqueaderos | 0.0470 | 0.0098 | 4.7977 | 0.0000 | 0.0278 | 0.0663 |
| banios | 0.0461 | 0.0125 | 3.6832 | 0.0003 | 0.0215 | 0.0707 |
cat("R² modelo lineal: ", round(summary(modelo1)$r.squared, 3), "\n")
## R² modelo lineal: 0.604
cat("R² modelo log-lineal: ", round(summary(modelo1_log)$r.squared, 3), "\n")
## R² modelo log-lineal: 0.697
En la escala logarítmica, el R² sube de 0.604 a 0.697, y los coeficientes se interpretan como semi-elasticidades: por ejemplo, cada baño adicional se asocia con un incremento aproximado de 4.6% en el precio, manteniendo lo demás constante. Este mejor ajuste dentro de muestra motiva evaluar si también mejora la capacidad predictiva fuera de muestra (Paso 4).
Ajuste general: en su escala original, el modelo lineal explica cerca del 60% de la variabilidad del precio de las casas en la Zona Norte. Es un ajuste razonable para datos de mercado inmobiliario real, pero deja una porción importante de la variación sin explicar por factores no observados antigüedad de la construcción, calidad de acabados, cercanía a vías principales, orientación, seguridad del sector, entre otros.
par(mfrow = c(2, 2))
plot(modelo1)
par(mfrow = c(1, 1))
shapiro_p <- shapiro.test(resid(modelo1))$p.value
bp_p <- bptest(modelo1)$p.value
dw <- dwtest(modelo1)
vif_vals <- vif(modelo1)
shapiro_p_log <- shapiro.test(resid(modelo1_log))$p.value
bp_p_log <- bptest(modelo1_log)$p.value
kable(data.frame(Prueba = c("Shapiro-Wilk (normalidad)", "Breusch-Pagan (homocedasticidad)",
"Durbin-Watson (independencia temporal/orden)"),
Lineal = c(format(shapiro_p, digits = 3), format(bp_p, digits = 3), round(dw$p.value, 4)),
`Log-lineal` = c(format(shapiro_p_log, digits = 3), format(bp_p_log, digits = 3), "—")),
caption = "Pruebas de supuestos: H0 = se cumple el supuesto (normalidad / homocedasticidad)")
| Prueba | Lineal | Log.lineal |
|---|---|---|
| Shapiro-Wilk (normalidad) | 8.77e-20 | 9.34e-05 |
| Breusch-Pagan (homocedasticidad) | 7.33e-16 | 6.63e-11 |
| Durbin-Watson (independencia temporal/orden) | 0.0055 | — |
kable(data.frame(Variable = names(vif_vals), VIF = round(vif_vals, 2)),
caption = "Factores de inflación de varianza (VIF) - modelo lineal")
| Variable | VIF | |
|---|---|---|
| areaconst | areaconst | 1.46 |
| estrato | estrato | 1.31 |
| habitaciones | habitaciones | 1.72 |
| parqueaderos | parqueaderos | 1.23 |
| banios | banios | 1.97 |
Independencia de los residuos (Durbin-Watson): aunque esta prueba se diseñó para series de tiempo, un valor del estadístico DW alejado de 2 en datos de corte transversal ordenados espacialmente puede ser indicio indirecto de estructura residual no capturada (p. ej., agrupamiento por barrio); se reporta en la tabla anterior junto con normalidad y homocedasticidad.
Validación fuera de muestra (partición 80/20): se compara el error de predicción de ambos modelos en la escala original (millones de pesos), aplicando la corrección de smearing de Duan al modelo log-lineal.
set.seed(2026)
idx1 <- sample(seq_len(nrow(base1)), floor(0.8 * nrow(base1)))
train1 <- base1[idx1, ]; test1 <- base1[-idx1, ]
m_lin_tr <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = train1)
m_log_tr <- lm(log(preciom) ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = train1)
pred_lin <- predict(m_lin_tr, newdata = test1)
smear1 <- mean(exp(resid(m_log_tr)))
pred_log <- exp(predict(m_log_tr, newdata = test1)) * smear1
kable(data.frame(Modelo = c("Lineal", "Log-lineal (con corrección de smearing)"),
RMSE = c(rmse(test1$preciom, pred_lin), rmse(test1$preciom, pred_log)),
MAE = c(mae(test1$preciom, pred_lin), mae(test1$preciom, pred_log))),
digits = 1, caption = "Error de predicción fuera de muestra (millones $) - base1")
| Modelo | RMSE | MAE |
|---|---|---|
| Lineal | 172.1 | 115.2 |
| Log-lineal (con corrección de smearing) | 204.0 | 121.2 |
Interpretación integrada:
spdep, pero se señala como una
limitación a validar en una siguiente iteración.spatialreg, o una prueba formal de I de Moran con
spdep).La solicitud pide una casa de 200 m², 1 parqueadero, 2 baños, 4 habitaciones, estrato 4 o 5, en la Zona Norte. Se predice con el modelo final (lineal) e intervalos de confianza al 95%.
nueva_vivienda1 <- data.frame(areaconst = 200, estrato = c(4, 5),
habitaciones = 4, parqueaderos = 1, banios = 2)
pred1 <- cbind(nueva_vivienda1, predict(modelo1, newdata = nueva_vivienda1, interval = "confidence"))
kable(pred1, digits = 1, caption = "Precio estimado para la vivienda solicitada (Solicitud 1)")
| areaconst | estrato | habitaciones | parqueaderos | banios | fit | lwr | upr |
|---|---|---|---|---|---|---|---|
| 200 | 4 | 4 | 1 | 2 | 312.1 | 287.2 | 337.0 |
| 200 | 5 | 4 | 1 | 2 | 392.7 | 360.9 | 424.6 |
Interpretación: para estrato 4, el modelo estima un precio de mercado de aproximadamente $312 millones (IC 95%: 287–337), dentro del crédito pre-aprobado de $350 millones. Para estrato 5, el precio estimado sube a aproximadamente $393 millones (IC 95%: 361–425), lo cual excede el presupuesto disponible. Esto indica que, con las características solicitadas, la búsqueda debe concentrarse preferentemente en estrato 4 para mantenerse dentro del crédito aprobado.
Se filtran ofertas reales de base1 que se ajusten al
presupuesto de $350 millones y a características similares a las
solicitadas (estrato 4 o 5, área construida amplia, al menos 3
habitaciones).
ofertas1 <- base1 %>%
filter(preciom <= 350, estrato %in% c(4, 5), areaconst >= 150, habitaciones >= 3) %>%
arrange(desc(preciom)) %>%
head(6)
kable(ofertas1 %>% select(barrio, preciom, areaconst, estrato, habitaciones, banios, parqueaderos),
caption = "Ofertas potenciales dentro del presupuesto (Solicitud 1)")
| barrio | preciom | areaconst | estrato | habitaciones | banios | parqueaderos |
|---|---|---|---|---|---|---|
| el bosque | 350 | 200 | 5 | 4 | 3 | 3 |
| el bosque | 350 | 300 | 5 | 6 | 5 | 3 |
| el bosque | 350 | 240 | 5 | 6 | 3 | 2 |
| el bosque | 350 | 203 | 5 | 5 | 2 | 2 |
| la flora | 350 | 264 | 5 | 4 | 3 | 2 |
| la flora | 350 | 160 | 5 | 3 | 3 | 2 |
plot_ly(ofertas1, lat = ~latitud, lon = ~longitud, type = "scattermapbox", mode = "markers",
marker = list(size = 14, color = "#d7301f"),
text = ~paste0("Barrio: ", barrio, "<br>Precio: $", preciom, "M",
"<br>Área: ", areaconst, "m²", "<br>Estrato: ", estrato)) %>%
layout(mapbox = list(style = "open-street-map", zoom = 11.5,
center = list(lat = mean(ofertas1$latitud), lon = mean(ofertas1$longitud))),
title = "Ofertas potenciales para la Solicitud 1 (≤ $350M)")
Discusión: se identifican al menos 6 ofertas reales dentro del presupuesto, concentradas principalmente en los barrios El Bosque y La Flora, con áreas entre 160 y 300 m² y estratos 4-5. Estas opciones cumplen simultáneamente el presupuesto y un perfil de características cercano al solicitado, por lo que constituyen una recomendación concreta para la familia de la Solicitud 1.
base2 <- vivienda %>% filter(tipo == "Apartamento", zona == "Zona Sur")
cat("Número de observaciones en base2:", nrow(base2), "\n")
## Número de observaciones en base2: 2381
kable(head(base2, 3), caption = "Primeros 3 registros de base2 (Apartamentos, Zona Sur)")
| id | zona | piso | estrato | preciom | areaconst | parqueaderos | banios | habitaciones | tipo | barrio | longitud | latitud |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 5098 | Zona Sur | 05 | 4 | 290 | 96 | 1 | 2 | 3 | Apartamento | acopi | -76.53464 | 3.44987 |
| 698 | Zona Sur | 02 | 3 | 78 | 40 | 1 | 1 | 2 | Apartamento | aguablanca | -76.50100 | 3.40000 |
| 8199 | Zona Sur | NA | 6 | 875 | 194 | 2 | 5 | 3 | Apartamento | aguacatal | -76.55700 | 3.45900 |
kable(base2 %>%
summarise(n = n(), precio_prom = mean(preciom), area_prom = mean(areaconst),
estrato_prom = round(mean(estrato), 1)),
caption = "Resumen de base2")
| n | precio_prom | area_prom | estrato_prom |
|---|---|---|---|
| 2381 | 318.2369 | 102.1566 | 4.7 |
plot_ly(base2, lat = ~latitud, lon = ~longitud, type = "scattermapbox",
mode = "markers", marker = list(size = 5, color = "#31a354", opacity = 0.5),
text = ~paste("Barrio:", barrio, "<br>Precio:", preciom, "M")) %>%
layout(mapbox = list(style = "open-street-map", zoom = 11,
center = list(lat = mean(base2$latitud), lon = mean(base2$longitud))),
title = "Ofertas de apartamentos en Zona Sur (base2)")
Discusión: al igual que en la Solicitud 1, los
rangos de coordenadas de la Zona Sur se traslapan con los de las zonas
vecinas (ver tabla de rangos en la sección anterior), por lo que se
ratifica que el filtro correcto es por la variable categórica
zona y no por coordenadas geográficas.
num_vars2 <- base2 %>% select(preciom, areaconst, estrato, banios, habitaciones)
kable(round(cor(num_vars2), 2), caption = "Matriz de correlación - base2")
| preciom | areaconst | estrato | banios | habitaciones | |
|---|---|---|---|---|---|
| preciom | 1.00 | 0.74 | 0.65 | 0.71 | 0.30 |
| areaconst | 0.74 | 1.00 | 0.45 | 0.66 | 0.41 |
| estrato | 0.65 | 0.45 | 1.00 | 0.53 | 0.18 |
| banios | 0.71 | 0.66 | 0.53 | 1.00 | 0.52 |
| habitaciones | 0.30 | 0.41 | 0.18 | 0.52 | 1.00 |
p5 <- ggplot(base2, aes(x = areaconst, y = preciom, color = factor(estrato),
text = paste("Barrio:", barrio))) +
geom_point(alpha = 0.5) +
labs(title = "Precio vs. Área construida (coloreado por estrato)",
x = "Área construida (m²)", y = "Precio (millones $)", color = "Estrato") +
theme_minimal()
ggplotly(p5, tooltip = c("x", "y", "text", "colour"))
p6 <- ggplot(base2, aes(x = factor(estrato), y = preciom)) +
geom_boxplot(fill = "#74a9cf") +
labs(title = "Precio por estrato", x = "Estrato", y = "Precio (millones $)") +
theme_minimal()
ggplotly(p6)
p7 <- ggplot(base2, aes(x = factor(habitaciones), y = preciom)) +
geom_boxplot(fill = "#a1d99b") +
labs(title = "Precio por número de habitaciones", x = "Habitaciones", y = "Precio (millones $)") +
theme_minimal()
ggplotly(p7)
p8 <- ggplot(base2, aes(x = factor(banios), y = preciom)) +
geom_boxplot(fill = "#fdae6b") +
labs(title = "Precio por número de baños", x = "Baños", y = "Precio (millones $)") +
theme_minimal()
ggplotly(p8)
Interpretación: en apartamentos de la Zona Sur, el área construida y el estrato vuelven a ser las variables más asociadas al precio. A diferencia de las casas, el número de baños muestra una relación positiva muy marcada, coherente con que en el segmento de apartamentos de mayor valor los baños adicionales suelen asociarse a acabados de lujo (walk-in closets, baños en suite, etc.).
modelo2 <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = base2)
modelo2_log <- lm(log(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
## Multiple R-squared: 0.7485, Adjusted R-squared: 0.748
## F-statistic: 1414 on 5 and 2375 DF, p-value: < 2.2e-16
kable(tidy(modelo2, conf.int = TRUE), digits = 3, caption = "Coeficientes del modelo lineal (Apartamentos, Zona Sur)")
| term | estimate | std.error | statistic | p.value | conf.low | conf.high |
|---|---|---|---|---|---|---|
| (Intercept) | -261.625 | 15.632 | -16.736 | 0 | -292.279 | -230.971 |
| areaconst | 1.285 | 0.054 | 23.785 | 0 | 1.179 | 1.391 |
| estrato | 60.897 | 3.084 | 19.746 | 0 | 54.849 | 66.945 |
| habitaciones | -24.837 | 3.892 | -6.381 | 0 | -32.470 | -17.204 |
| parqueaderos | 72.915 | 3.958 | 18.422 | 0 | 65.153 | 80.676 |
| banios | 50.697 | 3.396 | 14.927 | 0 | 44.037 | 57.357 |
kable(glance(modelo2)[, c("r.squared", "adj.r.squared", "sigma", "statistic", "p.value")],
digits = 3, caption = "Bondad de ajuste - modelo lineal")
| r.squared | adj.r.squared | sigma | statistic | p.value |
|---|---|---|---|---|
| 0.749 | 0.748 | 98.019 | 1413.802 | 0 |
Interpretación de los coeficientes (modelo lineal):
kable(tidy(coeftest(modelo2, vcov = vcovHC(modelo2, type = "HC3"))), digits = 4,
caption = "Coeficientes con errores estándar robustos (HC3) - modelo lineal, base2")
| term | estimate | std.error | statistic | p.value |
|---|---|---|---|---|
| (Intercept) | -261.6250 | 23.3820 | -11.1891 | 0.0000 |
| areaconst | 1.2850 | 0.3987 | 3.2230 | 0.0013 |
| estrato | 60.8971 | 3.9981 | 15.2314 | 0.0000 |
| habitaciones | -24.8369 | 6.9558 | -3.5707 | 0.0004 |
| parqueaderos | 72.9147 | 15.1745 | 4.8051 | 0.0000 |
| banios | 50.6967 | 9.1456 | 5.5433 | 0.0000 |
A diferencia de base1, bajo errores robustos todas las
variables se mantienen altamente significativas en base2,
lo que sugiere que las conclusiones sobre significancia son más estables
en este segmento, probablemente por el mayor tamaño muestral (n = 2381
vs. n = 435).
kable(tidy(modelo2_log, conf.int = TRUE), digits = 4, caption = "Coeficientes del modelo log-lineal (Apartamentos, Zona Sur)")
| term | estimate | std.error | statistic | p.value | conf.low | conf.high |
|---|---|---|---|---|---|---|
| (Intercept) | 3.7841 | 0.0359 | 105.4917 | 0.000 | 3.7138 | 3.8544 |
| areaconst | 0.0025 | 0.0001 | 20.0743 | 0.000 | 0.0022 | 0.0027 |
| estrato | 0.2352 | 0.0071 | 33.2398 | 0.000 | 0.2214 | 0.2491 |
| habitaciones | -0.0187 | 0.0089 | -2.0983 | 0.036 | -0.0363 | -0.0012 |
| parqueaderos | 0.1460 | 0.0091 | 16.0703 | 0.000 | 0.1281 | 0.1638 |
| banios | 0.1256 | 0.0078 | 16.1122 | 0.000 | 0.1103 | 0.1409 |
cat("R² modelo lineal: ", round(summary(modelo2)$r.squared, 3), "\n")
## R² modelo lineal: 0.749
cat("R² modelo log-lineal: ", round(summary(modelo2_log)$r.squared, 3), "\n")
## R² modelo log-lineal: 0.788
Ajuste general: el modelo lineal alcanza un R² ajustado de aproximadamente 0.75, notablemente superior al de las casas de la Zona Norte, lo que sugiere que en apartamentos las características estructurales explican mejor el precio (posiblemente porque los edificios son más homogéneos que las casas, donde factores idiosincrásicos del lote pesan más). El modelo log-lineal mejora aún más el ajuste dentro de muestra; se evalúa en el Paso 4 si esta mejora se traduce en mejor capacidad predictiva.
par(mfrow = c(2, 2))
plot(modelo2)
par(mfrow = c(1, 1))
shapiro_p2 <- shapiro.test(resid(modelo2))$p.value
bp_p2 <- bptest(modelo2)$p.value
dw2 <- dwtest(modelo2)
vif_vals2 <- vif(modelo2)
shapiro_p2_log <- shapiro.test(resid(modelo2_log))$p.value
bp_p2_log <- bptest(modelo2_log)$p.value
kable(data.frame(Prueba = c("Shapiro-Wilk (normalidad)", "Breusch-Pagan (homocedasticidad)",
"Durbin-Watson (independencia temporal/orden)"),
Lineal = c(format(shapiro_p2, digits = 3), format(bp_p2, digits = 3), round(dw2$p.value, 4)),
`Log-lineal` = c(format(shapiro_p2_log, digits = 3), format(bp_p2_log, digits = 3), "—")),
caption = "Pruebas de supuestos - H0 = se cumple el supuesto")
| Prueba | Lineal | Log.lineal |
|---|---|---|
| Shapiro-Wilk (normalidad) | 4.84e-48 | 4.23e-21 |
| Breusch-Pagan (homocedasticidad) | 6.91e-161 | 3.91e-147 |
| Durbin-Watson (independencia temporal/orden) | 0 | — |
kable(data.frame(Variable = names(vif_vals2), VIF = round(vif_vals2, 2)),
caption = "Factores de inflación de varianza (VIF) - modelo lineal")
| Variable | VIF | |
|---|---|---|
| areaconst | areaconst | 2.07 |
| estrato | estrato | 1.55 |
| habitaciones | habitaciones | 1.43 |
| parqueaderos | parqueaderos | 1.74 |
| banios | banios | 2.53 |
set.seed(2026)
idx2 <- sample(seq_len(nrow(base2)), floor(0.8 * nrow(base2)))
train2 <- base2[idx2, ]; test2 <- base2[-idx2, ]
m2_lin_tr <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = train2)
m2_log_tr <- lm(log(preciom) ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = train2)
pred2_lin <- predict(m2_lin_tr, newdata = test2)
smear2 <- mean(exp(resid(m2_log_tr)))
pred2_log <- exp(predict(m2_log_tr, newdata = test2)) * smear2
kable(data.frame(Modelo = c("Lineal", "Log-lineal (con corrección de smearing)"),
RMSE = c(rmse(test2$preciom, pred2_lin), rmse(test2$preciom, pred2_log)),
MAE = c(mae(test2$preciom, pred2_lin), mae(test2$preciom, pred2_log))),
digits = 1, caption = "Error de predicción fuera de muestra (millones $) - base2")
| Modelo | RMSE | MAE |
|---|---|---|
| Lineal | 93.1 | 59.9 |
| Log-lineal (con corrección de smearing) | 84.9 | 54.1 |
Interpretación integrada:
base1 y base2
(tablas anteriores), la desviación respecto a 2 es más marcada en
apartamentos, lo que sugiere una estructura residual más fuerte,
coherente con la idea de submercados dentro de la Zona Sur (p. ej.,
cercanía a Pance vs. sectores más consolidados) que las variables
estructurales no capturan del todo. Al igual que en base1,
no se dispone de una prueba espacial formal (I de Moran) en esta versión
por limitaciones de instalación de spdep, pero es la
limitación más relevante a validar en una siguiente iteración, junto con
modelos espaciales (SAR/SEM, disponibles en
spatialreg).base1 y refuerza la importancia de evaluar cada segmento de
forma independiente en lugar de asumir que una misma especificación es
óptima para todo el mercado.La solicitud pide un apartamento de 300 m², 3 parqueaderos, 3 baños, 5 habitaciones, estrato 5 o 6, en la Zona Sur. Se predice con el modelo final (log-lineal, corregido por smearing).
nueva_vivienda2 <- data.frame(areaconst = 300, estrato = c(5, 6),
habitaciones = 5, parqueaderos = 3, banios = 3)
pred2_log_ic <- predict(modelo2_log, newdata = nueva_vivienda2, interval = "confidence")
smear_final2 <- mean(exp(resid(modelo2_log)))
pred2 <- cbind(nueva_vivienda2,
fit = exp(pred2_log_ic[, "fit"]) * smear_final2,
lwr = exp(pred2_log_ic[, "lwr"]) * smear_final2,
upr = exp(pred2_log_ic[, "upr"]) * smear_final2)
kable(pred2, digits = 1, caption = "Precio estimado para la vivienda solicitada (Solicitud 2, modelo log-lineal + smearing)")
| areaconst | estrato | habitaciones | parqueaderos | banios | fit | lwr | upr |
|---|---|---|---|---|---|---|---|
| 300 | 5 | 5 | 3 | 3 | 634.2 | 601.7 | 668.3 |
| 300 | 6 | 5 | 3 | 3 | 802.3 | 760.7 | 846.2 |
Interpretación: para estrato 5, el modelo estima un precio de mercado de aproximadamente $634 millones (IC 95% aproximado: 602–668), y para estrato 6, aproximadamente $802 millones (IC 95% aproximado: 761–846). El intervalo se obtiene exponenciando el intervalo en escala logarítmica y corrigiendo por el factor de smearing; es una aproximación razonable pero, al no ser una transformación lineal, no conserva exactamente el nivel de confianza nominal del 95%. Ambos escenarios están dentro del crédito pre-aprobado de $850 millones, con un margen considerable, lo que da flexibilidad para buscar opciones con mejores acabados o ubicación dentro de la Zona Sur.
ofertas2 <- base2 %>%
filter(preciom <= 850, estrato %in% c(5, 6), areaconst >= 200, habitaciones >= 4) %>%
arrange(desc(preciom)) %>%
head(6)
kable(ofertas2 %>% select(barrio, preciom, areaconst, estrato, habitaciones, banios, parqueaderos),
caption = "Ofertas potenciales dentro del presupuesto (Solicitud 2)")
| barrio | preciom | areaconst | estrato | habitaciones | banios | parqueaderos |
|---|---|---|---|---|---|---|
| pance | 750 | 200.00 | 6 | 4 | 5 | 3 |
| guadalupe | 730 | 573.00 | 5 | 5 | 8 | 3 |
| el ingenio | 700 | 250.00 | 6 | 5 | 4 | 2 |
| el ingenio | 690 | 486.00 | 5 | 4 | 4 | 2 |
| seminario | 670 | 300.00 | 5 | 6 | 5 | 3 |
| quintas de don | 660 | 224.41 | 6 | 4 | 5 | 2 |
plot_ly(ofertas2, lat = ~latitud, lon = ~longitud, type = "scattermapbox", mode = "markers",
marker = list(size = 14, color = "#d7301f"),
text = ~paste0("Barrio: ", barrio, "<br>Precio: $", preciom, "M",
"<br>Área: ", areaconst, "m²", "<br>Estrato: ", estrato)) %>%
layout(mapbox = list(style = "open-street-map", zoom = 11.5,
center = list(lat = mean(ofertas2$latitud), lon = mean(ofertas2$longitud))),
title = "Ofertas potenciales para la Solicitud 2 (≤ $850M)")
Discusión: se identifican ofertas en barrios como Pance, Guadalupe, El Ingenio, Seminario y Ciudadela Pasoancho, todas por debajo del presupuesto de $850 millones y con áreas entre 200 y 570 m². Al haber más margen presupuestal que en la Solicitud 1, esta familia tiene mayor variedad de opciones, incluyendo alternativas con áreas considerablemente superiores a los 300 m² solicitados.
comparacion <- data.frame(
Solicitud = c("1: Casas, Zona Norte", "2: Apartamentos, Zona Sur"),
n = c(nrow(base1), nrow(base2)),
Modelo_final = c("Lineal", "Log-lineal + smearing"),
R2_ajustado_ins = c(round(summary(modelo1)$adj.r.squared, 3), round(summary(modelo2_log)$adj.r.squared, 3)),
RMSE_holdout = c(round(rmse(test1$preciom, pred_lin), 1), round(rmse(test2$preciom, pred2_log), 1)),
Durbin_Watson = c(round(dw$statistic, 2), round(dw2$statistic, 2))
)
kable(comparacion, caption = "Comparación de los modelos finales seleccionados por solicitud")
| Solicitud | n | Modelo_final | R2_ajustado_ins | RMSE_holdout | Durbin_Watson |
|---|---|---|---|---|---|
| 1: Casas, Zona Norte | 435 | Lineal | 0.599 | 172.1 | 1.76 |
| 2: Apartamentos, Zona Sur | 2381 | Log-lineal + smearing | 0.787 | 84.9 | 1.53 |
Las dos solicitudes requirieron especificaciones distintas: para
casas en Zona Norte el modelo lineal generalizó mejor fuera de muestra a
pesar de un R² menor, mientras que para apartamentos en Zona Sur la
transformación logarítmica mejoró tanto el ajuste como la predicción. En
ambos casos el estadístico de Durbin-Watson se aleja de 2, de forma más
marcada en apartamentos, lo que sugiere posible dependencia espacial
residual la principal limitación metodológica compartida y el punto de
partida natural para una extensión futura del análisis con modelos de
econometría espacial (idealmente confirmando esta hipótesis con una
prueba formal como la I de Moran una vez se pueda instalar
spdep).
Solicitud 1 (Casa, Zona Norte, crédito $350M): con el modelo lineal (seleccionado por su mejor desempeño predictivo fuera de muestra), el precio de mercado estimado es de ~$312M en estrato 4 (dentro de presupuesto) y ~$393M en estrato 5 (fuera de presupuesto). Se recomienda enfocar la búsqueda en estrato 4, donde se identificaron ofertas reales concretas (p. ej., en El Bosque y La Flora) dentro del crédito aprobado.
Solicitud 2 (Apartamento, Zona Sur, crédito $850M): con el modelo log-lineal corregido por smearing (seleccionado por mejor ajuste y desempeño predictivo), el precio de mercado estimado es de ~$634M (estrato 5) a ~$802M (estrato 6). El escenario de estrato 5 deja un margen amplio frente al presupuesto; el de estrato 6 sigue siendo viable, pero con un margen más ajustado (~$48M) que el que sugería una estimación lineal simple. Se identificaron ofertas reales dentro de presupuesto en barrios como Pance, El Ingenio y Ciudadela Pasoancho.
Limitaciones y trabajo futuro: ambos modelos dejan una fracción relevante de la variabilidad del precio sin explicar y muestran autocorrelación espacial en los residuos, lo que indica que el submercado geográfico (barrio, cercanía a vías o centros comerciales) importa más allá de lo que capturan las variables estructurales disponibles. Se recomienda a C&A, para una siguiente iteración del ejercicio, (i) incorporar variables adicionales como antigüedad, calidad de acabados o distancia a puntos de interés, y (ii) explorar modelos de regresión espacial (rezago o error espacial) que incorporen explícitamente la dependencia entre viviendas vecinas.