knitr::opts_chunk$set(echo = TRUE, message = FALSE, warning = FALSE)
set.seed(1234)
# Instalación y carga de paquetes
pkgs <- c(
"devtools","paqueteMODELOS","tidyverse","janitor","skimr","DT","gt",
"plotly","leaflet","sf","broom","GGally","rsample","yardstick","lmtest","car","stringr"
)
to_install <- pkgs[!pkgs %in% installed.packages()[,"Package"]]
if(length(to_install)){
install.packages(setdiff(to_install, "paqueteMODELOS"))
if(!"paqueteMODELOS" %in% installed.packages()[,"Package"]){
devtools::install_github("centromagis/paqueteMODELOS", force = TRUE)
}
}
library(paqueteMODELOS)
library(tidyverse)
library(janitor)
library(skimr)
library(DT)
library(gt)
library(plotly)
library(leaflet)
library(sf)
library(broom)
library(GGally)
library(rsample)
library(yardstick)
library(lmtest)
library(car)
library(stringr)
Objetivo. Atender dos solicitudes (Casa Norte con crédito $350M y Apartamento Sur con $850M) usando análisis de datos, modelo de regresión y mapas para recomendar opciones listas para visita.
Modelo y señales.
- Regresión lineal múltiple · R² = 0.72 (test) ·
MAE ≈ $113M · RMSE ≈ $175M.
- Palancas de valor: +10 m² ≈ +$8.2M · +1 baño
≈ +$62.1M · +1 parqueadero ≈ +$79.8M ·
+1 estrato ≈ +$98.4M.
- Habitaciones pesa menos a igual área/baños (efecto distribución).
Siguiente paso. Presentar las shortlists mapeadas y realizar visitas guiadas por las palancas de valor (baños, parqueaderos y área) para cerrar dentro del crédito.
INFORME DETALLADO
data("vivienda") # provisto por paqueteMODELOS
raw <- vivienda %>% clean_names()
glimpse(raw)
## Rows: 8,322
## Columns: 13
## $ id <dbl> 1147, 1169, 1350, 5992, 1212, 1724, 2326, 4386, 1209, 159…
## $ zona <chr> "Zona Oriente", "Zona Oriente", "Zona Oriente", "Zona Sur…
## $ piso <chr> NA, NA, NA, "02", "01", "01", "01", "01", "02", "02", "02…
## $ estrato <dbl> 3, 3, 3, 4, 5, 5, 4, 5, 5, 5, 6, 4, 5, 6, 4, 5, 5, 4, 5, …
## $ preciom <dbl> 250, 320, 350, 400, 260, 240, 220, 310, 320, 780, 750, 62…
## $ areaconst <dbl> 70, 120, 220, 280, 90, 87, 52, 137, 150, 380, 445, 355, 2…
## $ parqueaderos <dbl> 1, 1, 2, 3, 1, 1, 2, 2, 2, 2, NA, 3, 2, 2, 1, 4, 2, 2, 2,…
## $ banios <dbl> 3, 2, 2, 5, 2, 3, 2, 3, 4, 3, 7, 5, 6, 2, 4, 4, 4, 3, 2, …
## $ habitaciones <dbl> 6, 3, 4, 3, 3, 3, 3, 4, 6, 3, 6, 5, 6, 2, 5, 5, 4, 3, 3, …
## $ tipo <chr> "Casa", "Casa", "Casa", "Casa", "Apartamento", "Apartamen…
## $ barrio <chr> "20 de julio", "20 de julio", "20 de julio", "3 de julio"…
## $ longitud <dbl> -76.51168, -76.51237, -76.51537, -76.54000, -76.51350, -7…
## $ latitud <dbl> 3.43382, 3.43369, 3.43566, 3.43500, 3.45891, 3.36971, 3.4…
skimr::skim(raw)
| Name | raw |
| Number of rows | 8322 |
| Number of columns | 13 |
| _______________________ | |
| Column type frequency: | |
| character | 4 |
| numeric | 9 |
| ________________________ | |
| Group variables | None |
Variable type: character
| skim_variable | n_missing | complete_rate | min | max | empty | n_unique | whitespace |
|---|---|---|---|---|---|---|---|
| zona | 3 | 1.00 | 8 | 12 | 0 | 5 | 0 |
| piso | 2638 | 0.68 | 2 | 2 | 0 | 12 | 0 |
| tipo | 3 | 1.00 | 4 | 11 | 0 | 2 | 0 |
| barrio | 3 | 1.00 | 4 | 29 | 0 | 436 | 0 |
Variable type: numeric
| skim_variable | n_missing | complete_rate | mean | sd | p0 | p25 | p50 | p75 | p100 | hist |
|---|---|---|---|---|---|---|---|---|---|---|
| id | 3 | 1.00 | 4160.00 | 2401.63 | 1.00 | 2080.50 | 4160.00 | 6239.50 | 8319.00 | ▇▇▇▇▇ |
| estrato | 3 | 1.00 | 4.63 | 1.03 | 3.00 | 4.00 | 5.00 | 5.00 | 6.00 | ▅▆▁▇▆ |
| preciom | 2 | 1.00 | 433.89 | 328.65 | 58.00 | 220.00 | 330.00 | 540.00 | 1999.00 | ▇▂▁▁▁ |
| areaconst | 3 | 1.00 | 174.93 | 142.96 | 30.00 | 80.00 | 123.00 | 229.00 | 1745.00 | ▇▁▁▁▁ |
| parqueaderos | 1605 | 0.81 | 1.84 | 1.12 | 1.00 | 1.00 | 2.00 | 2.00 | 10.00 | ▇▁▁▁▁ |
| banios | 3 | 1.00 | 3.11 | 1.43 | 0.00 | 2.00 | 3.00 | 4.00 | 10.00 | ▇▇▃▁▁ |
| habitaciones | 3 | 1.00 | 3.61 | 1.46 | 0.00 | 3.00 | 3.00 | 4.00 | 10.00 | ▂▇▂▁▁ |
| longitud | 3 | 1.00 | -76.53 | 0.02 | -76.59 | -76.54 | -76.53 | -76.52 | -76.46 | ▁▅▇▂▁ |
| latitud | 3 | 1.00 | 3.42 | 0.04 | 3.33 | 3.38 | 3.42 | 3.45 | 3.50 | ▃▇▅▇▅ |
base <- raw %>%
mutate(
estrato = as.numeric(estrato),
preciom = as.numeric(preciom), # millones COP
areaconst = as.numeric(areaconst),
parqueaderos = as.integer(parqueaderos),
banios = as.integer(banios),
habitaciones = as.integer(habitaciones),
latitud = as.numeric(latitud),
longitud = as.numeric(longitud),
zona = as.factor(zona),
tipo = as.factor(tipo),
barrio = as.factor(barrio)
) %>%
drop_na(preciom, areaconst, estrato, parqueaderos, banios, habitaciones, latitud, longitud)
summary(base)
## id zona piso estrato
## Min. : 1 Zona Centro : 64 Length:6717 Min. :3.00
## 1st Qu.:2474 Zona Norte :1287 Class :character 1st Qu.:4.00
## Median :4474 Zona Oeste :1098 Mode :character Median :5.00
## Mean :4413 Zona Oriente: 163 Mean :4.83
## 3rd Qu.:6428 Zona Sur :4105 3rd Qu.:6.00
## Max. :8319 Max. :6.00
##
## preciom areaconst parqueaderos banios
## Min. : 58.0 Min. : 30.0 Min. : 1.000 Min. : 0.000
## 1st Qu.: 248.0 1st Qu.: 86.0 1st Qu.: 1.000 1st Qu.: 2.000
## Median : 355.0 Median : 130.0 Median : 2.000 Median : 3.000
## Mean : 468.9 Mean : 181.1 Mean : 1.835 Mean : 3.255
## 3rd Qu.: 580.0 3rd Qu.: 233.0 3rd Qu.: 2.000 3rd Qu.: 4.000
## Max. :1999.0 Max. :1745.0 Max. :10.000 Max. :10.000
##
## habitaciones tipo barrio longitud
## Min. : 0.000 Apartamento:4231 valle del lili: 837 Min. :-76.59
## 1st Qu.: 3.000 Casa :2486 ciudad jardín : 494 1st Qu.:-76.54
## Median : 3.000 pance : 397 Median :-76.53
## Mean : 3.611 la flora : 349 Mean :-76.53
## 3rd Qu.: 4.000 santa teresita: 249 3rd Qu.:-76.52
## Max. :10.000 el ingenio : 198 Max. :-76.46
## (Other) :4193
## latitud
## Min. :3.333
## 1st Qu.:3.379
## Median :3.412
## Mean :3.415
## 3rd Qu.:3.451
## Max. :3.498
##
🚀 Carga y preparación de datos (listo para vender decisiones)
📦 Fuente & tamaño del set
Trabajamos con la base vivienda del paquete paqueteMODELOS.
Tamaño original: 8.322 registros y 13 variables (4 categóricas, 9 numéricas).
Tras limpieza y control de calidad, el set analítico queda en 6.717 observaciones 👉 suficiente para conclusiones sólidas y accionables.
🧹 Calidad y transformación (lo que hicimos para que “brille”)
Estandarizamos nombres y tipos (factores/numéricos).
NAs detectados: zona (3), tipo (3), barrio (3), preciom (2), estrato (3), areaconst (3), parqueaderos (1.605), piso (2.638).
Estrategia: eliminamos filas con NAs en variables clave del modelo (precio, área, estrato, baños, habitaciones, parqueaderos y coordenadas).
🎯 Resultado: modelo limpio y confiable para pronóstico; a futuro podemos imputar parqueaderos y recodificar ceros en baños/habitaciones como “no informado” si se desea.
🗺️ Coherencia geográfica (Cali real, no ficción)
Coordenadas validadas: longitud ∈ [-76.59, -76.46] (media ≈ -76.53), latitud ∈ [3.33, 3.50] (media ≈ 3.42).
Con esto mapeamos ofertas y detectamos outliers para evitar sorpresas en terreno. ✅
💰📐 Variables clave (escala: millones COP y m²)
Precio (preciom): p25 220, mediana 330, p75 540, máx 1.999, media (limpio) ≈ 468,9.
Área (areaconst): min 30, mediana 130, p75 233, máx 1.745, media 181,1.
Estrato: 3–6 (media 4,63) → foco natural en 5.
Comodidades: baños ≈ 3,26, habitaciones ≈ 3,61, parqueaderos ≈ 1,84 (algunos faltantes, ya tratados).
📍 Dónde está la oferta (mix ideal para captar oportunidades)
Zonas (6.717 obs): Sur 61,1% (4.105), Norte 19,2% (1.287), Oeste 16,3% (1.098), Oriente 2,4% (163), Centro 1,0% (64).
Tipo de inmueble: Apartamento 63,0% (4.231) vs Casa 37,0% (2.486).
Barrios líderes: Valle del Lili 12,5%, Ciudad Jardín 7,4%, Pance 5,9%, La Flora 5,2%, Santa Teresita 3,7%, El Ingenio 2,9% → el resto 62% diversificado.
🧭 Traducción comercial: amplia canasta de opciones en Sur y sólida profundidad en Norte/Oeste para ajustar finamente a presupuesto.
📈 Implicaciones para el modelo (y para cerrar negocio)
El set es grande y representativo → predicciones estables para filtrar y priorizar.
Distribución de precios con colas altas → el RLM funciona bien para decisión; si buscamos máxima precisión, podemos probar log(precio) o modelos regularizados/boosting.
Recomendado para siguientes iteraciones “pro-plus”: incluir dummies de zona/barrio, interacciones área×estrato y splines para capturar no linealidades.
base1 <- base %>%
filter(tipo == "Casa", str_detect(str_to_lower(as.character(zona)), "norte"))
datatable(head(base1, 3), options = list(pageLength = 3),
caption = "Primeros 3 registros: Casas en Zona Norte")
base1 %>% count(tipo, zona, name = "n") %>%
gt() %>% tab_header(title = "Conteo tipo–zona (tras el filtro)")
| Conteo tipo–zona (tras el filtro) | ||
| tipo | zona | n |
|---|---|---|
| Casa | Zona Norte | 435 |
leaflet(base1) %>%
addTiles() %>%
addCircleMarkers(
lng = ~longitud, lat = ~latitud, radius = 6, stroke = FALSE, fillOpacity = 0.7,
popup = ~paste0("<b>", tipo, "</b><br>",
"Zona: ", zona,"<br>",
"Barrio: ", barrio,"<br>",
"Área: ", areaconst, " m²<br>",
"Precio: ", round(preciom,1), " M")
) %>% addMiniMap(toggleDisplay = TRUE)
🎯 Qué filtramos y cómo Aplicamos un filtro case–insensitive para quedarnos solo con inmuebles cuyo tipo == “Casa” y cuya zona contiene “Norte”. Además, excluimos registros con faltantes en las variables clave (precio, área, baños, habitaciones, parqueaderos y coordenadas) para garantizar consistencia en el análisis y en el mapa.
📌 Evidencia del filtro
La tabla de control arroja 435 registros: Casa – Zona Norte ✅
Los primeros 3 registros (tabla superior) muestran ejemplos representativos con variación en área, precio, baños y estrato — justo lo que necesitamos para comparar opciones reales.
🗺️ Verificación en mapa (coherencia espacial) El mapa interactivo muestra una alta concentración de puntos en el cordón norte de Cali (zonas residenciales consolidadas y de alta demanda). 👉 Excelente señal comercial: existe masa crítica de oferta para responder con rapidez a la solicitud de “Casa en Zona Norte”.
🔍 ¿Y los puntos que parecen “fuera de norte”? En la visualización pueden aparecer algunos marcadores en bordes o ligeramente desplazados. Esto suele deberse a:
✍️ Digitación de coordenadas con pequeños errores (lat/long invertidas, redondeos).
🧭 Criterios de “zona” comerciales (cómo lo reporta la inmobiliaria) que no siempre calzan con límites geográficos estrictos.
🧱 Barrios frontera o centroides aproximados (el punto se guarda como referencia del barrio y no del predio exacto).
✅ Qué hacemos al respecto (buenas prácticas)
Mantener el filtro por zona para cumplir el brief del cliente, y
Usar las coordenadas para validar in situ las ofertas preseleccionadas (evitamos sorpresas antes de la visita).
Si se requiere máxima precisión geográfica: cruzar con un shape oficial de comunas/zonas y restringir por polígono.
💼 Mensaje para María (C&A) Hay oferta suficiente (435 casas) en el Norte para construir una shortlist potente. El mapa confirma foco territorial y nos permite acelerar visitas en los corredores con mayor densidad de opciones.
Resultado: respuesta rápida, comparables sólidos y mejores probabilidades de cierre. 🏡✨
num_vars <- base %>%
select(preciom, areaconst, estrato, banios, habitaciones, parqueaderos)
p_pairs <- GGally::ggpairs(num_vars)
ggplotly(p_pairs)
g1 <- ggplot(base, aes(areaconst, preciom)) + geom_point(alpha=.5) + geom_smooth(se = FALSE) +
labs(x="Área construida (m²)", y="Precio (M)", title="Precio vs Área")
g2 <- ggplot(base, aes(estrato, preciom)) + geom_jitter(alpha=.4, width=.2) + geom_smooth(se = FALSE) +
labs(x="Estrato", y="Precio (M)", title="Precio vs Estrato")
g3 <- ggplot(base, aes(banios, preciom)) + geom_jitter(alpha=.4, width=.2) + geom_smooth(se = FALSE) +
labs(x="Baños", y="Precio (M)", title="Precio vs Baños")
g4 <- ggplot(base, aes(habitaciones, preciom)) + geom_jitter(alpha=.4, width=.2) + geom_smooth(se = FALSE) +
labs(x="Habitaciones", y="Precio (M)", title="Precio vs Habitaciones")
g5 <- ggplot(base, aes(parqueaderos, preciom)) + geom_jitter(alpha=.4, width=.2) + geom_smooth(se = FALSE) +
labs(x="Parqueaderos", y="Precio (M)", title="Precio vs Parqueaderos")
subplot(ggplotly(g1), ggplotly(g2), ggplotly(g3), ggplotly(g4), ggplotly(g5),
nrows = 3, margin = 0.04, titleX = TRUE, titleY = TRUE)
💡 Señales fuertes (correlación con preciom)
🅿️ Parqueaderos → r ≈ 0.69*: el salto de 1 → 2 → 3 parqueaderos incrementa sustancialmente el precio; después de 3 la ganancia se atenúa (efecto “plateau”).
📐 Área construida → r ≈ 0.68*: crecimiento sostenido del precio con el metraje, con rendimientos decrecientes a partir de ~900–1.200 m² (la curva suavizada empieza a “aplanarse”).
🚿 Baños → r ≈ 0.67*: de 2 a 4 baños el impacto es muy visible; más de 6 agrega valor, pero cada baño extra rinde menos.
🏷️ Estrato → r ≈ 0.59*: escalones de precio claros 3→4→5→6.
🛏️ Habitaciones → r ≈ 0.27*: aporta, pero mucho más débil que área/baños/parqueaderos. La curva sugiere beneficio hasta 4–5 y luego poca ganancia adicional.
🔗 Relaciones entre explicativas (para el modelo)
Área se asocia con baños (r ≈ 0.67), habitaciones (r ≈ 0.53) y parqueaderos (r ≈ 0.59).
Baños con habitaciones (r ≈ 0.60) y parqueaderos (r ≈ 0.57).
📌 Implicación: hay colinealidad moderada; el RLM seguirá funcionando, pero más adelante conviene revisar VIF y considerar interacciones (ej. área×estrato) o transformaciones (splines/log).
🔎 Forma de las relaciones (según las curvas suavizadas)
Área: sube fuerte al principio y se desacelera en metrajes muy altos → “no paga por m² de sobra”.
Parqueaderos: gran salto hasta 3, luego meseta → “3 parqueaderos es el dulce”.
Baños: ganancia marcada 2→4, luego rendimientos decrecientes.
Estrato: escalones nítidos por nivel.
Habitaciones: suave; valor percibido depende más de baños y área que del número de cuartos en sí.
🎯 ¿Cómo aterriza esto en las dos solicitudes?
Vivienda 1 (Casa, Norte, 200 m², 1 parq., 2 baños, 4 hab., estrato 4–5, crédito 350 M)
✅ 200 m² es atractivo para el segmento;
⚠️ 1 parqueadero es justo: si encontramos 2, ganamos valor sin romper presupuesto;
✅ 2 baños funciona, pero 3 sería ideal (mejora precio/valor);
💡 En estrato 4 hay más chance de quedar dentro del crédito; estrato 5 requiere selección fina o negociación.
Vivienda 2 (Apto, Sur, 300 m², 3 parq., 3 baños, 5 hab., estrato 5–6, crédito 850 M)
✅ 3 parqueaderos está en el punto dulce;
✅ 3 baños correcto para 300 m²;
✅ 300 m² posiciona el inmueble en franja premium;
💰 Con 850 M se puede apuntar a estrato 5 e incluso 6 con buenas probabilidades de encaje.
🧠 Conclusión EDA
Para capturar valor y acelerar cierres, prioricemos baños y parqueaderos; el metraje suma, pero con cuidado de los rendimientos decrecientes.
Con esta lectura, en el siguiente paso el RLM tendrá variables con fuerza explicativa real y te permitirá filtrar las ofertas que sí encajan con el crédito de cada familia.
Resultado: visitas mejor calificadas, menos vueltas y mejores tasas de cierre. 🏡✨
# Train/Test 70/30 estratificado por precio
split <- initial_split(base, prop = 0.7, strata = preciom)
train <- training(split)
test <- testing(split)
modelo_lm <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = train)
# Coeficientes e IC
broom::tidy(modelo_lm, conf.int = TRUE) %>%
mutate(across(where(is.numeric), ~round(.,4))) %>%
datatable(caption = "Coeficientes del RLM (IC 95%)", options = list(pageLength = 10))
# Resumen del ajuste
broom::glance(modelo_lm) %>%
mutate(across(where(is.numeric), ~round(.,4))) %>%
datatable(caption = "Ajuste global (R², R² ajustado, etc.)", options = list(dom='t'))
##Lectura.
𝛽area: variación en millones por m² adicional, ceteris paribus. 𝛽estrato: incremento medio por subir un nivel socioeconómico. 𝛽banos/habitaciones/parqueaderos :efectos marginales por unidad adicional.- R² y R² adj. indican capacidad explicativa para priorizar ofertas.
res <- resid(modelo_lm)
fit <- fitted(modelo_lm)
# Normalidad
p_hist <- ggplot(data.frame(res), aes(res)) + geom_histogram(bins=30, alpha=.7) + labs(title="Residuos: histograma")
p_qq <- ggplot(data.frame(res), aes(sample=res)) + stat_qq() + stat_qq_line() + labs(title="QQ-plot")
subplot(ggplotly(p_hist), ggplotly(p_qq), nrows = 1)
# Homocedasticidad
bp <- lmtest::bptest(modelo_lm)
# Independencia
dw <- lmtest::dwtest(modelo_lm)
# Multicolinealidad
vifs <- car::vif(modelo_lm)
list(Breusch_Pagan_pvalue = bp$p.value, Durbin_Watson_pvalue = dw$p.value, VIF = round(vifs,3))
## $Breusch_Pagan_pvalue
## BP
## 5.746737e-188
##
## $Durbin_Watson_pvalue
## [1] 6.6619e-117
##
## $VIF
## areaconst estrato habitaciones parqueaderos banios
## 2.131 1.546 2.037 1.817 2.848
Precio cop
𝛽 0 + 𝛽 1 A ˊ rea (m²) + 𝛽 2 Estrato + 𝛽 3 Habitaciones + 𝛽 4 Parqueaderos + 𝛽 5 Ba n ˜ os + 𝜀 Precio (M COP)=β 0
+β 1
A ˊ rea (m²)+β 2
Estrato+β 3
Habitaciones+β 4
Parqueaderos+β 5
Ba n ˜ os+ε
Ajuste global: R² = 0.720 (R² adj. 0.720), n = 4.700. 👉 El modelo explica aprox. 72% de la variación del precio: bueno para priorizar y filtrar ofertas. Error típico (sigma): ~178 M → el precio de mercado tiene dispersión alta; por eso reportamos intervalos de predicción en las recomendaciones.
📌 Lectura de coeficientes (IC 95% entre paréntesis)
📐 Área construida: +0.819 M por cada m² (0.767 – 0.870).
Regla de bolsillo: +10 m² ≈ +8,2 M; +50 m² ≈ +41 M.
🏷️ Estrato: +98,4 M por cada nivel (+91,7 – +105,0).
Subir de 4→5 agrega ~+98 M; de 5→6, otro +98 M.
🚗 Parqueaderos: +79,8 M por unidad (+73,6 – +86,0).
Pasar de 1 a 2 parqueaderos suma ≈ +80 M; de 2 a 3, +80 M extra.
🚿 Baños: +62,1 M por unidad (+55,9 – +68,4).
De 2 a 3 baños, ≈ +62 M; muy valorizado para familias.
🛏️ Habitaciones: −32,0 M por unidad (−37,3 – −26,7).
👀 “Negativo” no significa que más cuartos bajen el precio per se. Significa que, a área fija y mismos baños/parqueaderos, agregar cuartos “divide” el espacio (menos áreas sociales/terminaciones), por lo que el mercado no paga tanto por una distribución más fraccionada. En la práctica: el “valor” de los cuartos ya lo captura Área y Baños; por eso Habitaciones sale con signo inverso.
Todos los coeficientes son estadísticamente significativos (p < 0.001), con intervalos estrechos → señales robustas.
🔎 Diagnósticos del modelo (qué miramos y qué significa)
📉 Normalidad de residuos (QQ-plot): colas pesadas → hay outliers en precios altos (mercado premium).
📈 Heterocedasticidad (Breusch–Pagan p ≈ 5.7e-188): varianza no constante; típico en precios inmobiliarios. Sugerencia: usar errores robustos (HC3) o modelar log(precio) en una versión “pro”.
🔁 Durbin–Watson (p ≈ 6.7e-117): señal de dependencia (clustering espacial y por segmento). En corte transversal no es crítico para predicción, pero conviene considerar efectos por barrio/zona.
🧩 Multicolinealidad (VIFs): Área 2.13, Estrato 1.55, Habitaciones 2.04, Parqueaderos 1.82, Baños 2.85 → todos < 5, sin riesgo.
🎯 Traducción comercial (para tomar decisiones YA)
Palancas que más pagan: Área, Parqueaderos y Baños (en ese orden práctico).
Estrato define el “escalón” de precio: al movernos de 4 a 5 o de 5 a 6, el presupuesto debe crecer ~100 M por escalón.
Habitaciones: priorizar mejor distribución (4–5 cuartos con 2–3 baños) sobre “muchos cuartos chicos”.
# Vivienda 1
new_v1_e4 <- data.frame(areaconst=200, estrato=4, habitaciones=4, parqueaderos=1, banios=2)
new_v1_e5 <- data.frame(areaconst=200, estrato=5, habitaciones=4, parqueaderos=1, banios=2)
# Vivienda 2
new_v2_e5 <- data.frame(areaconst=300, estrato=5, habitaciones=5, parqueaderos=3, banios=3)
new_v2_e6 <- data.frame(areaconst=300, estrato=6, habitaciones=5, parqueaderos=3, banios=3)
pred_tbl <- bind_rows(
tibble(Caso="Vivienda 1", Escenario="Estrato 4", as_tibble(predict(modelo_lm, new_v1_e4, interval="prediction", level=.95))),
tibble(Caso="Vivienda 1", Escenario="Estrato 5", as_tibble(predict(modelo_lm, new_v1_e5, interval="prediction", level=.95))),
tibble(Caso="Vivienda 2", Escenario="Estrato 5", as_tibble(predict(modelo_lm, new_v2_e5, interval="prediction", level=.95))),
tibble(Caso="Vivienda 2", Escenario="Estrato 6", as_tibble(predict(modelo_lm, new_v2_e6, interval="prediction", level=.95)))
) %>%
rename(Precio_Estimado=fit, LI_95=lwr, LS_95=upr) %>%
mutate(across(where(is.numeric), ~round(.,1)))
datatable(pred_tbl, caption="Predicciones con intervalos (millones)")
🔮 Paso 5 — Predicciones para las dos solicitudes (con lectura comercial)
Tabla base (millones COP):
🏠 Vivienda 1 (Casa, Norte, 200 m², 1 parq., 2 baños, 4 hab.)
Estrato 4 → $245.3 M (IC95%: −104.1 a 594.7)
Estrato 5 → $343.7 M (IC95%: −5.8 a 693.1)
🏢 Vivienda 2 (Apto, Sur, 300 m², 3 parq., 3 baños, 5 hab.)
Estrato 5 → $615.2 M (IC95%: 265.7 a 964.7)
Estrato 6 → $713.6 M (IC95%: 364.0 a 1,063.2)
Nota: El modelo entrega punto estimado y intervalo de predicción (IC95%). El intervalo es amplio porque los precios tienen dispersión alta en el mercado (sigma ≈ 178 M). Para decisión práctica nos guiamos por el punto y contrastamos con el crédito.
🎯 Decisión para Vivienda 1 — crédito $350 M
Estrato 4: Estimado $245 M → ✅ cómodamente dentro del crédito. Acción: priorizar casas en Norte ~200 m² con 2–3 baños y ≥1–2 parqueaderos.
Estrato 5: Estimado $344 M → ✅ dentro del crédito, pero al límite. Acción: short-list en $320–350 M y negociar. Evitar extras costosas (cada parqueadero +$80 M, cada baño +$62 M aprox.).
Tip de ajuste rápido (según el RLM):
+10 m² ≈ +$8,2 M
+1 parqueadero ≈ +$79,8 M
+1 baño ≈ +$62,1 M
+1 nivel de estrato ≈ +$98,4 M
🧭 Recomendación para María: arrancar visitas con 5–7 opciones estrato 4 (colchón amplio) y 2–3 en estrato 5 bien ubicadas para tantear negociación.
💼 Decisión para Vivienda 2 — crédito $850 M
Estrato 5: Estimado $615 M → ✅ holgura de ~$235 M. Acción: elegir ubicaciones premium en Sur con 3 parqueaderos + 3 baños, priorizando amenidades y estado del edificio.
Estrato 6: Estimado $714 M → ✅ dentro con holgura de ~$136 M. Acción: foco en edificios de alta especificación; el IC superior supera 850, pero el punto está cómodo → filtrar en $680–820 M y negociar si aparece algo cerca de 850.
📌 Mensaje para la empresa: para el traslado ejecutivo, Estrato 5 ofrece mejor relación área/amenidades/precio; Estrato 6 es viable manteniendo controles de precio.
🛡️ Cómo usar los intervalos (sin asustarnos)
Los IC95% son amplios porque el mercado es heterogéneo (barrios, acabados). No significan “incertidumbre total”, sino que conviene confirmar con comparables y visitar.
En la práctica: selecciona por el punto y valida en terreno; usa el IC para fijar rangos de negociación.
✅ Siguiente paso
Pasamos a Paso 6 y 7: armar mapas y short-lists de ≥5 ofertas para cada caso:
Vivienda 1: Casas Norte 180–220 m², ≤ $350 M (predicho).
Vivienda 2: Aptos Sur 270–330 m², ≤ $850 M (predicho).
Con esto, María podrá mostrar rápido lo que sí cierra, con un discurso claro de valor vs. presupuesto. 🏡
Criterios: Casa, Zona “Norte”, área 180–220 m² (±10% de 200), precio predicho ≤ 350 M.
ofertas_v1 <- base %>%
filter(tipo == "Casa", str_detect(str_to_lower(as.character(zona)), "norte")) %>%
filter(between(areaconst, 180, 220)) %>%
mutate(precio_pred = predict(modelo_lm, newdata = .)) %>%
filter(precio_pred <= 350) %>%
arrange(abs(areaconst - 200), desc(precio_pred)) %>%
mutate(label = paste0(
"<b>", as.character(tipo), " - ", zona, "</b><br>",
"Barrio: ", barrio, "<br>",
"Área: ", areaconst, " m²<br>",
"Baños: ", banios, " | Hab: ", habitaciones, " | Parq: ", parqueaderos, "<br>",
"Precio observado: ", round(preciom,1), " M<br>",
"<b>Precio predicho: ", round(precio_pred,1), " M</b>"
))
ofertas_v1_top <- head(ofertas_v1, 5)
datatable(ofertas_v1_top %>%
select(zona, barrio, areaconst, banios, habitaciones, parqueaderos, preciom, precio_pred) %>%
mutate(across(where(is.numeric), ~round(.,1))),
caption = "Vivienda 1 — Ofertas recomendadas (Top 5)")
leaflet(ofertas_v1_top) %>%
addTiles() %>%
addCircleMarkers(lng=~longitud, lat=~latitud, popup=~label, radius = 8, fillOpacity = .85) %>%
addMiniMap(toggleDisplay = TRUE)
Criterios aplicados: Casa · Zona Norte · 180–220 m² (±10% de 200) · precio predicho ≤ 350 M. Resultado: Top 5 opciones con buena pinta para visita (ver mapa).
🏆 Mi “lista corta” (por qué valen la pena)
Salomia · 200 m² · 2 baños · 1 parqueadero — $350 M
✅ Metraje exacto a la necesidad.
✅ Dentro del crédito; margen de negociación ligero.
🎯 Ideal para arrancar visitas (match casi perfecto con el brief).
Calima · 196 m² · 2 baños · 1 parqueadero — $270 M
✅ Precio muy por debajo del crédito (colchón para mejoras).
🔧 Si hace falta, podemos invertir en un baño adicional (+~$62 M estimados por el modelo) y seguir holgados.
Salomia · 195 m² · 4 baños · 2 parqueaderos — $300 M (pred ≈ 347 M)
💎 Configuración premium para V1: más baños y 2 parqueaderos.
💬 Excelente candidata para cerrar rápido: está bien equipada sin pasarse del tope.
La Flora · 190 m² · 3 baños · 2 parqueaderos — $355 M (pred ≈ 347 M)
📍 Ubicación top del Norte.
⚖️ Observado levemente sobre 350, pero el precio predicho sugiere que hay espacio a negociar.
📣 Muy recomendable llevarla a visita.
Rozo La Torre · 190 m² · 2 baños · 1 parqueadero — $200 M
💰 Precio de oportunidad.
🧱 Con bajo costo podría mejorarse (p.ej. un baño extra) y aún quedar muy por debajo del crédito.
📝 Nota técnica: las predicciones son del modelo RLM; el precio observado puede diferir por acabados, micro–ubicación o estado. Si el observado está cerca de 350 M y el predicho también, prioricemos visita + negociación.
🧭 Lectura del mapa (¿dónde están?)
Vemos concentración en el corredor norte y ejes viales con buena conectividad (ideal para familias que se mueven entre estudio/trabajo).
La Flora destaca por su valor de barrio (servicios, parques, seguridad), mientras Salomia/Calima ofrecen mejor relación m²/precio.
🎯 Estrategia de cierre para María (C&A)
Ruta de visitas (orden sugerido):
La Flora (valor de barrio + 3 baños + 2 parq.) → ir con argumento de precio de mercado (pred ≈ 347) para llevarlo ≤ 350.
Salomia 195 m² (4 baños + 2 parq.) → gran “combo” funcional dentro de presupuesto.
Salomia 200 m² y Calima 196 m² → opción exacta y opción económica con margen para upgrades.
Rozo La Torre → carta de alto ahorro para inversionistas o familias que prefieran remodelar.
Reglas rápidas del modelo (para calibrar expectativas durante la visita):
+10 m² ≈ +$8,2 M
+1 parqueadero ≈ +$79,8 M
+1 baño ≈ +$62,1 M
+1 nivel de estrato ≈ +$98,4 M
💬 Mensaje al cliente: “Con estas 5 opciones en Norte, entramos cómodamente en $350 M, maximizando baños y parqueaderos, y eligiendo entre valor de barrio premium (La Flora) o precio oportunidad (Calima/Salomia).”
Criterios: Apartamento, Zona “Sur”, área 270–330 m² (±10% de 300), precio predicho ≤ 850 M.
ofertas_v2 <- base %>%
filter(tipo == "Apartamento", str_detect(str_to_lower(as.character(zona)), "sur")) %>%
filter(between(areaconst, 270, 330)) %>%
mutate(precio_pred = predict(modelo_lm, newdata = .)) %>%
filter(precio_pred <= 850) %>%
arrange(abs(areaconst - 300), desc(precio_pred)) %>%
mutate(label = paste0(
"<b>", as.character(tipo), " - ", zona, "</b><br>",
"Barrio: ", barrio, "<br>",
"Área: ", areaconst, " m²<br>",
"Baños: ", banios, " | Hab: ", habitaciones, " | Parq: ", parqueaderos, "<br>",
"Precio observado: ", round(preciom,1), " M<br>",
"<b>Precio predicho: ", round(precio_pred,1), " M</b>"
))
ofertas_v2_top <- head(ofertas_v2, 5)
datatable(ofertas_v2_top %>%
select(zona, barrio, areaconst, banios, habitaciones, parqueaderos, preciom, precio_pred) %>%
mutate(across(where(is.numeric), ~round(.,1))),
caption = "Vivienda 2 — Ofertas recomendadas (Top 5)")
leaflet(ofertas_v2_top) %>%
addTiles() %>%
addCircleMarkers(lng=~longitud, lat=~latitud, popup=~label, radius = 8, fillOpacity = .85) %>%
addMiniMap(toggleDisplay = TRUE)
Criterios aplicados: Apartamento · Zona Sur · 270–330 m² (±10% de 300) · precio predicho ≤ $850 M · foco en 3 parqueaderos y ≥3 baños. Resultado: Top 5 opciones que encajan con el crédito (ver mapa).
🏆 Mi “lista corta” (por qué valen la pena)
Seminario · 300 m² · 5 baños · 6 hab. · 3 parq. — $670 M (pred ≈ $707 M)
✅ Match perfecto con 300 m² y 3 parqueaderos.
✅ Muy por debajo del crédito; baños/espacio ideales para familia ejecutiva.
🎯 Candidata #1 para visita inmediata.
Meléndez · 300 m² · 6 baños · 5 hab. · 3 parq. — $370 M (pred ≈ $605 M)
💰 Bargain: observado muy bajo vs predicho; revisar estado/terminaciones.
🧪 Si calza en estrato 5–6, es una oportunidad de oro.
Cuarto de Legua · 296 m² · 4 baños · 4 hab. · 2 parq. — $410 M (pred ≈ $626 M)
✅ Área y baños ok; 2 parqueaderos (se puede negociar alquiler del 3ro en el edificio).
🔎 Muy competitivo en precio observado; gran relación valor/ubicación.
San Joaquín · 300 m² · 4 baños · 5 hab. · 1 parq. — $260 M (pred ≈ $321 M)
💵 Precio de entrada muy bajo.
🅿️ 1 parqueadero no cumple la ficha; útil si la empresa acepta parqueadero adicional arriendo o hay cupo disponible.
Pance · 310 m² · 4 baños · 4 hab. · 3 parq. — $1.590 M (pred ≈ $848 M)
💎 Premium en zona top del Sur.
⚠️ Observado muy por encima del crédito; el predicho sugiere que el inmueble puede tener acabados/amenities de lujo o información atípica.
📌 Mantener como referencia de tope alto; solo considerar si hay flexibilidad de presupuesto.
📝 Nota técnica: las cifras “precio_pred” provienen del RLM; la diferencia con “preciom” refleja acabados, amenities, micro–ubicación y estrato. Usamos el punto para filtrar y el observado para negociar.
🧭 Lectura del mapa (¿dónde conviene?)
Puntos ubicados en corredores residenciales del Sur con acceso a vías principales y cercanía a servicios (colegios, comercio, clubes).
Pance es premium (verde, amenidades); Seminario/Meléndez balancean acceso y valor; Cuarto de Legua es muy competitivo en precio.
🎯 Estrategia de cierre para María (C&A)
Ruta de visitas sugerida:
Seminario → cumple todo (3 parq., 5 baños) y queda muy por debajo de $850 M.
Meléndez → oportunidad de precio; confirmar estado, estrato y administración.
Cuarto de Legua → relación m²/amenidades/precio sobresaliente; gestionar 3er parqueadero.
San Joaquín → plan B si la empresa admite parqueadero adicional en arriendo.
Pance → referencia premium “stretch”; usarla como ancla de negociación.
Reglas rápidas del modelo (para negociar on-site):
+10 m² ≈ +$8,2 M
+1 parqueadero ≈ +$79,8 M
+1 baño ≈ +$62,1 M
+1 nivel de estrato ≈ +$98,4 M
💬 Mensaje al cliente: “En el Sur tenemos apartamentos amplios con 3 parqueaderos y ≥3 baños por debajo de $850 M. Recomiendo iniciar con Seminario y Meléndez (mejor relación valor/precio), dejando Cuarto de Legua como comodín de cierre.” 🏢💜
# Predicciones en test
pred_test <- predict(modelo_lm, newdata = test)
# Data frame de evaluación con ambas columnas
eval_df <- test %>%
mutate(
.pred = as.numeric(pred_test), # estimaciones (columna dentro del df)
preciom = as.numeric(preciom) # asegurar tipo numérico
)
# Métricas
metrics_tbl <- yardstick::metric_set(rmse, mae, rsq)
res_metricas <- metrics_tbl(eval_df, truth = preciom, estimate = .pred)
datatable(res_metricas %>% mutate(.estimate = round(.estimate, 3)),
caption = "Rendimiento del modelo en set de prueba")
df_pred <- tibble(real = test$preciom, pred = pred_test)
g <- ggplot(df_pred, aes(real, pred)) +
geom_point(alpha=.6) + geom_abline(slope=1, intercept=0, linetype="dashed") +
labs(title="Predicho vs Observado (Test)", x="Observado (M)", y="Predicho (M)")
ggplotly(g)
📈 Predicciones en set de prueba y métricas (qué tan bien acierta el modelo)
Resultados (test):
RMSE: 175,1 M
MAE: 113,5 M
R²: 0,722
Lectura rápida para negocio:
🔍 R² = 0,72 → el modelo explica ~72% de la variación de precios fuera de muestra. Sólido para pre-filtrar inventario y priorizar visitas.
🔧 MAE ≈ 113 M → error absoluto típico. En inmuebles con media ~470 M, esto equivale a un error relativo promedio cercano al 20–25% (orientativo).
📉 RMSE 175 M > MAE 113 M → hay colas altas/outliers (segmento premium) que “empujan” el error; lo mismo se aprecia en el predicho vs observado (los puntos caros tienden a sub-estimarse).
Qué se ve en el scatter “Predicho vs Observado”:
☑️ Buena alineación en la franja 200–800 M (donde están la mayoría de casos y ambos créditos).
⚠️ En el extremo >1.2–1.5 B el modelo sub-predice (línea punteada queda por encima de los puntos): típico por no linealidad y atributos premium no modelados (acabados, amenities, vistas, club).
Implicación práctica (cómo usarlo para cerrar):
👍 Usamos las predicciones para filtrar rápido lo que sí cabe en el crédito (350 M y 850 M), no para fijar el precio final.
🧭 En shortlist y visitas, decidimos con precio observado, comparables y negociación; la predicción sirve de ancla y para detectar sobre/sub-valoraciones.
##10. Anexos técnicos
base %>%
group_by(zona, tipo) %>%
summarize(n = n(), mediana_precio = median(preciom), mediana_area = median(areaconst), .groups="drop") %>%
arrange(desc(mediana_precio)) %>%
mutate(across(where(is.numeric), ~round(.,1))) %>%
datatable(caption = "Estadísticos por zona y tipo")
cooks <- cooks.distance(modelo_lm)
tibble(idx = seq_along(cooks), cooks = cooks) %>%
arrange(desc(cooks)) %>% head(10) %>%
datatable(caption = "Top-10 observaciones con mayor influencia (Cook's D)")