# install.packages("devtools") # solo la primera vez
# devtools::install_github("centromagis/paqueteMODELOS", force = TRUE)
library(paqueteMODELOS)
library(dplyr)
library(tidyr)
library(plotly)
library(ggplot2)
library(car)
library(lmtest)
library(knitr)
library(scales)
library(broom)
library(ggrepel)

data("vivienda")
vivienda <- as.data.frame(vivienda)

Informe ejecutivo

Para: María, C&A (Casas y Apartamentos) De: Análisis de datos - Actividad 2 Asunto: Recomendación para la compra de vivienda 1 (casa, presupuesto $350 millones) y vivienda 2 (apartamento, presupuesto $850 millones)

María, para responder la solicitud de la compañía internacional se construyó un modelo de regresión lineal múltiple del precio de vivienda en función del área construida, el estrato, el número de habitaciones, parqueaderos y baños, uno para cada perfil solicitado (casas en zona norte y apartamentos en zona sur), usando los datos de ofertas de los últimos tres meses. A continuación el resumen; todo el detalle técnico (estimaciones, validaciones y comparación de los dos modelos) está en la sección de Anexos.

Vivienda 1: Casa, Zona Norte (crédito $350 millones)

  • El precio de las casas en zona norte depende principalmente del área construida, el estrato, el número de parqueaderos y el número de baños; el número de habitaciones no resultó estadísticamente significativo una vez se controla por el área. El modelo explica cerca del 60% de la variación de precios (R² ≈ 0.60).
  • Para una casa de 200 m², 1 parqueadero, 2 baños y 4 habitaciones, el modelo predice un precio de aproximadamente $312 millones si es estrato 4, y $393 millones si es estrato 5. Con un presupuesto de $350 millones, el estrato 4 es viable y el estrato 5 queda ajustado/por encima del presupuesto según el valor puntual estimado (aunque el intervalo de predicción es amplio, ver anexo).
  • Se identificaron 5 ofertas reales dentro del presupuesto que se ajustan al perfil solicitado (ver mapa y tabla en el Anexo 1, sección 1.6), ubicadas principalmente en barrios como El Bosque, La Merced, Vipasa, Álamos y Prados del Norte.
  • Recomendación: enfocar la búsqueda en casas estrato 4 en zona norte con área cercana a 200 m²; para estrato 5 se sugiere negociar el precio o ampliar ligeramente el presupuesto, ya que el valor esperado supera los $350 millones.

Vivienda 2: Apartamento, Zona Sur (crédito $850 millones)

  • En apartamentos de zona sur el modelo ajusta mejor (R² ≈ 0.75). El área, el estrato, los parqueaderos y los baños aumentan el precio de forma significativa; el número de habitaciones tiene un efecto negativo y significativo, lo que sugiere que, a igual área, más habitaciones implican espacios más pequeños y por lo tanto menor valor percibido.
  • Para un apartamento de 300 m², 3 parqueaderos, 3 baños y 5 habitaciones, el modelo predice $675 millones en estrato 5 y $736 millones en estrato 6. Ambos escenarios caben dentro del presupuesto de $850 millones, incluso considerando el margen de error de la predicción.
  • Se identificaron 5 ofertas reales cercanas al perfil solicitado dentro del presupuesto (ver Anexo 2, sección 2.6), en barrios como Capri, San Fernando, El Ingenio, Seminario y Cuarto de Legua.
  • Recomendación: este perfil tiene más holgura presupuestal; se puede negociar por estrato 6 sin exceder el crédito preaprobado, o usar el margen disponible para mejorar ubicación/acabados.

Consideraciones generales

Ambos modelos presentan heterocedasticidad y residuos que no son perfectamente normales (comunes en precios de vivienda, que suelen tener colas largas hacia arriba), por lo que las estimaciones puntuales son confiables pero los intervalos de predicción son amplios. Se recomienda para una siguiente iteración transformar el precio (por ejemplo log(preciom)) e incorporar variables de ubicación más finas (barrio) y de calidad/acabados para mejorar el ajuste. El detalle completo de estimaciones y supuestos está en los Anexos.



Anexo 1: Vivienda 1 - Casa, Zona Norte

1.1 Filtro de la base de datos

Se filtra la base para incluir únicamente ofertas de casas ubicadas en la zona norte de Cali (base1).

base1 <- vivienda %>%
  filter(tipo == "Casa", zona == "Zona Norte") %>%
  drop_na(preciom, areaconst, estrato, parqueaderos, banios, habitaciones, longitud, latitud)

cat("Número de registros en base1:", nrow(base1), "\n")
## Número de registros en base1: 435
kable(head(base1, 3), caption = "Primeros 3 registros de base1")
Primeros 3 registros de base1
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(table(Tipo = vivienda$tipo, useNA = "ifany"), caption = "Conteo por tipo de vivienda (base completa)")
Conteo por tipo de vivienda (base completa)
Tipo Freq
Apartamento 5100
Casa 3219
NA 3
kable(as.data.frame(table(Zona = base1$zona)), caption = "Verificación: todos los registros de base1 son Zona Norte")
Verificación: todos los registros de base1 son Zona Norte
Zona Freq
Zona Norte 435
kable(as.data.frame(table(Estrato = base1$estrato)), caption = "Distribución de estratos en base1")
Distribución de estratos en base1
Estrato Freq
3 81
4 103
5 223
6 28

Mapa de puntos y verificación geográfica. Para revisar si las ofertas etiquetadas como “Zona Norte” están efectivamente ubicadas al norte de la ciudad, se grafican todas las viviendas de la base completa coloreadas por zona, y luego se resalta base1.

mapa_zonas <- vivienda %>% drop_na(longitud, latitud, zona)

ggplot(mapa_zonas, aes(x = longitud, y = latitud, color = zona)) +
  geom_point(alpha = 0.35, size = 0.8) +
  coord_fixed() +
  labs(title = "Ubicación de todas las ofertas por zona", x = "Longitud", y = "Latitud", color = "Zona") +
  theme_minimal()

mediana_lat <- median(vivienda$latitud, na.rm = TRUE)
base1_fuera <- base1 %>% mutate(fuera_de_zona = latitud < mediana_lat)

ggplot(base1_fuera, aes(x = longitud, y = latitud, color = fuera_de_zona)) +
  geom_point(alpha = 0.6) +
  scale_color_manual(values = c("FALSE" = "steelblue", "TRUE" = "firebrick"),
                      labels = c("Coherente con Zona Norte", "Atípico (más al sur)")) +
  coord_fixed() +
  labs(title = "Puntos de base1 (Casas, Zona Norte)",
       subtitle = "Resaltados en rojo los puntos con latitud por debajo de la mediana de la ciudad",
       x = "Longitud", y = "Latitud", color = "") +
  theme_minimal()

n_fuera <- sum(base1_fuera$fuera_de_zona)

Discusión: la mediana de latitud de las viviendas etiquetadas “Zona Norte” (3.4731) es claramente mayor que la de “Zona Sur” (3.3848), lo que confirma que en general la variable zona sí refleja la ubicación geográfica real. Sin embargo, 25 de los 435 registros de base1 (5.7%) tienen una latitud más baja de lo esperado para la zona norte. Esto probablemente se debe a imprecisiones del geocodificador usado en el web scraping (algunas direcciones mal geocodificadas) o a que la clasificación “zona” del portal inmobiliario es administrativa/comercial y no estrictamente geográfica (algunos barrios se comercializan como “zona norte” aunque su ubicación catastral esté más al centro). No se eliminan estos registros porque no hay evidencia suficiente para considerarlos errores de tipo de vivienda o zona, solo posible imprecisión de coordenadas.

1.2 Análisis exploratorio de datos

Se explora la relación entre el precio y el área construida, el estrato, el número de baños y el número de habitaciones (la zona ya es constante en base1).

num_vars <- base1 %>% select(preciom, areaconst, estrato, banios, habitaciones, parqueaderos)
kable(round(cor(num_vars), 2), caption = "Matriz de correlación - base1 (Casas, Zona Norte)")
Matriz de correlación - base1 (Casas, Zona Norte)
preciom areaconst estrato banios habitaciones parqueaderos
preciom 1.00 0.69 0.53 0.51 0.37 0.41
areaconst 0.69 1.00 0.35 0.46 0.42 0.31
estrato 0.53 0.35 1.00 0.35 0.06 0.26
banios 0.51 0.46 0.35 1.00 0.59 0.39
habitaciones 0.37 0.42 0.06 0.59 1.00 0.24
parqueaderos 0.41 0.31 0.26 0.39 0.24 1.00
p1 <- plot_ly(base1, x = ~areaconst, y = ~preciom, type = "scatter", mode = "markers",
              color = ~factor(estrato), text = ~paste("Barrio:", barrio),
              marker = list(size = 7, opacity = 0.7)) %>%
  layout(title = "Precio vs. área construida (color = estrato)",
         xaxis = list(title = "Área construida (m²)"),
         yaxis = list(title = "Precio (millones $)"))
p1
p2 <- plot_ly(base1, x = ~factor(banios), y = ~preciom, type = "box",
              color = ~factor(banios)) %>%
  layout(title = "Precio según número de baños", showlegend = FALSE,
         xaxis = list(title = "Número de baños"), yaxis = list(title = "Precio (millones $)"))
p2
p3 <- plot_ly(base1, x = ~factor(habitaciones), y = ~preciom, type = "box",
              color = ~factor(habitaciones)) %>%
  layout(title = "Precio según número de habitaciones", showlegend = FALSE,
         xaxis = list(title = "Número de habitaciones"), yaxis = list(title = "Precio (millones $)"))
p3

Interpretación: el precio tiene una correlación fuerte y positiva con el área construida (r = 0.69) y con el estrato (r = 0.53). La relación con el número de baños (r = 0.51) es positiva pero más moderada, y con habitaciones (r = 0.37) es la más débil de las cuatro; en los boxplots se observa traslape considerable entre categorías de habitaciones, lo que anticipa que esta variable podría no ser significativa en el modelo de regresión.

1.3 Modelo de regresión lineal múltiple

mod1 <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = base1)
summary(mod1)
## 
## 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

Interpretación de coeficientes (base1, casas zona norte, n = 435):

  • Área construida: por cada m² adicional, el precio esperado aumenta en 0.677 millones de pesos (~677 mil pesos/m²), manteniendo lo demás constante. Es significativo (p < 0.001) y el signo es lógico: a mayor tamaño, mayor precio.
  • Estrato: subir un estrato aumenta el precio esperado en 80.6 millones, significativo (p < 0.001). Es coherente: el estrato es un proxy de ubicación, acabados y entorno socioeconómico.
  • Habitaciones: el coeficiente es 7.65 millones y no es significativo (p = 0.177 > 0.05). Manteniendo el área fija, agregar habitaciones no aumenta el precio de forma medible; tiene sentido porque, a igual área, más habitaciones suelen significar cuartos más pequeños, no más espacio total.
  • Parqueaderos: cada parqueadero adicional suma 24 millones, significativo (p < 0.001). Lógico dado que el parqueadero es un atributo valorado y escaso.
  • Baños: cada baño adicional suma 18.9 millones, significativo (p = 0.012 < 0.05).

Ajuste del modelo (R²): el modelo tiene un R² de 0.604 (R² ajustado 0.599), es decir, explica cerca del 60% de la variabilidad del precio de las casas en zona norte. El 40% restante corresponde a factores no incluidos en el modelo: ubicación específica (barrio), antigüedad, acabados, estado de la vivienda, vista, seguridad, etc. Para mejorar el ajuste se podría: (i) incluir el barrio como variable (efectos fijos de ubicación), (ii) transformar el precio con logaritmo para estabilizar la varianza, (iii) incluir variables de calidad/acabados si estuvieran disponibles, o (iv) probar interacciones (p. ej. área × estrato).

1.4 Validación de supuestos

par(mfrow = c(1,2))
plot(mod1, which = 1)
plot(mod1, which = 2)

sw1 <- shapiro.test(resid(mod1))
bp1 <- bptest(mod1)
dw1 <- dwtest(mod1)
vif1 <- vif(mod1)

kable(data.frame(
  Prueba = c("Shapiro-Wilk (normalidad residuos)", "Breusch-Pagan (homocedasticidad)", "Durbin-Watson (independencia)"),
  Estadistico = c(round(sw1$statistic,3), round(bp1$statistic,3), round(dw1$statistic,3)),
  p_valor = c(signif(sw1$p.value,3), signif(bp1$p.value,3), signif(dw1$p.value,3))
))
Prueba Estadistico p_valor
W Shapiro-Wilk (normalidad residuos) 0.852 0.00000
BP Breusch-Pagan (homocedasticidad) 80.281 0.00000
DW Durbin-Watson (independencia) 1.762 0.00547
kable(t(round(vif1,2)), caption = "Factor de Inflación de Varianza (VIF)")
Factor de Inflación de Varianza (VIF)
areaconst estrato habitaciones parqueaderos banios
1.46 1.31 1.72 1.23 1.97

Interpretación de supuestos:

  • Linealidad: el gráfico de residuos vs. ajustados no muestra un patrón curvo marcado, aunque hay más dispersión en precios altos.
  • Normalidad: el QQ-plot muestra colas pesadas y la prueba de Shapiro-Wilk rechaza normalidad (p < 0.001). Es un patrón típico de precios de vivienda, que tienden a tener valores extremos hacia arriba. Sugerencia: modelar log(preciom) para acercar los residuos a la normalidad.
  • Homocedasticidad: la prueba de Breusch-Pagan rechaza homocedasticidad (p < 0.001), es decir la varianza del error crece con el precio. Sugerencia: usar errores estándar robustos (heterocedasticidad-consistentes) o transformar la variable respuesta.
  • Independencia: el estadístico Durbin-Watson es 1.76 (p = 0.00547), sugiriendo algo de autocorrelación. Como estos datos son de corte transversal (no una serie de tiempo), esto probablemente refleja autocorrelación espacial (viviendas del mismo barrio con precios parecidos) más que una verdadera dependencia temporal; no invalida las estimaciones pero sí sugiere que agrupar por barrio podría mejorar el modelo.
  • Multicolinealidad: todos los VIF son menores a 2, por lo que no hay evidencia de multicolinealidad relevante entre los predictores.
  • No es necesario corregir estos supuestos para efectos de esta actividad, pero las sugerencias anteriores (log-transformación, errores robustos, efectos de barrio) son el siguiente paso natural.

1.5 Predicción para la vivienda 1

La solicitud pide: casa, 200 m² construidos, 1 parqueadero, 2 baños, 4 habitaciones, estrato 4 o 5.

nueva_v1_e4 <- data.frame(areaconst = 200, estrato = 4, habitaciones = 4, parqueaderos = 1, banios = 2)
nueva_v1_e5 <- data.frame(areaconst = 200, estrato = 5, habitaciones = 4, parqueaderos = 1, banios = 2)

pred_e4 <- predict(mod1, nueva_v1_e4, interval = "prediction")
pred_e5 <- predict(mod1, nueva_v1_e5, interval = "prediction")

kable(rbind(
  data.frame(Estrato = 4, round(pred_e4,1)),
  data.frame(Estrato = 5, round(pred_e5,1))
), caption = "Predicción de precio (millones $) - Vivienda 1", col.names = c("Estrato","Estimado","Límite inferior","Límite superior"))
Predicción de precio (millones $) - Vivienda 1
Estrato Estimado Límite inferior Límite superior
1 4 312.1 6.2 618.0
11 5 392.7 86.2 699.3

Con un presupuesto preaprobado de $350 millones, el precio estimado para estrato 4 (312 millones) queda dentro del presupuesto, mientras que para estrato 5 (393 millones) queda por encima. El intervalo de predicción es amplio (dado el R² de 0.6), por lo que en la práctica es razonable buscar ofertas reales cercanas a estos valores en lugar de depender solo del punto estimado.

1.6 Ofertas potenciales dentro del presupuesto

Se buscan ofertas reales de base1 con precio ≤ $350 millones y estrato 4 o 5, seleccionando las 5 más cercanas al perfil solicitado (área 200 m², 1 parqueadero, 2 baños, 4 habitaciones) mediante distancia euclidiana estandarizada.

perfil1 <- list(areaconst = 200, parqueaderos = 1, banios = 2, habitaciones = 4)
vars1 <- names(perfil1)

candidatas1 <- base1 %>% filter(preciom <= 350, estrato %in% c(4,5))
sds1 <- sapply(candidatas1[vars1], sd)
candidatas1$distancia <- sqrt(rowSums(sapply(vars1, function(v)
  ((candidatas1[[v]] - perfil1[[v]]) / sds1[[v]])^2)))

ofertas1 <- candidatas1 %>% arrange(distancia) %>% slice_head(n = 5) %>%
  select(id, barrio, estrato, areaconst, parqueaderos, banios, habitaciones, preciom, longitud, latitud)

kable(ofertas1, caption = "Top 5 ofertas recomendadas - Vivienda 1 (Casa, Zona Norte)")
Top 5 ofertas recomendadas - Vivienda 1 (Casa, Zona Norte)
id barrio estrato areaconst parqueaderos banios habitaciones preciom longitud latitud
3586 la merced 4 240 1 2 3 330 -76.52720 3.48433
1666 alamos 4 120 1 2 4 275 -76.51700 3.47500
4779 prados del norte 5 140 1 2 3 280 -76.53300 3.45600
94 zona norte 4 162 1 3 4 265 -76.48238 3.46786
1924 vipasa 4 264 1 2 3 320 -76.51840 3.48459
mapa_ofertas1 <- ggplot(base1, aes(x = longitud, y = latitud)) +
  geom_point(color = "grey80", alpha = 0.5) +
  geom_point(data = ofertas1, aes(x = longitud, y = latitud), color = "darkgreen", size = 3) +
  ggrepel::geom_text_repel(data = ofertas1, aes(label = barrio), size = 3, max.overlaps = 20) +
  coord_fixed() +
  labs(title = "Top 5 ofertas recomendadas - Vivienda 1 (Casa, Zona Norte)",
       subtitle = "Puntos gris: universo de casas en zona norte. Puntos verdes: ofertas recomendadas",
       x = "Longitud", y = "Latitud") +
  theme_minimal()
mapa_ofertas1

ggsave("mapa_vivienda1.pdf", mapa_ofertas1, width = 8, height = 6)

Discusión: las 5 ofertas están en los barrios la merced, alamos, prados del norte, zona norte, vipasa, todas dentro del presupuesto y con características muy cercanas a lo solicitado (área entre 120 y 264 m²). Geográficamente se distribuyen dentro del clúster norte, lo que confirma que son opciones consistentes con la solicitud de ubicación.



Anexo 2: Vivienda 2 - Apartamento, Zona Sur

2.1 Filtro de la base de datos

base2 <- vivienda %>%
  filter(tipo == "Apartamento", zona == "Zona Sur") %>%
  drop_na(preciom, areaconst, estrato, parqueaderos, banios, habitaciones, longitud, latitud)

cat("Número de registros en base2:", nrow(base2), "\n")
## Número de registros en base2: 2381
kable(head(base2, 3), caption = "Primeros 3 registros de base2")
Primeros 3 registros de base2
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(as.data.frame(table(Zona = base2$zona)), caption = "Verificación: todos los registros de base2 son Zona Sur")
Verificación: todos los registros de base2 son Zona Sur
Zona Freq
Zona Sur 2381
kable(as.data.frame(table(Estrato = base2$estrato)), caption = "Distribución de estratos en base2")
Distribución de estratos en base2
Estrato Freq
3 107
4 835
5 990
6 449
base2_fuera <- base2 %>% mutate(fuera_de_zona = latitud > mediana_lat)

ggplot(base2_fuera, aes(x = longitud, y = latitud, color = fuera_de_zona)) +
  geom_point(alpha = 0.5) +
  scale_color_manual(values = c("FALSE" = "steelblue", "TRUE" = "firebrick"),
                      labels = c("Coherente con Zona Sur", "Atípico (más al norte)")) +
  coord_fixed() +
  labs(title = "Puntos de base2 (Apartamentos, Zona Sur)",
       subtitle = "Resaltados en rojo los puntos con latitud por encima de la mediana de la ciudad",
       x = "Longitud", y = "Latitud", color = "") +
  theme_minimal()

n_fuera2 <- sum(base2_fuera$fuera_de_zona)

Discusión: la mediana de latitud de base2 (3.3823) es claramente menor que la de zona norte, consistente con estar en el sur de la ciudad. 378 de 2381 registros (15.9%) aparecen con una latitud más alta de lo típico para zona sur, de nuevo atribuible a imprecisión de geocodificación o a la clasificación comercial/administrativa de “zona” usada por el portal, más que a un error de tipo de vivienda.

2.2 Análisis exploratorio de datos

num_vars2 <- base2 %>% select(preciom, areaconst, estrato, banios, habitaciones, parqueaderos)
kable(round(cor(num_vars2), 2), caption = "Matriz de correlación - base2 (Apartamentos, Zona Sur)")
Matriz de correlación - base2 (Apartamentos, Zona Sur)
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
plot_ly(base2, x = ~areaconst, y = ~preciom, type = "scatter", mode = "markers",
        color = ~factor(estrato), text = ~paste("Barrio:", barrio),
        marker = list(size = 6, opacity = 0.5)) %>%
  layout(title = "Precio vs. área construida (color = estrato)",
         xaxis = list(title = "Área construida (m²)"),
         yaxis = list(title = "Precio (millones $)"))
plot_ly(base2, x = ~factor(banios), y = ~preciom, type = "box", color = ~factor(banios)) %>%
  layout(title = "Precio según número de baños", showlegend = FALSE,
         xaxis = list(title = "Número de baños"), yaxis = list(title = "Precio (millones $)"))
plot_ly(base2, x = ~factor(habitaciones), y = ~preciom, type = "box", color = ~factor(habitaciones)) %>%
  layout(title = "Precio según número de habitaciones", showlegend = FALSE,
         xaxis = list(title = "Número de habitaciones"), yaxis = list(title = "Precio (millones $)"))

Interpretación: en apartamentos de zona sur, el precio también correlaciona fuertemente con el área (r = 0.74) y el estrato (r = 0.65). Los baños (r = 0.71) muestran una correlación más alta que en las casas, y las habitaciones (r = 0.3) tienen una correlación baja/cercana a cero a pesar de que, como se ve más adelante, resulta significativa (y negativa) en el modelo multivariado — un caso típico donde el efecto marginal (sin controlar) difiere del efecto condicional (controlando por área).

2.3 Modelo de regresión lineal múltiple

mod2 <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = base2)
summary(mod2)
## 
## 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

Interpretación de coeficientes (base2, apartamentos zona sur, n = 2381):

  • Área construida: cada m² adicional aumenta el precio esperado en 1.285 millones, significativo (p < 0.001). El efecto por m² es mayor que en casas (0.68 en base1), consistente con que los apartamentos de zona sur analizados tienden a ser de estratos altos y edificios con más amenidades por metro cuadrado.
  • Estrato: subir un estrato aumenta el precio esperado en 60.9 millones (p < 0.001).
  • Habitaciones: el coeficiente es -24.8 millones y es significativo y negativo (p < 0.001). Es decir, manteniendo fijos el área, los baños y los parqueaderos, tener más habitaciones reduce el precio esperado. La explicación más plausible es que, a igual área total, más habitaciones implica alcobas más pequeñas (menor calidad percibida del espacio), algo común en apartamentos donde el área es más restringida que en casas.
  • Parqueaderos: cada parqueadero adicional suma 72.9 millones (p < 0.001); el efecto es más fuerte que en casas, coherente con que el parqueadero es un bien más escaso y valorado en zonas de apartamentos.
  • Baños: cada baño adicional suma 50.7 millones (p < 0.001).

Ajuste del modelo (R²): R² = 0.749 (ajustado 0.748), es decir el modelo explica cerca del 75% de la variación del precio, un ajuste notablemente mejor que en casas. Aun así, el 25% restante podría explicarse por antigüedad del edificio, piso, vista, amenidades del conjunto (piscina, gimnasio, seguridad) y ubicación específica (barrio), variables no incluidas aquí.

2.4 Validación de supuestos

par(mfrow = c(1,2))
plot(mod2, which = 1)
plot(mod2, which = 2)

sw2 <- shapiro.test(sample(resid(mod2), min(5000, length(resid(mod2)))))
bp2 <- bptest(mod2)
dw2 <- dwtest(mod2)
vif2 <- vif(mod2)

kable(data.frame(
  Prueba = c("Shapiro-Wilk (normalidad residuos)*", "Breusch-Pagan (homocedasticidad)", "Durbin-Watson (independencia)"),
  Estadistico = c(round(sw2$statistic,3), round(bp2$statistic,3), round(dw2$statistic,3)),
  p_valor = c(signif(sw2$p.value,3), signif(bp2$p.value,3), signif(dw2$p.value,3))
))
Prueba Estadistico p_valor
W Shapiro-Wilk (normalidad residuos)* 0.791 0
BP Breusch-Pagan (homocedasticidad) 754.805 0
DW Durbin-Watson (independencia) 1.533 0
kable(t(round(vif2,2)), caption = "Factor de Inflación de Varianza (VIF)")
Factor de Inflación de Varianza (VIF)
areaconst estrato habitaciones parqueaderos banios
2.07 1.55 1.43 1.74 2.53

*Shapiro-Wilk calculado sobre una muestra aleatoria de máx. 5000 residuos por límite del test.

Interpretación: al igual que en el modelo de casas, se rechaza normalidad (p < 0.001) y homocedasticidad (Breusch-Pagan p < 0.001), con un patrón de varianza creciente en precios altos. El Durbin-Watson (1.53) también sugiere autocorrelación, de nuevo probablemente espacial (por barrio) más que temporal. Los VIF son todos menores a 3, por lo que no hay multicolinealidad problemática. Las mismas sugerencias del Anexo 1 (log-transformación del precio, errores robustos, efectos de barrio) aplican aquí para mejorar el modelo.

2.5 Predicción para la vivienda 2

La solicitud pide: apartamento, 300 m² construidos, 3 parqueaderos, 3 baños, 5 habitaciones, estrato 5 o 6.

nueva_v2_e5 <- data.frame(areaconst = 300, estrato = 5, habitaciones = 5, parqueaderos = 3, banios = 3)
nueva_v2_e6 <- data.frame(areaconst = 300, estrato = 6, habitaciones = 5, parqueaderos = 3, banios = 3)

pred2_e5 <- predict(mod2, nueva_v2_e5, interval = "prediction")
pred2_e6 <- predict(mod2, nueva_v2_e6, interval = "prediction")

kable(rbind(
  data.frame(Estrato = 5, round(pred2_e5,1)),
  data.frame(Estrato = 6, round(pred2_e6,1))
), caption = "Predicción de precio (millones $) - Vivienda 2", col.names = c("Estrato","Estimado","Límite inferior","Límite superior"))
Predicción de precio (millones $) - Vivienda 2
Estrato Estimado Límite inferior Límite superior
1 5 675.0 481.5 868.6
11 6 735.9 542.3 929.5

Con un presupuesto preaprobado de $850 millones, ambos escenarios (675 millones en estrato 5 y 736 millones en estrato 6) quedan dentro del presupuesto, con un margen razonable incluso para el estrato más alto.

2.6 Ofertas potenciales dentro del presupuesto

perfil2 <- list(areaconst = 300, parqueaderos = 3, banios = 3, habitaciones = 5)
vars2 <- names(perfil2)

candidatas2 <- base2 %>% filter(preciom <= 850, estrato %in% c(5,6))
sds2 <- sapply(candidatas2[vars2], sd)
candidatas2$distancia <- sqrt(rowSums(sapply(vars2, function(v)
  ((candidatas2[[v]] - perfil2[[v]]) / sds2[[v]])^2)))

ofertas2 <- candidatas2 %>% arrange(distancia) %>% slice_head(n = 5) %>%
  select(id, barrio, estrato, areaconst, parqueaderos, banios, habitaciones, preciom, longitud, latitud)

kable(ofertas2, caption = "Top 5 ofertas recomendadas - Vivienda 2 (Apartamento, Zona Sur)")
Top 5 ofertas recomendadas - Vivienda 2 (Apartamento, Zona Sur)
id barrio estrato areaconst parqueaderos banios habitaciones preciom longitud latitud
6175 capri 5 270.00 3 3 4 350 -76.54100 3.39200
2308 San Fernando 5 258.00 2 4 5 350 -76.51972 3.44000
4266 el ingenio 6 250.00 2 4 5 700 -76.53043 3.37062
8036 seminario 5 256.00 3 5 5 530 -76.55408 3.40748
8113 cuarto de legua 5 295.55 2 4 4 410 -76.55527 3.40750
mapa_ofertas2 <- ggplot(base2, aes(x = longitud, y = latitud)) +
  geom_point(color = "grey80", alpha = 0.5) +
  geom_point(data = ofertas2, aes(x = longitud, y = latitud), color = "darkorange", size = 3) +
  ggrepel::geom_text_repel(data = ofertas2, aes(label = barrio), size = 3, max.overlaps = 20) +
  coord_fixed() +
  labs(title = "Top 5 ofertas recomendadas - Vivienda 2 (Apartamento, Zona Sur)",
       subtitle = "Puntos gris: universo de apartamentos en zona sur. Puntos naranjas: ofertas recomendadas",
       x = "Longitud", y = "Latitud") +
  theme_minimal()
mapa_ofertas2

ggsave("mapa_vivienda2.pdf", mapa_ofertas2, width = 8, height = 6)

Discusión: las 5 ofertas recomendadas están en los barrios capri, San Fernando, el ingenio, seminario, cuarto de legua, todas dentro de presupuesto. A diferencia de la vivienda 1, aquí el presupuesto es más holgado frente a los precios estimados, por lo que hay más margen de negociación en ubicación o acabados.



Anexo 3: Comparación de los dos modelos

Comparación de coeficientes. Se comparan lado a lado las estimaciones de ambos modelos (estimación y significancia estadística) para las mismas cinco variables.

tabla_coefs <- bind_rows(
  tidy(mod1) %>% mutate(Modelo = "Vivienda 1 (Casa, Zona Norte)"),
  tidy(mod2) %>% mutate(Modelo = "Vivienda 2 (Apartamento, Zona Sur)")
) %>%
  mutate(
    Estimacion = paste0(
      round(estimate, 2),
      case_when(
        p.value < 0.001 ~ "***",
        p.value < 0.01  ~ "**",
        p.value < 0.05  ~ "*",
        p.value < 0.10  ~ ".",
        TRUE ~ ""
      )
    )
  ) %>%
  select(term, Modelo, Estimacion) %>%
  pivot_wider(names_from = Modelo, values_from = Estimacion) %>%
  rename(Variable = term)

kable(tabla_coefs, caption = "Comparación de coeficientes - Vivienda 1 vs Vivienda 2 (significancia: *** p<0.001, ** p<0.01, * p<0.05, . p<0.10)",
      align = "lcc")
Comparación de coeficientes - Vivienda 1 vs Vivienda 2 (significancia: *** p<0.001, ** p<0.01, * p<0.05, . p<0.10)
Variable Vivienda 1 (Casa, Zona Norte) Vivienda 2 (Apartamento, Zona Sur)
(Intercept) -238.17*** -261.63***
areaconst 0.68*** 1.29***
estrato 80.63*** 60.9***
habitaciones 7.65 -24.84***
parqueaderos 24.01*** 72.91***
banios 18.9* 50.7***

Comparación de ajuste global.

comparacion <- data.frame(
  Modelo = c("Vivienda 1 (Casa, Zona Norte)", "Vivienda 2 (Apartamento, Zona Sur)"),
  n = c(nrow(base1), nrow(base2)),
  R2 = round(c(summary(mod1)$r.squared, summary(mod2)$r.squared), 3),
  R2_ajustado = round(c(summary(mod1)$adj.r.squared, summary(mod2)$adj.r.squared), 3),
  Sigma = round(c(summary(mod1)$sigma, summary(mod2)$sigma), 1),
  Habitaciones_signif = c("No", "Sí (negativo)")
)
kable(comparacion, caption = "Comparación de los modelos de vivienda 1 y vivienda 2")
Comparación de los modelos de vivienda 1 y vivienda 2
Modelo n R2 R2_ajustado Sigma Habitaciones_signif
Vivienda 1 (Casa, Zona Norte) 435 0.604 0.599 155.1 No
Vivienda 2 (Apartamento, Zona Sur) 2381 0.749 0.748 98.0 Sí (negativo)

El modelo de apartamentos (zona sur) tiene mejor ajuste (R² más alto) y un error estándar residual más bajo en términos relativos al rango de precios, lo que indica que el área, el estrato, los parqueaderos y los baños explican mejor el precio en ese segmento. En ambos modelos el signo y la significancia de área, estrato, parqueaderos y baños son consistentes (todos positivos y significativos); la diferencia más notable es el efecto de las habitaciones, no significativo en casas y significativo-negativo en apartamentos, reflejo de que en apartamentos el espacio es más restringido y “más cuartos” compite directamente con el tamaño de cada uno.



Adaptado de Weiers, Ronald M, Introducción a la estadística para Negocios., 2006.