María fundó C&A hace unos años en Cali luego de trabajar como agente de bienes raíces en Cali y Bogotá. Hoy la compañía recibió una solicitud de una empresa internacional que necesita ubicar a dos empleados y sus familias en la ciudad. La solicitud trae condiciones puntuales para dos viviendas y un crédito preaprobado independiente para cada una:
| Característica | Vivienda 1 | Vivienda 2 |
|---|---|---|
| Tipo | Casa | Apartamento |
| Área construida (m²) | 200 | 300 |
| Parqueaderos | 1 | 3 |
| Baños | 2 | 3 |
| Habitaciones | 4 | 5 |
| Estrato | 4 o 5 | 5 o 6 |
| Zona | Norte | Sur |
| Crédito preaprobado | $350 millones | $850 millones |
Este informe resuelve la solicitud en dos partes: un informe
ejecutivo con la recomendación para María, y dos
anexos técnicos donde se documenta la metodología y los
resultados de la modelación estadística que sustentan esa recomendación
segun las indicaciones de la actividad. Toda la evidencia se construye
sobre la base de ofertas de vivienda de los últimos tres meses en Cali,
disponible en el paquete paqueteMODELOS.
Para responder a la empresa se estimó, para cada vivienda, un modelo de regresión lineal múltiple sobre el segmento de mercado que corresponde exactamente al tipo de inmueble y la zona solicitados (casas en zona norte para la vivienda 1; apartamentos en zona sur para la vivienda 2). Con cada modelo se calculó el precio esperado de una vivienda con las características pedidas, y luego se buscaron ofertas reales del mercado que se ajustan a esas características y al presupuesto disponible.
Con base en 435 ofertas de casas en zona norte con información completa, el modelo explica el 60.4 % de la variación del precio (R² ajustado 59.9 %) a partir del área construida, el estrato, el número de habitaciones, parqueaderos y baños. Todas las variables tienen el signo esperado; el número de habitaciones es la única que no resulta estadísticamente significativa una vez se controla por área construida.
Para una casa de 200 m², 4 habitaciones, 1 parqueadero y 2 baños, el modelo predice:
Recomendación: el presupuesto de $350 millones es suficiente para una casa de estas características en estrato 4, con un margen razonable frente al precio esperado. En estrato 5 el precio esperado ya supera el crédito preaprobado, así que solo sería viable si aparece una oferta puntual por debajo del promedio de su segmento, o si la empresa está dispuesta a negociar un área ligeramente menor o financiación adicional. En el Anexo 2 se presentan 8 ofertas reales del mercado que cumplen el presupuesto y las condiciones solicitadas, concentradas en los barrios El Bosque, La Flora y La Merced.
Con base en 2,381 ofertas de apartamentos en zona sur, el modelo explica el 74.9 % de la variación del precio (R² ajustado 74.8 %), un ajuste notablemente mejor que el de la vivienda 1, y todas las variables son altamente significativas.
Para un apartamento de 300 m², 5 habitaciones, 3 parqueaderos y 3 baños, el modelo predice:
Recomendación: el crédito de $850 millones cubre holgadamente el precio esperado en ambos estratos, dejando entre $115 y $175 millones de margen. Este margen es importante porque un apartamento de 300 m² es un inmueble grande para la zona sur (por encima del percentil 90 del área construida en ese segmento), por lo que la oferta disponible con exactamente esas características es limitada; el margen de presupuesto permite a la empresa considerar alternativas ligeramente distintas en área o ubicación sin salirse del crédito aprobado. El Anexo 2 presenta 8 ofertas reales dentro de presupuesto, entre ellas inmuebles en Guadalupe, El Ingenio y Ciudadela Pasoancho.
Las ventas de vivienda en Cali han disminuido durante este año debido a la coyuntura política y social. Sin embargo, las entidades de crédito hipotecario siguen ofreciendo diferentes alternativas de financiación. Esta situación puede resultar favorable para un comprador que ya cuenta con un crédito preaprobado, como es el caso de la empresa solicitante, ya que actualmente existe una mayor disponibilidad de inventario y los vendedores podrían tener una mayor disposición para negociar el precio.
El modelo presenta un mejor ajuste para los apartamentos ubicados en la zona sur, con un R² de 74.9 %, frente al modelo de casas en la zona norte, que alcanza un R² de 60.4 %. En términos prácticos, esto quiere decir que la estimación del precio para la vivienda 2 tiene un mayor nivel de confiabilidad que la estimación realizada para la vivienda 1. En el Anexo 2 se presenta con mayor detalle la razón de esta diferencia y se identifican las variables adicionales que podrían ayudar a mejorar el modelo utilizado para las casas.
Los dos modelos presentan heterocedasticidad, es decir, la variabilidad de los errores aumenta a medida que aumenta el precio de la vivienda. También se observan colas pesadas en los residuales, un comportamiento que es común en este tipo de modelos de precios de vivienda. Estas características no hacen que las estimaciones puntuales pierdan validez, pero sí deben tenerse en cuenta al interpretar los intervalos de predicción individuales, ya que estos son más amplios que los intervalos asociados a la media y pueden ser especialmente amplios en los inmuebles de mayor precio. El análisis detallado de estos resultados se encuentra en el Anexo 2.
Al revisar la ubicación geográfica de las ofertas, se encontró que algunos registros que están etiquetados dentro de una determinada zona se encuentran, de acuerdo con sus coordenadas, más cerca del centroide correspondiente a otra zona. Esta situación se analiza con mayor detalle en el Anexo 2 y tiene un mayor impacto en la vivienda 1, correspondiente a las casas de la zona norte, que en la vivienda 2.
Para responder la solicitud de la empresa se siguió la misma secuencia de trabajo para cada una de las dos viviendas:
vivienda por tipo de inmueble y zona (casas en zona
norte para la vivienda 1; apartamentos en zona sur para la vivienda 2),
y se eliminan los registros con valores faltantes en las variables que
entran al modelo. Se verifica la consulta con las primeras
observaciones, conteos por variable, y un mapa de los puntos filtrados
sobre el conjunto completo de la ciudad para detectar posibles
inconsistencias en la zona declarada.plotly.precio ~ área construida + estrato + habitaciones + parqueaderos + baños
sobre cada base filtrada, y se interpretan los coeficientes, su
significancia y el R².Los pasos 1 a 6 se aplican primero a la vivienda 1 y luego se replican, en la misma secuencia, para la vivienda 2, cambiando únicamente el filtro de tipo/zona y el presupuesto disponible.
# Instalación del paquete que contiene la base de ofertas de vivienda
# (ejecutar solo la primera vez / si el paquete no está instalado)
if (!requireNamespace("paqueteMODELOS", quietly = TRUE)) {
if (!requireNamespace("devtools", quietly = TRUE)) install.packages("devtools")
devtools::install_github("centromagis/paqueteMODELOS", force = TRUE)
}
library(paqueteMODELOS)
data("vivienda")
vars_modelo <- c("preciom", "areaconst", "estrato", "banios", "habitaciones", "parqueaderos")
kable(head(vivienda, 3), caption = "Primeros registros de la base completa `vivienda`")
| id | zona | piso | estrato | preciom | areaconst | parqueaderos | banios | habitaciones | tipo | barrio | longitud | latitud |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1147 | Zona Oriente | NA | 3 | 250 | 70 | 1 | 3 | 6 | Casa | 20 de julio | -76.51168 | 3.43382 |
| 1169 | Zona Oriente | NA | 3 | 320 | 120 | 1 | 2 | 3 | Casa | 20 de julio | -76.51237 | 3.43369 |
| 1350 | Zona Oriente | NA | 3 | 350 | 220 | 2 | 2 | 4 | Casa | 20 de julio | -76.51537 | 3.43566 |
La base completa tiene 8322 ofertas y 13 variables. Antes de cualquier filtro, se revisan los valores faltantes por columna:
kable(t(colSums(is.na(vivienda))), caption = "Valores faltantes por variable, base completa")
| id | zona | piso | estrato | preciom | areaconst | parqueaderos | banios | habitaciones | tipo | barrio | longitud | latitud |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 3 | 3 | 2638 | 3 | 2 | 3 | 1605 | 3 | 3 | 3 | 3 | 3 | 3 |
Casi todas las variables tienen apenas 2 o 3 registros incompletos,
probablemente errores puntuales de captura, salvo piso y
parqueaderos, con muchos mas vacios que se revisan dentro
de cada base filtrada.
base1_bruta <- vivienda %>% filter(tipo == "Casa", zona == "Zona Norte")
kable(head(base1_bruta, 3), caption = "Primeros 3 registros de base1 (Casa, Zona Norte)")
| id | zona | piso | estrato | preciom | areaconst | parqueaderos | banios | habitaciones | tipo | barrio | longitud | latitud |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1209 | Zona Norte | 02 | 5 | 320 | 150 | 2 | 4 | 6 | Casa | acopi | -76.51341 | 3.47968 |
| 1592 | Zona Norte | 02 | 5 | 780 | 380 | 2 | 3 | 3 | Casa | acopi | -76.51674 | 3.48721 |
| 4057 | Zona Norte | 02 | 6 | 750 | 445 | NA | 7 | 6 | Casa | acopi | -76.52950 | 3.38527 |
base1 reúne 722 ofertas de casas en zona norte. Antes de depurar, los faltantes en esta base son:
kable(t(colSums(is.na(base1_bruta))), caption = "Valores faltantes en base1 antes de depurar")
| id | zona | piso | estrato | preciom | areaconst | parqueaderos | banios | habitaciones | tipo | barrio | longitud | latitud |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 372 | 0 | 0 | 0 | 287 | 0 | 0 | 0 | 0 | 0 | 0 |
El vacío en piso es esperable: piso
identifica el piso de un apartamento y no aplica a una casa, por lo que
no se depura, adicional, no es una variable de relevancia dentro de la
solicitud inicial. El vacío en parqueaderos (287 registros,
39.8 % de la base) sí afecta al modelo y se depura eliminando esos
registros:
base1 <- base1_bruta %>% filter(if_all(all_of(vars_modelo), ~ !is.na(.)))
cat("Registros después de depurar (722-287):", nrow(base1))
## Registros después de depurar (722-287): 435
Verificación de la consulta. Como comprobación,
todas las observaciones de base1 deben tener
tipo == "Casa" y zona == "Zona Norte":
kable(table(base1$tipo, base1$zona), caption = "Comprobación cruzada tipo x zona en base1")
| Zona Norte | |
|---|---|
| Casa | 435 |
Mapa de los puntos. Se ubican geográficamente las 722 casas de zona norte sobre el mapa de coordenadas de toda la ciudad, para ver si en efecto se agrupan en el norte:
p_mapa1 <- plot_ly() %>%
add_trace(data = vivienda %>% filter(!is.na(longitud)),
x = ~longitud, y = ~latitud, type = "scattergl", mode = "markers",
marker = list(size = 4, color = "lightgrey"),
name = "Resto del mercado", hoverinfo = "skip") %>%
add_trace(data = base1_bruta %>% filter(!is.na(longitud)),
x = ~longitud, y = ~latitud, type = "scattergl", mode = "markers",
marker = list(size = 6, color = "firebrick"),
name = "Casas · Zona Norte",
text = ~paste0("Barrio: ", barrio, "<br>Estrato: ", estrato, "<br>Precio: $", preciom, "M"),
hoverinfo = "text") %>%
layout(title = "Casas etiquetadas como Zona Norte sobre el conjunto de la ciudad",
xaxis = list(title = "Longitud"), yaxis = list(title = "Latitud"))
p_mapa1
Discusión. Los puntos rojos se concentran en el sector norte, pero no todos: hay observaciones rojas dispersas hacia el sur y el centro del mapa. Para cuantificarlo, se calculó el centroide geográfico de cada zona declarada y se comparó, para cada casa de zona norte, cuál es la zona cuyo centroide le queda geográficamente más cerca:
centroides <- vivienda %>% filter(!is.na(longitud)) %>%
group_by(zona) %>% summarise(lon_c = mean(longitud), lat_c = mean(latitud), .groups = "drop")
zona_mas_cercana <- function(lon, lat) {
d <- sqrt((lon - centroides$lon_c)^2 + (lat - centroides$lat_c)^2)
centroides$zona[which.min(d)]
}
base1_geo <- base1_bruta %>% filter(!is.na(longitud)) %>%
rowwise() %>% mutate(zona_cercana = zona_mas_cercana(longitud, latitud)) %>% ungroup()
tab_geo1 <- table(declarada = base1_geo$zona, mas_cercana = base1_geo$zona_cercana)
kable(tab_geo1, caption = "Zona declarada vs. zona geográficamente más cercana (casas zona norte)")
| Zona Centro | Zona Norte | Zona Oeste | Zona Oriente | Zona Sur | |
|---|---|---|---|---|---|
| Zona Norte | 55 | 515 | 37 | 30 | 85 |
pct_mismatch1 <- round(mean(base1_geo$zona_cercana != "Zona Norte") * 100, 1)
Un 28.7% de las casas que están etiquetadas como “Zona Norte” se encuentran, según sus coordenadas, más cerca del centroide de otra zona, principalmente de la zona sur y la zona oeste. Sin embargo, esto no quiere decir necesariamente que la zona haya sido registrada de manera incorrecta. Las zonas comerciales que maneja una inmobiliaria no tienen por qué ser círculos definidos alrededor de un punto central, sino que corresponden a polígonos irregulares que pueden seguir vías, barrios o límites administrativos. Por esta razón, una vivienda ubicada cerca del límite de la Zona Norte puede estar geográficamente más cerca del centroide de una zona vecina y, aun así, pertenecer correctamente a la Zona Norte.
No obstante, el porcentaje encontrado es lo suficientemente alto como para considerar también otras posibles causas, como una imprecisión en la georreferenciación de las viviendas, especialmente si algunas coordenadas fueron capturadas manualmente, o la existencia de fronteras poco claras entre zonas en determinados barrios. Esto es importante para el análisis porque el precio de la vivienda depende en buena medida de la zona en la que se encuentra, como se evidenció en la sección exploratoria. Por lo tanto, una clasificación incorrecta de la zona podría afectar la comparación de precios y generar sesgos al seleccionar inmuebles comparables. Por esta razón, se recomienda que María valide la zona de los barrios que presentan una mayor distancia respecto a su centroide antes de utilizarlos como comparables directos.
Antes de fijar la zona, conviene ver cómo se comporta el precio por zona y tipo en todo el mercado, para justificar por qué la zona importa como filtro:
resumen_mercado <- vivienda %>% filter(!is.na(preciom)) %>%
group_by(zona, tipo) %>% summarise(mediana_precio = median(preciom), n = n(), .groups = "drop")
plot_ly(resumen_mercado, x = ~zona, y = ~mediana_precio, color = ~tipo, type = "bar",
text = ~paste0("n = ", n), hoverinfo = "text+y") %>%
layout(title = "Precio mediano por zona y tipo de vivienda (mercado completo)",
xaxis = list(title = ""), yaxis = list(title = "Precio mediano (millones $)"),
barmode = "group")
Las casas cuestan sistemáticamente más que los apartamentos en una misma zona, lo cual puede explicarse porque normalmente tienen una mayor área y más espacios o servicios propios. Además, el precio varía bastante según la zona: la zona norte tiene un precio mediano de casas notablemente menor que la zona oeste, mientras que la zona sur, donde se concentra la mayoría del mercado, presenta precios medianos altos tanto para casas como para apartamentos. Esto confirma que la zona es una variable relevante para determinar el precio y justifica analizar cada solicitud dentro de su propio segmento de mercado, en lugar de utilizar un único modelo para toda la ciudad.
Dentro de base1 (ya fija la zona), se examina la
correlación del precio con las variables numéricas:
corr1 <- round(cor(base1[, vars_modelo]), 2)
plot_ly(x = colnames(corr1), y = colnames(corr1), z = corr1, type = "heatmap",
colors = colorRamp(c("steelblue", "white", "firebrick")), zmin = -1, zmax = 1) %>%
layout(title = "Matriz de correlación — Casas, Zona Norte")
plot_ly(base1, x = ~areaconst, y = ~preciom, color = ~factor(estrato),
type = "scatter", mode = "markers",
text = ~paste0("Barrio: ", barrio, "<br>Baños: ", banios, "<br>Habitaciones: ", habitaciones),
hoverinfo = "text+x+y") %>%
layout(title = "Precio vs. área construida por estrato — Casas, Zona Norte",
xaxis = list(title = "Área construida (m²)"), yaxis = list(title = "Precio (millones $)"))
El área construida es la variable con mayor correlación con el precio (0.69), seguida del estrato (0.53) y el número de baños (0.51). El número de habitaciones tiene la correlación más baja con el precio (0.37) a pesar de estar moderadamente correlacionado con el área (0.42); Esto permite anticipar que, al controlar por el área construida en el modelo múltiple, el número de habitaciones podría perder parte de su capacidad explicativa, ya que dos casas con la misma área pero diferente número de habitaciones pueden simplemente distribuir el espacio de manera distinta, sin que esto genere un cambio importante en su valor comercial. En el gráfico de dispersión también se observa claramente el efecto del estrato: para una misma área construida, las casas de estrato 5 (y algunas de estrato 6, aunque son pocas en la base) se ubican sistemáticamente por encima de las de estrato 3 y 4.
m1 <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = base1)
summary(m1)
##
## 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 los coeficientes
Ajuste del modelo (R²). El modelo explica el 60.4% de la variación del precio de las casas en zona norte (R² ajustado 59.9%). Es un ajuste razonable pero deja una parte considerable del precio sin explicar. Esto es esperable en un mercado de casas, donde factores que no están en la base, el barrio específico, el estado de la construcción, los acabados interiores, si tiene o no zonas verdes o piscina, la antigüedad del inmueble, pesan bastante y aquí no se miden. Para mejorar el ajuste se podría: (i) incorporar el barrio como variable categórica (hay bastante variación de precio entre barrios de la misma zona), (ii) usar la ubicación exacta (longitud/latitud) en vez de la zona agregada, y (iii) recoger variables de calidad de acabados o antigüedad que hoy no están en la base.
par(mfrow = c(2,2))
plot(m1)
par(mfrow = c(1,1))
shapiro.test(residuals(m1))
##
## Shapiro-Wilk normality test
##
## data: residuals(m1)
## W = 0.85246, p-value < 2.2e-16
Con un p-valor prácticamente nulo se rechaza la normalidad de los residuales. En muestras grandes esta prueba es muy sensible a desviaciones pequeñas, así que el gráfico Q-Q es más informativo que el p-valor por sí solo: el problema real está concentrado en observaciones de precio alto, no en el grueso de los datos.
bptest(m1)
##
## studentized Breusch-Pagan test
##
## data: m1
## BP = 80.281, df = 5, p-value = 7.33e-16
El p-valor es prácticamente cero: se rechaza la homocedasticidad. Se confirma lo que ya se veía en el gráfico de residuales: el error del modelo crece con el precio, algo típico en variables de precio, que suelen tener una distribución sesgada a la derecha.
vif(m1)
## areaconst estrato habitaciones parqueaderos banios
## 1.460998 1.307757 1.721015 1.226334 1.967421
Todos los factores de inflación de varianza están por debajo de 2, muy lejos del umbral habitual de preocupación (5 o 10), así que la multicolinealidad no es un problema en este modelo.
Qué se podría hacer: dado que el problema principal es la heterocedasticidad, es decir, que la variabilidad de los errores aumenta a medida que aumenta el precio, y la presencia de colas pesadas, que indica que existen algunos errores extremos más alejados de lo habitual, una alternativa natural es modelar el logaritmo del precio en lugar del precio en niveles. Esta transformación suele ayudar a estabilizar la variabilidad y reducir el efecto de los valores extremos en este tipo de variables. Se prueba a continuación como modelo de comparación, sin reemplazar el modelo principal, que se mantiene en niveles porque las predicciones del informe ejecutivo, expresadas en millones de pesos, son más directas de comunicar a la empresa.
m1_log <- lm(log(preciom) ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = base1)
summary(m1_log)
##
## Call:
## lm(formula = log(preciom) ~ areaconst + estrato + habitaciones +
## parqueaderos + banios, data = base1)
##
## Residuals:
## Min 1Q Median 3Q Max
## -1.18034 -0.16258 -0.01715 0.14973 1.06925
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 4.415e+00 7.418e-02 59.516 < 2e-16 ***
## areaconst 1.137e-03 8.822e-05 12.889 < 2e-16 ***
## estrato 2.152e-01 1.641e-02 13.113 < 2e-16 ***
## habitaciones 1.646e-02 9.453e-03 1.742 0.08229 .
## parqueaderos 4.704e-02 9.804e-03 4.798 2.22e-06 ***
## banios 4.607e-02 1.251e-02 3.683 0.00026 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.2591 on 429 degrees of freedom
## Multiple R-squared: 0.6974, Adjusted R-squared: 0.6939
## F-statistic: 197.7 on 5 and 429 DF, p-value: < 2.2e-16
comparacion1 <- data.frame(
Modelo = c("Lineal (preciom)", "Log-lineal (log(preciom))"),
R2 = c(round(summary(m1)$r.squared,3), round(summary(m1_log)$r.squared,3)),
R2_ajustado = c(round(summary(m1)$adj.r.squared,3), round(summary(m1_log)$adj.r.squared,3))
)
kable(comparacion1, caption = "Comparación de modelos — Vivienda 1")
| Modelo | R2 | R2_ajustado |
|---|---|---|
| Lineal (preciom) | 0.604 | 0.599 |
| Log-lineal (log(preciom)) | 0.697 | 0.694 |
El modelo log-lineal sube el R² de 0.604 a 0.697, una mejora real en el ajuste (el resultado de los dos modelos no es directamente comparable porque cambia la escala de la variable respuesta). En el modelo log-lineal los coeficientes se leen como efectos porcentuales: por ejemplo, cada m² adicional aumenta el precio esperado en cerca de 0.11%, y cada estrato adicional en cerca de 21.5%. Esta especificación es una buena candidata para reemplazar el modelo en niveles en una siguiente iteración, aunque para efectos de este informe se conserva el modelo en niveles por su interpretación directa en millones de pesos frente a un crédito preaprobado también en pesos.
nueva1 <- data.frame(areaconst = 200, estrato = c(4,5), habitaciones = 4, parqueaderos = 1, banios = 2)
pred1 <- predict(m1, nueva1, interval = "confidence")
kable(cbind(nueva1, round(pred1,1)), caption = "Precio esperado — Vivienda 1 (200 m², 4 hab., 1 parq., 2 baños)")
| areaconst | estrato | habitaciones | parqueaderos | banios | fit | lwr | upr |
|---|---|---|---|---|---|---|---|
| 200 | 4 | 4 | 1 | 2 | 312.1 | 287.2 | 337.0 |
| 200 | 5 | 4 | 1 | 2 | 392.7 | 360.9 | 424.6 |
Para la casa solicitada, el precio esperado es de $312 millones en estrato 4 y $393 millones en estrato 5 (intervalos de confianza al 95% para el precio medio del segmento). Frente al crédito preaprobado de $350 millones, estrato 4 queda cómodamente dentro de presupuesto y estrato 5 lo supera.
ofertas1 <- base1 %>%
filter(preciom <= 350, estrato %in% c(4,5), areaconst >= 150, habitaciones >= 3, banios >= 2) %>%
arrange(desc(preciom)) %>%
select(barrio, estrato, preciom, areaconst, parqueaderos, banios, habitaciones, longitud, latitud)
datatable(ofertas1 %>% select(-longitud, -latitud) %>% head(8),
caption = "Ofertas potenciales — Casa, Zona Norte, presupuesto $350M",
options = list(dom = 't'), rownames = FALSE)
top1 <- head(ofertas1, 8)
plot_ly(top1, x = ~longitud, y = ~latitud, type = "scatter", mode = "markers",
marker = list(size = ~areaconst/15, color = ~preciom, colorscale = "Reds", showscale = TRUE),
text = ~paste0(barrio, "<br>$", preciom, "M · ", areaconst, " m²<br>Estrato ", estrato,
" · ", habitaciones, " hab · ", banios, " baños"),
hoverinfo = "text") %>%
layout(title = "Ofertas potenciales para la Vivienda 1 (tamaño = área, color = precio)",
xaxis = list(title = "Longitud"), yaxis = list(title = "Latitud"))
Se encontraron 53 ofertas que cumplen simultáneamente el presupuesto, el estrato solicitado y áreas y baños razonablemente cercanos a lo pedido. Las 8 más relevantes están concentradas en El Bosque, La Flora y La Merced, todos sectores dentro de zona norte, con precios entre $335 y $350 millones y áreas de 160 a 300 m². Estas opciones dejan poco margen de negociación frente al presupuesto, por lo que se recomienda a María priorizar las de menor precio dentro del grupo como primer punto de partida para la negociación.
Se repite la misma secuencia de seis pasos, ahora para apartamentos en zona sur.
base2_bruta <- vivienda %>% filter(tipo == "Apartamento", zona == "Zona Sur")
kable(head(base2_bruta, 3), caption = "Primeros 3 registros de base2 (Apartamento, Zona Sur)")
| id | zona | piso | estrato | preciom | areaconst | parqueaderos | banios | habitaciones | tipo | barrio | longitud | latitud |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 5098 | Zona Sur | 05 | 4 | 290 | 96 | 1 | 2 | 3 | Apartamento | acopi | -76.53464 | 3.44987 |
| 698 | Zona Sur | 02 | 3 | 78 | 40 | 1 | 1 | 2 | Apartamento | aguablanca | -76.50100 | 3.40000 |
| 8199 | Zona Sur | NA | 6 | 875 | 194 | 2 | 5 | 3 | Apartamento | aguacatal | -76.55700 | 3.45900 |
kable(t(colSums(is.na(base2_bruta))), caption = "Valores faltantes en base2 antes de depurar")
| id | zona | piso | estrato | preciom | areaconst | parqueaderos | banios | habitaciones | tipo | barrio | longitud | latitud |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 622 | 0 | 0 | 0 | 406 | 0 | 0 | 0 | 0 | 0 | 0 |
base2 reúne 2787 ofertas. Igual que
antes, piso no se depura, y sí se depuran los 406 registros
sin parqueaderos:
base2 <- base2_bruta %>% filter(if_all(all_of(vars_modelo), ~ !is.na(.)))
cat("Registros después de depurar:", nrow(base2))
## Registros después de depurar: 2381
kable(table(base2$tipo, base2$zona), caption = "Comprobación cruzada tipo x zona en base2")
| Zona Sur | |
|---|---|
| Apartamento | 2381 |
Mapa de los puntos:
plot_ly() %>%
add_trace(data = vivienda %>% filter(!is.na(longitud)),
x = ~longitud, y = ~latitud, type = "scattergl", mode = "markers",
marker = list(size = 4, color = "lightgrey"),
name = "Resto del mercado", hoverinfo = "skip") %>%
add_trace(data = base2_bruta %>% filter(!is.na(longitud)),
x = ~longitud, y = ~latitud, type = "scattergl", mode = "markers",
marker = list(size = 6, color = "darkgreen"),
name = "Apartamentos · Zona Sur",
text = ~paste0("Barrio: ", barrio, "<br>Estrato: ", estrato, "<br>Precio: $", preciom, "M"),
hoverinfo = "text") %>%
layout(title = "Apartamentos etiquetados como Zona Sur sobre el conjunto de la ciudad",
xaxis = list(title = "Longitud"), yaxis = list(title = "Latitud"))
base2_geo <- base2_bruta %>% filter(!is.na(longitud)) %>%
rowwise() %>% mutate(zona_cercana = zona_mas_cercana(longitud, latitud)) %>% ungroup()
tab_geo2 <- table(declarada = base2_geo$zona, mas_cercana = base2_geo$zona_cercana)
kable(tab_geo2, caption = "Zona declarada vs. zona geográficamente más cercana (apartamentos zona sur)")
| Zona Centro | Zona Norte | Zona Oeste | Zona Oriente | Zona Sur | |
|---|---|---|---|---|---|
| Zona Sur | 188 | 75 | 135 | 95 | 2294 |
pct_mismatch2 <- round(mean(base2_geo$zona_cercana != "Zona Sur") * 100, 1)
Aquí el desajuste es menor: 17.7% de los apartamentos etiquetados como zona sur quedan geográficamente más cerca de otro centroide, frente al 28.7% de la vivienda 1. Zona sur es, además, la zona con más inventario del mercado (2787 apartamentos), lo que puede explicar un polígono más consolidado y con fronteras más claras que zona norte. Aun así, se recomienda el mismo cuidado: no descartar del todo estos puntos, pero sí tratarlos con precaución si aparecen entre las comparables finales.
corr2 <- round(cor(base2[, vars_modelo]), 2)
plot_ly(x = colnames(corr2), y = colnames(corr2), z = corr2, type = "heatmap",
colors = colorRamp(c("steelblue", "white", "firebrick")), zmin = -1, zmax = 1) %>%
layout(title = "Matriz de correlación — Apartamentos, Zona Sur")
plot_ly(base2, x = ~areaconst, y = ~preciom, color = ~factor(estrato),
type = "scatter", mode = "markers",
text = ~paste0("Barrio: ", barrio, "<br>Baños: ", banios, "<br>Habitaciones: ", habitaciones),
hoverinfo = "text+x+y") %>%
layout(title = "Precio vs. área construida por estrato — Apartamentos, Zona Sur",
xaxis = list(title = "Área construida (m²)"), yaxis = list(title = "Precio (millones $)"))
En apartamentos las correlaciones con el precio son más altas que en casas: área construida (0.74), baños (0.71), parqueaderos (0.69) y estrato (0.65) están todas por encima de 0.6. El número de habitaciones vuelve a ser la más débil (0.3); en apartamentos esto es todavía más marcado que en casas, porque en un mismo edificio el área del apartamento está más estandarizada por el diseño arquitectónico, y añadir una habitación casi siempre implica subdividir el espacio existente en vez de ampliarlo.
m2 <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = base2)
summary(m2)
##
## 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 los coeficientes:
Ajuste del modelo. El R² es 74.9% (ajustado 74.8%), muy superior al de la vivienda 1. Con una base casi 6 veces más grande y variables de mayor peso relativo, el modelo de apartamentos en zona sur captura mucho mejor el precio. Aun así, queda cerca de una cuarta parte de la variación sin explicar, atribuible a las mismas variables no observadas (barrio específico, antigüedad, acabados, vista, amenidades del edificio) que limitan al modelo de la vivienda 1.
par(mfrow = c(2,2))
plot(m2)
par(mfrow = c(1,1))
shapiro.test(residuals(m2))
##
## Shapiro-Wilk normality test
##
## data: residuals(m2)
## W = 0.79118, p-value < 2.2e-16
bptest(m2)
##
## studentized Breusch-Pagan test
##
## data: m2
## BP = 754.81, df = 5, p-value < 2.2e-16
vif(m2)
## areaconst estrato habitaciones parqueaderos banios
## 2.066518 1.545162 1.429280 1.737878 2.529494
El patrón es similar al de la vivienda 1, aunque más marcado debido al mayor tamaño de la muestra: se rechaza la normalidad y la homocedasticidad, con p-valores prácticamente nulos en ambas pruebas. Los residuales presentan colas pesadas y una varianza que aumenta en los precios más altos, algo que también se puede observar en los gráficos Q-Q y de residuales vs. ajustados. La multicolinealidad tampoco representa un problema en este modelo, ya que todos los VIF se encuentran por debajo de 3. Al trabajar con una muestra grande, estas pruebas son muy sensibles, por lo que incluso pequeñas desviaciones de los supuestos ideales pueden resultar significativas. Para el negocio, lo más importante es que, al igual que en la vivienda 1, el error de predicción aumenta con el precio del inmueble, por lo que las predicciones son más confiables para apartamentos de precio medio que para los más costosos del segmento.
m2_log <- lm(log(preciom) ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = base2)
comparacion2 <- data.frame(
Modelo = c("Lineal (preciom)", "Log-lineal (log(preciom))"),
R2 = c(round(summary(m2)$r.squared,3), round(summary(m2_log)$r.squared,3)),
R2_ajustado = c(round(summary(m2)$adj.r.squared,3), round(summary(m2_log)$adj.r.squared,3))
)
kable(comparacion2, caption = "Comparación de modelos — Vivienda 2")
| Modelo | R2 | R2_ajustado |
|---|---|---|
| Lineal (preciom) | 0.749 | 0.748 |
| Log-lineal (log(preciom)) | 0.788 | 0.787 |
Igual que en la vivienda 1, el modelo log-lineal mejora el ajuste (R² de 0.749 a 0.788), reforzando la recomendación de explorar esta especificación en una siguiente iteración del modelo, sin que cambie la conclusión de negocio de esta sección.
nueva2 <- data.frame(areaconst = 300, estrato = c(5,6), habitaciones = 5, parqueaderos = 3, banios = 3)
pred2 <- predict(m2, nueva2, interval = "confidence")
kable(cbind(nueva2, round(pred2,1)), caption = "Precio esperado — Vivienda 2 (300 m², 5 hab., 3 parq., 3 baños)")
| areaconst | estrato | habitaciones | parqueaderos | banios | fit | lwr | upr |
|---|---|---|---|---|---|---|---|
| 300 | 5 | 5 | 3 | 3 | 675.0 | 652.1 | 697.9 |
| 300 | 6 | 5 | 3 | 3 | 735.9 | 712.7 | 759.1 |
Para el apartamento solicitado, el precio esperado es de $675 millones en estrato 5 y $736 millones en estrato 6. Frente al crédito preaprobado de $850 millones, ambos escenarios quedan dentro de presupuesto, con un margen de $115 a $175 millones.
ofertas2 <- base2 %>%
filter(preciom <= 850, estrato %in% c(5,6), areaconst >= 200, habitaciones >= 4,
banios >= 3, parqueaderos >= 2) %>%
arrange(desc(preciom)) %>%
select(barrio, estrato, preciom, areaconst, parqueaderos, banios, habitaciones, longitud, latitud)
datatable(ofertas2 %>% select(-longitud, -latitud) %>% head(8),
caption = "Ofertas potenciales — Apartamento, Zona Sur, presupuesto $850M",
options = list(dom = 't'), rownames = FALSE)
top2 <- head(ofertas2, 8)
plot_ly(top2, x = ~longitud, y = ~latitud, type = "scatter", mode = "markers",
marker = list(size = ~areaconst/25, color = ~preciom, colorscale = "Greens", showscale = TRUE),
text = ~paste0(barrio, "<br>$", preciom, "M · ", areaconst, " m²<br>Estrato ", estrato,
" · ", habitaciones, " hab · ", banios, " baños"),
hoverinfo = "text") %>%
layout(title = "Ofertas potenciales para la Vivienda 2 (tamaño = área, color = precio)",
xaxis = list(title = "Longitud"), yaxis = list(title = "Latitud"))
Se encontraron 23 ofertas que cumplen presupuesto, estrato y áreas/baños/parqueaderos cercanos a lo solicitado. A diferencia de la vivienda 1, aquí el presupuesto sí deja margen: los precios de las 8 ofertas destacadas van desde $650 hasta $750 millones, todos por debajo del tope de $850 millones, en barrios como Guadalupe, El Ingenio, Pance y Ciudadela Pasoancho. Se recomienda a María priorizar las de mayor área dentro de este grupo, ya que el presupuesto lo permite y el área construida es la variable que más explica el precio en este segmento.