Este documento resume el análisis de modelación estadística realizado para asesorar a la compañía internacional en la compra de dos viviendas en Cali para dos de sus empleados:
Los datos provienen del paquete paqueteMODELOS (conjunto
vivienda), con 8.322 registros de ofertas
inmobiliarias de los últimos tres meses en Cali. El documento tiene dos
partes: un Resumen Ejecutivo (hallazgos y
recomendaciones) y los Anexos técnicos (código,
estimaciones y validaciones completas que soportan el informe).
data("vivienda")
vivienda_raw <- vivienda
cat("Dimensión original:", nrow(vivienda_raw), "registros x",
ncol(vivienda_raw), "variables\n")
## Dimensión original: 8322 registros x 13 variables
cat("\nValores faltantes por variable:\n")
##
## Valores faltantes por variable:
print(colSums(is.na(vivienda_raw)))
## id zona piso estrato preciom areaconst
## 3 3 2638 3 2 3
## parqueaderos banios habitaciones tipo barrio longitud
## 1605 3 3 3 3 3
## latitud
## 3
Se eliminan los pocos registros (3 en total) con información faltante
en variables clave (zona, tipo,
estrato, preciom, coordenadas). La variable
piso tiene un alto porcentaje de faltantes (32%) y
no se usa en el modelo. La variable
parqueaderos (~19% de faltantes) se imputa como
0 cuando no se reporta el dato, supuesto razonable en
un mercado donde muchos apartamentos pequeños no ofrecen parqueadero; se
documenta como limitación.
vivienda <- vivienda_raw %>%
filter(!is.na(zona), !is.na(tipo), !is.na(estrato), !is.na(preciom),
!is.na(areaconst), !is.na(banios), !is.na(habitaciones),
!is.na(latitud), !is.na(longitud)) %>%
mutate(parqueaderos = ifelse(is.na(parqueaderos), 0, parqueaderos))
cat("Registros finales para el análisis:", nrow(vivienda), "\n")
## Registros finales para el análisis: 8319
Se construyen los subconjuntos, modelos y predicciones que se usan tanto en el resumen ejecutivo como en los anexos, para que ambas secciones sean 100% consistentes entre sí.
# --- Vivienda 1: Casas, Zona Norte ---
base1 <- vivienda %>% filter(tipo == "Casa", zona == "Zona Norte")
modelo1 <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = base1)
r2_1 <- summary(modelo1)$r.squared
ar2_1 <- summary(modelo1)$adj.r.squared
nuevo1 <- data.frame(areaconst = 200, estrato = c(4,5), habitaciones = 4, parqueaderos = 1, banios = 2)
pred1 <- predict(modelo1, newdata = nuevo1, interval = "prediction", level = 0.95)
ofertas1 <- base1 %>%
filter(preciom <= 350, estrato %in% c(4,5), parqueaderos >= 1, banios >= 2, habitaciones >= 3) %>%
mutate(dist_area = abs(areaconst - 200)) %>%
arrange(dist_area, desc(preciom)) %>% head(5)
# --- Vivienda 2: Apartamentos, Zona Sur ---
base2 <- vivienda %>% filter(tipo == "Apartamento", zona == "Zona Sur")
modelo2 <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = base2)
r2_2 <- summary(modelo2)$r.squared
ar2_2 <- summary(modelo2)$adj.r.squared
nuevo2 <- data.frame(areaconst = 300, estrato = c(5,6), habitaciones = 5, parqueaderos = 3, banios = 3)
pred2 <- predict(modelo2, newdata = nuevo2, interval = "prediction", level = 0.95)
ofertas2 <- base2 %>%
filter(preciom <= 850, estrato %in% c(5,6), parqueaderos >= 2, banios >= 3, habitaciones >= 4) %>%
mutate(dist_area = abs(areaconst - 300)) %>%
arrange(dist_area, desc(preciom)) %>% head(5)
# Geo-clasificación (para discutir puntos "atípicos" en el Paso 1 de cada vivienda)
sur_q75 <- quantile(vivienda$latitud[vivienda$zona == "Zona Sur"], 0.75)
norte_q25 <- quantile(vivienda$latitud[vivienda$zona == "Zona Norte"], 0.25)
base1_sosp <- base1 %>% mutate(sospechoso = latitud < sur_q75)
base2_sosp <- base2 %>% mutate(sospechoso = latitud > norte_q25)
precio_est_1 <- round(mean(pred1[,"fit"]))
precio_est_2 <- round(mean(pred2[,"fit"]))
pal <- colorFactor(c("#2c7fb8", "#e34a33"), domain = c(FALSE, TRUE))
cat("Vivienda 1 -> n =", nrow(base1), " R2 =", round(r2_1,3), " Precio estimado ~", precio_est_1, "M\n")
## Vivienda 1 -> n = 722 R2 = 0.651 Precio estimado ~ 376 M
cat("Vivienda 2 -> n =", nrow(base2), " R2 =", round(r2_2,3), " Precio estimado ~", precio_est_2, "M\n")
## Vivienda 2 -> n = 2787 R2 = 0.754 Precio estimado ~ 708 M
resumen <- data.frame(
Indicador = c("Ofertas analizadas (n)", "Ajuste del modelo (R²)",
"Precio estimado para las características solicitadas",
"Crédito preaprobado", "Diferencia vs. presupuesto", "Conclusión"),
Vivienda_1 = c(as.character(nrow(base1)), paste0(round(100*r2_1,1),"%"),
paste0("$", precio_est_1, " millones"), "$350 millones",
paste0("$", precio_est_1-350, " millones (", ifelse(precio_est_1>350,"por encima","por debajo"), ")"),
ifelse(precio_est_1>350, "Ajustar expectativas o negociar", "Presupuesto suficiente")),
Vivienda_2 = c(as.character(nrow(base2)), paste0(round(100*r2_2,1),"%"),
paste0("$", precio_est_2, " millones"), "$850 millones",
paste0("$", precio_est_2-850, " millones (", ifelse(precio_est_2>850,"por encima","por debajo"), ")"),
ifelse(precio_est_2>850, "Ajustar expectativas o negociar", "Presupuesto suficiente, hay margen"))
)
kable(resumen, col.names = c("Indicador", "Vivienda 1 (Casa, Norte)", "Vivienda 2 (Apto, Sur)"), align = "lcc") %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = TRUE)
| Indicador | Vivienda 1 (Casa, Norte) | Vivienda 2 (Apto, Sur) |
|---|---|---|
| Ofertas analizadas (n) | 722 | 2787 |
| Ajuste del modelo (R²) | 65.1% | 75.4% |
| Precio estimado para las características solicitadas | $376 millones | $708 millones |
| Crédito preaprobado | $350 millones | $850 millones |
| Diferencia vs. presupuesto | $26 millones (por encima) | $-142 millones (por debajo) |
| Conclusión | Ajustar expectativas o negociar | Presupuesto suficiente, hay margen |
Conclusión principal: para la Vivienda 1, el precio de mercado esperado (≈ $376 millones) supera el crédito preaprobado de $350 millones. Para la Vivienda 2, el precio esperado (≈ $708 millones) está cómodamente dentro de el presupuesto de $850 millones, dando margen para negociar mejores condiciones.
ggplot(base1_sosp, aes(longitud, latitud, color = sospechoso)) +
geom_point(alpha = 0.6, size = 1.3) +
scale_color_manual(values = c("FALSE"="#2c7fb8","TRUE"="#e34a33"),
labels = c("Zona Norte típica","Latitud atípica (parece Sur)")) +
labs(title = "Ubicación de casas ofertadas — Zona Norte", x = "Longitud", y = "Latitud", color = "") +
theme(legend.position = "bottom")
Se analizaron 722 casas en zona Norte. El 9.4% de los puntos etiquetados como “Zona Norte” tiene una latitud más propia de la Zona Sur — posible error de geo-referenciación que vale la pena auditar con el equipo comercial.
El modelo
(precio = f(área, estrato, habitaciones, parqueaderos, baños))
logra un R² de 65.1%. El área construida (+$0.83M/m²) y
el estrato (+$86.4M/estrato) son los factores más determinantes y son
estadísticamente significativos; habitaciones y parqueaderos no lo son
una vez controlado por el área.
tp1 <- cbind(Estrato = nuevo1$estrato, round(pred1,0))
colnames(tp1) <- c("Estrato solicitado","Precio estimado","Límite inferior","Límite superior")
kable(tp1, row.names = FALSE, caption = "Predicción de precio (millones $) — Vivienda 1") %>%
kable_styling(bootstrap_options = c("striped","condensed"), full_width = FALSE)
| Estrato solicitado | Precio estimado | Límite inferior | Límite superior |
|---|---|---|---|
| 4 | 333 | 20 | 646 |
| 5 | 419 | 106 | 732 |
Para 200 m², 1 parqueadero, 2 baños y 4 habitaciones, el modelo estima $333 a $419 millones según estrato — por encima de los $350 millones disponibles.
5 ofertas potenciales dentro del presupuesto:
tof1 <- ofertas1 %>% select(barrio, estrato, areaconst, parqueaderos, banios, habitaciones, preciom)
names(tof1) <- c("Barrio","Estrato","Área (m²)","Parqueaderos","Baños","Habitaciones","Precio ($M)")
kable(tof1, row.names = FALSE) %>% kable_styling(bootstrap_options = c("striped","condensed"), full_width = TRUE)
| Barrio | Estrato | Área (m²) | Parqueaderos | Baños | Habitaciones | Precio ($M) |
|---|---|---|---|---|---|---|
| el bosque | 5 | 200 | 3 | 3 | 4 | 350 |
| la flora | 5 | 200 | 2 | 4 | 4 | 320 |
| la merced | 4 | 200 | 2 | 4 | 4 | 320 |
| el bosque | 5 | 202 | 1 | 4 | 5 | 335 |
| el bosque | 5 | 203 | 2 | 2 | 5 | 350 |
Recomendación: presentar estas 5 alternativas al cliente. Si el área exacta de 200 m² es innegociable, considerar ampliar el presupuesto o negociar precio con el vendedor.
ggplot(base2_sosp, aes(longitud, latitud, color = sospechoso)) +
geom_point(alpha = 0.5, size = 1) +
scale_color_manual(values = c("FALSE"="#08519c","TRUE"="#e34a33"),
labels = c("Zona Sur típica","Latitud atípica (parece Norte)")) +
labs(title = "Ubicación de apartamentos ofertados — Zona Sur", x = "Longitud", y = "Latitud", color = "") +
theme(legend.position = "bottom")
Se analizaron 2787 apartamentos en zona Sur (el segmento con mayor oferta de la ciudad). El modelo logra un R² de 75.4%, superior al de casas, por la mayor homogeneidad de los apartamentos.
Hallazgo relevante: el coeficiente de habitaciones es negativo (-$22.7M por habitación adicional, significativo): a igual área, subdividir en más cuartos (más pequeños) reduce el valor percibido.
tp2 <- cbind(Estrato = nuevo2$estrato, round(pred2,0))
colnames(tp2) <- c("Estrato solicitado","Precio estimado","Límite inferior","Límite superior")
kable(tp2, row.names = FALSE, caption = "Predicción de precio (millones $) — Vivienda 2") %>%
kable_styling(bootstrap_options = c("striped","condensed"), full_width = FALSE)
| Estrato solicitado | Precio estimado | Límite inferior | Límite superior |
|---|---|---|---|
| 5 | 679 | 492 | 867 |
| 6 | 737 | 549 | 924 |
Para 300 m², 3 parqueaderos, 3 baños y 5 habitaciones, el modelo estima $679 a $737 millones — dentro de los $850 millones disponibles.
5 ofertas potenciales dentro del presupuesto:
tof2 <- ofertas2 %>% select(barrio, estrato, areaconst, parqueaderos, banios, habitaciones, preciom)
names(tof2) <- c("Barrio","Estrato","Área (m²)","Parqueaderos","Baños","Habitaciones","Precio ($M)")
kable(tof2, row.names = FALSE) %>% kable_styling(bootstrap_options = c("striped","condensed"), full_width = TRUE)
| Barrio | Estrato | Área (m²) | Parqueaderos | Baños | Habitaciones | Precio ($M) |
|---|---|---|---|---|---|---|
| seminario | 5 | 300.00 | 3 | 5 | 6 | 670 |
| cuarto de legua | 5 | 295.55 | 2 | 4 | 4 | 410 |
| cuarto de legua | 5 | 320.00 | 2 | 4 | 4 | 520 |
| ciudadela pasoancho | 5 | 275.00 | 2 | 5 | 5 | 650 |
| san fernando | 5 | 330.00 | 2 | 4 | 4 | 500 |
Recomendación: como el presupuesto es holgado, destacar al cliente las opciones con mejores atributos (mayor área, más baños/parqueaderos, mejor ubicación) en vez de limitarse a la más económica.
log(precio) y
errores robustos como siguiente paso.Esta sección contiene el detalle completo: filtros, análisis
exploratorio interactivo (plotly), mapas interactivos
(leaflet), estimación y validación de los modelos, y la
construcción de las ofertas recomendadas. El código está disponible
desplegando cada bloque (“Code”).
datatable(head(base1, 3), options = list(dom = 't', scrollX = TRUE),
caption = "Primeros 3 registros de base1 (Casas, Zona Norte)")
cat("Comprobación de filtro:\n")
## Comprobación de filtro:
print(table(base1$tipo)); print(table(base1$zona))
##
## Casa
## 722
##
## Zona Norte
## 722
cat("\nDistribución por estrato:\n"); print(table(base1$estrato))
##
## Distribución por estrato:
##
## 3 4 5 6
## 235 161 271 55
leaflet(base1_sosp) %>%
addProviderTiles(providers$CartoDB.Positron) %>%
addCircleMarkers(lng = ~longitud, lat = ~latitud, radius = 4, weight = 1,
color = ~pal(sospechoso), fillOpacity = 0.7,
popup = ~paste0("Barrio: ", barrio, "<br>Precio: $", preciom, "M<br>Área: ", areaconst, " m²")) %>%
addLegend("bottomright", pal = pal, values = ~sospechoso,
labels = c("Zona Norte típica", "Zona Norte (latitud atípica)"), title = "Clasificación geográfica") %>%
setView(lng = mean(base1$longitud), lat = mean(base1$latitud), zoom = 12)
Discusión. Aproximadamente un 9.4%
de los registros etiquetados “Zona Norte” tiene latitud típica de “Zona
Sur”. Puede deberse a errores de captura/asignación manual de zona,
barrios de transición en el límite administrativo, o coordenadas mal
geo-referenciadas. Se conserva la clasificación original de
zona (variable de negocio de la inmobiliaria), pero se
recomienda auditar el proceso.
M <- cor(base1 %>% select(preciom, areaconst, estrato, banios, habitaciones), use = "complete.obs")
corrplot(M, method = "color", type = "upper", addCoef.col = "black", tl.col = "black", tl.srt = 45,
number.cex = 0.8, title = "Matriz de correlación - base1", mar = c(0,0,2,0))
ggplotly(ggplot(base1, aes(areaconst, preciom, text = paste0("Barrio: ", barrio))) +
geom_point(alpha = 0.5, color = "#2c7fb8") + geom_smooth(method = "lm", se = FALSE, color = "darkred") +
labs(title = "Precio vs. Área construida", x = "Área construida (m²)", y = "Precio (millones $)"),
tooltip = c("x","y","text"))
ggplotly(ggplot(base1, aes(factor(estrato), preciom, fill = factor(estrato))) + geom_boxplot(alpha = 0.7) +
labs(title = "Precio por estrato", x = "Estrato", y = "Precio (millones $)") + theme(legend.position = "none"))
ggplotly(ggplot(base1, aes(factor(habitaciones), preciom)) + geom_boxplot(fill = "#74c476", alpha = 0.7) +
labs(title = "Precio por número de habitaciones", x = "Habitaciones", y = "Precio (millones $)"))
ggplotly(ggplot(base1, aes(factor(banios), preciom)) + geom_boxplot(fill = "#fd8d3c", alpha = 0.7) +
labs(title = "Precio por número de baños", x = "Baños", y = "Precio (millones $)"))
Interpretación. La correlación más fuerte con el precio la presenta el área construida (r = 0.73), seguida del estrato (r = 0.61), baños (r = 0.52) y habitaciones (r = 0.32). Todas positivas y de magnitud moderada-alta, coherente con la intuición del mercado. También hay correlación entre predictores, lo que anticipa multicolinealidad moderada (revisada en el Paso 4).
summary(modelo1)
##
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos +
## banios, data = base1)
##
## Residuals:
## Min 1Q Median 3Q Max
## -964.04 -80.10 -17.08 50.06 1069.45
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -236.47551 30.36582 -7.788 0.000000000000024 ***
## areaconst 0.82677 0.04368 18.926 < 0.0000000000000002 ***
## estrato 86.42579 7.39747 11.683 < 0.0000000000000002 ***
## habitaciones 1.44443 4.16411 0.347 0.729
## parqueaderos -1.67672 4.31505 -0.389 0.698
## banios 26.97978 5.34384 5.049 0.000000564653292 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 159.1 on 716 degrees of freedom
## Multiple R-squared: 0.6508, Adjusted R-squared: 0.6484
## F-statistic: 266.9 on 5 and 716 DF, p-value: < 0.00000000000000022
datatable(tidy(modelo1) %>% mutate(across(where(is.numeric), ~round(.,4))),
options = list(dom = 't'), caption = "Coeficientes del modelo - Vivienda 1")
Interpretación de los coeficientes (demás variables constantes):
Ajuste (R²). El modelo explica 65.1% de la variabilidad del precio (R² ajustado = 64.8%): ajuste moderado. El resto probablemente responde a barrio específico, estado/antigüedad, acabados y cercanía a vías/comercio — variables no incluidas en la base. Para mejorar el modelo: incorporar barrio o efectos espaciales, transformaciones no lineales (log-precio, área²), variables de acabados/antigüedad si están disponibles, y comparar contra modelos no paramétricos (árboles, random forest).
par(mfrow = c(2,2)); plot(modelo1); par(mfrow = c(1,1))
cat("--- Breusch-Pagan (homocedasticidad) ---\n"); print(bptest(modelo1))
## --- Breusch-Pagan (homocedasticidad) ---
##
## studentized Breusch-Pagan test
##
## data: modelo1
## BP = 139, df = 5, p-value < 0.00000000000000022
cat("\n--- Shapiro-Wilk (normalidad, muestra 500) ---\n")
##
## --- Shapiro-Wilk (normalidad, muestra 500) ---
set.seed(1); print(shapiro.test(sample(residuals(modelo1), min(500, nrow(base1)))))
##
## Shapiro-Wilk normality test
##
## data: sample(residuals(modelo1), min(500, nrow(base1)))
## W = 0.82703, p-value < 0.00000000000000022
cat("\n--- Durbin-Watson (independencia) ---\n"); print(dwtest(modelo1))
##
## --- Durbin-Watson (independencia) ---
##
## Durbin-Watson test
##
## data: modelo1
## DW = 1.633, p-value = 0.0000003068
## alternative hypothesis: true autocorrelation is greater than 0
cat("\n--- VIF (multicolinealidad) ---\n"); print(vif(modelo1))
##
## --- VIF (multicolinealidad) ---
## areaconst estrato habitaciones parqueaderos banios
## 1.518244 1.504946 1.649049 1.234902 1.887923
Interpretación: (1) Linealidad: cierta curvatura en residuales vs. ajustados. (2) Homocedasticidad: Breusch-Pagan significativo → heterocedasticidad, dispersión crece con el precio. (3) Normalidad: colas pesadas, sobre todo en precios altos. (4) Independencia: Durbin-Watson cercano a 2, sin autocorrelación relevante. (5) Multicolinealidad: VIF moderados, sin problema severo.
Sugerencias (sin corregir): transformar
log(preciom) para estabilizar varianza y mejorar
normalidad; considerar un índice combinado de “tamaño”; usar errores
estándar robustos (HC) para inferencia válida.
datatable(cbind(nuevo1, round(pred1,1)), options = list(dom = 't'),
caption = "Predicción de precio - Vivienda 1 (millones $)")
Con crédito de $350M, el precio estimado (376M en promedio) queda por encima del presupuesto.
datatable(ofertas1 %>% select(barrio, estrato, areaconst, parqueaderos, banios, habitaciones, preciom),
options = list(dom = 't'), caption = "5 mejores ofertas potenciales - Vivienda 1")
leaflet(ofertas1) %>%
addProviderTiles(providers$CartoDB.Positron) %>%
addCircleMarkers(lng = ~longitud, lat = ~latitud, radius = 7, color = "#238b45", fillOpacity = 0.85,
popup = ~paste0("<b>", barrio, "</b><br>Precio: $", preciom, "M<br>Área: ", areaconst,
" m²<br>Estrato: ", estrato, "<br>Habitaciones: ", habitaciones,
"<br>Baños: ", banios, "<br>Parqueaderos: ", parqueaderos)) %>%
setView(lng = mean(ofertas1$longitud), lat = mean(ofertas1$latitud), zoom = 12)
Discusión. 5 ofertas reales cumplen o superan los mínimos dentro de presupuesto. Como el modelo predice un precio superior al presupuesto para 200 m² exactos, las ofertas encontradas tienden a área algo menor o estrato 4. Se recomienda presentar estas alternativas y/o negociar precio o presupuesto.
datatable(head(base2, 3), options = list(dom = 't', scrollX = TRUE),
caption = "Primeros 3 registros de base2 (Apartamentos, Zona Sur)")
print(table(base2$tipo)); print(table(base2$zona)); print(table(base2$estrato))
##
## Apartamento
## 2787
##
## Zona Sur
## 2787
##
## 3 4 5 6
## 201 1091 1033 462
leaflet(base2_sosp) %>%
addProviderTiles(providers$CartoDB.Positron) %>%
addCircleMarkers(lng = ~longitud, lat = ~latitud, radius = 4, weight = 1,
color = ~pal(sospechoso), fillOpacity = 0.7,
popup = ~paste0("Barrio: ", barrio, "<br>Precio: $", preciom, "M<br>Área: ", areaconst, " m²")) %>%
addLegend("bottomright", pal = pal, values = ~sospechoso,
labels = c("Zona Sur típica", "Zona Sur (latitud atípica)"), title = "Clasificación geográfica") %>%
setView(lng = mean(base2$longitud), lat = mean(base2$latitud), zoom = 11)
Discusión. Solo el 2.4% de los puntos “Zona Sur” cae en rango típico de “Zona Norte” — menor inconsistencia que en Vivienda 1, lo que sugiere que la calidad de geo-referenciación no es homogénea entre zonas.
M2 <- cor(base2 %>% select(preciom, areaconst, estrato, banios, habitaciones), use = "complete.obs")
corrplot(M2, method = "color", type = "upper", addCoef.col = "black", tl.col = "black", tl.srt = 45,
number.cex = 0.8, title = "Matriz de correlación - base2", mar = c(0,0,2,0))
ggplotly(ggplot(base2, aes(areaconst, preciom, text = paste0("Barrio: ", barrio))) +
geom_point(alpha = 0.4, color = "#e6550d") + geom_smooth(method = "lm", se = FALSE, color = "darkblue") +
labs(title = "Precio vs. Área construida", x = "Área construida (m²)", y = "Precio (millones $)"),
tooltip = c("x","y","text"))
ggplotly(ggplot(base2, aes(factor(estrato), preciom, fill = factor(estrato))) + geom_boxplot(alpha = 0.7) +
labs(title = "Precio por estrato", x = "Estrato", y = "Precio (millones $)") + theme(legend.position = "none"))
ggplotly(ggplot(base2, aes(factor(habitaciones), preciom)) + geom_boxplot(fill = "#74c476", alpha = 0.7) +
labs(title = "Precio por número de habitaciones", x = "Habitaciones", y = "Precio (millones $)"))
Interpretación. El área construida tiene la correlación más fuerte con el precio (r = 0.76), seguida del estrato (r = 0.67). El rango de precios es más amplio que en casas (estratos 3 a 6), con mayor dispersión en estratos altos.
summary(modelo2)
##
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos +
## banios, data = base2)
##
## Residuals:
## Min 1Q Median 3Q Max
## -1252.31 -42.15 -2.06 36.32 934.06
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -221.04614 13.47771 -16.401 < 0.0000000000000002 ***
## areaconst 1.46061 0.04876 29.956 < 0.0000000000000002 ***
## estrato 57.00608 2.79648 20.385 < 0.0000000000000002 ***
## habitaciones -22.71789 3.39549 -6.691 0.0000000000268 ***
## parqueaderos 48.36353 3.02343 15.996 < 0.0000000000000002 ***
## banios 48.60871 3.04050 15.987 < 0.0000000000000002 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 95.17 on 2781 degrees of freedom
## Multiple R-squared: 0.7536, Adjusted R-squared: 0.7531
## F-statistic: 1701 on 5 and 2781 DF, p-value: < 0.00000000000000022
Interpretación. El modelo explica 75.4% de la variabilidad del precio (R² ajustado = 75.3%), superior al de casas, porque los apartamentos son más homogéneos y suelen agruparse en conjuntos con precios por m² más estandarizados. Área (1.46M/m²) y estrato (57.01M/estrato) son positivos y muy significativos. Parqueaderos y baños adicionales también se asocian con incrementos significativos (48.36 y 48.61 millones respectivamente).
Resultado destacable: habitaciones tiene coeficiente negativo y significativo (-22.72M por habitación, p < 0.001). No implica que “menos habitaciones” sea mejor en general — para un área fija, subdividir en más cuartos implica alcobas más pequeñas, que el mercado valora menos que pocas alcobas amplias. Es un patrón conocido en precios hedónicos de apartamentos y resulta lógico interpretado junto al área construida.
par(mfrow = c(2,2)); plot(modelo2); par(mfrow = c(1,1))
cat("--- Breusch-Pagan ---\n"); print(bptest(modelo2))
## --- Breusch-Pagan ---
##
## studentized Breusch-Pagan test
##
## data: modelo2
## BP = 956.83, df = 5, p-value < 0.00000000000000022
cat("\n--- Shapiro-Wilk (muestra 500) ---\n")
##
## --- Shapiro-Wilk (muestra 500) ---
set.seed(1); print(shapiro.test(sample(residuals(modelo2), min(500, nrow(base2)))))
##
## Shapiro-Wilk normality test
##
## data: sample(residuals(modelo2), min(500, nrow(base2)))
## W = 0.71981, p-value < 0.00000000000000022
cat("\n--- Durbin-Watson ---\n"); print(dwtest(modelo2))
##
## --- Durbin-Watson ---
##
## Durbin-Watson test
##
## data: modelo2
## DW = 1.5041, p-value < 0.00000000000000022
## alternative hypothesis: true autocorrelation is greater than 0
cat("\n--- VIF ---\n"); print(vif(modelo2))
##
## --- VIF ---
## areaconst estrato habitaciones parqueaderos banios
## 2.020543 1.705639 1.418810 1.776016 2.482470
Interpretación. Igual que en Vivienda 1: heterocedasticidad y colas no normales (más marcadas por el mayor rango de precios de zona Sur). VIF sin multicolinealidad severa. Aplican las mismas sugerencias (log-transformación, errores robustos).
datatable(cbind(nuevo2, round(pred2,1)), options = list(dom = 't'),
caption = "Predicción de precio - Vivienda 2 (millones $)")
Con crédito de $850M, el precio estimado (708M en promedio) queda dentro del presupuesto, con margen para negociar o mejorar acabados/ubicación.
datatable(ofertas2 %>% select(barrio, estrato, areaconst, parqueaderos, banios, habitaciones, preciom),
options = list(dom = 't'), caption = "5 mejores ofertas potenciales - Vivienda 2")
leaflet(ofertas2) %>%
addProviderTiles(providers$CartoDB.Positron) %>%
addCircleMarkers(lng = ~longitud, lat = ~latitud, radius = 7, color = "#08519c", fillOpacity = 0.85,
popup = ~paste0("<b>", barrio, "</b><br>Precio: $", preciom, "M<br>Área: ", areaconst,
" m²<br>Estrato: ", estrato, "<br>Habitaciones: ", habitaciones,
"<br>Baños: ", banios, "<br>Parqueaderos: ", parqueaderos)) %>%
setView(lng = mean(ofertas2$longitud), lat = mean(ofertas2$latitud), zoom = 12)
Discusión. 5 ofertas cumplen o superan los mínimos dentro de presupuesto. Con presupuesto holgado frente al precio estimado, las opciones encontradas se acercan o superan el área solicitada, dando margen para ofrecer alternativas de mayor calidad.
comp <- data.frame(
Vivienda = c("1 (Casa, Zona Norte)", "2 (Apartamento, Zona Sur)"),
N_obs = c(nrow(base1), nrow(base2)),
R2 = round(c(r2_1, r2_2), 3),
R2_ajustado = round(c(ar2_1, ar2_2), 3),
Precio_estimado_M = round(c(mean(pred1[,"fit"]), mean(pred2[,"fit"])), 0),
Presupuesto_M = c(350, 850),
Diferencia_M = round(c(350 - mean(pred1[,"fit"]), 850 - mean(pred2[,"fit"])), 0)
)
datatable(comp, options = list(dom = 't'), caption = "Comparación de modelos y resultados")