En esta sección se presenta el análisis correspondiente a la primera solicitud realizada por la compañía internacional interesada en adquirir una vivienda en la ciudad de Cali para alojar a uno de sus empleados. De acuerdo con las condiciones establecidas, se trata de una casa ubicada en la zona Norte, con un área construida de 200 m², cuatro habitaciones, dos baños, un parqueadero y perteneciente a un estrato socioeconómico 4 o 5. Asimismo, la familia cuenta con un crédito preaprobado de 350 millones de pesos para la adquisición del inmueble. A partir de estas características, se desarrolla el análisis exploratorio y predictivo que permitirá determinar si el crédito disponible resulta suficiente frente a los precios estimados en el mercado y, de igual manera, ofrecer recomendaciones sobre la viabilidad de la compra.
library(gt)
tabla_viv1 <- data.frame(
Características = c("Tipo", "área construida", "parqueaderos",
"baños", "habitaciones", "estrato",
"zona", "crédito preaprobado"),
`Vivienda 1` = c("Casa", "200", "1", "2", "4",
"4 o 5", "Norte", "350 millones")
)
tabla_viv1 %>%
gt() %>%
tab_header(title = md("**Características de la Vivienda 1**"))| Características de la Vivienda 1 | |
| Características | Vivienda.1 |
|---|---|
| Tipo | Casa |
| área construida | 200 |
| parqueaderos | 1 |
| baños | 2 |
| habitaciones | 4 |
| estrato | 4 o 5 |
| zona | Norte |
| crédito preaprobado | 350 millones |
A continuación, se presenta el primer filtro aplicado a la base de datos, en el cual se incluyen únicamente las ofertas correspondientes a casas ubicadas en la Zona Norte de la ciudad. Para comprobar la validez de la consulta, se muestran los primeros tres registros de la base resultante, así como tablas de conteo que permiten verificar la clasificación de las viviendas por zona y tipo. Además, se incorpora un mapa con la ubicación de las ofertas seleccionadas, lo que facilita identificar si los puntos geográficos se concentran efectivamente en la zona correspondiente o si aparecen registros en otras áreas de la ciudad, discutiendo las posibles razones de estas discrepancias.
# LIMPIEZA Y PREPRACIÓN DE LOS DATOS
library(dplyr)
library(tidyr)
library(stringr)
# Tipos consistentes
vivienda <- vivienda %>%
mutate(
# numéricas
areaconst = as.numeric(areaconst),
banios = as.numeric(banios),
habitaciones = as.numeric(habitaciones),
preciom = as.numeric(preciom),
longitud = as.numeric(longitud),
latitud = as.numeric(latitud),
# categóricas
tipo = as.factor(tipo),
zona = as.factor(zona),
barrio = as.factor(barrio),
estrato = as.factor(estrato),
# piso a entero (valores no numéricos -> NA)
piso = suppressWarnings(as.integer(piso))
)
# Normalizar strings vacíos -> NA (por si hay columnas character)
vivienda <- vivienda %>%
mutate(across(where(is.character), ~ na_if(str_squish(.), "")))
# Selección de variables + reglas mínimas de calidad
vivienda_limpia <- vivienda %>%
dplyr::select(tipo, areaconst, parqueaderos, banios, habitaciones,
estrato, zona, preciom, piso, barrio, longitud, latitud) %>%
tidyr::drop_na(preciom, areaconst, estrato, zona) %>%
dplyr::filter(preciom > 0, areaconst > 0) %>%
dplyr::filter(!is.na(longitud), !is.na(latitud)) %>%
mutate(
parqueaderos = if_else(is.na(parqueaderos), 0L, as.integer(parqueaderos)),
estrato_num = suppressWarnings(as.numeric(as.character(estrato))),
precio_m2 = preciom / areaconst
) %>%
distinct()
# Winsorización 1%–99% en precio_m2
q01 <- quantile(vivienda_limpia$precio_m2, 0.01, na.rm = TRUE)
q99 <- quantile(vivienda_limpia$precio_m2, 0.99, na.rm = TRUE)
vivienda_limpia <- vivienda_limpia %>%
mutate(precio_m2_wins = pmin(pmax(precio_m2, q01), q99))
# ── 2) Base para primera solicitud: Casas en Zona Norte ────────────────────────
base1 <- vivienda_limpia %>%
dplyr::filter(.data$tipo == "Casa") %>%
dplyr::filter(stringr::str_detect(.data$zona,
stringr::regex("^Zona\\s*Norte$", ignore_case = TRUE)))
# Chequeos cortos (opcionales)
# head(base1, 3)
# base1 %>% count(zona, sort = TRUE)
# base1 %>% count(tipo, zona, sort = TRUE)# --- Cargar paquetes en un chunk de setup antes ---
library(dplyr)
library(stringr)
library(tidyr)
library(gt)
# --- Crear base1 con Casas de Zona Norte ---
base1 <- vivienda_limpia %>%
dplyr::filter(.data$tipo == "Casa") %>%
dplyr::filter(stringr::str_detect(.data$zona,
stringr::regex("^Zona\\s*Norte$", ignore_case = TRUE))) %>%
tidyr::drop_na(zona)
# --- Verificaciones con tablas formateadas ---
# Primeros 3 registros
base1 %>%
head(3) %>%
gt() %>%
tab_header(
title = md("**Tabla 1. Primeros 3 registros de `base1` (Casas Zona Norte)**")
)Tabla 1. Primeros 3 registros de base1 (Casas Zona Norte) |
||||||||||||||
| tipo | areaconst | parqueaderos | banios | habitaciones | estrato | zona | preciom | piso | barrio | longitud | latitud | estrato_num | precio_m2 | precio_m2_wins |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Casa | 150 | 2 | 4 | 6 | 5 | Zona Norte | 320 | 2 | acopi | -76.51341 | 3.47968 | 5 | 2.133333 | 2.133333 |
| Casa | 380 | 2 | 3 | 3 | 5 | Zona Norte | 780 | 2 | acopi | -76.51674 | 3.48721 | 5 | 2.052632 | 2.052632 |
| Casa | 445 | 0 | 7 | 6 | 6 | Zona Norte | 750 | 2 | acopi | -76.52950 | 3.38527 | 6 | 1.685393 | 1.685393 |
# Conteo por zona
base1 %>%
count(zona, sort = TRUE) %>%
gt() %>%
tab_header(
title = md("**Tabla 2. Número de registros por zona en `base1`**")
)Tabla 2. Número de registros por zona en base1 |
|
| zona | n |
|---|---|
| Zona Norte | 717 |
# Conteo por tipo y zona
base1 %>%
count(tipo, zona, sort = TRUE) %>%
gt() %>%
tab_header(
title = md("**Tabla 3. Número de registros por tipo y zona en `base1`**")
)Tabla 3. Número de registros por tipo y zona en base1 |
||
| tipo | zona | n |
|---|---|---|
| Casa | Zona Norte | 717 |
Tras aplicar los filtros a la base de datos se construyó base1, conformada únicamente por casas de la Zona Norte. La verificación inicial muestra que los tres primeros registros cumplen con estas condiciones y que en total se cuentan 717 observaciones, todas clasificadas como casas en dicha zona. Esto confirma que el filtrado fue correcto y que la base resultante concentra únicamente la información necesaria para el análisis de la primera solicitud.
library(leaflet)
leaflet(base1) %>%
addTiles() %>%
addCircleMarkers(
~longitud, ~latitud,
popup = ~paste0("<b>", tipo, " - ", zona, "</b><br>",
"Barrio: ", barrio, "<br>",
"Precio: ", preciom, " M<br>",
"Área: ", areaconst, " m²<br>",
"Parq: ", parqueaderos,
" | Baños: ", banios,
" | Hab: ", habitaciones),
radius = 6, stroke = FALSE, fillOpacity = 0.8
)Aunque el filtro de datos selecciona únicamente las viviendas clasificadas como Casas de Zona Norte, al visualizar los puntos en el mapa se observa que no todos se concentran estrictamente en el norte de la ciudad. Algunos aparecen en zonas más centrales o incluso hacia el sur. Esto puede deberse a varias razones:
Diferencias entre la clasificación comercial y la división geográfica oficial de la ciudad.
Posibles errores de geocodificación en las coordenadas de algunos registros.
Inclusión de barrios limítrofes que, según ciertas agencias, forman parte del “Norte” aunque geográficamente se ubiquen en áreas intermedias.
Ante esta situación, sería pertinente dialogar con los expertos inmobiliarios de C&A para revisar los límites que ellos reconocen como “Zona Norte” o “Zona Sur”, y comprender si existen criterios comerciales o excepciones en la clasificación. Esto permitiría ajustar la delimitación de manera más consistente.
Por el momento, se trabajará con la información tal cual aparece en la base de datos. Sin embargo, a futuro se podría contemplar una delimitación espacial basada en coordenadas, que permita marcar como valores atípicos o incongruentes aquellas observaciones que no estén dentro de un polígono definido como “Zona Norte”. Este sería un paso útil en análisis posteriores para garantizar mayor coherencia entre la variable categórica zona y la localización geográfica real.
En esta sección se realiza un análisis exploratorio enfocado en la variable respuesta, precio de la casa (preciom), en función de las variables: área construida (areaconst), estrato, número de baños, número de habitaciones y zona. Se emplean gráficos interactivos con el paquete plotly, lo que permite visualizar de forma dinámica las relaciones entre las variables y facilitar la interpretación de patrones en los datos.
library(plotly)
library(dplyr)
library(tidyr)
casas <- vivienda_limpia %>%
filter(tipo == "Casa") %>%
select(preciom, areaconst, estrato, banios, habitaciones, zona) %>%
drop_na()# --- Correlaciones numéricas (referencia) ---
matriz_cor <- round(cor(casas %>% select(where(is.numeric))), 2)
matriz_cor %>%
as.data.frame() %>%
tibble::rownames_to_column(var = "Variable") %>%
gt() %>%
tab_header(
title = md("**Tabla 4. Matriz de correlación de variables numéricas (Casas)**")
) %>%
# aplicar colores a todas las columnas numéricas
data_color(
columns = -Variable,
colors = scales::col_numeric(
palette = c("red", "white", "blue"), # azul = negativo, blanco = 0, rojo = positivo
domain = c(-1, 1)
)
)## Warning: Since gt v0.9.0, the `colors` argument has been deprecated.
## • Please use the `fn` argument instead.
## This warning is displayed once every 8 hours.
| Tabla 4. Matriz de correlación de variables numéricas (Casas) | ||||
| Variable | preciom | areaconst | banios | habitaciones |
|---|---|---|---|---|
| preciom | 1.00 | 0.65 | 0.56 | 0.10 |
| areaconst | 0.65 | 1.00 | 0.49 | 0.29 |
| banios | 0.56 | 0.49 | 1.00 | 0.48 |
| habitaciones | 0.10 | 0.29 | 0.48 | 1.00 |
Los resultados de la matriz de correlación refuerzan las observaciones anteriores. El precio se relaciona principalmente con el área construida (r = 0,65) y el número de baños (r = 0,56), mientras que las habitaciones tienen una relación marginal (r = 0,10). Asimismo, área y baños también están correlacionados (r = 0,49), lo cual refleja que a mayor tamaño, suele incrementarse el número de baños. Este panorama confirma la relevancia del área, el estrato y los baños como predictores del valor de la vivienda, y la menor importancia relativa de las habitaciones.
# Precio vs Área (color por zona)
plot_ly(casas, x=~areaconst, y=~preciom, color=~zona,
type="scatter", mode="markers") %>%
plotly::layout(title="CASAS: Precio vs Área por zona",
xaxis=list(title="Área (m²)"),
yaxis=list(title="Precio (M)"))El análisis muestra una relación positiva entre el área construida y el precio de las viviendas: a mayor tamaño, mayor valor en el mercado. La correlación de 0,65 confirma esta tendencia moderada-alta, aunque persiste una dispersión considerable, dado que casas con áreas similares pueden diferir notablemente en precio según su ubicación o estrato. En términos de distribución geográfica, se observa una concentración de registros en las zonas Sur y Norte, mientras que en las zonas Centro y Oriente el número de observaciones es menor.
# Precio por estrato (boxplot)
plot_ly(casas, x=~as.factor(estrato), y=~preciom, color=~as.factor(estrato),
type="box") %>%
plotly::layout(title="CASAS: Precio por estrato",
xaxis=list(title="Estrato"),
yaxis=list(title="Precio (M)"))La variable estrato evidencia un patrón claramente ascendente: las viviendas de estratos superiores presentan precios más altos y mayor dispersión. En particular, el estrato 3 concentra inmuebles de menor valor, mientras que el estrato 6 registra las medianas más elevadas y un rango intercuartílico considerablemente mayor. Este comportamiento refleja cómo el estrato socioeconómico actúa como un diferenciador clave en la valoración de las viviendas.
# Precio vs número de baños (boxplot)
plot_ly(casas, x=~banios, y=~preciom, color=~zona, type="box") %>%
plotly::layout(title="CASAS: Precio según número de baños",
xaxis=list(title="Baños"),
yaxis=list(title="Precio (M)"))Existe una relación positiva entre el número de baños y el precio de las casas. Aquellas con más de cuatro baños tienden a superar los 1.000 millones, lo que confirma que esta característica añade valor al inmueble. La correlación de 0,56 entre ambas variables refuerza este hallazgo. No obstante, se detectan excepciones: algunas viviendas con pocos baños muestran precios elevados, lo que puede explicarse por factores como la zona o el área construida.
# Precio vs número de habitaciones (boxplot)
plot_ly(casas, x=~habitaciones, y=~preciom, color=~zona, type="box") %>%
plotly::layout(title="CASAS: Precio según número de habitaciones",
xaxis=list(title="Habitaciones"),
yaxis=list(title="Precio (M)"))En contraste con los baños, el número de habitaciones no guarda una relación clara con el precio. La correlación es baja (0,10), lo que indica que, por sí sola, esta variable no explica las variaciones en el valor de mercado. Se observa que incluso viviendas con muchas habitaciones (entre ocho y diez) no necesariamente alcanzan precios altos, lo que sugiere que el área y el estrato pesan más en la determinación del precio final.
En esta etapa se realizó la preparación de los datos, filtrando únicamente las viviendas clasificadas como casas y depurando los registros con información incompleta. Se generaron variables numéricas y categóricas consistentes (como el estrato en formato numérico y la zona como factor), con el fin de contar con una base limpia y adecuada para el ajuste del modelo de regresión lineal múltiple.
# -------- 3.1 Preparación de datos --------
library(dplyr)
library(tidyr)
library(gt)
# Filtrar y limpiar datos de casas
casas_df <- vivienda_limpia %>%
dplyr::filter(tipo == "Casa") %>% # Filtrar solo casas
tidyr::drop_na(preciom, areaconst, estrato,
habitaciones, parqueaderos,
banios, zona) %>% # Eliminar NA en variables clave
mutate(
estrato_num = as.numeric(estrato), # Convertir estrato a numérico
zona = factor(zona) # Asegurar zona como factor
)
# Vista previa formateada (primeros 6 registros)
casas_df %>%
head(6) %>%
gt() %>%
tab_header(
title = md("**Tabla X. Vista previa de los datos de Casas (base limpia)**")
)| Tabla X. Vista previa de los datos de Casas (base limpia) | ||||||||||||||
| tipo | areaconst | parqueaderos | banios | habitaciones | estrato | zona | preciom | piso | barrio | longitud | latitud | estrato_num | precio_m2 | precio_m2_wins |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Casa | 70 | 1 | 3 | 6 | 3 | Zona Oriente | 250 | NA | 20 de julio | -76.51168 | 3.43382 | 1 | 3.571429 | 3.571429 |
| Casa | 120 | 1 | 2 | 3 | 3 | Zona Oriente | 320 | NA | 20 de julio | -76.51237 | 3.43369 | 1 | 2.666667 | 2.666667 |
| Casa | 220 | 2 | 2 | 4 | 3 | Zona Oriente | 350 | NA | 20 de julio | -76.51537 | 3.43566 | 1 | 1.590909 | 1.590909 |
| Casa | 280 | 3 | 5 | 3 | 4 | Zona Sur | 400 | 2 | 3 de julio | -76.54000 | 3.43500 | 2 | 1.428571 | 1.428571 |
| Casa | 150 | 2 | 4 | 6 | 5 | Zona Norte | 320 | 2 | acopi | -76.51341 | 3.47968 | 3 | 2.133333 | 2.133333 |
| Casa | 380 | 2 | 3 | 3 | 5 | Zona Norte | 780 | 2 | acopi | -76.51674 | 3.48721 | 3 | 2.052632 | 2.052632 |
# -------- 3.2 Estimación por MCO --------
mod_casas <- lm(preciom ~ areaconst + estrato_num + habitaciones + parqueaderos + banios + zona,
data = casas_df)
# Resumen del modelo
summary(mod_casas) ##
## Call:
## lm(formula = preciom ~ areaconst + estrato_num + habitaciones +
## parqueaderos + banios + zona, data = casas_df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -1317.53 -114.86 -23.96 71.35 1188.99
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -39.97030 23.42247 -1.706 0.08801 .
## areaconst 0.80184 0.02558 31.348 < 2e-16 ***
## estrato_num 114.78591 4.84287 23.702 < 2e-16 ***
## habitaciones -14.40638 2.59282 -5.556 2.98e-08 ***
## parqueaderos 40.56096 2.80314 14.470 < 2e-16 ***
## banios 38.93029 3.32707 11.701 < 2e-16 ***
## zonaZona Norte -105.78405 22.08973 -4.789 1.75e-06 ***
## zonaZona Oeste -3.84196 26.55571 -0.145 0.88498
## zonaZona Oriente -70.60017 23.53059 -3.000 0.00272 **
## zonaZona Sur -84.44637 21.92996 -3.851 0.00012 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 202.7 on 3190 degrees of freedom
## Multiple R-squared: 0.682, Adjusted R-squared: 0.6811
## F-statistic: 760.3 on 9 and 3190 DF, p-value: < 2.2e-16
## Anova Table (Type II tests)
##
## Response: preciom
## Sum Sq Df F value Pr(>F)
## areaconst 40396511 1 982.726 < 2.2e-16 ***
## estrato_num 23093124 1 561.787 < 2.2e-16 ***
## habitaciones 1269040 1 30.872 2.983e-08 ***
## parqueaderos 8606697 1 209.375 < 2.2e-16 ***
## banios 5628110 1 136.915 < 2.2e-16 ***
## zona 2131626 4 12.964 1.793e-10 ***
## Residuals 131129948 3190
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
El modelo estimado explica un 68,2% de la variabilidad en el precio de las casas (R² = 0.682), lo cual representa un ajuste sólido. Todas las variables incluidas resultan globalmente significativas (p < 0.001 en el test F).
Área construida (β = 0.80, p < 0.001): por cada metro cuadrado adicional, el precio de la vivienda aumenta en promedio 0,8 millones de pesos, manteniendo constantes las demás variables.
Estrato socioeconómico (β = 114.8, p < 0.001): cada incremento de un nivel en el estrato se asocia con un aumento de aproximadamente 115 millones de pesos en el precio.
Habitaciones (β = –14.4, p < 0.001): controlando por área, más habitaciones reducen el precio promedio, lo cual refleja colinealidad: casas con muchas habitaciones en espacios reducidos tienden a ser menos valorizadas.
Parqueaderos (β = 40.6, p < 0.001) y Baños (β = 38.9, p < 0.001): cada parqueadero y cada baño adicional incrementan el precio en torno a 40 y 39 millones respectivamente.
Zona: en comparación con la categoría base (probablemente Zona Centro), vivir en Norte, Oriente o Sur se asocia a precios más bajos, siendo la penalización mayor en la Zona Norte (–105 millones) y en la Zona Sur (–84 millones). La Zona Oeste no mostró diferencias significativas respecto a la base (p = 0.88).
El análisis ANOVA confirma que las variables más influyentes en el precio son área construida, estrato socioeconómico, parqueaderos y baños, mientras que el número de habitaciones aporta de forma marginal y el efecto de la zona depende de la localización específica.
En conclusión: el valor de la vivienda se explica principalmente por el tamaño, el estrato y los atributos de confort (baños y parqueaderos), mientras que el efecto de la zona es heterogéneo y en algunos casos negativo.
# -------- 3.3 Diagnósticos --------
library(car)
library(lmtest)
library(gt)
library(broom)
# --- Multicolinealidad (VIF) ---
vif_vals <- car::vif(mod_casas)
# Convertir a data frame para gt
vif_tbl <- tibble::tibble(
Variable = names(vif_vals),
VIF = round(as.numeric(vif_vals), 2)
)
vif_tbl %>%
gt() %>%
tab_header(
title = md("**Tabla X. Factores de inflación de la varianza (VIF)**")
)| Tabla X. Factores de inflación de la varianza (VIF) |
| VIF |
|---|
| 1.50 |
| 2.18 |
| 1.64 |
| 1.58 |
| 2.13 |
| 1.50 |
| 1.00 |
| 1.00 |
| 1.00 |
| 1.00 |
| 1.00 |
| 4.00 |
| 1.23 |
| 1.48 |
| 1.28 |
| 1.26 |
| 1.46 |
| 1.05 |
# --- Heterocedasticidad (Breusch-Pagan) ---
bp_test <- lmtest::bptest(mod_casas)
# Pasar resultado a tibble para formatear
bp_tbl <- broom::tidy(bp_test)
bp_tbl %>%
gt() %>%
tab_header(
title = md("**Tabla X. Prueba de Breusch-Pagan para heterocedasticidad**")
) %>%
fmt_number(columns = c(statistic, p.value), decimals = 4)| Tabla X. Prueba de Breusch-Pagan para heterocedasticidad | |||
| statistic | p.value | parameter | method |
|---|---|---|---|
| 399.5891 | 0.0000 | 9 | studentized Breusch-Pagan test |
# --- Gráficos de supuestos
par(mfrow = c(1, 2))
plot(mod_casas, which = 1) # Residuos vs ajustados (linealidad y homocedasticidad)
plot(mod_casas, which = 2) # QQ-plot de residuos (aprox. normalidad)Los valores de VIF para todas las variables están entre 1.2 y 2.2, muy por debajo del umbral crítico (10, o incluso 5 como criterio conservador). Esto indica que no existe un problema grave de multicolinealidad entre los predictores: cada variable aporta información distinta al modelo.
El test de Breusch-Pagan arroja un estadístico BP = 399.6, p < 0.001, lo que lleva a rechazar la hipótesis nula de homocedasticidad. En consecuencia, los residuos presentan heterocedasticidad: la varianza del error no es constante a lo largo de los valores ajustados.
El diagrama muestra un patrón en forma de abanico, con mayor dispersión a medida que aumentan los valores ajustados. Esto confirma la heterocedasticidad detectada en la prueba estadística. Además, se observan algunos puntos atípicos (ej. observaciones 1401, 1439, 1404) que influyen de manera importante en los residuos.
El QQ-plot refleja una desviación notable en las colas: los residuos no siguen perfectamente una distribución normal. Hay observaciones extremas (outliers) que se apartan de la línea teórica. Aunque la normalidad no es crítica para la estimación de coeficientes (sí lo es para inferencia clásica), conviene tenerlo presente.
En conclusión:
No hay problemas de colinealidad.
Sí existe heterocedasticidad → se justifica el uso de errores estándar robustos.
Los residuos presentan outliers y ligera desviación de la normalidad, lo que sugiere considerar transformaciones o modelos alternativos en análisis futuros.
# -------- 3.4 Tabla de coeficientes con SE robustos --------
coefs_rob <- lmtest::coeftest(mod_casas, vcov = sandwich::vcovHC(mod_casas, type = "HC3"))
tab_coef <- broom::tidy(coefs_rob) %>%
dplyr::mutate(
term = dplyr::recode(
as.character(term),
"(Intercept)" = "Intercepto",
"areaconst" = "Área construida (m²)",
"estrato_num" = "Estrato",
"habitaciones"= "Habitaciones",
"parqueaderos"= "Parqueaderos",
"banios" = "Baños",
.default = term
)
) %>%
dplyr::rename(
`Término` = term,
`Estimación` = estimate,
`Error Std. (rob.)` = std.error,
`t (rob.)` = statistic,
`p-valor` = p.value
)
# -------- Mostrar tabla con estilo --------
# install.packages("kableExtra") # <- ya instalada (comentado)
# library(kableExtra) # <- ya cargada (comentado)
tab_coef %>%
dplyr::mutate(dplyr::across(where(is.numeric), ~round(.x, 3))) %>% # redondeo a 3 decimales
kableExtra::kable("html", align = "lcccc",
caption = "Coeficientes del modelo con errores estándar robustos (HC3)") %>%
kableExtra::kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover"))| Término | Estimación | Error Std. (rob.) | t (rob.) | p-valor |
|---|---|---|---|---|
| Intercepto | -39.970 | 17.291 | -2.312 | 0.021 |
| Área construida (m²) | 0.802 | 0.054 | 14.964 | 0.000 |
| Estrato | 114.786 | 5.381 | 21.331 | 0.000 |
| Habitaciones | -14.406 | 2.919 | -4.936 | 0.000 |
| Parqueaderos | 40.561 | 4.050 | 10.014 | 0.000 |
| Baños | 38.930 | 4.110 | 9.472 | 0.000 |
| zonaZona Norte | -105.784 | 14.123 | -7.490 | 0.000 |
| zonaZona Oeste | -3.842 | 24.725 | -0.155 | 0.877 |
| zonaZona Oriente | -70.600 | 14.162 | -4.985 | 0.000 |
| zonaZona Sur | -84.446 | 13.906 | -6.072 | 0.000 |
El modelo confirma que las variables área construida, estrato, parqueaderos y baños son determinantes significativos del precio de las viviendas, incluso tras corregir por heterocedasticidad. En promedio:
Cada m² adicional aumenta el precio en 0,8 millones de pesos.
Subir un nivel de estrato incrementa el valor en 115 millones.
Cada parqueadero y cada baño añaden alrededor de 40 y 39 millones, respectivamente.
El efecto de las habitaciones es negativo (–14 millones por unidad), lo que refleja que, manteniendo constantes el área y otros factores, un mayor número de habitaciones no necesariamente se traduce en un mayor precio.
En cuanto a la zona, en comparación con la categoría base (Zona Centro), Norte, Oriente y Sur presentan precios significativamente menores, mientras que la Zona Oeste no muestra diferencias relevantes.
# -------- 3.5 Predicción para el Caso 1 --------
#install.packages("gt")
library(gt)
new_casa <- tidyr::expand_grid(
areaconst = 200,
estrato_num = c(4,5),
habitaciones = 4,
parqueaderos = 1,
banios = 2,
zona = factor("Zona Norte", levels = levels(casas_df$zona))
)
pred_casa <- cbind(new_casa,
predict(mod_casas, newdata = new_casa, interval = "prediction")) %>%
rename(pred = fit, li = lwr, ls = upr)
# Mostrar tabla con predicción e intervalo
pred_casa %>%
gt() %>%
fmt_number(columns = c(pred, li, ls), decimals = 1) %>%
tab_header(
title = md("**Predicción de precio – Casa Zona Norte (200 m², 4 hab., 2 baños, 1 parqueadero)**"),
subtitle = "Comparar con crédito: 350 M"
)| Predicción de precio – Casa Zona Norte (200 m², 4 hab., 2 baños, 1 parqueadero) | ||||||||
| Comparar con crédito: 350 M | ||||||||
| areaconst | estrato_num | habitaciones | parqueaderos | banios | zona | pred | li | ls |
|---|---|---|---|---|---|---|---|---|
| 200 | 4 | 4 | 1 | 2 | Zona Norte | 534.6 | 136.1 | 933.0 |
| 200 | 5 | 4 | 1 | 2 | Zona Norte | 649.3 | 250.3 | 1,048.4 |
Con base en el modelo estimado, se proyecta que una casa ubicada en la Zona Norte, con 200 m² de área construida, 4 habitaciones, 2 baños y 1 parqueadero, tendría un precio esperado de aproximadamente 535 millones de pesos en estrato 4 y 649 millones en estrato 5. En ambos casos, el valor se encuentra por encima del crédito preaprobado de 350 millones, lo cual indica que la oferta disponible excede la capacidad de financiamiento establecida.
# ====== 6. Ofertas potenciales (<= 350M) y mapa ======
library(dplyr)
library(tidyr)
library(stringr)
library(leaflet)
library(scales)
library(gt)
# Especificación de la Vivienda 1
target <- list(
zona = "Zona Norte",
area = 200,
hab = 4,
ban = 2,
parq = 1,
estratos_ok = c(4, 5),
precio_max = 350
)
# 1) Filtro inicial estricto (tolerancias razonables)
filtrar_ofertas <- function(df, area_tol = 0.20, estratos = target$estratos_ok) {
df %>%
filter(
tipo == "Casa",
zona == target$zona,
!is.na(latitud), !is.na(longitud),
!is.na(areaconst), !is.na(habitaciones), !is.na(banios), !is.na(parqueaderos),
!is.na(preciom), preciom <= target$precio_max,
# tolerancia sobre área (±20% por defecto)
areaconst >= target$area * (1 - area_tol),
areaconst <= target$area * (1 + area_tol),
# preferimos estratos 4-5
as.numeric(estrato) %in% estratos
)
}
candidatas <- filtrar_ofertas(vivienda_limpia, area_tol = 0.20, estratos = target$estratos_ok)
# Si no alcanza 5, relajamos criterios gradualmente
if (nrow(candidatas) < 5) {
candidatas <- filtrar_ofertas(vivienda_limpia, area_tol = 0.30, estratos = c(3,4,5))
}
if (nrow(candidatas) < 5) {
# Ampliar también a parques >= 0 y permitir zona norte con distintas mayúsculas (por si acaso)
candidatas <- vivienda_limpia %>%
filter(
tipo == "Casa",
str_detect(zona, regex("Zona Norte", ignore_case = TRUE)),
!is.na(latitud), !is.na(longitud),
!is.na(areaconst), !is.na(habitaciones), !is.na(banios), !is.na(parqueaderos),
!is.na(preciom), preciom <= target$precio_max,
areaconst >= target$area * 0.70,
areaconst <= target$area * 1.30,
as.numeric(estrato) %in% c(3,4,5)
)
}
# 2) Scoring de similitud a la Vivienda 1 (menor = mejor)
# ponderamos cercanía de área (40%), habitaciones (20%), baños (20%), parqueaderos (20%)
rankeadas <- candidatas %>%
mutate(
# diferencias relativas / absolutas
score_area = abs(areaconst - target$area) / target$area,
score_hab = abs(habitaciones - target$hab),
score_ban = abs(banios - target$ban),
score_parq = abs(parqueaderos - target$parq),
# pequeña penalización si estrato distinto a 4-5
pen_estrato = ifelse(as.numeric(estrato) %in% target$estratos_ok, 0, 0.25),
# score total (ajusta pesos si lo deseas)
score_total = 0.40*score_area + 0.20*score_hab + 0.20*score_ban + 0.20*score_parq + pen_estrato
) %>%
arrange(score_total, preciom)
# 3) Seleccionar las 5 mejores
ofertas_top5 <- rankeadas %>%
slice_head(n = 5) %>%
mutate(
precio_fmt = label_number(big.mark = ".", decimal.mark = ",", suffix = " M")(preciom),
area_fmt = paste0(round(areaconst,1), " m²")
)
# 4) Tabla ejecutiva de ofertas
ofertas_top5 %>%
select(
barrio, zona, estrato, preciom, areaconst, habitaciones, banios, parqueaderos, score_total
) %>%
mutate(
`Precio (M)` = round(preciom, 1),
`Área (m²)` = round(areaconst, 1),
`Score similitud` = round(score_total, 3)
) %>%
select(
barrio, zona, estrato, `Precio (M)`, `Área (m²)`, habitaciones, banios, parqueaderos, `Score similitud`
) %>%
gt() %>%
tab_header(
title = md("**Ofertas potenciales (≤ 350 M) – Zona Norte**"),
subtitle = "Ordenadas por mayor similitud con Vivienda 1"
)| Ofertas potenciales (≤ 350 M) – Zona Norte | ||||||||
| Ordenadas por mayor similitud con Vivienda 1 | ||||||||
| barrio | zona | estrato | Precio (M) | Área (m²) | habitaciones | banios | parqueaderos | Score similitud |
|---|---|---|---|---|---|---|---|---|
| la merced | Zona Norte | 5 | 350 | 216 | 4 | 2 | 2 | 0.482 |
| la flora | Zona Norte | 5 | 320 | 160 | 4 | 3 | 1 | 0.530 |
| prados del norte | Zona Norte | 5 | 300 | 146 | 4 | 3 | 1 | 0.558 |
| prados del norte | Zona Norte | 5 | 280 | 140 | 3 | 2 | 1 | 0.570 |
| la flora | Zona Norte | 5 | 320 | 140 | 4 | 3 | 1 | 0.570 |
# 5) Mapa interactivo (Leaflet)
# Popups con info clave
make_popup <- function(df_row) {
paste0(
"<b>", ifelse(!is.na(df_row$barrio), df_row$barrio, "Sin barrio"), "</b><br/>",
df_row$zona, " | Estrato ", df_row$estrato, "<br/>",
"Precio: <b>", df_row$precio_fmt, "</b><br/>",
"Área: ", df_row$area_fmt, " | Hab: ", df_row$habitaciones,
" | Baños: ", df_row$banios, " | Parq: ", df_row$parqueaderos, "<br/>",
"Score similitud: ", round(df_row$score_total, 3)
)
}
pal <- colorNumeric(palette = "viridis", domain = ofertas_top5$preciom)
leaflet(ofertas_top5) %>%
addTiles() %>%
addCircleMarkers(
lng = ~longitud, lat = ~latitud,
radius = 7,
stroke = TRUE, weight = 1, fillOpacity = 0.85,
color = ~pal(preciom),
label = ~paste0(barrio, " (", round(preciom,1), " M)"),
popup = ~vapply(seq_len(nrow(ofertas_top5)), function(i) make_popup(ofertas_top5[i, ]), character(1))
) %>%
addLegend("bottomright", pal = pal, values = ~preciom,
title = "Precio (M)", opacity = 1)Tras aplicar filtros y un sistema de similitud respecto a la Vivienda 1, se identificaron cinco alternativas de compra en la Zona Norte con precios iguales o inferiores a 350 millones. Estas propiedades presentan características cercanas al perfil buscado (200 m², 4 habitaciones, 2 baños, 1 parqueadero, estrato 4–5), y fueron ordenadas según su grado de similitud con el caso de referencia.
El mapa interactivo permite visualizar su localización exacta en la ciudad, mientras que la tabla ejecutiva resume las principales especificaciones de cada oferta. Destacan opciones en los barrios La Merced, La Flora y Prados del Norte, con áreas entre 140 y 216 m² y precios en el rango de 280–350 millones.
Para la segunda solicitud, la compañía requiere la adquisición de un apartamento en la zona Sur de Cali, con un área construida de 300 m², cinco habitaciones, tres baños, tres parqueaderos y perteneciente a un estrato socioeconómico 5 o 6. La familia dispone de un crédito preaprobado de 850 millones de pesos, y el análisis busca establecer si dicho presupuesto resulta suficiente frente a los precios de mercado, así como identificar las alternativas más viables de acuerdo con estas características.
library(gt)
tabla_viv2 <- data.frame(
Características = c("Tipo", "área construida", "parqueaderos",
"baños", "habitaciones", "estrato",
"zona", "crédito preaprobado"),
`Vivienda 2` = c("Apartamento", "300", "3", "3", "5",
"5 o 6", "Sur", "850 millones")
)
tabla_viv2 %>%
gt() %>%
tab_header(title = md("**Características de la Vivienda 2**"))| Características de la Vivienda 2 | |
| Características | Vivienda.2 |
|---|---|
| Tipo | Apartamento |
| área construida | 300 |
| parqueaderos | 3 |
| baños | 3 |
| habitaciones | 5 |
| estrato | 5 o 6 |
| zona | Sur |
| crédito preaprobado | 850 millones |
# --- Crear base2 con Apartamentos de Zona Sur ---
base2 <- vivienda_limpia %>%
dplyr::filter(.data$tipo == "Apartamento") %>%
dplyr::filter(stringr::str_detect(
.data$zona,
stringr::regex("^Zona\\s*Sur$", ignore_case = TRUE)
)) %>%
tidyr::drop_na(zona)
# --- Verificaciones con tablas formateadas ---
# Primeros 3 registros
base2 %>%
head(3) %>%
gt::gt() %>%
gt::tab_header(
title = gt::md("**Tabla 1b. Primeros 3 registros de `base2` (Apartamentos Zona Sur)**")
)Tabla 1b. Primeros 3 registros de base2 (Apartamentos Zona Sur) |
||||||||||||||
| tipo | areaconst | parqueaderos | banios | habitaciones | estrato | zona | preciom | piso | barrio | longitud | latitud | estrato_num | precio_m2 | precio_m2_wins |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Apartamento | 96 | 1 | 2 | 3 | 4 | Zona Sur | 290 | 5 | acopi | -76.53464 | 3.44987 | 4 | 3.020833 | 3.020833 |
| Apartamento | 40 | 1 | 1 | 2 | 3 | Zona Sur | 78 | 2 | aguablanca | -76.50100 | 3.40000 | 3 | 1.950000 | 1.950000 |
| Apartamento | 194 | 2 | 5 | 3 | 6 | Zona Sur | 875 | NA | aguacatal | -76.55700 | 3.45900 | 6 | 4.510309 | 4.510309 |
# Conteo por zona
base2 %>%
dplyr::count(zona, sort = TRUE) %>%
gt::gt() %>%
gt::tab_header(
title = gt::md("**Tabla 2b. Número de registros por zona en `base2`**")
)Tabla 2b. Número de registros por zona en base2 |
|
| zona | n |
|---|---|
| Zona Sur | 2760 |
# Conteo por tipo y zona
base2 %>%
dplyr::count(tipo, zona, sort = TRUE) %>%
gt::gt() %>%
gt::tab_header(
title = gt::md("**Tabla 3b. Número de registros por tipo y zona en `base2`**")
)Tabla 3b. Número de registros por tipo y zona en base2 |
||
| tipo | zona | n |
|---|---|---|
| Apartamento | Zona Sur | 2760 |
# --- Mapa interactivo de Apartamentos en Zona Sur (base2) ---
library(leaflet)
leaflet(base2) %>%
addTiles() %>%
addCircleMarkers(
~longitud, ~latitud,
popup = ~paste0("<b>", tipo, " - ", zona, "</b><br>",
"Barrio: ", barrio, "<br>",
"Precio: ", preciom, " M<br>",
"Área: ", areaconst, " m²<br>",
"Parq: ", parqueaderos,
" | Baños: ", banios,
" | Hab: ", habitaciones),
radius = 6, stroke = FALSE, fillOpacity = 0.8
)El mapa interactivo muestra la localización de los apartamentos filtrados en la Zona Sur de la ciudad. Se observa una alta concentración de puntos en la franja sur-occidental, particularmente hacia sectores de expansión residencial que se extienden desde el límite sur con Jamundí hasta las inmediaciones del área urbana consolidada.
La distribución espacial evidencia que gran parte de la oferta se encuentra efectivamente dentro de los límites esperados de la Zona Sur, aunque algunos puntos aparecen dispersos hacia áreas aledañas, lo cual puede deberse a dos factores principales:
Errores en la geocodificación de las direcciones, que ubican algunos registros fuera de la delimitación estricta.
Definiciones comerciales de “zona” que no siempre coinciden con divisiones administrativas oficiales, lo que puede generar discrepancias en los mapas.
En conjunto, la visualización permite corroborar la existencia de una oferta significativa de apartamentos en la zona solicitada, confirmando que la base2 constituye un insumo adecuado para los análisis descriptivos y predictivos relacionados con la segunda solicitud de vivienda.
En esta sección se presenta el análisis exploratorio de la variable respuesta, precio de la vivienda (preciom), para el segmento de apartamentos, en función del área construida (areaconst), estrato, número de baños, número de habitaciones y zona. Se utilizan gráficos interactivos con el paquete plotly, lo que permite examinar de manera dinámica las relaciones entre las variables y comparar comportamientos entre zonas, con énfasis en la Zona Sur por corresponder a la segunda solicitud. Este EDA servirá como base para la especificación del modelo y la posterior predicción del precio bajo las características requeridas.
library(plotly)
library(dplyr)
library(tidyr)
apartamentos <- vivienda_limpia %>%
filter(tipo == "Apartamento") %>%
select(preciom, areaconst, estrato, banios, habitaciones, zona) %>%
drop_na()# --- Correlaciones numéricas (referencia) - Apartamentos ---
matriz_cor_apto <- apartamentos %>%
select(where(is.numeric)) %>%
cor(use = "pairwise.complete.obs") %>%
round(2)
matriz_cor_apto %>%
as.data.frame() %>%
tibble::rownames_to_column(var = "Variable") %>%
gt() %>%
tab_header(
title = md("**Tabla X. Matriz de correlación de variables numéricas (Apartamentos)**")
) %>%
fmt_number(columns = -Variable, decimals = 2) %>%
data_color(
columns = -Variable,
colors = scales::col_numeric(
palette = c("blue", "white", "red"), # azul = negativo, blanco = 0, rojo = positivo
domain = c(-1, 1)
)
)| Tabla X. Matriz de correlación de variables numéricas (Apartamentos) | ||||
| Variable | preciom | areaconst | banios | habitaciones |
|---|---|---|---|---|
| preciom | 1.00 | 0.83 | 0.74 | 0.30 |
| areaconst | 0.83 | 1.00 | 0.73 | 0.41 |
| banios | 0.74 | 0.73 | 1.00 | 0.50 |
| habitaciones | 0.30 | 0.41 | 0.50 | 1.00 |
La matriz de correlaciones para los apartamentos muestra que el precio se relaciona de manera muy fuerte con el área construida (0.83) y con el número de baños (0.74), lo que indica que estas son las variables más determinantes en la formación del valor de mercado. En contraste, la correlación con el número de habitaciones (0.30) es baja, lo que sugiere que esta característica por sí sola no explica de manera significativa el precio. En conjunto, los resultados reflejan que el tamaño del apartamento y su dotación de baños tienen un mayor peso en la fijación de precios que la cantidad de habitaciones.
# Precio vs Área (color por zona) - Apartamentos
plot_ly(apartamentos, x = ~areaconst, y = ~preciom, color = ~zona,
type = "scatter", mode = "markers") %>%
plotly::layout(
title = "APARTAMENTOS: Precio vs Área por zona",
xaxis = list(title = "Área (m²)"),
yaxis = list(title = "Precio (M)")
)La gráfica de dispersión muestra una relación positiva clara entre el área construida y el precio de los apartamentos, lo que confirma que a mayor tamaño tiende a registrarse un mayor valor de mercado. Sin embargo, la nube de puntos evidencia una gran dispersión, especialmente en áreas intermedias (100–300 m²), donde apartamentos de tamaño similar presentan precios muy distintos según la zona de ubicación. Esto refleja que, además del área, factores como la localización geográfica inciden de manera importante en la valorización. En particular, se observan apartamentos de alto precio en la Zona Oeste, mientras que en la Zona Sur los valores tienden a concentrarse en un rango medio, incluso para áreas construidas mayores.
# Precio por estrato (boxplot) - Apartamentos
plot_ly(apartamentos,
x = ~as.factor(estrato),
y = ~preciom,
color = ~as.factor(estrato),
type = "box") %>%
plotly::layout(
title = "APARTAMENTOS: Precio por estrato",
xaxis = list(title = "Estrato"),
yaxis = list(title = "Precio (M)")
)El boxplot muestra una relación clara entre el estrato socioeconómico y el precio de los apartamentos: a medida que aumenta el estrato, los precios tienden a ser más altos. En estratos 3 y 4, los precios se concentran en rangos bajos y medios, con poca dispersión, lo que indica que la mayoría de las ofertas en estos niveles se mantienen relativamente homogéneas. En cambio, los estratos 5 y 6 presentan precios medianos más elevados y una mayor dispersión, reflejando tanto la diversidad de oferta como la presencia de apartamentos de alto valor. En particular, el estrato 6 concentra las propiedades con precios más altos, incluso superando los 1.500 millones en algunos casos. En síntesis, el gráfico confirma que el estrato es un fuerte diferenciador de precios en el mercado de apartamentos, siendo un factor clave en la segmentación de la oferta.
# Precio vs número de baños (boxplot) - Apartamentos
plot_ly(apartamentos,
x = ~banios,
y = ~preciom,
color = ~zona,
type = "box") %>%
plotly::layout(
title = "APARTAMENTOS: Precio según número de baños",
xaxis = list(title = "Baños"),
yaxis = list(title = "Precio (M)")
)El gráfico de cajas muestra que el precio de los apartamentos aumenta a medida que crece el número de baños, lo cual confirma la relación positiva ya observada en la matriz de correlaciones. En los apartamentos con 2 a 4 baños se concentra la mayor parte de la oferta, presentando precios que van desde niveles medios hasta inmuebles de alto valor. En los casos de 5 baños o más, aunque la muestra es menor, los precios se ubican en rangos considerablemente más altos, reflejando unidades residenciales de lujo o de gran tamaño. La dispersión dentro de cada categoría indica que, aun con el mismo número de baños, el precio varía según otros factores como el área construida y la zona de ubicación, siendo estas variables complementarias en la explicación del valor de mercado.
# Precio vs número de habitaciones (boxplot) - Apartamentos
plot_ly(apartamentos,
x = ~habitaciones,
y = ~preciom,
color = ~zona,
type = "box") %>%
plotly::layout(
title = "APARTAMENTOS: Precio según número de habitaciones",
xaxis = list(title = "Habitaciones"),
yaxis = list(title = "Precio (M)")
)El gráfico de cajas evidencia que el precio de los apartamentos tiende a aumentar conforme crece el número de habitaciones, aunque la relación no es tan marcada ni consistente como en el caso del área construida o el número de baños. Para apartamentos de 2 a 4 habitaciones se concentra la mayor parte de la oferta, con precios que van desde rangos medios hasta inmuebles de alto valor. En unidades con 5 o más habitaciones se observan precios más elevados, pero también una gran dispersión, lo que refleja que la variable habitaciones por sí sola no determina el precio, sino que interactúa con otros factores como la ubicación y el tamaño total del apartamento. En síntesis, el número de habitaciones influye en la valorización, pero su efecto es más débil y heterogéneo en comparación con otras variables estructurales.
En esta etapa se llevó a cabo la preparación de los datos, considerando únicamente las viviendas clasificadas como apartamentos y eliminando los registros con información incompleta. Se generaron variables numéricas y categóricas consistentes (como el estrato transformado a formato numérico y la zona definida como factor), de manera que se dispusiera de una base depurada y adecuada para el ajuste del modelo de regresión lineal múltiple.
# -------- 3.1 Preparación de datos (APARTAMENTOS) --------
# Filtrar y limpiar datos de apartamentos
apartamentos_df <- vivienda_limpia %>%
dplyr::filter(tipo == "Apartamento") %>% # Filtrar solo apartamentos
tidyr::drop_na(preciom, areaconst, estrato,
habitaciones, parqueaderos,
banios, zona) %>% # Eliminar NA en variables clave
mutate(
estrato_num = as.numeric(estrato), # Convertir estrato a numérico
zona = factor(zona) # Asegurar zona como factor
)
# Vista previa formateada (primeros 6 registros)
apartamentos_df %>%
head(6) %>%
gt() %>%
tab_header(
title = md("**Tabla X. Vista previa de los datos de Apartamentos (base limpia)**")
)| Tabla X. Vista previa de los datos de Apartamentos (base limpia) | ||||||||||||||
| tipo | areaconst | parqueaderos | banios | habitaciones | estrato | zona | preciom | piso | barrio | longitud | latitud | estrato_num | precio_m2 | precio_m2_wins |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Apartamento | 90 | 1 | 2 | 3 | 5 | Zona Norte | 260 | 1 | acopi | -76.51350 | 3.45891 | 3 | 2.888889 | 2.888889 |
| Apartamento | 87 | 1 | 3 | 3 | 5 | Zona Norte | 240 | 1 | acopi | -76.51700 | 3.36971 | 3 | 2.758621 | 2.758621 |
| Apartamento | 52 | 2 | 2 | 3 | 4 | Zona Norte | 220 | 1 | acopi | -76.51974 | 3.42627 | 2 | 4.230769 | 4.230769 |
| Apartamento | 137 | 2 | 3 | 4 | 5 | Zona Norte | 310 | 1 | acopi | -76.53105 | 3.38296 | 3 | 2.262774 | 2.262774 |
| Apartamento | 98 | 2 | 2 | 2 | 6 | Zona Norte | 520 | 2 | acopi | -76.54999 | 3.43505 | 4 | 5.306122 | 5.306122 |
| Apartamento | 108 | 2 | 3 | 3 | 4 | Zona Norte | 320 | 3 | acopi | -76.53638 | 3.40770 | 2 | 2.962963 | 2.962963 |
# -------- 3.2 Estimación por MCO (APARTAMENTOS) --------
mod_apart <- lm(preciom ~ areaconst + estrato_num + habitaciones + parqueaderos + banios + zona,
data = apartamentos_df)
# Resumen del modelo
summary(mod_apart)##
## Call:
## lm(formula = preciom ~ areaconst + estrato_num + habitaciones +
## parqueaderos + banios + zona, data = apartamentos_df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -1728.49 -52.33 -0.50 45.17 985.68
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -105.70321 28.15586 -3.754 0.000176 ***
## areaconst 2.03649 0.04223 48.226 < 2e-16 ***
## estrato_num 43.61882 2.72435 16.011 < 2e-16 ***
## habitaciones -33.56196 3.24818 -10.333 < 2e-16 ***
## parqueaderos 52.32387 2.91669 17.939 < 2e-16 ***
## banios 51.91020 2.96824 17.489 < 2e-16 ***
## zonaZona Norte 14.34334 27.05959 0.530 0.596090
## zonaZona Oeste 89.57782 27.43431 3.265 0.001101 **
## zonaZona Oriente 5.84467 31.56656 0.185 0.853116
## zonaZona Sur -3.03863 26.98230 -0.113 0.910340
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 130.8 on 5052 degrees of freedom
## Multiple R-squared: 0.7968, Adjusted R-squared: 0.7964
## F-statistic: 2201 on 9 and 5052 DF, p-value: < 2.2e-16
## Anova Table (Type II tests)
##
## Response: preciom
## Sum Sq Df F value Pr(>F)
## areaconst 39821229 1 2325.78 < 2.2e-16 ***
## estrato_num 4389043 1 256.34 < 2.2e-16 ***
## habitaciones 1827938 1 106.76 < 2.2e-16 ***
## parqueaderos 5510157 1 321.82 < 2.2e-16 ***
## banios 5236661 1 305.85 < 2.2e-16 ***
## zona 4956349 4 72.37 < 2.2e-16 ***
## Residuals 86498690 5052
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
El modelo ajustado presenta un alto poder explicativo, con un 𝑅 2 R 2 de 0.797 (ajustado 0.796), lo que indica que cerca del 80 % de la variabilidad en el precio de los apartamentos puede ser explicada por las variables incluidas. El estadístico F es altamente significativo (p < 0.001), confirmando la validez global del modelo.
En cuanto a los coeficientes:
Área construida (β = 2.04, p < 0.001): es el principal determinante del precio. Por cada metro cuadrado adicional, el valor del apartamento aumenta en promedio en 2 millones de pesos, manteniendo constantes las demás variables.
Estrato socioeconómico (β = 43.62, p < 0.001): cada incremento en el estrato se asocia con un aumento aproximado de 43 millones en el precio, lo que refleja la fuerte segmentación del mercado según nivel socioeconómico.
Habitaciones (β = –33.56, p < 0.001): muestra un efecto negativo inesperado; controlando por área y otras variables, un mayor número de habitaciones reduce el precio estimado. Esto puede deberse a que, en apartamentos de la misma área, más habitaciones implican espacios más reducidos, percibidos como de menor valor.
Parqueaderos (β = 52.32, p < 0.001) y Baños (β = 51.91, p < 0.001): ambos tienen efectos positivos y significativos, indicando que cada parqueadero o baño adicional incrementa el precio en más de 50 millones de pesos.
Zona: el efecto global de la localización es estadísticamente significativo (ANOVA p < 0.001). No obstante, al comparar con la categoría base (no indicada explícitamente, probablemente “Centro”), solo la Zona Oeste (β = 89.58, p = 0.001) muestra un incremento sustancial y significativo en el precio; las demás zonas no difieren significativamente.
El modelo confirma que el precio de los apartamentos depende principalmente del área construida, el estrato, el número de parqueaderos y de baños, mientras que la variable habitaciones adquiere un efecto negativo al controlar por área. La localización también influye, destacándose la Zona Oeste con precios superiores. En conjunto, los resultados son lógicos y coherentes con el comportamiento del mercado, aunque la relación negativa de habitaciones merece análisis adicional como posible indicador de densidad interna de espacios.
# -------- 3.3 Diagnósticos (APARTAMENTOS) --------
library(car)
library(lmtest)
library(gt)
library(broom)
library(tibble)##
## Adjuntando el paquete: 'tibble'
## The following object is masked from 'package:summarytools':
##
## view
# --- Multicolinealidad (VIF) ---
vif_vals_ap <- car::vif(mod_apart)
vif_tbl_ap <- tibble(
Variable = names(vif_vals_ap),
VIF = round(as.numeric(vif_vals_ap), 2)
)
vif_tbl_ap %>%
gt() %>%
tab_header(title = md("**Tabla X. Factores de inflación de la varianza (VIF) – Apartamentos**"))| Tabla X. Factores de inflación de la varianza (VIF) – Apartamentos |
| VIF |
|---|
| 2.55 |
| 2.10 |
| 1.43 |
| 2.03 |
| 2.99 |
| 1.47 |
| 1.00 |
| 1.00 |
| 1.00 |
| 1.00 |
| 1.00 |
| 4.00 |
| 1.60 |
| 1.45 |
| 1.20 |
| 1.43 |
| 1.73 |
| 1.05 |
# --- Heterocedasticidad (Breusch–Pagan) ---
bp_test_ap <- lmtest::bptest(mod_apart)
bp_tbl_ap <- broom::tidy(bp_test_ap)
bp_tbl_ap %>%
gt() %>%
tab_header(title = md("**Tabla X. Prueba de Breusch–Pagan – Apartamentos**")) %>%
fmt_number(columns = c(statistic, p.value), decimals = 4)| Tabla X. Prueba de Breusch–Pagan – Apartamentos | |||
| statistic | p.value | parameter | method |
|---|---|---|---|
| 1,441.1942 | 0.0000 | 9 | studentized Breusch-Pagan test |
# --- Gráficos de supuestos (sí, aquí están) ---
par(mfrow = c(1, 2))
plot(mod_apart, which = 1) # Residuos vs. ajustados (linealidad y varianza constante)
plot(mod_apart, which = 2) # QQ-plot de residuos (aprox. normalidad)Multicolinealidad (VIF) Los valores de VIF para todas las variables se encuentran entre 1.0 y 4.0, muy por debajo del umbral crítico (10, o incluso 5 en criterios más conservadores). Esto indica que no existe un problema grave de multicolinealidad entre los predictores: cada variable aporta información diferenciada al modelo.
Heterocedasticidad (Breusch–Pagan) El test de Breusch–Pagan arroja un estadístico BP = 1441.2, p < 0.001, por lo que se rechaza la hipótesis nula de homocedasticidad. En consecuencia, los residuos presentan heterocedasticidad, es decir, la varianza del error no es constante a lo largo de los valores ajustados. Esto implica que los errores estándar pueden estar sesgados; por ello es recomendable emplear errores estándar robustos (HC3) para obtener inferencias más confiables.
Residuos vs Ajustados (izquierda) El gráfico de residuos contra valores ajustados muestra un patrón de abanico, con mayor dispersión en valores altos, lo cual confirma la heterocedasticidad. También se identifican algunos outliers que influyen de manera importante en la variabilidad de los residuos.
Normalidad de los residuos (QQ-plot, derecha) El QQ-plot refleja una desviación en las colas respecto a la línea teórica, evidenciando que los residuos no siguen completamente una distribución normal. Existen observaciones extremas que se apartan de manera marcada. Aunque la normalidad no es estrictamente necesaria para la estimación de coeficientes, sí afecta a la inferencia clásica y conviene considerarlo en análisis posteriores.
En conclusión: No hay problemas de colinealidad. Sí existe heterocedasticidad → se justifica el uso de errores estándar robustos. Los residuos presentan outliers y cierta desviación de la normalidad, lo que sugiere evaluar transformaciones de variables o modelos alternativos en estudios futuros.
# -------- 3.4 Tabla de coeficientes con SE robustos (APARTAMENTOS) --------
# Coeficientes con errores estándar robustos (HC3)
coefs_rob_ap <- lmtest::coeftest(mod_apart,
vcov = sandwich::vcovHC(mod_apart, type = "HC3"))
tab_coef_ap <- broom::tidy(coefs_rob_ap) %>%
dplyr::mutate(
term = dplyr::recode(
as.character(term),
"(Intercept)" = "Intercepto",
"areaconst" = "Área construida (m²)",
"estrato_num" = "Estrato",
"habitaciones"= "Habitaciones",
"parqueaderos"= "Parqueaderos",
"banios" = "Baños",
.default = term # dejará las dummies de zona como "zonaZona Norte", etc.
)
) %>%
dplyr::rename(
`Término` = term,
`Estimación` = estimate,
`Error Std. (rob.)` = std.error,
`t (rob.)` = statistic,
`p-valor` = p.value
)
# -------- Mostrar tabla con estilo --------
# install.packages("kableExtra") # <- ya instalada (comentado)
# library(kableExtra) # <- ya cargada (comentado)
tab_coef_ap %>%
dplyr::mutate(dplyr::across(where(is.numeric), ~ round(.x, 3))) %>% # redondeo a 3 decimales
kableExtra::kable("html", align = "lcccc",
caption = "Coeficientes del modelo (APARTAMENTOS) con errores estándar robustos (HC3)") %>%
kableExtra::kable_styling(full_width = FALSE,
bootstrap_options = c("striped", "hover"))| Término | Estimación | Error Std. (rob.) | t (rob.) | p-valor |
|---|---|---|---|---|
| Intercepto | -105.703 | 20.410 | -5.179 | 0.000 |
| Área construida (m²) | 2.036 | 0.203 | 10.042 | 0.000 |
| Estrato | 43.619 | 2.998 | 14.548 | 0.000 |
| Habitaciones | -33.562 | 5.453 | -6.154 | 0.000 |
| Parqueaderos | 52.324 | 5.781 | 9.051 | 0.000 |
| Baños | 51.910 | 6.125 | 8.475 | 0.000 |
| zonaZona Norte | 14.343 | 16.858 | 0.851 | 0.395 |
| zonaZona Oeste | 89.578 | 17.751 | 5.046 | 0.000 |
| zonaZona Oriente | 5.845 | 24.001 | 0.244 | 0.808 |
| zonaZona Sur | -3.039 | 17.576 | -0.173 | 0.863 |
La estimación del modelo para apartamentos confirma la importancia de varias variables estructurales en la explicación del precio. El área construida es el factor más determinante: cada metro cuadrado adicional incrementa el valor en aproximadamente 2 millones de pesos, manteniendo constantes las demás variables. El estrato socioeconómico también resulta altamente significativo, con un aumento cercano a 43 millones por cada nivel adicional. De forma similar, el número de parqueaderos y baños tiene un efecto positivo relevante (más de 50 millones cada uno), lo que refleja su peso en la valorización. Por el contrario, el número de habitaciones muestra un efecto negativo: controlando por área, más cuartos reducen el precio, posiblemente por asociarse a espacios más pequeños por unidad. En cuanto a la localización, únicamente la Zona Oeste se diferencia significativamente, con un sobreprecio estimado de casi 90 millones frente a la zona base, mientras que las demás zonas no presentan efectos estadísticamente distintos. En conjunto, el modelo es coherente con el funcionamiento del mercado: el valor de los apartamentos está determinado principalmente por el área, la calidad socioeconómica, los baños y parqueaderos, mientras que la zona sólo marca una diferencia clara en el caso de la Zona Oeste.
# -------- 3.5 Predicción para el Caso 2 (APARTAMENTO) --------
library(gt)
new_apart <- tidyr::expand_grid(
areaconst = 300,
estrato_num = c(5, 6),
habitaciones = 5,
parqueaderos = 3,
banios = 3,
zona = factor("Zona Sur", levels = levels(apartamentos_df$zona))
)
pred_apart <- cbind(
new_apart,
predict(mod_apart, newdata = new_apart, interval = "prediction")
) %>%
dplyr::rename(pred = fit, li = lwr, ls = upr)
# Mostrar tabla con predicción e intervalo
pred_apart %>%
gt() %>%
fmt_number(columns = c(pred, li, ls), decimals = 1) %>%
tab_header(
title = md("**Predicción de precio – Apartamento Zona Sur (300 m², 5 hab., 3 baños, 3 parqueaderos)**"),
subtitle = "Comparar con crédito: 850 M"
)| Predicción de precio – Apartamento Zona Sur (300 m², 5 hab., 3 baños, 3 parqueaderos) | ||||||||
| Comparar con crédito: 850 M | ||||||||
| areaconst | estrato_num | habitaciones | parqueaderos | banios | zona | pred | li | ls |
|---|---|---|---|---|---|---|---|---|
| 300 | 5 | 5 | 3 | 3 | Zona Sur | 865.2 | 607.8 | 1,122.6 |
| 300 | 6 | 5 | 3 | 3 | Zona Sur | 908.8 | 651.2 | 1,166.5 |
La predicción para un apartamento en la Zona Sur con 300 m², 5 habitaciones, 3 baños y 3 parqueaderos muestra un valor estimado de 865 millones de pesos para estrato 5 y de 909 millones de pesos para estrato 6. En ambos casos, los precios se ubican ligeramente por encima del crédito preaprobado de 850 millones, lo que sugiere que el presupuesto es ajustado y podría no ser suficiente en escenarios menos favorables.
El intervalo de predicción al 95 % es amplio (aproximadamente entre 608 y 1,123 millones para estrato 5, y entre 651 y 1,167 millones para estrato 6), lo que refleja la variabilidad natural del mercado: es posible encontrar apartamentos con las mismas características tanto por debajo como bastante por encima del crédito disponible.
En conclusión, aunque el crédito de 850 millones podría cubrir parte de la oferta, existe un riesgo significativo de que se quede corto, sobre todo si se buscan unidades en sectores más valorizados de la Zona Sur o con acabados de alta gama.
# Especificación de la Vivienda 2 (Apartamento)
target_apto <- list(
zona = "Zona Sur",
area = 300,
hab = 5,
ban = 3,
parq = 3,
estratos_ok = c(5, 6),
precio_max = 850
)
# 1) Filtro inicial (tolerancias razonables)
filtrar_ofertas_apto <- function(df, area_tol = 0.20, estratos = target_apto$estratos_ok) {
df %>%
filter(
tipo == "Apartamento",
# aceptar "Zona Sur" con posibles variaciones de mayúsculas/espacios
str_detect(zona, regex("^\\s*Zona\\s*Sur\\s*$", ignore_case = TRUE)),
!is.na(latitud), !is.na(longitud),
!is.na(areaconst), !is.na(habitaciones), !is.na(banios), !is.na(parqueaderos),
!is.na(preciom), preciom <= target_apto$precio_max,
# tolerancia sobre área
areaconst >= target_apto$area * (1 - area_tol),
areaconst <= target_apto$area * (1 + area_tol),
# estratos preferidos 5–6
as.numeric(estrato) %in% estratos
)
}
candidatas_apto <- filtrar_ofertas_apto(vivienda_limpia, area_tol = 0.20, estratos = target_apto$estratos_ok)
# Si no alcanza 5, relajar criterios gradualmente
if (nrow(candidatas_apto) < 5) {
candidatas_apto <- filtrar_ofertas_apto(vivienda_limpia, area_tol = 0.30, estratos = c(4,5,6))
}
if (nrow(candidatas_apto) < 5) {
# Ampliar zona (cualquier "sur"), tolerancia de área 30%, estratos 4–6
candidatas_apto <- vivienda_limpia %>%
filter(
tipo == "Apartamento",
str_detect(zona, regex("sur", ignore_case = TRUE)),
!is.na(latitud), !is.na(longitud),
!is.na(areaconst), !is.na(habitaciones), !is.na(banios), !is.na(parqueaderos),
!is.na(preciom), preciom <= target_apto$precio_max,
areaconst >= target_apto$area * 0.70,
areaconst <= target_apto$area * 1.30,
as.numeric(estrato) %in% c(4,5,6)
)
}
# 2) Scoring de similitud a la Vivienda 2 (menor = mejor)
# Pesos: área (40%), habitaciones (20%), baños (20%), parqueaderos (20%)
rankeadas_apto <- candidatas_apto %>%
mutate(
score_area = abs(areaconst - target_apto$area) / target_apto$area,
score_hab = abs(habitaciones - target_apto$hab),
score_ban = abs(banios - target_apto$ban),
score_parq = abs(parqueaderos - target_apto$parq),
pen_estrato = ifelse(as.numeric(estrato) %in% target_apto$estratos_ok, 0, 0.25),
score_total = 0.40*score_area + 0.20*score_hab + 0.20*score_ban + 0.20*score_parq + pen_estrato
) %>%
arrange(score_total, preciom)
# 3) Seleccionar las 5 mejores
ofertas_top5_apto <- rankeadas_apto %>%
slice_head(n = 5) %>%
mutate(
precio_fmt = label_number(big.mark = ".", decimal.mark = ",", suffix = " M")(preciom),
area_fmt = paste0(round(areaconst, 1), " m²")
)
# 4) Tabla ejecutiva de ofertas
ofertas_top5_apto %>%
select(barrio, zona, estrato, preciom, areaconst, habitaciones, banios, parqueaderos, score_total) %>%
mutate(
`Precio (M)` = round(preciom, 1),
`Área (m²)` = round(areaconst, 1),
`Score similitud` = round(score_total, 3)
) %>%
select(barrio, zona, estrato, `Precio (M)`, `Área (m²)`,
habitaciones, banios, parqueaderos, `Score similitud`) %>%
gt() %>%
tab_header(
title = md("**Ofertas potenciales (≤ 850 M) – Apartamentos Zona Sur**"),
subtitle = "Ordenadas por mayor similitud con Vivienda 2"
)| Ofertas potenciales (≤ 850 M) – Apartamentos Zona Sur | ||||||||
| Ordenadas por mayor similitud con Vivienda 2 | ||||||||
| barrio | zona | estrato | Precio (M) | Área (m²) | habitaciones | banios | parqueaderos | Score similitud |
|---|---|---|---|---|---|---|---|---|
| el ingenio | Zona Sur | 6 | 700 | 250 | 5 | 4 | 2 | 0.717 |
| ciudad jardín | Zona Sur | 6 | 695 | 227 | 3 | 3 | 3 | 0.747 |
| pance | Zona Sur | 6 | 850 | 352 | 3 | 3 | 4 | 0.919 |
| mayapan las vegas | Zona Sur | 6 | 655 | 241 | 3 | 3 | 2 | 0.929 |
| santa teresita | Zona Sur | 6 | 850 | 222 | 3 | 3 | 2 | 0.954 |
# 5) Mapa interactivo (Leaflet)
make_popup_apto <- function(df_row) {
paste0(
"<b>", ifelse(!is.na(df_row$barrio), df_row$barrio, "Sin barrio"), "</b><br/>",
df_row$zona, " | Estrato ", df_row$estrato, "<br/>",
"Precio: <b>", df_row$precio_fmt, "</b><br/>",
"Área: ", df_row$area_fmt, " | Hab: ", df_row$habitaciones,
" | Baños: ", df_row$banios, " | Parq: ", df_row$parqueaderos, "<br/>",
"Score similitud: ", round(df_row$score_total, 3)
)
}
pal_apto <- colorNumeric(palette = "viridis", domain = ofertas_top5_apto$preciom)
leaflet(ofertas_top5_apto) %>%
addTiles() %>%
addCircleMarkers(
lng = ~longitud, lat = ~latitud,
radius = 7,
stroke = TRUE, weight = 1, fillOpacity = 0.85,
color = ~pal_apto(preciom),
label = ~paste0(barrio, " (", round(preciom, 1), " M)"),
popup = ~vapply(seq_len(nrow(ofertas_top5_apto)),
function(i) make_popup_apto(ofertas_top5_apto[i, ]), character(1))
) %>%
addLegend("bottomright", pal = pal_apto, values = ~preciom,
title = "Precio (M)", opacity = 1)Las ofertas identificadas en la Zona Sur con precios inferiores o iguales a 850 millones corresponden principalmente a barrios de estrato 6 como El Ingenio, Ciudad Jardín, Pance, Mayapán Las Vegas y Santa Teresita.
En términos de similitud con la Vivienda 2 (apartamento de 300 m², 5 habitaciones, 3 baños y 3 parqueaderos), se observa lo siguiente:
El Ingenio (700 M, 250 m²) presenta la mejor correspondencia, con 5 habitaciones y 4 baños, ajustándose bien a las condiciones aunque con un área algo menor y un parqueadero menos.
Ciudad Jardín (695 M, 227 m²) es una opción económica dentro del rango, aunque su menor número de habitaciones (3) lo aleja parcialmente de las características solicitadas.
Pance (850 M, 352 m²) cumple con el área esperada e incluso la supera, pero ofrece menos habitaciones (3), lo que afecta su similitud a pesar de estar exactamente en el límite del crédito.
Mayapán Las Vegas (655 M, 241 m²) representa una alternativa más asequible, aunque con menor área y habitaciones, y un parqueadero menos.
Santa Teresita (850 M, 222 m²) es la opción con menor área dentro del grupo y también con menos habitaciones (3), lo que la ubica como menos alineada con el perfil solicitado pese a estar en el rango de precio.
En conclusión, las opciones más atractivas son El Ingenio y, en segundo lugar, Ciudad Jardín, por combinar precios ajustados y mayor cercanía a las características requeridas. Las demás opciones cumplen parcialmente, pero sacrifican espacio en habitaciones o área, aun cuando se mantienen en estrato alto y dentro del crédito máximo disponible.