María fundó C&A (Casas y Apartamentos), una agencia inmobiliaria en Cali con más de 10 años de experiencia en el mercado. Recientemente recibió una solicitud de una empresa internacional interesada en adquirir dos viviendas para alojar a sus empleados y sus familias en la ciudad. Las propiedades deben cumplir condiciones específicas en cuanto a tipo de vivienda, área construida, número de habitaciones, estrato socioeconómico, ubicación y presupuesto máximo disponible.
Con el fin de apoyar la toma de decisiones, en este informe se
aplican técnicas de Regresión Lineal Múltiple (RLM)
para estimar el precio esperado de viviendas con características
similares en el mercado inmobiliario de Cali. El análisis utiliza datos
de ofertas reales de los últimos tres meses, suministrados a través del
paquete paqueteMODELOS.
A partir de este modelo se busca:
Las condiciones de las dos solicitudes son:
| Característica | Vivienda 1 | Vivienda 2 |
|---|---|---|
| Tipo | Casa | Apartamento |
| Área construida (m²) | 200 | 300 |
| Parqueaderos | 1 | 3 |
| Baños | 2 | 3 |
| Habitaciones | 4 | 5 |
| Estrato | 4 o 5 | 5 o 6 |
| Zona | Norte | Sur |
| Crédito pre-aprobado | $350 millones COP | $850 millones COP |
# Instalación del paquete del curso (solo la primera vez):
# install.packages("devtools")
# devtools::install_github("centromagis/paqueteMODELOS", force = TRUE)
library(paqueteMODELOS) # Datos del curso
library(tidyverse) # Manipulación y visualización de datos
library(plotly) # Gráficos interactivos
library(leaflet) # Mapas interactivos
library(corrplot) # Visualización de matrices de correlación
library(lmtest) # Tests estadísticos: Breusch-Pagan, Durbin-Watson
library(car) # VIF para detección de multicolinealidad
library(nortest) # Test de Lilliefors para normalidad
library(knitr) # Generación de tablas
library(kableExtra) # Estilo y formato de tablas HTML
# Carga del dataset desde el paquete del curso
data("vivienda")
# Eliminación de registros con valores faltantes en variables clave del modelo
vivienda <- vivienda %>%
drop_na(zona, tipo, preciom, areaconst, estrato,
banios, habitaciones, parqueaderos, longitud, latitud)
cat("Registros disponibles tras limpieza:", nrow(vivienda), "\n")## Registros disponibles tras limpieza: 6717
## Rows: 6,717
## 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, 4, 5, 6, 4, 5, 5, 4, 5, 3, …
## $ preciom <dbl> 250, 320, 350, 400, 260, 240, 220, 310, 320, 780, 625, 75…
## $ areaconst <dbl> 70, 120, 220, 280, 90, 87, 52, 137, 150, 380, 355, 237, 9…
## $ parqueaderos <dbl> 1, 1, 2, 3, 1, 1, 2, 2, 2, 2, 3, 2, 2, 1, 4, 2, 2, 2, 1, …
## $ banios <dbl> 3, 2, 2, 5, 2, 3, 2, 3, 4, 3, 5, 6, 2, 4, 4, 4, 3, 2, 2, …
## $ habitaciones <dbl> 6, 3, 4, 3, 3, 3, 3, 4, 6, 3, 5, 6, 2, 5, 5, 4, 3, 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…
Tratamiento de datos faltantes (NA)
La función drop_na() se aplica únicamente sobre las
variables que serán utilizadas directamente en el modelo de regresión y
en los mapas: zona, tipo,
preciom, areaconst, estrato,
banios, habitaciones,
parqueaderos, longitud y latitud.
Variables como piso o barrio que contienen NAs
pero no forman parte del modelo no son afectadas por este filtro.
La eliminación de filas con NA en estas variables es necesaria por tres razones concretas.
Primero, la función lm() en R no puede estimar
coeficientes cuando alguna observación tiene valores faltantes en las
variables del modelo — si no se limpian previamente, el modelo los omite
de forma automática y silenciosa, lo que puede generar inconsistencias
entre los datos explorados y los datos usados en la estimación.
Segundo, leaflet no puede representar puntos en el mapa
si las coordenadas longitud o latitud son
NA.
Tercero, los cálculos de correlación y los gráficos exploratorios también requieren valores completos para producir resultados confiables.
Esta decisión no compromete la validez del análisis posterior. El dataset original cuenta con 8.322 registros, por lo que incluso eliminando un porcentaje de filas con datos faltantes, la muestra resultante sigue siendo suficientemente grande para estimar modelos de regresión robustos, construir intervalos de predicción confiables e identificar ofertas reales de vivienda. La representatividad del mercado no se ve afectada de manera significativa.
## id zona piso estrato
## Min. : 1 Length:6717 Length:6717 Min. :3.00
## 1st Qu.:2474 Class :character Class :character 1st Qu.:4.00
## Median :4474 Mode :character Mode :character Median :5.00
## Mean :4413 Mean :4.83
## 3rd Qu.:6428 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 Length:6717 Length:6717 Min. :-76.59
## 1st Qu.: 3.000 Class :character Class :character 1st Qu.:-76.54
## Median : 3.000 Mode :character Mode :character Median :-76.53
## Mean : 3.611 Mean :-76.53
## 3rd Qu.: 4.000 3rd Qu.:-76.52
## Max. :10.000 Max. :-76.46
## latitud
## Min. :3.333
## 1st Qu.:3.379
## Median :3.412
## Mean :3.415
## 3rd Qu.:3.451
## Max. :3.498
El resumen estadístico muestra que el dataset cubre un amplio rango de precios (desde viviendas económicas hasta propiedades de alto valor), áreas construidas, estratos y características físicas. Esto garantiza que los modelos de regresión tendrán suficiente variabilidad en los datos para estimar con precisión el efecto de cada variable sobre el precio.
Se construye base1 conservando únicamente las
casas ubicadas en la Zona Norte de Cali, que
corresponden exactamente al tipo y zona solicitados para la primera
vivienda.
base1 <- vivienda %>%
filter(tipo == "Casa",
zona == "Zona Norte")
cat("Registros en base1 (Casas – Zona Norte):", nrow(base1), "\n")## Registros en base1 (Casas – Zona Norte): 435
## Variables disponibles: 13
El filtro reduce el dataset original a un subconjunto específico del mercado inmobiliario. Trabajar con esta submuestra es metodológicamente correcto porque los precios varían significativamente entre zonas y tipos de vivienda — mezclar casas del sur con apartamentos del norte en un mismo modelo produciría estimaciones poco precisas e interpretaciones sin sentido para el contexto de la solicitud.
base1 %>%
head(3) %>%
kable(caption = "Tabla 1. Primeros 3 registros – Casas Zona Norte") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE)| id | zona | piso | estrato | preciom | areaconst | parqueaderos | banios | habitaciones | tipo | barrio | longitud | latitud |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1209 | Zona Norte | 02 | 5 | 320 | 150 | 2 | 4 | 6 | Casa | acopi | -76.51341 | 3.47968 |
| 1592 | Zona Norte | 02 | 5 | 780 | 380 | 2 | 3 | 3 | Casa | acopi | -76.51674 | 3.48721 |
| 4460 | Zona Norte | 02 | 4 | 625 | 355 | 3 | 5 | 5 | Casa | acopi | -76.53179 | 3.40590 |
Los primeros tres registros permiten verificar que la estructura del subconjunto es correcta: todas las filas corresponden a casas en la Zona Norte, con valores numéricos en precio, área, estrato y demás variables del modelo.
# Verificación por tipo de vivienda
base1 %>%
count(tipo, name = "Frecuencia") %>%
kable(caption = "Tabla 2. Verificación: distribución por tipo de vivienda") %>%
kable_styling(bootstrap_options = "striped", full_width = FALSE)| tipo | Frecuencia |
|---|---|
| Casa | 435 |
# Verificación por zona
base1 %>%
count(zona, name = "Frecuencia") %>%
kable(caption = "Tabla 3. Verificación: distribución por zona") %>%
kable_styling(bootstrap_options = "striped", full_width = FALSE)| zona | Frecuencia |
|---|---|
| Zona Norte | 435 |
# Distribución por estrato
base1 %>%
count(estrato, name = "Frecuencia") %>%
kable(caption = "Tabla 4. Distribución por estrato socioeconómico – base1") %>%
kable_styling(bootstrap_options = "striped", full_width = FALSE)| estrato | Frecuencia |
|---|---|
| 3 | 81 |
| 4 | 103 |
| 5 | 223 |
| 6 | 28 |
Las tablas de verificación confirman que el filtro funciona correctamente: la base1 contiene exclusivamente registros de tipo “Casa” en “Zona Norte”. La distribución por estrato muestra qué niveles socioeconómicos están representados en este segmento del mercado, información clave para anticipar si la solicitud de estrato 4 o 5 tiene suficiente oferta disponible.
leaflet(base1) %>%
addTiles() %>%
addCircleMarkers(
lng = ~longitud,
lat = ~latitud,
radius = 5,
color = "#2c7bb6",
fillOpacity = 0.75,
popup = ~paste0(
"<b>Barrio:</b> ", barrio, "<br>",
"<b>Precio:</b> $", preciom, " millones<br>",
"<b>Área:</b> ", areaconst, " m²<br>",
"<b>Estrato:</b> ", estrato, "<br>",
"<b>Habitaciones:</b> ", habitaciones, "<br>",
"<b>Baños:</b> ", banios, "<br>",
"<b>Zona:</b> ", zona
),
label = ~paste0(barrio, " – $", preciom, "M")
) %>%
addLegend("bottomright",
colors = "#2c7bb6",
labels = "Casas – Zona Norte",
title = "Base 1")Interpretación del mapa: La mayoría de los puntos se concentra geográficamente en la zona norte de Cali, lo que valida la calidad del filtro aplicado. Sin embargo, es posible identificar algunos puntos ubicados fuera del área norte esperada. Este fenómeno puede tener dos explicaciones: por un lado, la clasificación de zona en la base de datos puede seguir criterios comerciales o administrativos de la agencia inmobiliaria, que no siempre coinciden exactamente con los límites geográficos oficiales de la ciudad. Por otro lado, pueden existir errores de digitación en las coordenadas de algunos registros. En cualquier caso, dado que estos puntos representan una minoría dentro de la base, su impacto sobre el modelo de regresión es marginal y no compromete la validez del análisis.
El análisis exploratorio tiene como objetivo entender la relación
entre el precio de la vivienda (preciom) y
las variables candidatas a ser predictoras en el modelo: área
construida, estrato, número de baños, habitaciones y parqueaderos.
base1 %>%
select(preciom, areaconst, estrato, banios, habitaciones, parqueaderos) %>%
summary() %>%
kable(caption = "Tabla 5. Resumen estadístico – Variables del modelo (base1)") %>%
kable_styling(bootstrap_options = "striped", full_width = FALSE)| preciom | areaconst | estrato | banios | habitaciones | parqueaderos | |
|---|---|---|---|---|---|---|
| Min. : 89.0 | Min. : 30.0 | Min. :3.000 | Min. : 0.000 | Min. : 0.000 | Min. : 1.000 | |
| 1st Qu.: 330.0 | 1st Qu.: 170.5 | 1st Qu.:4.000 | 1st Qu.: 3.000 | 1st Qu.: 4.000 | 1st Qu.: 1.000 | |
| Median : 425.0 | Median : 264.5 | Median :5.000 | Median : 4.000 | Median : 4.000 | Median : 2.000 | |
| Mean : 479.8 | Mean : 292.7 | Mean :4.455 | Mean : 3.782 | Mean : 4.809 | Mean : 2.182 | |
| 3rd Qu.: 582.5 | 3rd Qu.: 357.0 | 3rd Qu.:5.000 | 3rd Qu.: 5.000 | 3rd Qu.: 5.000 | 3rd Qu.: 3.000 | |
| Max. :1940.0 | Max. :1440.0 | Max. :6.000 | Max. :10.000 | Max. :10.000 | Max. :10.000 |
El resumen estadístico revela el rango y la distribución de cada variable en el segmento de casas de la Zona Norte. La media y la mediana del precio permiten anticipar si la solicitud de la empresa (crédito de $350 millones) está dentro del rango típico del mercado o en un extremo de la distribución.
plot_ly(base1,
x = ~areaconst,
y = ~preciom,
type = "scatter",
mode = "markers",
marker = list(color = "#2c7bb6", opacity = 0.6, size = 7),
text = ~paste0("Barrio: ", barrio,
"<br>Precio: $", preciom, "M",
"<br>Área: ", areaconst, " m²",
"<br>Estrato: ", estrato)) %>%
layout(title = "Precio vs Área Construida – Casas Zona Norte",
xaxis = list(title = "Área construida (m²)"),
yaxis = list(title = "Precio (millones COP)"))Se observa una relación positiva y aproximadamente lineal entre el área construida y el precio: a mayor área, mayor precio. Esta relación es coherente con la lógica del mercado inmobiliario, donde el tamaño de la propiedad es uno de los factores de valoración más directos. La dispersión que se observa alrededor de la tendencia central sugiere que el área no es el único determinante del precio — otras variables como el estrato o la ubicación específica dentro de la zona también aportan al valor de la vivienda.
plot_ly(base1,
x = ~factor(estrato),
y = ~preciom,
type = "box",
color = ~factor(estrato),
colors = "Blues") %>%
layout(title = "Distribución de Precio por Estrato – Casas Zona Norte",
xaxis = list(title = "Estrato socioeconómico"),
yaxis = list(title = "Precio (millones COP)"),
showlegend = FALSE)Los resultados muestran que los estratos socioeconómicos más altos presentan precios medianos superiores y una mayor dispersión. Esto indica que el estrato no solo influye en el valor promedio de las viviendas, sino también en la heterogeneidad del mercado, ya que en estratos altos existen propiedades con características y niveles de lujo muy variados.
plot_ly(base1,
x = ~factor(banios),
y = ~preciom,
type = "box",
color = ~factor(banios),
colors = "Greens") %>%
layout(title = "Precio por Número de Baños – Casas Zona Norte",
xaxis = list(title = "Número de baños"),
yaxis = list(title = "Precio (millones COP)"),
showlegend = FALSE)El número de baños presenta una relación positiva con el precio de la vivienda. Este resultado es coherente con el mercado inmobiliario, donde un mayor número de baños suele asociarse con propiedades más amplias o con un mayor nivel de confort y acabados.
plot_ly(base1,
x = ~factor(habitaciones),
y = ~preciom,
type = "box",
color = ~factor(habitaciones),
colors = "Oranges") %>%
layout(title = "Precio por Número de Habitaciones – Casas Zona Norte",
xaxis = list(title = "Número de habitaciones"),
yaxis = list(title = "Precio (millones COP)"),
showlegend = FALSE)Se observa una tendencia creciente del precio a medida que aumenta el número de habitaciones. Sin embargo, la alta dispersión en las categorías superiores sugiere que el número de habitaciones por sí solo no determina el precio, ya que propiedades con el mismo número de cuartos pueden diferir significativamente en área, estrato o ubicación.
plot_ly(base1,
x = ~factor(parqueaderos),
y = ~preciom,
type = "box",
color = ~factor(parqueaderos),
colors = "Purples") %>%
layout(title = "Precio por Número de Parqueaderos – Casas Zona Norte",
xaxis = list(title = "Número de parqueaderos"),
yaxis = list(title = "Precio (millones COP)"),
showlegend = FALSE)Las viviendas con mayor número de parqueaderos tienden a presentar precios más altos. Este atributo tiene especial valor en mercados urbanos donde el espacio para estacionamiento es limitado. Sin embargo, su efecto sobre el precio puede estar parcialmente asociado con otras variables, como el área construida o el estrato.
vars_cor1 <- base1 %>%
select(preciom, areaconst, estrato, banios, habitaciones, parqueaderos) %>%
na.omit()
cor_matrix1 <- cor(vars_cor1)
corrplot(cor_matrix1,
method = "color",
type = "upper",
tl.col = "black",
addCoef.col = "black",
number.cex = 0.8,
title = "Matriz de Correlación – Casas Zona Norte",
mar = c(0, 0, 1, 0))
La matriz de correlación cuantifica la fuerza de la relación lineal entre cada par de variables. El área construida y el estrato muestran las correlaciones más altas con el precio, confirmando que serán los predictores más influyentes en el modelo. También se observa correlación moderada entre las variables predictoras entre sí — por ejemplo, entre baños y habitaciones — lo que anticipa la posibilidad de multicolinealidad moderada en el modelo. Este aspecto se verificará formalmente con el Factor de Inflación de Varianza (VIF) en el Paso 4.
Se estima el modelo con la variable respuesta preciom en
función de las cinco características disponibles: área construida,
estrato, habitaciones, parqueaderos y baños.
modelo1 <- lm(preciom ~ areaconst + estrato + habitaciones +
parqueaderos + banios,
data = base1)
summary(modelo1)##
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos +
## banios, data = base1)
##
## Residuals:
## Min 1Q Median 3Q Max
## -784.29 -77.56 -16.03 47.67 978.61
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -238.17090 44.40551 -5.364 1.34e-07 ***
## areaconst 0.67673 0.05281 12.814 < 2e-16 ***
## estrato 80.63495 9.82632 8.206 2.70e-15 ***
## habitaciones 7.64511 5.65873 1.351 0.177
## parqueaderos 24.00598 5.86889 4.090 5.14e-05 ***
## banios 18.89938 7.48800 2.524 0.012 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 155.1 on 429 degrees of freedom
## Multiple R-squared: 0.6041, Adjusted R-squared: 0.5995
## F-statistic: 130.9 on 5 and 429 DF, p-value: < 2.2e-16
coef1 <- as.data.frame(summary(modelo1)$coefficients)
coef1$Variable <- rownames(coef1)
coef1 <- coef1[, c("Variable", "Estimate", "Std. Error", "t value", "Pr(>|t|)")]
names(coef1) <- c("Variable", "Estimado", "Error Estándar", "Estadístico t", "Valor p")
coef1 %>%
mutate(Significativo = ifelse(`Valor p` < 0.05, "✔ Sí", "✘ No")) %>%
kable(digits = 4,
caption = "Tabla 6. Coeficientes estimados – Modelo Vivienda 1 (Casas Zona Norte)") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
column_spec(6, bold = TRUE,
color = ifelse(coef1$`Valor p` < 0.05, "darkgreen", "red"))| Variable | Estimado | Error Estándar | Estadístico t | Valor p | Significativo | |
|---|---|---|---|---|---|---|
| (Intercept) | (Intercept) | -238.1709 | 44.4055 | -5.3635 | 0.0000 | ✔ Sí |
| areaconst | areaconst | 0.6767 | 0.0528 | 12.8140 | 0.0000 | ✔ Sí |
| estrato | estrato | 80.6349 | 9.8263 | 8.2060 | 0.0000 | ✔ Sí |
| habitaciones | habitaciones | 7.6451 | 5.6587 | 1.3510 | 0.1774 | ✘ No |
| parqueaderos | parqueaderos | 24.0060 | 5.8689 | 4.0904 | 0.0001 | ✔ Sí |
| banios | banios | 18.8994 | 7.4880 | 2.5240 | 0.0120 | ✔ Sí |
r2_v1 <- round(summary(modelo1)$r.squared * 100, 1)
r2adj_v1 <- round(summary(modelo1)$adj.r.squared * 100, 1)
cat("R² =", r2_v1, "%\n")## R² = 60.4 %
## R² ajustado = 59.9 %
Interpretación de coeficientes:
Los coeficientes estimados representan el cambio esperado en el precio de la vivienda ante un aumento de una unidad en cada variable explicativa, manteniendo constantes las demás variables del modelo (ceteris paribus). Se interpretan únicamente los coeficientes estadísticamente significativos, es decir, aquellos con valor p menor a 0.05.
areaconst): El
coeficiente positivo indica que un incremento de un metro cuadrado en el
área de la vivienda se asocia con un aumento en el precio promedio
estimado. Este resultado es consistente con la teoría económica de
valoración de bienes inmobiliarios.estrato): Un
mayor estrato se relaciona con precios más elevados, lo cual refleja las
diferencias en calidad urbana, infraestructura y percepción de seguridad
entre zonas de la ciudad.banios): Cada baño
adicional incrementa el precio estimado de la vivienda, lo que sugiere
que este atributo captura parte del nivel de confort y del tamaño
general de la propiedad.habitaciones) y
parqueaderos (parqueaderos): Si estos
coeficientes resultan significativos, se interpretan de manera análoga.
Si no lo son, su efecto sobre el precio ya está capturado por otras
variables del modelo — por ejemplo, el área construida tiende a absorber
gran parte del efecto de las habitaciones cuando ambas variables están
correlacionadas.Interpretación del R²: El coeficiente de determinación indica que el modelo explica aproximadamente el 60.4% de la variabilidad observada en los precios de las viviendas (R² ajustado = 59.9%). Esto sugiere que las variables incluidas capturan una proporción importante de los factores que determinan el precio. No obstante, el porcentaje restante de variabilidad puede estar asociado con variables no observadas en el dataset, como la antigüedad del inmueble, el estado de conservación, la ubicación específica dentro del barrio o características del conjunto residencial.
La validación de supuestos es esencial para determinar si las estimaciones e intervalos de predicción del modelo son confiables. Un modelo de regresión lineal se apoya en cuatro supuestos principales: normalidad de los residuales, homocedasticidad (varianza constante), independencia de los residuales y ausencia de multicolinealidad entre las variables predictoras.

Los cuatro gráficos de diagnóstico ofrecen una primera lectura visual de los supuestos. El gráfico Residuals vs Fitted permite detectar patrones no lineales; el QQ-Plot evalúa normalidad; el Scale-Location evalúa homocedasticidad; y el Residuals vs Leverage identifica observaciones influyentes.
res1 <- residuals(modelo1)
lt1 <- lillie.test(res1)
cat("Test de Lilliefors (Kolmogorov-Smirnov):\n")## Test de Lilliefors (Kolmogorov-Smirnov):
## Estadístico D = 0.1325
## p-valor = 0
cat(" Conclusión:", ifelse(lt1$p.value > 0.05,
"No se rechaza la normalidad de los residuales (p > 0.05).",
"Se rechaza la normalidad de los residuales (p < 0.05)."))## Conclusión: Se rechaza la normalidad de los residuales (p < 0.05).
# QQ-Plot interactivo
qq1 <- data.frame(
teoricos = qqnorm(res1, plot.it = FALSE)$x,
muestra = qqnorm(res1, plot.it = FALSE)$y
)
plot_ly(qq1, x = ~teoricos, y = ~muestra,
type = "scatter", mode = "markers",
name = "Residuales",
marker = list(color = "#2c7bb6", size = 6)) %>%
add_lines(x = ~teoricos, y = ~teoricos,
name = "Línea de referencia",
line = list(color = "red", dash = "dash")) %>%
layout(title = "QQ-Plot de Residuales – Modelo Vivienda 1",
xaxis = list(title = "Cuantiles teóricos"),
yaxis = list(title = "Cuantiles muestrales"))Interpretación: Si el p-valor del test de Lilliefors
es mayor a 0.05, los residuales siguen una distribución aproximadamente
normal y el supuesto se cumple. Si el p-valor es menor a 0.05, los
residuales se desvían significativamente de la normalidad. En ese caso,
una solución habitual es aplicar una transformación logarítmica
(log(preciom)) o Box-Cox a la variable respuesta, lo que
suele corregir asimetrías en los residuales de datos de precios. El
QQ-Plot complementa esta lectura: si los puntos se alinean sobre la
diagonal roja, los residuales son normales; desviaciones sistemáticas en
los extremos indican colas pesadas o asimetría.
## Test de Breusch-Pagan:
## Estadístico BP = 80.2808
## p-valor = 0
cat(" Conclusión:", ifelse(bp1$p.value > 0.05,
"No hay evidencia de heterocedasticidad (p > 0.05). Varianza constante.",
"Se detecta heterocedasticidad (p < 0.05). La varianza de los errores no es constante."))## Conclusión: Se detecta heterocedasticidad (p < 0.05). La varianza de los errores no es constante.
Interpretación: El test de Breusch-Pagan evalúa si
la varianza de los residuales cambia según los valores ajustados. Un
p-valor mayor a 0.05 indica que la varianza es constante
(homocedasticidad), lo que valida este supuesto. Si se detecta
heterocedasticidad, las estimaciones de los coeficientes siguen siendo
insesgadas, pero los errores estándar son incorrectos, lo que afecta las
pruebas de significancia. En ese caso, la solución más práctica es usar
errores estándar robustos (paquete
sandwich con coeftest()), o bien aplicar una
transformación logarítmica a la variable respuesta.
## Test de Durbin-Watson:
## Estadístico DW = 1.7615
## p-valor = 0.0055
cat(" Conclusión:", ifelse(dw1$p.value > 0.05,
"No hay evidencia de autocorrelación en los residuales (p > 0.05).",
"Se detecta posible autocorrelación en los residuales (p < 0.05)."))## Conclusión: Se detecta posible autocorrelación en los residuales (p < 0.05).
Interpretación: El estadístico de Durbin-Watson toma valores entre 0 y 4; un valor cercano a 2 indica que no hay autocorrelación. Este supuesto es más crítico en datos de series de tiempo, donde las observaciones están ordenadas temporalmente. En datos de corte transversal como los de este ejercicio — donde cada fila es una vivienda independiente — la autocorrelación es menos frecuente. Si se detectara, podría indicar la omisión de una variable que genera un patrón sistemático en los errores, como el barrio específico o la fecha de la oferta.
vif1 <- vif(modelo1)
data.frame(Variable = names(vif1), VIF = round(vif1, 3)) %>%
mutate(Diagnóstico = ifelse(VIF > 10, "⚠ Alta multicolinealidad",
ifelse(VIF > 5, "⚡ Multicolinealidad moderada",
"✔ Aceptable"))) %>%
kable(caption = "Tabla 7. Factor de Inflación de Varianza (VIF) – Modelo Vivienda 1") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| Variable | VIF | Diagnóstico | |
|---|---|---|---|
| areaconst | areaconst | 1.461 | ✔ Aceptable |
| estrato | estrato | 1.308 | ✔ Aceptable |
| habitaciones | habitaciones | 1.721 | ✔ Aceptable |
| parqueaderos | parqueaderos | 1.226 | ✔ Aceptable |
| banios | banios | 1.967 | ✔ Aceptable |
Interpretación: El VIF mide cuánto se infla la varianza del estimador de cada coeficiente debido a la correlación con las demás variables predictoras. Un VIF menor a 5 se considera aceptable; entre 5 y 10 indica multicolinealidad moderada que puede afectar la estabilidad de los coeficientes; mayor a 10 indica multicolinealidad alta que compromete la interpretación individual de los estimadores. Si alguna variable supera este umbral, se recomienda eliminar la variable más correlacionada con las demás, o usar métodos de regularización como Ridge Regression que toleran la multicolinealidad.
La solicitud especifica estrato 4 o 5, por lo que se realizan dos predicciones — una por cada escenario — para dar a María un rango completo de estimaciones.
# Características exactas de la solicitud
nuevas_v1 <- data.frame(
areaconst = c(200, 200),
estrato = c(4, 5),
habitaciones = c(4, 4),
parqueaderos = c(1, 1),
banios = c(2, 2)
)
pred_v1 <- predict(modelo1, newdata = nuevas_v1,
interval = "prediction", level = 0.95)
resultados_v1 <- data.frame(
Escenario = c("Estrato 4", "Estrato 5"),
Precio_estimado = round(pred_v1[, "fit"], 1),
Limite_inferior = round(pred_v1[, "lwr"], 1),
Limite_superior = round(pred_v1[, "upr"], 1),
Presupuesto = 350,
Dentro_presupuesto = ifelse(pred_v1[, "fit"] <= 350, "✔ Sí", "✘ No")
)
resultados_v1 %>%
kable(caption = "Tabla 8. Predicciones para Vivienda 1 – Intervalo de predicción al 95%",
col.names = c("Escenario", "Precio estimado ($M)", "Límite inferior ($M)",
"Límite superior ($M)", "Presupuesto ($M)",
"Dentro del presupuesto")) %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| Escenario | Precio estimado (\(M) </th> <th style="text-align:right;"> Límite inferior (\)M) | Límite superior (\(M) </th> <th style="text-align:right;"> Presupuesto (\)M) | Dentro del presupuesto | ||
|---|---|---|---|---|---|
| Estrato 4 | 312.1 | 6.2 | 618.0 | 350 | ✔ Sí |
| Estrato 5 | 392.7 | 86.2 | 699.3 | 350 | ✘ No |
Interpretación de la predicción: El precio estimado corresponde al valor promedio esperado para una vivienda con las características especificadas en la solicitud. El intervalo de predicción al 95% refleja la incertidumbre asociada a la estimación y representa el rango dentro del cual se espera encontrar el precio real de una propiedad individual con estas características.
Comparar este resultado con el presupuesto disponible de $350 millones permite evaluar si las condiciones solicitadas son realistas dentro del mercado actual o si sería necesario ajustar alguna característica de la vivienda — por ejemplo, reducir el área o considerar un estrato inferior — para mantenerse dentro del crédito pre-aprobado.
# Búsqueda de propiedades que cumplan todos los criterios de la solicitud
ofertas_v1 <- base1 %>%
filter(
preciom <= 350,
estrato %in% c(4, 5),
habitaciones >= 4,
banios >= 2,
parqueaderos >= 1
) %>%
arrange(abs(areaconst - 200)) %>% # ordenadas por cercanía al área solicitada
head(10)
ofertas_v1 %>%
select(barrio, preciom, areaconst, estrato,
habitaciones, banios, parqueaderos, zona) %>%
kable(caption = "Tabla 9. Top 10 ofertas potenciales – Vivienda 1",
col.names = c("Barrio", "Precio ($M)", "Área (m²)", "Estrato",
"Habitaciones", "Baños", "Parqueaderos", "Zona")) %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| Barrio | Precio ($M) | Área (m²) | Estrato | Habitaciones | Baños | Parqueaderos | Zona |
|---|---|---|---|---|---|---|---|
| el bosque | 350 | 200 | 5 | 4 | 3 | 3 | Zona Norte |
| la flora | 320 | 200 | 5 | 4 | 4 | 2 | Zona Norte |
| la merced | 320 | 200 | 4 | 4 | 4 | 2 | Zona Norte |
| el bosque | 335 | 202 | 5 | 5 | 4 | 1 | Zona Norte |
| el bosque | 350 | 203 | 5 | 5 | 2 | 2 | Zona Norte |
| vipasa | 340 | 203 | 5 | 4 | 3 | 2 | Zona Norte |
| vipasa | 300 | 205 | 5 | 6 | 5 | 2 | Zona Norte |
| urbanización la merced | 320 | 210 | 5 | 5 | 3 | 2 | Zona Norte |
| la merced | 350 | 216 | 5 | 4 | 2 | 2 | Zona Norte |
| la flora | 340 | 180 | 5 | 4 | 4 | 2 | Zona Norte |
top5_v1 <- ofertas_v1 %>% head(5)
pal_v1 <- colorNumeric(palette = "Blues", domain = top5_v1$preciom)
leaflet(top5_v1) %>%
addTiles() %>%
addCircleMarkers(
lng = ~longitud,
lat = ~latitud,
radius = 9,
color = ~pal_v1(preciom),
fillOpacity = 0.9,
popup = ~paste0(
"<b>Barrio:</b> ", barrio, "<br>",
"<b>Precio:</b> $", preciom, " millones<br>",
"<b>Área:</b> ", areaconst, " m²<br>",
"<b>Estrato:</b> ", estrato, "<br>",
"<b>Habitaciones:</b> ", habitaciones, "<br>",
"<b>Baños:</b> ", banios, "<br>",
"<b>Parqueaderos:</b> ", parqueaderos
),
label = ~paste0("$", preciom, "M – ", barrio)
) %>%
addLegend("bottomright",
pal = pal_v1,
values = ~preciom,
title = "Precio ($M)",
labFormat = labelFormat(prefix = "$"))Análisis y recomendación: Las propiedades identificadas representan las alternativas del mercado que mejor se ajustan a los criterios definidos por la empresa solicitante. Estas opciones cumplen simultáneamente con las condiciones de zona, tipo de vivienda, características físicas y presupuesto máximo de $350 millones, ordenadas por cercanía al área de 200 m² requerida.
La visualización en el mapa permite evaluar la distribución geográfica de las ofertas dentro de la Zona Norte y facilita la comparación entre ellas, incorporando elementos espaciales relevantes para la decisión final, como accesibilidad, cercanía a servicios urbanos o conectividad vial. Se recomienda priorizar las opciones con mayor área y mejor estrato dentro del presupuesto disponible.
base2 <- vivienda %>%
filter(tipo == "Apartamento",
zona == "Zona Sur")
cat("Registros en base2 (Apartamentos – Zona Sur):", nrow(base2), "\n")## Registros en base2 (Apartamentos – Zona Sur): 2381
base2 %>%
head(3) %>%
kable(caption = "Tabla 10. Primeros 3 registros – Apartamentos Zona Sur") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE)| id | zona | piso | estrato | preciom | areaconst | parqueaderos | banios | habitaciones | tipo | barrio | longitud | latitud |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 5098 | Zona Sur | 05 | 4 | 290 | 96 | 1 | 2 | 3 | Apartamento | acopi | -76.53464 | 3.44987 |
| 698 | Zona Sur | 02 | 3 | 78 | 40 | 1 | 1 | 2 | Apartamento | aguablanca | -76.50100 | 3.40000 |
| 8199 | Zona Sur | NA | 6 | 875 | 194 | 2 | 5 | 3 | Apartamento | aguacatal | -76.55700 | 3.45900 |
base2 %>%
count(tipo, name = "Frecuencia") %>%
kable(caption = "Tabla 11. Verificación: distribución por tipo") %>%
kable_styling(bootstrap_options = "striped", full_width = FALSE)| tipo | Frecuencia |
|---|---|
| Apartamento | 2381 |
base2 %>%
count(zona, name = "Frecuencia") %>%
kable(caption = "Tabla 12. Verificación: distribución por zona") %>%
kable_styling(bootstrap_options = "striped", full_width = FALSE)| zona | Frecuencia |
|---|---|
| Zona Sur | 2381 |
base2 %>%
count(estrato, name = "Frecuencia") %>%
kable(caption = "Tabla 13. Distribución por estrato – base2") %>%
kable_styling(bootstrap_options = "striped", full_width = FALSE)| estrato | Frecuencia |
|---|---|
| 3 | 107 |
| 4 | 835 |
| 5 | 990 |
| 6 | 449 |
Las tablas confirman que base2 contiene exclusivamente
apartamentos en la Zona Sur. La distribución por estrato es
particularmente relevante para esta solicitud, ya que el cliente
requiere estrato 5 o 6 — la tabla permite verificar si hay suficientes
registros en esos estratos para sustentar el modelo y las
recomendaciones de oferta.
leaflet(base2) %>%
addTiles() %>%
addCircleMarkers(
lng = ~longitud,
lat = ~latitud,
radius = 5,
color = "#d7191c",
fillOpacity = 0.75,
popup = ~paste0(
"<b>Barrio:</b> ", barrio, "<br>",
"<b>Precio:</b> $", preciom, " millones<br>",
"<b>Área:</b> ", areaconst, " m²<br>",
"<b>Estrato:</b> ", estrato, "<br>",
"<b>Habitaciones:</b> ", habitaciones, "<br>",
"<b>Baños:</b> ", banios, "<br>",
"<b>Zona:</b> ", zona
),
label = ~paste0(barrio, " – $", preciom, "M")
) %>%
addLegend("bottomright",
colors = "#d7191c",
labels = "Apartamentos – Zona Sur",
title = "Base 2")Interpretación del mapa: La mayor concentración de puntos se ubica en la zona sur de Cali, validando el filtro aplicado. Al igual que en la base anterior, pueden aparecer algunos puntos fuera del área esperada debido a criterios comerciales de clasificación de zonas que no coinciden exactamente con los límites geográficos oficiales, o a posibles errores de registro en coordenadas. Estos casos representan una proporción mínima y no afectan de manera significativa las estimaciones del modelo.
base2 %>%
select(preciom, areaconst, estrato, banios, habitaciones, parqueaderos) %>%
summary() %>%
kable(caption = "Tabla 14. Resumen estadístico – Variables del modelo (base2)") %>%
kable_styling(bootstrap_options = "striped", full_width = FALSE)| preciom | areaconst | estrato | banios | habitaciones | parqueaderos | |
|---|---|---|---|---|---|---|
| Min. : 78.0 | Min. : 40.0 | Min. :3.000 | Min. :0.000 | Min. :0.000 | Min. : 1.000 | |
| 1st Qu.: 205.0 | 1st Qu.: 71.0 | 1st Qu.:4.000 | 1st Qu.:2.000 | 1st Qu.:3.000 | 1st Qu.: 1.000 | |
| Median : 260.0 | Median : 90.0 | Median :5.000 | Median :2.000 | Median :3.000 | Median : 1.000 | |
| Mean : 318.2 | Mean :102.2 | Mean :4.748 | Mean :2.588 | Mean :3.016 | Mean : 1.415 | |
| 3rd Qu.: 350.0 | 3rd Qu.:113.0 | 3rd Qu.:5.000 | 3rd Qu.:3.000 | 3rd Qu.:3.000 | 3rd Qu.: 2.000 | |
| Max. :1750.0 | Max. :932.0 | Max. :6.000 | Max. :8.000 | Max. :6.000 | Max. :10.000 |
El resumen estadístico de base2 permite comparar el
perfil del mercado de apartamentos en la Zona Sur con el de casas en la
Zona Norte. Se espera observar precios medios más altos en este
segmento, dado que los estratos 5 y 6 — requeridos para la segunda
solicitud — corresponden a propiedades de mayor valor. El presupuesto de
$850 millones es considerablemente mayor, lo que anticipa más opciones
disponibles dentro del rango permitido.
plot_ly(base2,
x = ~areaconst,
y = ~preciom,
type = "scatter",
mode = "markers",
marker = list(color = "#d7191c", opacity = 0.6, size = 7),
text = ~paste0("Barrio: ", barrio,
"<br>Precio: $", preciom, "M",
"<br>Área: ", areaconst, " m²",
"<br>Estrato: ", estrato)) %>%
layout(title = "Precio vs Área Construida – Apartamentos Zona Sur",
xaxis = list(title = "Área construida (m²)"),
yaxis = list(title = "Precio (millones COP)"))Se observa nuevamente una relación positiva entre área y precio. En este segmento, la dispersión tiende a ser mayor en áreas grandes, lo que refleja que en estratos altos (5-6) existen apartamentos de lujo con precios que se alejan significativamente del promedio, aun cuando tienen áreas similares a otras propiedades de menor valor. Esto refuerza la importancia de incluir el estrato como variable adicional en el modelo.
plot_ly(base2,
x = ~factor(estrato),
y = ~preciom,
type = "box",
color = ~factor(estrato),
colors = "Reds") %>%
layout(title = "Distribución de Precio por Estrato – Apartamentos Zona Sur",
xaxis = list(title = "Estrato socioeconómico"),
yaxis = list(title = "Precio (millones COP)"),
showlegend = FALSE)La diferencia de precios entre estratos es notoria en los apartamentos de la Zona Sur. El estrato 6 concentra las propiedades de mayor valor y mayor variabilidad, indicando un segmento premium del mercado donde factores como la vista, los acabados de lujo y los servicios del conjunto residencial generan diferencias significativas de precio incluso entre propiedades con características físicas similares.
plot_ly(base2,
x = ~factor(banios),
y = ~preciom,
type = "box",
color = ~factor(banios),
colors = "Purples") %>%
layout(title = "Precio por Número de Baños – Apartamentos Zona Sur",
xaxis = list(title = "Número de baños"),
yaxis = list(title = "Precio (millones COP)"),
showlegend = FALSE)El efecto del número de baños sobre el precio sigue siendo positivo. En el segmento de apartamentos de alto estrato, los baños adicionales generalmente corresponden a suites principales o baños de servicio, lo que refleja un mayor nivel de acabados y un precio más elevado.
plot_ly(base2,
x = ~factor(habitaciones),
y = ~preciom,
type = "box",
color = ~factor(habitaciones),
colors = "Oranges") %>%
layout(title = "Precio por Número de Habitaciones – Apartamentos Zona Sur",
xaxis = list(title = "Número de habitaciones"),
yaxis = list(title = "Precio (millones COP)"),
showlegend = FALSE)La relación entre habitaciones y precio es positiva, con mayor variabilidad en apartamentos con más habitaciones. Esto es esperable: un apartamento de 5 o 6 habitaciones en estrato 6 puede tener un precio muy distinto al de uno en estrato 5, aunque ambos tengan el mismo número de cuartos.
plot_ly(base2,
x = ~factor(parqueaderos),
y = ~preciom,
type = "box",
color = ~factor(parqueaderos),
colors = "Greens") %>%
layout(title = "Precio por Número de Parqueaderos – Apartamentos Zona Sur",
xaxis = list(title = "Número de parqueaderos"),
yaxis = list(title = "Precio (millones COP)"),
showlegend = FALSE)En el mercado de apartamentos de la Zona Sur, los parqueaderos tienen un efecto positivo sobre el precio. En conjuntos residenciales de estrato alto, múltiples parqueaderos son un atributo de lujo que puede añadir un valor considerable a la propiedad.
vars_cor2 <- base2 %>%
select(preciom, areaconst, estrato, banios, habitaciones, parqueaderos) %>%
na.omit()
cor_matrix2 <- cor(vars_cor2)
corrplot(cor_matrix2,
method = "color",
type = "upper",
tl.col = "black",
addCoef.col = "black",
number.cex = 0.8,
title = "Matriz de Correlación – Apartamentos Zona Sur",
mar = c(0, 0, 1, 0))
En los apartamentos de la Zona Sur, el área y el estrato se esperan nuevamente como los predictores con mayor correlación con el precio. La correlación entre baños y habitaciones puede ser moderada a alta en este segmento — apartamentos grandes tienden a tener más de ambos simultáneamente. Esto debe vigilarse en el VIF para asegurarse de que la multicolinealidad no afecta la estabilidad de los coeficientes del modelo.
modelo2 <- lm(preciom ~ areaconst + estrato + habitaciones +
parqueaderos + banios,
data = base2)
summary(modelo2)##
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos +
## banios, data = base2)
##
## Residuals:
## Min 1Q Median 3Q Max
## -1092.02 -42.28 -1.33 40.58 926.56
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -261.62501 15.63220 -16.736 < 2e-16 ***
## areaconst 1.28505 0.05403 23.785 < 2e-16 ***
## estrato 60.89709 3.08408 19.746 < 2e-16 ***
## habitaciones -24.83693 3.89229 -6.381 2.11e-10 ***
## parqueaderos 72.91468 3.95797 18.422 < 2e-16 ***
## banios 50.69675 3.39637 14.927 < 2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 98.02 on 2375 degrees of freedom
## Multiple R-squared: 0.7485, Adjusted R-squared: 0.748
## F-statistic: 1414 on 5 and 2375 DF, p-value: < 2.2e-16
coef2 <- as.data.frame(summary(modelo2)$coefficients)
coef2$Variable <- rownames(coef2)
coef2 <- coef2[, c("Variable", "Estimate", "Std. Error", "t value", "Pr(>|t|)")]
names(coef2) <- c("Variable", "Estimado", "Error Estándar", "Estadístico t", "Valor p")
coef2 %>%
mutate(Significativo = ifelse(`Valor p` < 0.05, "✔ Sí", "✘ No")) %>%
kable(digits = 4,
caption = "Tabla 15. Coeficientes estimados – Modelo Vivienda 2 (Apartamentos Zona Sur)") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
column_spec(6, bold = TRUE,
color = ifelse(coef2$`Valor p` < 0.05, "darkgreen", "red"))| Variable | Estimado | Error Estándar | Estadístico t | Valor p | Significativo | |
|---|---|---|---|---|---|---|
| (Intercept) | (Intercept) | -261.6250 | 15.6322 | -16.7363 | 0 | ✔ Sí |
| areaconst | areaconst | 1.2850 | 0.0540 | 23.7853 | 0 | ✔ Sí |
| estrato | estrato | 60.8971 | 3.0841 | 19.7457 | 0 | ✔ Sí |
| habitaciones | habitaciones | -24.8369 | 3.8923 | -6.3811 | 0 | ✔ Sí |
| parqueaderos | parqueaderos | 72.9147 | 3.9580 | 18.4223 | 0 | ✔ Sí |
| banios | banios | 50.6967 | 3.3964 | 14.9267 | 0 | ✔ Sí |
r2_v2 <- round(summary(modelo2)$r.squared * 100, 1)
r2adj_v2 <- round(summary(modelo2)$adj.r.squared * 100, 1)
cat("R² =", r2_v2, "%\n")## R² = 74.9 %
## R² ajustado = 74.8 %
Interpretación de coeficientes: Al igual que en el Modelo 1, se interpretan solo los coeficientes estadísticamente significativos. En el mercado de apartamentos de la Zona Sur, se espera que el área y el estrato sean nuevamente los predictores más influyentes. Los coeficientes de parqueaderos y habitaciones pueden o no resultar significativos, dependiendo de cuánto de su efecto ya está capturado por el área construida — en apartamentos grandes, estas variables tienden a moverse conjuntamente con el área.
Interpretación del R²: El modelo explica el 74.9% de la variabilidad en el precio de los apartamentos de la Zona Sur (R² ajustado = 74.8%). Comparar este R² con el del Modelo 1 permite entender cuál de los dos segmentos del mercado es más predecible con las variables disponibles. Un R² más bajo podría indicar mayor heterogeneidad en el segmento de apartamentos de alto valor, donde factores subjetivos como la vista, la marca del conjunto residencial o los servicios del edificio tienen mayor peso en el precio final.

## Test de Lilliefors:
## Estadístico D = 0.1244
## p-valor = 0
cat(" Conclusión:", ifelse(lt2$p.value > 0.05,
"No se rechaza la normalidad (p > 0.05).",
"Se rechaza la normalidad (p < 0.05). Considerar transformación logarítmica."))## Conclusión: Se rechaza la normalidad (p < 0.05). Considerar transformación logarítmica.
qq2 <- data.frame(
teoricos = qqnorm(res2, plot.it = FALSE)$x,
muestra = qqnorm(res2, plot.it = FALSE)$y
)
plot_ly(qq2, x = ~teoricos, y = ~muestra,
type = "scatter", mode = "markers",
name = "Residuales",
marker = list(color = "#d7191c", size = 6)) %>%
add_lines(x = ~teoricos, y = ~teoricos,
name = "Línea de referencia",
line = list(color = "darkred", dash = "dash")) %>%
layout(title = "QQ-Plot de Residuales – Modelo Vivienda 2",
xaxis = list(title = "Cuantiles teóricos"),
yaxis = list(title = "Cuantiles muestrales"))Interpretación: La interpretación sigue la misma
lógica que en el Modelo 1. En datos de precios de vivienda, es común que
los residuales muestren cierta asimetría positiva (cola derecha larga)
debido a la presencia de propiedades de lujo con precios muy superiores
al promedio del segmento. Una transformación logarítmica del precio
(log(preciom)) suele corregir este problema y mejorar el
cumplimiento del supuesto.
## Test de Breusch-Pagan:
## Estadístico BP = 754.8051
## p-valor = 0
cat(" Conclusión:", ifelse(bp2$p.value > 0.05,
"No hay evidencia de heterocedasticidad (p > 0.05).",
"Se detecta heterocedasticidad (p < 0.05). Considerar errores robustos o transformación."))## Conclusión: Se detecta heterocedasticidad (p < 0.05). Considerar errores robustos o transformación.
Interpretación: En el segmento de apartamentos de alto valor (estratos 5-6), la heterocedasticidad es más probable que en segmentos más homogéneos, porque los precios de propiedades de lujo tienden a tener mayor variabilidad que los precios de propiedades estándar. Si se detecta, el uso de errores estándar robustos es la corrección más directa sin necesidad de transformar el modelo.
## Test de Durbin-Watson:
## Estadístico DW = 1.5333
## p-valor = 0
cat(" Conclusión:", ifelse(dw2$p.value > 0.05,
"No hay evidencia de autocorrelación (p > 0.05).",
"Se detecta posible autocorrelación (p < 0.05)."))## Conclusión: Se detecta posible autocorrelación (p < 0.05).
Interpretación: Como se mencionó para el Modelo 1, la autocorrelación en datos de corte transversal suele ser baja. En datos inmobiliarios, si se detectara, podría estar relacionada con la agrupación espacial de las propiedades — viviendas en el mismo conjunto residencial o en el mismo barrio tienden a tener precios similares, lo que generaría dependencia entre sus residuales. Una solución en ese caso sería incluir el barrio como variable de control o usar modelos de regresión espacial.
vif2 <- vif(modelo2)
data.frame(Variable = names(vif2), VIF = round(vif2, 3)) %>%
mutate(Diagnóstico = ifelse(VIF > 10, "⚠ Alta multicolinealidad",
ifelse(VIF > 5, "⚡ Multicolinealidad moderada",
"✔ Aceptable"))) %>%
kable(caption = "Tabla 16. Factor de Inflación de Varianza (VIF) – Modelo Vivienda 2") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| Variable | VIF | Diagnóstico | |
|---|---|---|---|
| areaconst | areaconst | 2.067 | ✔ Aceptable |
| estrato | estrato | 1.545 | ✔ Aceptable |
| habitaciones | habitaciones | 1.429 | ✔ Aceptable |
| parqueaderos | parqueaderos | 1.738 | ✔ Aceptable |
| banios | banios | 2.529 | ✔ Aceptable |
Interpretación: En el segmento de apartamentos, la correlación entre habitaciones, baños y área construida puede ser más alta que en casas, dado que los apartamentos de gran tamaño tienden a tener más de todos estos atributos simultáneamente. Si alguna variable supera VIF = 10, se recomienda excluirla del modelo o combinarla con las demás a través de un índice compuesto.
La solicitud especifica estrato 5 o 6. Se realizan dos predicciones para cubrir ambos escenarios.
nuevas_v2 <- data.frame(
areaconst = c(300, 300),
estrato = c(5, 6),
habitaciones = c(5, 5),
parqueaderos = c(3, 3),
banios = c(3, 3)
)
pred_v2 <- predict(modelo2, newdata = nuevas_v2,
interval = "prediction", level = 0.95)
resultados_v2 <- data.frame(
Escenario = c("Estrato 5", "Estrato 6"),
Precio_estimado = round(pred_v2[, "fit"], 1),
Limite_inferior = round(pred_v2[, "lwr"], 1),
Limite_superior = round(pred_v2[, "upr"], 1),
Presupuesto = 850,
Dentro_presupuesto = ifelse(pred_v2[, "fit"] <= 850, "✔ Sí", "✘ No")
)
resultados_v2 %>%
kable(caption = "Tabla 17. Predicciones para Vivienda 2 – Intervalo de predicción al 95%",
col.names = c("Escenario", "Precio estimado ($M)", "Límite inferior ($M)",
"Límite superior ($M)", "Presupuesto ($M)",
"Dentro del presupuesto")) %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| Escenario | Precio estimado (\(M) </th> <th style="text-align:right;"> Límite inferior (\)M) | Límite superior (\(M) </th> <th style="text-align:right;"> Presupuesto (\)M) | Dentro del presupuesto | ||
|---|---|---|---|---|---|
| Estrato 5 | 675.0 | 481.5 | 868.6 | 850 | ✔ Sí |
| Estrato 6 | 735.9 | 542.3 | 929.5 | 850 | ✔ Sí |
Interpretación de la predicción: El precio estimado corresponde al valor promedio esperado para un apartamento con las características especificadas. El intervalo de predicción al 95% refleja la incertidumbre asociada a la estimación e indica el rango dentro del cual se espera encontrar el precio real de una propiedad individual con estas características.
Comparar este resultado con el presupuesto disponible de $850 millones permite evaluar si las condiciones solicitadas son compatibles con el mercado actual. La diferencia entre los precios estimados para estrato 5 y estrato 6 refleja directamente el efecto marginal del estrato según el modelo, y representa cuánto valora el mercado el acceso a un nivel socioeconómico superior.
# Búsqueda con criterios estrictos de la solicitud
ofertas_v2 <- base2 %>%
filter(
preciom <= 850,
estrato %in% c(5, 6),
habitaciones >= 5,
banios >= 3,
parqueaderos >= 3
) %>%
arrange(abs(areaconst - 300)) %>%
head(10)
# Si hay menos de 5 resultados, se relajan ligeramente los criterios
if (nrow(ofertas_v2) < 5) {
cat("Nota: con los criterios estrictos se encontraron", nrow(ofertas_v2),
"propiedades. Se amplían ligeramente los filtros para completar 5 opciones.\n")
ofertas_v2 <- base2 %>%
filter(
preciom <= 850,
estrato %in% c(5, 6),
habitaciones >= 4,
banios >= 3,
parqueaderos >= 2
) %>%
arrange(abs(areaconst - 300)) %>%
head(10)
}## Nota: con los criterios estrictos se encontraron 3 propiedades. Se amplían ligeramente los filtros para completar 5 opciones.
ofertas_v2 %>%
select(barrio, preciom, areaconst, estrato,
habitaciones, banios, parqueaderos, zona) %>%
kable(caption = "Tabla 18. Top 10 ofertas potenciales – Vivienda 2",
col.names = c("Barrio", "Precio ($M)", "Área (m²)", "Estrato",
"Habitaciones", "Baños", "Parqueaderos", "Zona")) %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| Barrio | Precio ($M) | Área (m²) | Estrato | Habitaciones | Baños | Parqueaderos | Zona |
|---|---|---|---|---|---|---|---|
| seminario | 670 | 300.00 | 5 | 6 | 5 | 3 | Zona Sur |
| cuarto de legua | 410 | 295.55 | 5 | 4 | 4 | 2 | Zona Sur |
| cuarto de legua | 520 | 320.00 | 5 | 4 | 4 | 2 | Zona Sur |
| ciudadela pasoancho | 650 | 275.00 | 5 | 5 | 5 | 2 | Zona Sur |
| capri | 350 | 270.00 | 5 | 4 | 3 | 3 | Zona Sur |
| san fernando | 500 | 330.00 | 5 | 4 | 4 | 2 | Zona Sur |
| san fernando viejo | 485 | 259.00 | 5 | 4 | 4 | 2 | Zona Sur |
| San Fernando | 350 | 258.00 | 5 | 5 | 4 | 2 | Zona Sur |
| seminario | 530 | 256.00 | 5 | 5 | 5 | 3 | Zona Sur |
| el ingenio | 700 | 250.00 | 6 | 5 | 4 | 2 | Zona Sur |
top5_v2 <- ofertas_v2 %>% head(5)
pal_v2 <- colorNumeric(palette = "Reds", domain = top5_v2$preciom)
leaflet(top5_v2) %>%
addTiles() %>%
addCircleMarkers(
lng = ~longitud,
lat = ~latitud,
radius = 9,
color = ~pal_v2(preciom),
fillOpacity = 0.9,
popup = ~paste0(
"<b>Barrio:</b> ", barrio, "<br>",
"<b>Precio:</b> $", preciom, " millones<br>",
"<b>Área:</b> ", areaconst, " m²<br>",
"<b>Estrato:</b> ", estrato, "<br>",
"<b>Habitaciones:</b> ", habitaciones, "<br>",
"<b>Baños:</b> ", banios, "<br>",
"<b>Parqueaderos:</b> ", parqueaderos
),
label = ~paste0("$", preciom, "M – ", barrio)
) %>%
addLegend("bottomright",
pal = pal_v2,
values = ~preciom,
title = "Precio ($M)",
labFormat = labelFormat(prefix = "$"))Análisis y recomendación: Las propiedades identificadas representan las alternativas del mercado que mejor se ajustan a los criterios de la segunda solicitud: apartamento en Zona Sur, estrato 5 o 6, con características físicas cercanas a las especificadas y precio dentro del presupuesto de $850 millones. Las opciones se ordenan por cercanía al área de 300 m² requerida.
La visualización en el mapa permite evaluar la distribución geográfica de las ofertas dentro de la Zona Sur y facilita la comparación entre ellas, incorporando elementos espaciales relevantes para la decisión final, como accesibilidad, cercanía a servicios urbanos o conectividad vial. Se recomienda priorizar las opciones de mayor estrato y área dentro del presupuesto, especialmente aquellas en conjuntos residenciales con amenidades que respondan al nivel de vida esperado por la empresa cliente.
El análisis realizado mediante Regresión Lineal Múltiple permitió identificar los principales determinantes del precio de la vivienda en los segmentos analizados del mercado inmobiliario de Cali. Los resultados muestran que el área construida y el estrato socioeconómico son las variables con mayor influencia sobre el valor de las propiedades, seguidas por características internas como el número de baños y habitaciones.
Para la Vivienda 1 (casa en Zona Norte), el modelo permitió estimar el precio esperado bajo distintos escenarios de estrato y verificar su compatibilidad con el presupuesto disponible de $350 millones. A partir de este análisis se identificaron varias propiedades reales que cumplen con las condiciones solicitadas y se encuentran dentro del límite de crédito pre-aprobado.
Para la Vivienda 2 (apartamento en Zona Sur), el presupuesto de $850 millones permite acceder a propiedades de mayor valor en estratos altos, lo que amplía las opciones disponibles dentro del mercado. Se estimaron precios para los escenarios de estrato 5 y 6, y se identificaron ofertas concretas que satisfacen los criterios de la solicitud.
En términos generales, los modelos presentan un buen nivel de ajuste y permiten generar predicciones útiles para apoyar la toma de decisiones inmobiliarias. Sin embargo, la incorporación de variables adicionales como la antigüedad del inmueble, el estado de conservación o la ubicación a nivel de barrio podría mejorar aún más la capacidad explicativa del modelo y la precisión de las predicciones.
library(rsconnect)
rsconnect::rpubsUpload(
title = "Actividad 2 - Caso C&A",
contentFile = "CyA_informe_final.html",
originalDoc = "CyA_informe_final.Rmd"
)## $id
## [1] "https://api.rpubs.com/api/v1/document/1407957/1fec019f098847ba913ba760fe3d7400"
##
## $continueUrl
## [1] "http://rpubs.com/publish/claim/1407957/4aae5debf452478f8c1d8c1c75c882fc"