Introducción

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:

  • Identificar los factores que influyen en el precio de las viviendas.
  • Estimar el valor esperado de las propiedades solicitadas.
  • Evaluar si las características requeridas se encuentran dentro del presupuesto disponible.
  • Proponer opciones reales del mercado que satisfagan los criterios de la empresa solicitante.

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

Configuración de paquetes y carga de datos

# 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
# Vista general de la estructura del dataset
glimpse(vivienda)
## 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.

# Resumen estadístico general del dataset limpio
summary(vivienda)
##        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.


VIVIENDA 1: Casa – Zona Norte


Paso 1: Filtrado de datos y mapa

1.1 Filtro de la base de datos

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
cat("Variables disponibles:", ncol(base1), "\n")
## 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.

1.2 Primeros 3 registros

base1 %>%
  head(3) %>%
  kable(caption = "Tabla 1. Primeros 3 registros – Casas Zona Norte") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE)
Tabla 1. Primeros 3 registros – Casas Zona Norte
id zona piso estrato preciom areaconst parqueaderos banios habitaciones tipo barrio longitud latitud
1209 Zona Norte 02 5 320 150 2 4 6 Casa acopi -76.51341 3.47968
1592 Zona Norte 02 5 780 380 2 3 3 Casa acopi -76.51674 3.48721
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.

1.3 Verificación del filtro

# 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)
Tabla 2. Verificación: distribución por tipo de vivienda
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)
Tabla 3. Verificación: distribución por zona
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)
Tabla 4. Distribución por estrato socioeconómico – base1
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.

1.4 Mapa de ubicación – Casas Zona Norte

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.


Paso 2: Análisis exploratorio de datos

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)
Tabla 5. Resumen estadístico – Variables del modelo (base1)
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.

Precio vs Área construida

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.

Precio vs Estrato

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.

Precio vs Número de Baños

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.

Precio vs Habitaciones

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.

Precio vs Parqueaderos

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.

Matriz de correlación

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.


Paso 3: Estimación del modelo de regresión lineal múltiple

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"))
Tabla 6. Coeficientes estimados – Modelo Vivienda 1 (Casas Zona Norte)
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 %
cat("R² ajustado =", r2adj_v1, "%\n")
## 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.

  • Área construida (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 socioeconómico (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.
  • Número de baños (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 (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.


Paso 4: Validación de supuestos del modelo

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.

par(mfrow = c(2, 2))
plot(modelo1, main = "Diagnóstico – Modelo Vivienda 1")

par(mfrow = c(1, 1))

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.

Supuesto 1: Normalidad de residuales

res1 <- residuals(modelo1)
lt1  <- lillie.test(res1)

cat("Test de Lilliefors (Kolmogorov-Smirnov):\n")
## Test de Lilliefors (Kolmogorov-Smirnov):
cat("  Estadístico D =", round(lt1$statistic, 4), "\n")
##   Estadístico D = 0.1325
cat("  p-valor       =", round(lt1$p.value, 4), "\n")
##   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.

Supuesto 2: Homocedasticidad (varianza constante)

bp1 <- bptest(modelo1)
cat("Test de Breusch-Pagan:\n")
## Test de Breusch-Pagan:
cat("  Estadístico BP =", round(bp1$statistic, 4), "\n")
##   Estadístico BP = 80.2808
cat("  p-valor        =", round(bp1$p.value, 4), "\n")
##   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.

Supuesto 3: Independencia de residuales

dw1 <- dwtest(modelo1)
cat("Test de Durbin-Watson:\n")
## Test de Durbin-Watson:
cat("  Estadístico DW =", round(dw1$statistic, 4), "\n")
##   Estadístico DW = 1.7615
cat("  p-valor        =", round(dw1$p.value, 4), "\n")
##   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.

Supuesto 4: Ausencia de multicolinealidad

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)
Tabla 7. Factor de Inflación de Varianza (VIF) – Modelo Vivienda 1
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.


Paso 5: Predicción para Vivienda 1

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)
Tabla 8. Predicciones para Vivienda 1 – Intervalo de predicción al 95%
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.


Paso 6: Ofertas potenciales para Vivienda 1 (≤ $350 millones)

# 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)
Tabla 9. Top 10 ofertas potenciales – Vivienda 1
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.


VIVIENDA 2: Apartamento – Zona Sur


Paso 1: Filtrado de datos y mapa

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)
Tabla 10. Primeros 3 registros – Apartamentos Zona Sur
id zona piso estrato preciom areaconst parqueaderos banios habitaciones tipo barrio longitud latitud
5098 Zona Sur 05 4 290 96 1 2 3 Apartamento acopi -76.53464 3.44987
698 Zona Sur 02 3 78 40 1 1 2 Apartamento aguablanca -76.50100 3.40000
8199 Zona Sur NA 6 875 194 2 5 3 Apartamento aguacatal -76.55700 3.45900
base2 %>%
  count(tipo, name = "Frecuencia") %>%
  kable(caption = "Tabla 11. Verificación: distribución por tipo") %>%
  kable_styling(bootstrap_options = "striped", full_width = FALSE)
Tabla 11. Verificación: distribución por tipo
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)
Tabla 12. Verificación: distribución por zona
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)
Tabla 13. Distribución por estrato – base2
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.


Paso 2: Análisis exploratorio de datos

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)
Tabla 14. Resumen estadístico – Variables del modelo (base2)
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.

Precio vs Área construida

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.

Precio vs Estrato

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.

Precio vs Número de Baños

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.

Precio vs Habitaciones

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.

Precio vs Parqueaderos

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.

Matriz de correlación

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.


Paso 3: Estimación del modelo de regresión lineal múltiple

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"))
Tabla 15. Coeficientes estimados – Modelo Vivienda 2 (Apartamentos Zona Sur)
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 %
cat("R² ajustado =", r2adj_v2, "%\n")
## 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.


Paso 4: Validación de supuestos del modelo

par(mfrow = c(2, 2))
plot(modelo2, main = "Diagnóstico – Modelo Vivienda 2")

par(mfrow = c(1, 1))

Supuesto 1: Normalidad de residuales

res2 <- residuals(modelo2)
lt2  <- lillie.test(res2)

cat("Test de Lilliefors:\n")
## Test de Lilliefors:
cat("  Estadístico D =", round(lt2$statistic, 4), "\n")
##   Estadístico D = 0.1244
cat("  p-valor       =", round(lt2$p.value, 4), "\n")
##   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.

Supuesto 2: Homocedasticidad

bp2 <- bptest(modelo2)
cat("Test de Breusch-Pagan:\n")
## Test de Breusch-Pagan:
cat("  Estadístico BP =", round(bp2$statistic, 4), "\n")
##   Estadístico BP = 754.8051
cat("  p-valor        =", round(bp2$p.value, 4), "\n")
##   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.

Supuesto 3: Independencia de residuales

dw2 <- dwtest(modelo2)
cat("Test de Durbin-Watson:\n")
## Test de Durbin-Watson:
cat("  Estadístico DW =", round(dw2$statistic, 4), "\n")
##   Estadístico DW = 1.5333
cat("  p-valor        =", round(dw2$p.value, 4), "\n")
##   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.

Supuesto 4: Multicolinealidad (VIF)

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)
Tabla 16. Factor de Inflación de Varianza (VIF) – Modelo Vivienda 2
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.


Paso 5: Predicción para Vivienda 2

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)
Tabla 17. Predicciones para Vivienda 2 – Intervalo de predicción al 95%
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.


Paso 6: Ofertas potenciales para Vivienda 2 (≤ $850 millones)

# 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)
Tabla 18. Top 10 ofertas potenciales – Vivienda 2
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.


Conclusiones

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"