# 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)
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.
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.
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")
| 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)")
| 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")
| Zona | Freq |
|---|---|
| Zona Norte | 435 |
kable(as.data.frame(table(Estrato = base1$estrato)), caption = "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.
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)")
| 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.
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):
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).
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)")
| areaconst | estrato | habitaciones | parqueaderos | banios |
|---|---|---|---|---|
| 1.46 | 1.31 | 1.72 | 1.23 | 1.97 |
Interpretación de supuestos:
log(preciom) para acercar los residuos a la
normalidad.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"))
| 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.
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)")
| 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.
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")
| 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")
| Zona | Freq |
|---|---|
| Zona Sur | 2381 |
kable(as.data.frame(table(Estrato = base2$estrato)), caption = "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.
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)")
| 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).
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):
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í.
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)")
| 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.
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"))
| 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.
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)")
| 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.
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")
| 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")
| 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.