Contexto

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:

  • Vivienda 1: Casa en zona Norte, 200 m², 1 parqueadero, 2 baños, 4 habitaciones, estrato 4 o 5 — crédito preaprobado $350 millones.
  • Vivienda 2: Apartamento en zona Sur, 300 m², 3 parqueaderos, 3 baños, 5 habitaciones, estrato 5 o 6 — crédito preaprobado $850 millones.

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

Cálculos base (compartidos por ambas partes del documento)

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

PARTE A — Resumen Ejecutivo

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.

Vivienda 1 — Casa, Zona Norte

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)
Predicción de precio (millones $) — Vivienda 1
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.

Vivienda 2 — Apartamento, Zona Sur

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)
Predicción de precio (millones $) — Vivienda 2
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.

Conclusiones y recomendaciones generales

  1. Vivienda 1: el crédito de $350M queda por debajo del precio esperado. Presentar las alternativas encontradas y/o negociar.
  2. Vivienda 2: el crédito de $850M es suficiente y deja margen de negociación.
  3. Ambos modelos muestran heterocedasticidad y desviaciones de normalidad (ver Anexos); se sugiere explorar log(precio) y errores robustos como siguiente paso.
  4. Se recomienda auditar el proceso de geo-referenciación/asignación de zona.
  5. El modelo de apartamentos ajusta mejor (R² 75.4%) que el de casas (R² 65.1%); para casas se recomienda incorporar barrio, antigüedad y estado de la construcción en futuras versiones.

PARTE B — Anexos técnicos

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”).

Vivienda 1: Casa, Zona Norte

Paso 1. Filtro de la base y validación geográfica

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.

Paso 2. Análisis exploratorio de datos

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).

Paso 3. Modelo de regresión lineal múltiple

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):

  • Área construida: +0.83M por m² adicional; altamente significativa (p < 0.001). Es el principal determinante del precio, resultado lógico y esperado.
  • Estrato: +86.43M por estrato adicional, razonable pues captura ubicación, acabados y entorno.
  • Baños: +26.98M por baño adicional, significativo (p < 0.001); razonable, más baños suele indicar más plantas o mayor amplitud.
  • Habitaciones y parqueaderos: no significativos (p = 0.73 y p = 0.7). Los VIF (ver Paso 4) son < 2, así que no es multicolinealidad severa: simplemente, controlando por área y estrato, subdividir en más cuartos o tener más parqueaderos no añade valor explicativo adicional en este segmento.

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).

Paso 4. Validación de supuestos

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.

Paso 5. Predicción — Vivienda 1

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.

Paso 6. Ofertas potenciales ($350 millones)

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.

Vivienda 2: Apartamento, Zona Sur

Paso 1. Filtro de la base y validación geográfica - Vivienda 2

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.

Paso 2. Análisis exploratorio de datos- Vivienda 2

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.

Paso 3. Modelo de regresión lineal múltiple- Vivienda 2

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.

Paso 4. Validación de supuestos- Vivienda 2

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).

Paso 5. Predicción — Vivienda 2

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.

Paso 6. Ofertas potenciales ($850 millones) - Vivienda 2

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.

Comparación de modelos- Vivienda 2

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")