INTRODUCCIÓN

El mercado inmobiliario constituye uno de los sectores más relevantes para el desarrollo económico de las ciudades, ya que involucra decisiones de inversión de alto valor y responde a dinámicas sociales, financieras y urbanísticas. En este contexto, las agencias de bienes raíces desempeñan un papel fundamental al actuar como intermediarias entre la oferta y la demanda de viviendas, proporcionando información, análisis y asesoría especializada para la toma de decisiones de compra.

María, profesional con más de diez años de experiencia en el sector inmobiliario, ha consolidado su trayectoria tras trabajar inicialmente en agencias reconocidas en Cali y Bogotá. Motivada por su experiencia y conocimiento del mercado, decidió fundar su propia empresa inmobiliaria denominada C&A (Casas y Apartamentos) en la ciudad de Cali, donde actualmente lidera un equipo de ocho agentes especializados en la comercialización de bienes raíces. A pesar de que el mercado inmobiliario de la ciudad ha presentado una desaceleración en los últimos meses, las perspectivas a mediano plazo sugieren una recuperación del sector impulsada por el financiamiento disponible para proyectos de construcción residencial y comercial.

En este escenario, María ha recibido una solicitud de asesoría por parte de una compañía internacional interesada en adquirir dos viviendas en la ciudad para ubicar a dos de sus empleados junto con sus familias. Cada vivienda debe cumplir con características específicas relacionadas con el tipo de inmueble, área construida, número de parqueaderos, baños, habitaciones, estrato socioeconómico, ubicación dentro de la ciudad y un presupuesto máximo previamente aprobado mediante crédito hipotecario.

Con el fin de apoyar esta decisión, el presente documento desarrolla un análisis basado en técnicas de modelación estadística y análisis de datos, utilizando información reciente del mercado inmobiliario de Cali correspondiente a los últimos tres meses. A partir de variables como ubicación, estrato socioeconómico, área construida, número de parqueaderos, baños, habitaciones y tipo de vivienda, se construirán y evaluarán modelos que permitan estimar el valor de las propiedades y determinar cuáles opciones se ajustan mejor a las condiciones solicitadas por la empresa.

Carga de datos

# Cargar paquete
library(paqueteMODELOS)
library(lmtest)
# Asignar a un dataframe
df <- vivienda;df
## # A tibble: 8,322 × 13
##       id zona   piso  estrato preciom areaconst parqueaderos banios habitaciones
##    <dbl> <chr>  <chr>   <dbl>   <dbl>     <dbl>        <dbl>  <dbl>        <dbl>
##  1  1147 Zona … <NA>        3     250        70            1      3            6
##  2  1169 Zona … <NA>        3     320       120            1      2            3
##  3  1350 Zona … <NA>        3     350       220            2      2            4
##  4  5992 Zona … 02          4     400       280            3      5            3
##  5  1212 Zona … 01          5     260        90            1      2            3
##  6  1724 Zona … 01          5     240        87            1      3            3
##  7  2326 Zona … 01          4     220        52            2      2            3
##  8  4386 Zona … 01          5     310       137            2      3            4
##  9  1209 Zona … 02          5     320       150            2      4            6
## 10  1592 Zona … 02          5     780       380            2      3            3
## # ℹ 8,312 more rows
## # ℹ 4 more variables: tipo <chr>, barrio <chr>, longitud <dbl>, latitud <dbl>
# Ver resumen
summary(df)
##        id           zona               piso              estrato     
##  Min.   :   1   Length:8322        Length:8322        Min.   :3.000  
##  1st Qu.:2080   Class :character   Class :character   1st Qu.:4.000  
##  Median :4160   Mode  :character   Mode  :character   Median :5.000  
##  Mean   :4160                                         Mean   :4.634  
##  3rd Qu.:6240                                         3rd Qu.:5.000  
##  Max.   :8319                                         Max.   :6.000  
##  NA's   :3                                            NA's   :3      
##     preciom         areaconst       parqueaderos        banios      
##  Min.   :  58.0   Min.   :  30.0   Min.   : 1.000   Min.   : 0.000  
##  1st Qu.: 220.0   1st Qu.:  80.0   1st Qu.: 1.000   1st Qu.: 2.000  
##  Median : 330.0   Median : 123.0   Median : 2.000   Median : 3.000  
##  Mean   : 433.9   Mean   : 174.9   Mean   : 1.835   Mean   : 3.111  
##  3rd Qu.: 540.0   3rd Qu.: 229.0   3rd Qu.: 2.000   3rd Qu.: 4.000  
##  Max.   :1999.0   Max.   :1745.0   Max.   :10.000   Max.   :10.000  
##  NA's   :2        NA's   :3        NA's   :1605     NA's   :3       
##   habitaciones        tipo              barrio             longitud     
##  Min.   : 0.000   Length:8322        Length:8322        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.605                                         Mean   :-76.53  
##  3rd Qu.: 4.000                                         3rd Qu.:-76.52  
##  Max.   :10.000                                         Max.   :-76.46  
##  NA's   :3                                              NA's   :3       
##     latitud     
##  Min.   :3.333  
##  1st Qu.:3.381  
##  Median :3.416  
##  Mean   :3.418  
##  3rd Qu.:3.452  
##  Max.   :3.498  
##  NA's   :3
library(dplyr)
## Warning: package 'dplyr' was built under R version 4.5.2
## 
## Adjuntando el paquete: 'dplyr'
## The following object is masked from 'package:gridExtra':
## 
##     combine
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
library(knitr)
library(kableExtra)
## Warning: package 'kableExtra' was built under R version 4.5.2
## 
## Adjuntando el paquete: 'kableExtra'
## The following object is masked from 'package:dplyr':
## 
##     group_rows
library(summarytools)
library(leaflet)
## Warning: package 'leaflet' was built under R version 4.5.2
tabla_zona <- df %>%
  count(zona) %>%
  arrange(desc(n))

kable(tabla_zona,
      col.names = c("Zona", "Número de viviendas"),
      caption = "Distribución de viviendas por zona en la base de datos") %>%
  kable_styling(full_width = FALSE, bootstrap_options = c("striped","hover","condensed"))
Distribución de viviendas por zona en la base de datos
Zona Número de viviendas
Zona Sur 4726
Zona Norte 1920
Zona Oeste 1198
Zona Oriente 351
Zona Centro 124
NA 3

Filtrado inicial para la generación de dos dataframe con respecto a las zonas correspondientes: Norte y Sur

df_norte <- df[df$zona == "Zona Norte", ]
df_sur <- df[df$zona == "Zona Sur", ]
summary(df_norte)
##        id             zona               piso              estrato     
##  Min.   :  31.0   Length:1923        Length:1923        Min.   :3.000  
##  1st Qu.: 832.8   Class :character   Class :character   1st Qu.:3.000  
##  Median :2400.5   Mode  :character   Mode  :character   Median :4.000  
##  Mean   :2558.0                                         Mean   :4.282  
##  3rd Qu.:3867.8                                         3rd Qu.:5.000  
##  Max.   :8319.0                                         Max.   :6.000  
##  NA's   :3                                              NA's   :3      
##     preciom         areaconst       parqueaderos        banios      
##  Min.   :  65.0   Min.   :  30.0   Min.   : 1.000   Min.   : 0.000  
##  1st Qu.: 160.0   1st Qu.:  70.0   1st Qu.: 1.000   1st Qu.: 2.000  
##  Median : 300.0   Median : 107.0   Median : 1.000   Median : 2.000  
##  Mean   : 345.6   Mean   : 161.1   Mean   : 1.646   Mean   : 2.778  
##  3rd Qu.: 430.0   3rd Qu.: 215.2   3rd Qu.: 2.000   3rd Qu.: 3.000  
##  Max.   :1940.0   Max.   :1440.0   Max.   :10.000   Max.   :10.000  
##  NA's   :3        NA's   :3        NA's   :636      NA's   :3       
##   habitaciones        tipo              barrio             longitud     
##  Min.   : 0.000   Length:1923        Length:1923        Min.   :-76.59  
##  1st Qu.: 3.000   Class :character   Class :character   1st Qu.:-76.53  
##  Median : 3.000   Mode  :character   Mode  :character   Median :-76.52  
##  Mean   : 3.501                                         Mean   :-76.52  
##  3rd Qu.: 4.000                                         3rd Qu.:-76.50  
##  Max.   :10.000                                         Max.   :-76.47  
##  NA's   :3                                              NA's   :3       
##     latitud     
##  Min.   :3.333  
##  1st Qu.:3.457  
##  Median :3.472  
##  Mean   :3.464  
##  3rd Qu.:3.485  
##  Max.   :3.498  
##  NA's   :3
summary(df_sur)
##        id           zona               piso              estrato     
##  Min.   :   1   Length:4729        Length:4729        Min.   :3.000  
##  1st Qu.:2574   Class :character   Class :character   1st Qu.:4.000  
##  Median :4378   Mode  :character   Mode  :character   Median :5.000  
##  Mean   :4361                                         Mean   :4.717  
##  3rd Qu.:6057                                         3rd Qu.:5.000  
##  Max.   :8305                                         Max.   :6.000  
##  NA's   :3                                            NA's   :3      
##     preciom         areaconst       parqueaderos        banios      
##  Min.   :  75.0   Min.   :  40.0   Min.   : 1.000   Min.   : 0.000  
##  1st Qu.: 222.0   1st Qu.:  78.0   1st Qu.: 1.000   1st Qu.: 2.000  
##  Median : 320.0   Median : 113.0   Median : 1.000   Median : 3.000  
##  Mean   : 426.5   Mean   : 173.3   Mean   : 1.835   Mean   : 3.179  
##  3rd Qu.: 520.0   3rd Qu.: 220.0   3rd Qu.: 2.000   3rd Qu.: 4.000  
##  Max.   :1900.0   Max.   :1600.0   Max.   :10.000   Max.   :10.000  
##  NA's   :3        NA's   :3        NA's   :624      NA's   :3       
##   habitaciones        tipo              barrio             longitud     
##  Min.   : 0.000   Length:4729        Length:4729        Min.   :-76.57  
##  1st Qu.: 3.000   Class :character   Class :character   1st Qu.:-76.54  
##  Median : 3.000   Mode  :character   Mode  :character   Median :-76.53  
##  Mean   : 3.601                                         Mean   :-76.53  
##  3rd Qu.: 4.000                                         3rd Qu.:-76.52  
##  Max.   :10.000                                         Max.   :-76.46  
##  NA's   :3                                              NA's   :3       
##     latitud     
##  Min.   :3.333  
##  1st Qu.:3.370  
##  Median :3.385  
##  Mean   :3.390  
##  3rd Qu.:3.409  
##  Max.   :3.497  
##  NA's   :3
nrow(df_norte)
## [1] 1923
nrow(df_sur)
## [1] 4729

1 ZONA NORTE

head(df_norte ,3)
## # A tibble: 3 × 13
##      id zona    piso  estrato preciom areaconst parqueaderos banios habitaciones
##   <dbl> <chr>   <chr>   <dbl>   <dbl>     <dbl>        <dbl>  <dbl>        <dbl>
## 1  1212 Zona N… 01          5     260        90            1      2            3
## 2  1724 Zona N… 01          5     240        87            1      3            3
## 3  2326 Zona N… 01          4     220        52            2      2            3
## # ℹ 4 more variables: tipo <chr>, barrio <chr>, longitud <dbl>, latitud <dbl>

1.1 Tabla descriptiva profesional de la Zona Norte

tabla_resumen <- df_norte %>%
  summarise(
    Viviendas = n(),
    Precio_promedio = mean(preciom, na.rm = TRUE),
    Area_promedio = mean(areaconst, na.rm = TRUE),
    Parqueaderos_prom = mean(parqueaderos, na.rm = TRUE),
    Banios_prom = mean(banios, na.rm = TRUE),
    Habitaciones_prom = mean(habitaciones, na.rm = TRUE)
  )

kable(tabla_resumen,
      caption = "Estadísticas descriptivas de viviendas en Zona Norte",
      digits = 2) %>%
  kable_styling(full_width = FALSE, bootstrap_options = c("striped","hover"))
Estadísticas descriptivas de viviendas en Zona Norte
Viviendas Precio_promedio Area_promedio Parqueaderos_prom Banios_prom Habitaciones_prom
1923 345.61 161.12 1.65 2.78 3.5

A partir de las estadísticas descriptivas obtenidas para las viviendas ubicadas en la Zona Norte de Cali, se identifican algunas características relevantes del mercado inmobiliario en este sector de la ciudad.

En primer lugar, la base de datos analizada incluye 1.923 viviendas, lo que representa una muestra amplia y representativa del comportamiento del mercado en esta zona. El precio promedio de las viviendas se sitúa alrededor de 345,61 millones de pesos, lo cual sugiere que la zona norte se posiciona como un sector de valor medio–alto dentro del mercado inmobiliario de la ciudad.

En términos de características físicas, las viviendas presentan un área construida promedio de 161,12 m², lo que indica que predominan inmuebles de tamaño moderado a amplio, adecuados para familias. Asimismo, se observa que las propiedades cuentan en promedio con 1,65 parqueaderos, 2,78 baños y 3,5 habitaciones, configuraciones que son consistentes con viviendas familiares de nivel socioeconómico medio y medio–alto.

1.2 MAPA INTERACTIVO DE PROPIEDDES ZONA NORTE

leaflet(df_norte) %>%
  addProviderTiles(providers$CartoDB.Positron) %>%
  setView(
    lng = mean(df_norte$longitud, na.rm = TRUE),
    lat = mean(df_norte$latitud, na.rm = TRUE),
    zoom = 12
  ) %>%
  addCircleMarkers(
    lng = ~longitud,
    lat = ~latitud,
    radius = 5,
    color = "darkred",
    fillOpacity = 0.7,
    popup = ~paste0(
      "<b>Barrio:</b> ", barrio,
      "<br><b>Precio:</b> ", preciom, " millones",
      "<br><b>Área:</b> ", areaconst, " m²",
      "<br><b>Habitaciones:</b> ", habitaciones
    )
  )
## Warning in validateCoords(lng, lat, funcName): Data contains 3 rows with either
## missing or invalid lat/lon values and will be ignored

En general, el mapa confirma que la mayoría de los inmuebles se ubican dentro del área esperada para la zona norte de Cali, lo cual respalda la validez del filtrado realizado. No obstante, la presencia de algunos puntos fuera de esta zona sugiere la necesidad de revisar posibles inconsistencias en la georreferenciación o en la clasificación de la variable zona, especialmente si se pretende realizar análisis espaciales más detallados o modelos que dependan de la ubicación geográfica de los inmuebles.

2 Correlación de Precio vs Variable independientes

library(plotly)
library(dplyr)

df_corr <- df_norte %>%
  select(preciom, areaconst, estrato, banios, habitaciones, latitud, longitud)

# Calcular matriz de correlación
matriz_corr <- cor(df_corr, use = "complete.obs")

# Etiquetas en español (7 variables)
etiquetas <- c(
  "Precio (M)",
  "Área Const.",
  "Estrato",
  "Baños",
  "Habitaciones",
  "Latitud",
  "Longitud"
)

# Texto de anotaciones
texto_corr <- matrix(
  sprintf("%.2f", matriz_corr),
  nrow = nrow(matriz_corr)
)

# Mapa de calor interactivo con Viridis
plot_ly(
  x = etiquetas,
  y = etiquetas,
  z = matriz_corr,
  type = "heatmap",
  colorscale = "Viridis",
  zmin = -1, zmax = 1,
  text = texto_corr,
  hovertemplate = "<b>%{y}</b> vs <b>%{x}</b><br>Correlación: <b>%{text}</b><extra></extra>"
) %>%
  add_annotations(
    x = rep(etiquetas, each = length(etiquetas)),
    y = rep(etiquetas, times = length(etiquetas)),
    text = as.vector(texto_corr),
    showarrow = FALSE,
    font = list(size = 11, color = "white")
  ) %>%
  layout(
    title = list(
      text = "<b>Mapa de Calor de Correlaciones — Zona Norte</b>",
      font = list(size = 16)
    ),
    xaxis = list(title = "", tickangle = -35),
    yaxis = list(title = "", autorange = "reversed"),
    margin = list(l = 120, b = 100, t = 60),
    paper_bgcolor = "#fafafa",
    plot_bgcolor  = "#fafafa"
  )

Precio (M) muestra correlaciones positivas moderadas-altas con Área Construida (0.73) y Estrato (0.61), lo que indica que a mayor tamaño del inmueble y mayor estrato socioeconómico, el precio tiende a incrementarse considerablemente. Baños (0.66) también presenta una correlación positiva relevante, sugiriendo que la cantidad de baños es un buen indicador del valor del inmueble, posiblemente porque refleja el tamaño y lujo de la propiedad. Habitaciones (0.39) tiene una relación positiva más moderada con el precio. Por otro lado, Longitud (-0.49) presenta la correlación negativa más fuerte con el precio, lo que geográficamente sugiere que los inmuebles ubicados más al occidente de Cali tienden a ser más costosos. Latitud (-0.17) tiene una relación negativa débil, indicando que la ubicación norte-sur tiene poca influencia directa sobre el precio en esta zona. En conjunto, las variables más relevantes para explicar el precio en df_norte son el área construida, el estrato y el número de baños, mientras que la ubicación geográfica (especialmente la longitud) también aporta información valiosa sobre la distribución espacial de los precios.

3 Modelado

# Modelo de regresión lineal múltiple
modelo <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, 
             data = df_norte)

# Resumen del modelo
summary(modelo)
## 
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos + 
##     banios, data = df_norte)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -721.33  -62.41   -5.97   40.70  941.76 
## 
## Coefficients:
##                Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  -265.61225   22.00179 -12.072  < 2e-16 ***
## areaconst       0.70321    0.03721  18.900  < 2e-16 ***
## estrato        84.49148    4.60020  18.367  < 2e-16 ***
## habitaciones  -11.18464    3.78370  -2.956  0.00317 ** 
## parqueaderos   31.69619    4.35742   7.274 6.06e-13 ***
## banios         42.75011    4.69749   9.101  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 128.3 on 1281 degrees of freedom
##   (636 observations deleted due to missingness)
## Multiple R-squared:  0.6868, Adjusted R-squared:  0.6856 
## F-statistic: 561.8 on 5 and 1281 DF,  p-value: < 2.2e-16

El modelo de regresión lineal muestra que las variables independientes explican aproximadamente el 68.7% de la variabilidad del precio de las viviendas (R² = 0.6868), indicando un buen ajuste para predicción. Entre los estimadores, el área construida y el estrato tienen un efecto positivo fuerte, aumentando el precio en 0.70 unidades por m² y 84.49 unidades por nivel de estrato, respectivamente. Los parqueaderos y los baños también contribuyen positivamente con incrementos de 31.70 y 42.75 unidades por cada uno, mientras que el coeficiente de habitaciones es negativo (-11.18), lo que podría reflejar correlaciones con otras variables o características específicas del mercado. Todos los coeficientes, excepto la variable de habitaciones con menor magnitud, son altamente significativos, confirmando la relevancia de estas variables para explicar el precio inmobiliario.

4 Validación de los supuestos

# Gráficos de diagnóstico
par(mfrow = c(2,2))
plot(modelo)

# Intervalos de confianza de los coeficientes
confint(modelo, level = 0.95)
##                     2.5 %       97.5 %
## (Intercept)  -308.7757584 -222.4487510
## areaconst       0.6302163    0.7762001
## estrato        75.4667322   93.5162222
## habitaciones  -18.6075752   -3.7617142
## parqueaderos   23.1477177   40.2446559
## banios         33.5344956   51.9657163
# VIF para detectar multicolinealidad
library(car)
## Cargando paquete requerido: carData
## 
## Adjuntando el paquete: 'car'
## The following object is masked from 'package:dplyr':
## 
##     recode
## The following object is masked from 'package:boot':
## 
##     logit
vif(modelo)
##    areaconst      estrato habitaciones parqueaderos       banios 
##     2.139878     1.314173     2.313930     1.523718     2.684230

Residuals vs Fitted: Se observa un patrón en forma de embudo (heterocedasticidad), donde la varianza de los residuos aumenta con los valores ajustados. Esto viola el supuesto de homocedasticidad del modelo lineal clásico. Q-Q Residuals: Los residuos se desvían de la línea diagonal teórica, especialmente en las colas, indicando que no siguen una distribución normal. Las observaciones 1419, 1425 y 1435 aparecen como valores atípicos influyentes. Scale-Location: La línea roja muestra una tendencia creciente en lugar de ser horizontal, lo que confirma la heterocedasticidad detectada anteriormente. La dispersión no es constante a lo largo de los valores ajustados. Residuals vs Leverage: Los puntos 1435, 469 y 1801 superan o se aproximan a la distancia de Cook, sugiriendo que son observaciones influyentes que pueden estar distorsionando los coeficientes estimados.

Si bien el modelo presenta un R² = 0.687 aceptable y coeficientes significativos, los diagnósticos revelan violaciones importantes a los supuestos de MCO: heterocedasticidad y no normalidad de residuos. Se recomienda considerar una transformación de la variable objetivo precio.

# Extraer residuos del modelo
residuos <- resid(lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = df_norte))

# Test de normalidad Shapiro-Wilk
shapiro.test(residuos)
## 
##  Shapiro-Wilk normality test
## 
## data:  residuos
## W = 0.83799, p-value < 2.2e-16
# Gráfico Q-Q
qqnorm(residuos)
qqline(residuos, col = "red")

El test de Shapiro-Wilk arroja un valor de W = 0.838 con p < 2.2e-16, lo que significa que rechazamos la hipótesis nula de normalidad. En otras palabras, los residuos no se distribuyen normalmente.

dwtest(lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = df_norte))
## 
##  Durbin-Watson test
## 
## data:  lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos +     banios, data = df_norte)
## DW = 1.7967, p-value = 0.0001161
## alternative hypothesis: true autocorrelation is greater than 0

DW = 1.797, lo que está algo por debajo de 2.

p-value = 0.0001161, indicando que rechazamos la hipótesis nula de no autocorrelación positiva.

Esto sugiere que existe ligera autocorrelación positiva en los residuos, lo que puede afectar la eficiencia de los coeficientes y los errores estándar, especialmente en series temporales o datos espaciales.

precio = β₀ + β₁(areaconst) + β₂(estrato) + β₃(habitaciones) + β₄(parqueaderos) + β₅(baños)

# ============================================
# PREDICCIÓN DE PRECIO - MODELO LM ZONA NORTE
# ============================================

# 1. CREAR EL DATA FRAME CON LOS DATOS DE LA VIVIENDA
nueva_vivienda <- data.frame(
  areaconst    = 200,
  parqueaderos = 1,
  banios       = 2,
  habitaciones = 4,
  estrato      = 4   # valor medio del rango 4-5
)

# 2. REALIZAR LA PREDICCIÓN CON EL MODELO
prediccion <- predict(modelo, 
                      newdata = nueva_vivienda, 
                      interval = "prediction",  # intervalo de predicción
                      level = 0.95)             # 95% de confianza

# 3. MOSTRAR RESULTADOS
cat("===============================================\n")
## ===============================================
cat("       PREDICCIÓN DE PRECIO - ZONA NORTE       \n")
##        PREDICCIÓN DE PRECIO - ZONA NORTE
cat("===============================================\n")
## ===============================================
cat("Características de la vivienda:\n")
## Características de la vivienda:
cat("  - Tipo:          Casa\n")
##   - Tipo:          Casa
cat("  - Área construida:", nueva_vivienda$areaconst, "m²\n")
##   - Área construida: 200 m²
cat("  - Parqueaderos:  ", nueva_vivienda$parqueaderos, "\n")
##   - Parqueaderos:   1
cat("  - Baños:         ", nueva_vivienda$banios, "\n")
##   - Baños:          2
cat("  - Habitaciones:  ", nueva_vivienda$habitaciones, "\n")
##   - Habitaciones:   4
cat("  - Estrato:       ", nueva_vivienda$estrato, "\n")
##   - Estrato:        4
cat("-----------------------------------------------\n")
## -----------------------------------------------
cat("Precio estimado:  ", round(prediccion[,"fit"], 2), "millones COP\n")
## Precio estimado:   285.45 millones COP
cat("Límite inferior:  ", round(prediccion[,"lwr"], 2), "millones COP\n")
## Límite inferior:   33.51 millones COP
cat("Límite superior:  ", round(prediccion[,"upr"], 2), "millones COP\n")
## Límite superior:   537.4 millones COP
cat("===============================================\n")
## ===============================================
# 4. PREDICCIÓN CON ESTRATO 5 (valor alto del rango)
nueva_vivienda_e5 <- nueva_vivienda
nueva_vivienda_e5$estrato <- 5

prediccion_e5 <- predict(modelo,
                         newdata = nueva_vivienda_e5,
                         interval = "prediction",
                         level = 0.95)

cat("\nComparación por estrato:\n")
## 
## Comparación por estrato:
cat("  Estrato 4 -> Precio estimado:", round(prediccion[,"fit"], 2), "millones COP\n")
##   Estrato 4 -> Precio estimado: 285.45 millones COP
cat("  Estrato 5 -> Precio estimado:", round(prediccion_e5[,"fit"], 2), "millones COP\n")
##   Estrato 5 -> Precio estimado: 369.94 millones COP
# ============================================================
# ALGORITMO: TOP 5 OFERTAS POTENCIALES PARA EL CLIENTE
# Modelo: modelo | Base de datos: df_norte
# Presupuesto máximo: 350 millones COP
# Perfil: Casa, 200m², 4 hab, 2 baños, 1 parq, estrato 4-5
# ============================================================

library(dplyr)
library(leaflet)

# ------------------------------------------------------------
# PASO 1: DEFINIR PERFIL DEL CLIENTE Y PRESUPUESTO
# ------------------------------------------------------------
presupuesto_max <- 350
tolerancia_area <- 0.20   # ±20% del área solicitada (160-240m²)
area_objetivo   <- 200

# ------------------------------------------------------------
# PASO 2: FILTRAR CANDIDATOS EN df_norte
# ------------------------------------------------------------
candidatos <- df_norte %>%
  filter(
    tipo         == "Casa",
    habitaciones == 4,
    banios       >= 2,
    parqueaderos >= 1,
    estrato      %in% c(4, 5),
    areaconst    >= area_objetivo * (1 - tolerancia_area),
    areaconst    <= area_objetivo * (1 + tolerancia_area),
    preciom      <= presupuesto_max
  ) %>%
  filter(!is.na(areaconst), !is.na(estrato),
         !is.na(habitaciones), !is.na(parqueaderos), !is.na(banios))

cat("✔ Candidatos filtrados:", nrow(candidatos), "\n")
## ✔ Candidatos filtrados: 14
# ------------------------------------------------------------
# PASO 3: CALCULAR PRECIO ESTIMADO CON EL MODELO
# ------------------------------------------------------------
candidatos$precio_estimado <- predict(modelo, newdata = candidatos)

# ------------------------------------------------------------
# PASO 4: CALCULAR MÉTRICAS DE VALOR
# ------------------------------------------------------------
candidatos <- candidatos %>%
  mutate(
    # Diferencia entre precio real y estimado por el modelo
    diferencia_precio = preciom - precio_estimado,
    
    # % de diferencia (negativo = oferta por debajo del mercado = oportunidad)
    pct_diferencia    = (diferencia_precio / precio_estimado) * 100,
    
    # Margen disponible del crédito
    margen_credito    = presupuesto_max - preciom,
    
    # Score de valor: penaliza sobreprecios, premia oportunidades y margen
    score_valor       = -pct_diferencia + (margen_credito / presupuesto_max) * 50
  )

# ------------------------------------------------------------
# PASO 5: SELECCIONAR TOP 5 POR SCORE DE VALOR
# ------------------------------------------------------------
top5 <- candidatos %>%
  arrange(desc(score_valor)) %>%
  slice_head(n = 5) %>%
  select(id, barrio, estrato, areaconst, habitaciones, banios,
         parqueaderos, preciom, precio_estimado,
         diferencia_precio, pct_diferencia, margen_credito, score_valor,
         latitud, longitud)

# ------------------------------------------------------------
# PASO 6: MOSTRAR RESULTADOS EN CONSOLA
# ------------------------------------------------------------
cat("\n===============================================================\n")
## 
## ===============================================================
cat("         TOP 5 OFERTAS POTENCIALES - ZONA NORTE               \n")
##          TOP 5 OFERTAS POTENCIALES - ZONA NORTE
cat("         Presupuesto máximo: $350M COP                        \n")
##          Presupuesto máximo: $350M COP
cat("===============================================================\n\n")
## ===============================================================
for (i in 1:nrow(top5)) {
  oferta <- top5[i, ]
  cat(sprintf("🏠 OFERTA #%d — ID: %d | Barrio: %s\n", i, oferta$id, oferta$barrio))
  cat(sprintf("   Área: %dm² | Estrato: %d | Hab: %d | Baños: %d | Parq: %d\n",
              oferta$areaconst, oferta$estrato, oferta$habitaciones,
              oferta$banios, oferta$parqueaderos))
  cat(sprintf("   Precio oferta:   $%.1f M\n", oferta$preciom))
  cat(sprintf("   Precio modelo:   $%.1f M\n", oferta$precio_estimado))
  cat(sprintf("   Diferencia:      %+.1f M (%+.1f%%)\n",
              oferta$diferencia_precio, oferta$pct_diferencia))
  cat(sprintf("   Margen crédito:  $%.1f M\n", oferta$margen_credito))
  cat(sprintf("   Score de valor:  %.2f\n", oferta$score_valor))
  
  if (oferta$pct_diferencia < -5) {
    cat("   PORTUNIDAD: precio por debajo del mercado\n")
  } else if (oferta$pct_diferencia <= 5) {
    cat("   PRECIO JUSTO: alineado con el mercado\n")
  } else {
    cat("   SOBREPRECIO: evaluar negociación\n")
  }
  cat("---------------------------------------------------------------\n")
}
## 🏠 OFERTA #1 — ID: 1644 | Barrio: vipasa
##    Área: 171m² | Estrato: 4 | Hab: 4 | Baños: 4 | Parq: 3
##    Precio oferta:   $270.0 M
##    Precio modelo:   $414.0 M
##    Diferencia:      -144.0 M (-34.8%)
##    Margen crédito:  $80.0 M
##    Score de valor:  46.20
##    PORTUNIDAD: precio por debajo del mercado
## ---------------------------------------------------------------
## 🏠 OFERTA #2 — ID: 3053 | Barrio: la flora
##    Área: 230m² | Estrato: 5 | Hab: 4 | Baños: 4 | Parq: 2
##    Precio oferta:   $320.0 M
##    Precio modelo:   $508.2 M
##    Diferencia:      -188.2 M (-37.0%)
##    Margen crédito:  $30.0 M
##    Score de valor:  41.32
##    PORTUNIDAD: precio por debajo del mercado
## ---------------------------------------------------------------
## 🏠 OFERTA #3 — ID: 1343 | Barrio: la flora
##    Área: 200m² | Estrato: 5 | Hab: 4 | Baños: 4 | Parq: 2
##    Precio oferta:   $320.0 M
##    Precio modelo:   $487.1 M
##    Diferencia:      -167.1 M (-34.3%)
##    Margen crédito:  $30.0 M
##    Score de valor:  38.60
##    PORTUNIDAD: precio por debajo del mercado
## ---------------------------------------------------------------
## 🏠 OFERTA #4 — ID: 1641 | Barrio: la flora
##    Área: 170m² | Estrato: 5 | Hab: 4 | Baños: 4 | Parq: 3
##    Precio oferta:   $343.0 M
##    Precio modelo:   $497.7 M
##    Diferencia:      -154.7 M (-31.1%)
##    Margen crédito:  $7.0 M
##    Score de valor:  32.09
##    PORTUNIDAD: precio por debajo del mercado
## ---------------------------------------------------------------
## 🏠 OFERTA #5 — ID: 1506 | Barrio: la flora
##    Área: 180m² | Estrato: 5 | Hab: 4 | Baños: 4 | Parq: 2
##    Precio oferta:   $340.0 M
##    Precio modelo:   $473.1 M
##    Diferencia:      -133.1 M (-28.1%)
##    Margen crédito:  $10.0 M
##    Score de valor:  29.56
##    PORTUNIDAD: precio por debajo del mercado
## ---------------------------------------------------------------
# ------------------------------------------------------------
# PASO 7: TABLA RESUMEN
# ------------------------------------------------------------
resumen <- top5 %>%
  select(id, barrio, estrato, areaconst, preciom,
         precio_estimado, pct_diferencia, margen_credito, score_valor) %>%
  rename(
    "ID"          = id,
    "Barrio"      = barrio,
    "Estrato"     = estrato,
    "Área (m²)"   = areaconst,
    "Precio ($M)" = preciom,
    "Modelo ($M)" = precio_estimado,
    "Dif. (%)"    = pct_diferencia,
    "Margen ($M)" = margen_credito,
    "Score"       = score_valor
  )

print(resumen)
## # A tibble: 5 × 9
##      ID Barrio   Estrato `Área (m²)` `Precio ($M)` `Modelo ($M)` `Dif. (%)`
##   <dbl> <chr>      <dbl>       <dbl>         <dbl>         <dbl>      <dbl>
## 1  1644 vipasa         4         171           270          414.      -34.8
## 2  3053 la flora       5         230           320          508.      -37.0
## 3  1343 la flora       5         200           320          487.      -34.3
## 4  1641 la flora       5         170           343          498.      -31.1
## 5  1506 la flora       5         180           340          473.      -28.1
## # ℹ 2 more variables: `Margen ($M)` <dbl>, Score <dbl>
# ------------------------------------------------------------
# PASO 8: MAPA LEAFLET CON TOP 5
# ------------------------------------------------------------
top5_geo <- top5 %>% filter(!is.na(latitud), !is.na(longitud))

leaflet(top5_geo) %>%
  addTiles() %>%
  addMarkers(
    lng   = ~longitud,
    lat   = ~latitud,
    popup = ~paste0(
      "<b>Oferta ID: ", id, "</b><br>",
      "Barrio: ", barrio, "<br>",
      "Área: ", areaconst, " m² | Estrato: ", estrato, "<br>",
      "Precio: $", preciom, "M<br>",
      "Modelo: $", round(precio_estimado, 1), "M<br>",
      "Diferencia: ", round(pct_diferencia, 1), "%<br>",
      "Margen crédito: $", margen_credito, "M"
    ),
    label = ~paste0("$", preciom, "M | ", barrio)
  ) %>%
  addLegend("bottomright",
            colors = "blue",
            labels = "Oferta potencial",
            title  = "Top 5 Ofertas ≤ $350M")

Conclusión — Top 5 Ofertas Potenciales Zona Norte

Hallazgo principal: todas son oportunidades de mercado El resultado más destacado del análisis es que las 5 propiedades están valoradas por debajo del precio estimado por el modelo, con diferencias que oscilan entre -28% y -37%. Esto indica que el mercado actual en la zona norte, específicamente en los barrios Vipasa y La Flora, está ofreciendo propiedades a precios significativamente inferiores a su valor estadístico estimado, lo que representa una ventana de oportunidad real para el comprador.

Análisis por oferta

Oferta #1 — Vipasa ($270M) es la recomendación principal. Presenta el mayor descuento absoluto respecto al modelo (-$144M), el mayor margen de crédito disponible ($80M) y el score más alto (46.20). Aunque su área es ligeramente menor (171m²), compensa con 4 baños y 3 parqueaderos, superando las especificaciones solicitadas. Es la opción con menor riesgo financiero.

Ofertas #2 y #3 — La Flora ($320M) son prácticamente equivalentes en precio pero la Oferta #2 tiene 30m² adicionales (230m²), lo que la hace más atractiva en términos de valor por metro cuadrado. Ambas son estrato 5, con 4 baños y 2 parqueaderos, superando el perfil solicitado. El margen de crédito restante es ajustado ($30M), lo que deja poco espacio para imprevistos.

Ofertas #4 y #5 — La Flora ($340M–$343M) están muy cerca del límite del presupuesto, con márgenes de apenas $7M y $10M respectivamente. Aunque siguen siendo oportunidades frente al modelo, el riesgo financiero es alto ante cualquier costo adicional como escrituración, adecuaciones o impuestos.

5 ZONA SUR

head(df_sur ,3)
## # A tibble: 3 × 13
##      id zona    piso  estrato preciom areaconst parqueaderos banios habitaciones
##   <dbl> <chr>   <chr>   <dbl>   <dbl>     <dbl>        <dbl>  <dbl>        <dbl>
## 1  5992 Zona S… 02          4     400       280            3      5            3
## 2  5098 Zona S… 05          4     290        96            1      2            3
## 3   698 Zona S… 02          3      78        40            1      1            2
## # ℹ 4 more variables: tipo <chr>, barrio <chr>, longitud <dbl>, latitud <dbl>

5.1 Tabla descriptiva profesional de la Zona Sur

tabla_resumen <- df_sur %>%
  summarise(
    Viviendas = n(),
    Precio_promedio = mean(preciom, na.rm = TRUE),
    Area_promedio = mean(areaconst, na.rm = TRUE),
    Parqueaderos_prom = mean(parqueaderos, na.rm = TRUE),
    Banios_prom = mean(banios, na.rm = TRUE),
    Habitaciones_prom = mean(habitaciones, na.rm = TRUE)
  )

kable(tabla_resumen,
      caption = "Estadísticas descriptivas de viviendas en Zona Sur",
      digits = 2) %>%
  kable_styling(full_width = FALSE, bootstrap_options = c("striped","hover"))
Estadísticas descriptivas de viviendas en Zona Sur
Viviendas Precio_promedio Area_promedio Parqueaderos_prom Banios_prom Habitaciones_prom
4729 426.52 173.31 1.83 3.18 3.6

La Zona Sur concentra 4.729 viviendas, representando el segmento más amplio del dataset analizado. Con un precio promedio de $426.52M COP, esta zona se posiciona como un mercado de valor medio-alto, aunque con características físicas que no necesariamente justifican ese precio frente a la Zona Norte.

5.2 MAPA INTERACTIVO DE PROPIEDDES ZONA SUR

leaflet(df_sur) %>%
  addProviderTiles(providers$CartoDB.Positron) %>%
  setView(
    lng = mean(df_norte$longitud, na.rm = TRUE),
    lat = mean(df_norte$latitud, na.rm = TRUE),
    zoom = 12
  ) %>%
  addCircleMarkers(
    lng = ~longitud,
    lat = ~latitud,
    radius = 5,
    color = "darkred",
    fillOpacity = 0.7,
    popup = ~paste0(
      "<b>Barrio:</b> ", barrio,
      "<br><b>Precio:</b> ", preciom, " millones",
      "<br><b>Área:</b> ", areaconst, " m²",
      "<br><b>Habitaciones:</b> ", habitaciones
    )
  )
## Warning in validateCoords(lng, lat, funcName): Data contains 3 rows with either
## missing or invalid lat/lon values and will be ignored

El mapa muestra la distribución espacial de las 4.729 viviendas de la Zona Sur. A primera vista, la concentración principal es coherente con el sur de Cali, sin embargo se identifican anomalías geográficas importantes que requieren discusión.

Hallazgos por zona del mapa Concentración principal (masa central) — La gran nube roja central corresponde efectivamente al sur de Cali, abarcando comunas como Ciudad Jardín, El Ingenio, Valle del Lili y sectores aledaños. Esta distribución es geográficamente coherente con lo esperado para la Zona Sur. Puntos dispersos al norte del cluster — Se observan varios puntos aislados que se extienden hacia la parte superior del mapa, alejados de la masa principal. Estos puntos podrían corresponder a:

Errores de geocodificación en el dataset Registros con coordenadas mal asignadas al momento de la captura Viviendas clasificadas como “Zona Sur” administrativamente pero ubicadas físicamente fuera de ese perímetro

Puntos hacia el oriente (derecha del mapa) — El grupo de puntos que se desprende hacia la derecha sugiere viviendas en sectores limítrofes o periféricos que podrían pertenecer geográficamente a otra zona pero fueron etiquetadas como Zona Sur en la base de datos. Referencia a Pichindé (occidente) — La aparición del municipio de Pichindé en el margen izquierdo del mapa es una señal de alerta. Pichindé es una zona rural montañosa al occidente de Cali, completamente fuera del perímetro urbano sur. Ninguna vivienda urbana debería aparecer en esa dirección, lo que confirma la presencia de coordenadas incorrectas o atípicas en el dataset.

6 Correlación de Precio vs Variable independientes

library(plotly)
library(dplyr)

df_corr <- df_sur %>%
  select(preciom, areaconst, estrato, banios, habitaciones, latitud, longitud)

# Calcular matriz de correlación
matriz_corr <- cor(df_corr, use = "complete.obs")

# Etiquetas en español (7 variables)
etiquetas <- c(
  "Precio (M)",
  "Área Const.",
  "Estrato",
  "Baños",
  "Habitaciones",
  "Latitud",
  "Longitud"
)

# Texto de anotaciones
texto_corr <- matrix(
  sprintf("%.2f", matriz_corr),
  nrow = nrow(matriz_corr)
)

# Mapa de calor interactivo con Viridis
plot_ly(
  x = etiquetas,
  y = etiquetas,
  z = matriz_corr,
  type = "heatmap",
  colorscale = "Viridis",
  zmin = -1, zmax = 1,
  text = texto_corr,
  hovertemplate = "<b>%{y}</b> vs <b>%{x}</b><br>Correlación: <b>%{text}</b><extra></extra>"
) %>%
  add_annotations(
    x = rep(etiquetas, each = length(etiquetas)),
    y = rep(etiquetas, times = length(etiquetas)),
    text = as.vector(texto_corr),
    showarrow = FALSE,
    font = list(size = 11, color = "white")
  ) %>%
  layout(
    title = list(
      text = "<b>Mapa de Calor de Correlaciones — Zona Sur</b>",
      font = list(size = 16)
    ),
    xaxis = list(title = "", tickangle = -35),
    yaxis = list(title = "", autorange = "reversed"),
    margin = list(l = 120, b = 100, t = 60),
    paper_bgcolor = "#fafafa",
    plot_bgcolor  = "#fafafa"
  )

El área construida es el predictor más poderoso con r = 0.76, confirmando que el tamaño físico de la vivienda es el principal determinante del precio en la Zona Sur, incluso por encima del estrato socioeconómico. Esto contrasta con mercados donde la ubicación domina sobre el tamaño. Los baños superan al estrato como predictor (0.71 vs 0.60), lo que sugiere que en la Zona Sur el nivel de acabados y funcionalidad interna de la vivienda pesa más que la clasificación socioeconómica del sector al momento de fijar el precio. Habitaciones tiene correlación moderada y baja (0.39), consistente con lo observado en el modelo de Zona Norte donde el coeficiente de habitaciones era negativo. Esto refleja una multicolinealidad con baños (r = 0.65) y área construida (r = 0.56): viviendas con más habitaciones tienden a tener más baños y mayor área, por lo que su efecto independiente sobre el precio se diluye. Multicolinealidad entre predictores. Se identifican correlaciones relevantes entre variables independientes que deben considerarse en el modelo:

Baños ↔︎ Área construida: 0.69 — alta Baños ↔︎ Habitaciones: 0.65 — alta Área construida ↔︎ Habitaciones: 0.56 — moderada Estrato ↔︎ Baños: 0.45 — moderada

Estas correlaciones entre predictores pueden inflar los errores estándar de los coeficientes y dificultar la interpretación individual de cada variable, lo que explicaría el signo negativo de habitaciones observado en el modelo de Zona Norte. Las coordenadas geográficas tienen correlación débil y negativa (-0.25 y -0.18), lo que indica que la ubicación dentro de la Zona Sur no discrimina fuertemente el precio. Esto es coherente con los problemas de coordenadas atípicas identificados en el mapa anterior, que podrían estar atenuando artificialmente estas correlaciones.

7 Modelado

# Modelo de regresión lineal múltiple
modelos <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, 
             data = df_sur)

# Resumen del modelo
summary(modelos)
## 
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos + 
##     banios, data = df_sur)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -826.25  -73.90  -12.28   51.37  997.38 
## 
## Coefficients:
##               Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  -364.7058    17.2411 -21.153   <2e-16 ***
## areaconst       0.8779     0.0249  35.252   <2e-16 ***
## estrato        90.8114     3.4971  25.968   <2e-16 ***
## habitaciones  -23.2891     2.7545  -8.455   <2e-16 ***
## parqueaderos   73.3075     2.8449  25.768   <2e-16 ***
## banios         50.9761     2.9917  17.039   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 156 on 4099 degrees of freedom
##   (624 observations deleted due to missingness)
## Multiple R-squared:  0.7723, Adjusted R-squared:  0.772 
## F-statistic:  2780 on 5 and 4099 DF,  p-value: < 2.2e-16

El modelo de regresión lineal para la Zona Sur muestra un ajuste sólido, con un R² de 0.7723, indicando que aproximadamente el 77% de la variabilidad en el precio de las viviendas (preciom) se explica por las variables independientes incluidas: área construida, estrato, habitaciones, parqueaderos y baños. Entre los coeficientes, área construida (0.878), estrato (90.81), parqueaderos (73.31) y baños (50.98) tienen un efecto positivo y altamente significativo sobre el precio, mientras que habitaciones (-23.29) presenta un efecto negativo, posiblemente asociado a correlaciones con otras características de la vivienda o a tipologías específicas del mercado en esta zona. El intercepto negativo (-364.71) funciona como referencia del precio base. En conjunto, los resultados reflejan que la Zona Sur valora principalmente el área, el estrato, los baños y los parqueaderos, mientras que la cantidad de habitaciones no incrementa necesariamente el valor del inmueble, y el modelo presenta un ajuste robusto y estadísticamente significativo para la predicción del precio de las viviendas en esta zona.

8 Validación de los supuestos

# Gráficos de diagnóstico
par(mfrow = c(2,2))
plot(modelos)

# Intervalos de confianza de los coeficientes
confint(modelos, level = 0.95)
##                     2.5 %      97.5 %
## (Intercept)  -398.5076936 -330.903959
## areaconst       0.8290774    0.926727
## estrato        83.9552601   97.667646
## habitaciones  -28.6893717  -17.888793
## parqueaderos   67.7298798   78.885126
## banios         45.1106772   56.841496
# VIF para detectar multicolinealidad
library(car)
vif(modelos)
##    areaconst      estrato habitaciones parqueaderos       banios 
##     2.378045     1.522590     2.106874     1.988851     3.069858
  1. Residuals vs Fitted — Heterocedasticidad severa El patrón en embudo es aún más pronunciado que en el modelo de Zona Norte. La varianza de los residuos crece notablemente con los valores ajustados, pasando de residuos pequeños en valores bajos a dispersiones superiores a ±1000M en valores ajustados altos (>1500M). Esto viola claramente el supuesto de homocedasticidad y es más grave que lo observado en Zona Norte, probablemente agravado por las coordenadas atípicas identificadas previamente.
  2. Q-Q Residuals — No normalidad marcada Los residuos se desvían sustancialmente de la línea teórica en ambas colas, con residuos estandarizados que alcanzan valores de -4 y +4, indicando colas muy pesadas. Esto confirma que los errores no siguen una distribución normal, lo cual invalida las pruebas de hipótesis clásicas sobre los coeficientes bajo MCO.
  3. Scale-Location — Tendencia creciente confirmada La línea roja muestra una pendiente positiva clara a lo largo de todo el rango de valores ajustados (0 a 2000+), confirmando la heterocedasticidad. A diferencia de la Zona Norte donde la tendencia era moderada, aquí la dispersión se acelera progresivamente, sugiriendo que el modelo pierde capacidad predictiva para viviendas de alto valor.
  4. Residuals vs Leverage — Observaciones influyentes críticas Se identifican tres observaciones que superan la distancia de Cook: 4410, 2757 y 984. Estos puntos tienen leverage elevado (hasta 0.04) y residuos estandarizados negativos superiores a -5, lo que indica que están distorsionando activamente los coeficientes del modelo hacia abajo. Es muy probable que correspondan a los registros con coordenadas atípicas detectados en el mapa geográfico.
# Extraer residuos del modelo
residuos <- resid(lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = df_sur))

# Test de normalidad Shapiro-Wilk
shapiro.test(residuos)
## 
##  Shapiro-Wilk normality test
## 
## data:  residuos
## W = 0.8332, p-value < 2.2e-16
# Gráfico Q-Q
qqnorm(residuos)
qqline(residuos, col = "red")

El resultado del Shapiro-Wilk para los residuos del modelo de la Zona Sur muestra W = 0.8332 con p < 2.2e-16, lo que indica que los residuos no se distribuyen normalmente. Esto significa que la hipótesis nula de normalidad se rechaza y sugiere que los supuestos clásicos de regresión lineal no se cumplen completamente en términos de normalidad.

Implicaciones:

Los intervalos de confianza y los valores p podrían no ser totalmente confiables.

Se recomienda considerar transformaciones de la variable precio (por ejemplo, logaritmo,sqrt) o modelos robustos que no dependan de la normalidad estricta de los residuos.

A pesar de esto, el modelo sigue siendo útil para predicción, dado su alto R² de 0.7723, pero los análisis inferenciales deben interpretarse con precaución.

Conclusión — Normal Q-Q Plot Zona Sur Interpretación del gráfico Este Q-Q Plot amplificado confirma y detalla con mayor claridad la no normalidad severa de los residuos del modelo de Zona Sur identificada en los diagnósticos anteriores.

Análisis por segmentos de la distribución Cola izquierda (cuantiles teóricos < -2) Los puntos caen muy por debajo de la línea roja de referencia, con residuos que alcanzan valores de hasta -700M. Esto indica que el modelo sobreestima sistemáticamente los precios de las viviendas más baratas, generando errores negativos extremos que no serían esperables bajo normalidad. Zona central (-1 a +1) Es la única región donde los puntos se aproximan razonablemente a la línea roja, sugiriendo que el modelo funciona aceptablemente solo para viviendas de precio medio, que constituyen la mayor parte del dataset. Cola derecha (cuantiles teóricos > +2) Los puntos se despegan fuertemente hacia arriba, con residuos que superan los +700M y llegan cerca de +1000M. Esto significa que el modelo subestima significativamente los precios de las viviendas más costosas, un comportamiento típico cuando existen propiedades de lujo que escapan al patrón lineal general.

dwtest(lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = df_sur))
## 
##  Durbin-Watson test
## 
## data:  lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos +     banios, data = df_sur)
## DW = 1.7056, p-value < 2.2e-16
## alternative hypothesis: true autocorrelation is greater than 0

El test de Durbin-Watson para el modelo de la Zona Sur arrojó DW = 1.706 con p < 2.2e-16, lo que indica la presencia de autocorrelación positiva significativa en los residuos. Esto sugiere que los errores no son completamente independientes, lo que puede afectar la precisión de los errores estándar y, por lo tanto, la confiabilidad de los intervalos de confianza y los valores p. Aunque el modelo presenta un R² alto (0.7723) y coeficientes significativos, esta autocorrelación sugiere que podrían existir patrones espaciales o características no incluidas en el modelo que influyen en los precios de las viviendas, por lo que sería recomendable considerar modelos robustos, correcciones por autocorrelación o incluir variables adicionales que capturen estos efectos.

# ============================================
# PREDICCIÓN DE PRECIO - MODELO LM ZONA SUR
# ============================================

# 1. CREAR EL DATA FRAME CON LOS DATOS DE LA VIVIENDA
nueva_vivienda <- data.frame(
  areaconst    = 300,
  parqueaderos = 3,
  banios       = 3,
  habitaciones = 5,
  estrato      = 5   # valor medio del rango 5-6
)

# 2. REALIZAR LA PREDICCIÓN CON EL MODELO
prediccion <- predict(modelos, 
                      newdata = nueva_vivienda, 
                      interval = "prediction",
                      level = 0.95)

# 3. MOSTRAR RESULTADOS
cat("===============================================\n")
## ===============================================
cat("       PREDICCIÓN DE PRECIO - ZONA SUR         \n")
##        PREDICCIÓN DE PRECIO - ZONA SUR
cat("===============================================\n")
## ===============================================
cat("Características de la vivienda:\n")
## Características de la vivienda:
cat("  - Tipo:            Apartamento\n")
##   - Tipo:            Apartamento
cat("  - Área construida:", nueva_vivienda$areaconst, "m2\n")
##   - Área construida: 300 m2
cat("  - Parqueaderos:   ", nueva_vivienda$parqueaderos, "\n")
##   - Parqueaderos:    3
cat("  - Baños:          ", nueva_vivienda$banios, "\n")
##   - Baños:           3
cat("  - Habitaciones:   ", nueva_vivienda$habitaciones, "\n")
##   - Habitaciones:    5
cat("  - Estrato:        ", nueva_vivienda$estrato, "\n")
##   - Estrato:         5
cat("-----------------------------------------------\n")
## -----------------------------------------------
cat("Precio estimado:  ", round(prediccion[,"fit"], 2), "millones COP\n")
## Precio estimado:   609.13 millones COP
cat("Límite inferior:  ", round(prediccion[,"lwr"], 2), "millones COP\n")
## Límite inferior:   303.05 millones COP
cat("Límite superior:  ", round(prediccion[,"upr"], 2), "millones COP\n")
## Límite superior:   915.2 millones COP
cat("===============================================\n")
## ===============================================
# 4. COMPARACIÓN ESTRATO 5 vs ESTRATO 6
nueva_vivienda_e6 <- nueva_vivienda
nueva_vivienda_e6$estrato <- 6

prediccion_e6 <- predict(modelos,
                         newdata = nueva_vivienda_e6,
                         interval = "prediction",
                         level = 0.95)

cat("\nComparación por estrato:\n")
## 
## Comparación por estrato:
cat("  Estrato 5 -> Precio estimado:", round(prediccion[,"fit"],   2), "millones COP\n")
##   Estrato 5 -> Precio estimado: 609.13 millones COP
cat("  Estrato 6 -> Precio estimado:", round(prediccion_e6[,"fit"], 2), "millones COP\n")
##   Estrato 6 -> Precio estimado: 699.94 millones COP
# ============================================================
# ALGORITMO: TOP 5 OFERTAS POTENCIALES PARA EL CLIENTE
# Modelo: modelo_sur | Base de datos: df_sur
# Presupuesto máximo: 850 millones COP
# Perfil: Apartamento, 300m², 5 hab, 3 baños, 3 parq, estrato 5-6
# ============================================================

library(dplyr)
library(leaflet)

# ------------------------------------------------------------
# PASO 1: DEFINIR PERFIL DEL CLIENTE Y PRESUPUESTO
# ------------------------------------------------------------
presupuesto_max <- 850
tolerancia_area <- 0.20   # ±20% del área solicitada (240-360m²)
area_objetivo   <- 300

# ------------------------------------------------------------
# PASO 2: FILTRAR CANDIDATOS EN df_sur
# ------------------------------------------------------------
candidatos <- df_sur %>%
  filter(
    tipo         == "Apartamento",
    habitaciones == 5,
    banios       >= 3,
    parqueaderos >= 3,
    estrato      %in% c(5, 6),
    areaconst    >= area_objetivo * (1 - tolerancia_area),
    areaconst    <= area_objetivo * (1 + tolerancia_area),
    preciom      <= presupuesto_max
  ) %>%
  filter(!is.na(areaconst), !is.na(estrato),
         !is.na(habitaciones), !is.na(parqueaderos), !is.na(banios))

cat("✔ Candidatos filtrados:", nrow(candidatos), "\n")
## ✔ Candidatos filtrados: 1
# ------------------------------------------------------------
# PASO 3: CALCULAR PRECIO ESTIMADO CON EL MODELO
# ------------------------------------------------------------
candidatos$precio_estimado <- predict(modelos, newdata = candidatos)

# ------------------------------------------------------------
# PASO 4: CALCULAR MÉTRICAS DE VALOR
# ------------------------------------------------------------
candidatos <- candidatos %>%
  mutate(
    # Diferencia entre precio real y estimado por el modelo
    diferencia_precio = preciom - precio_estimado,

    # % de diferencia (negativo = oferta por debajo del mercado = oportunidad)
    pct_diferencia    = (diferencia_precio / precio_estimado) * 100,

    # Margen disponible del crédito
    margen_credito    = presupuesto_max - preciom,

    # Score de valor: penaliza sobreprecios, premia oportunidades y margen
    score_valor       = -pct_diferencia + (margen_credito / presupuesto_max) * 50
  )

# ------------------------------------------------------------
# PASO 5: SELECCIONAR TOP 5 POR SCORE DE VALOR
# ------------------------------------------------------------
top5 <- candidatos %>%
  arrange(desc(score_valor)) %>%
  slice_head(n = 5) %>%
  select(id, barrio, estrato, areaconst, habitaciones, banios,
         parqueaderos, preciom, precio_estimado,
         diferencia_precio, pct_diferencia, margen_credito, score_valor,
         latitud, longitud)

# ------------------------------------------------------------
# PASO 6: MOSTRAR RESULTADOS EN CONSOLA
# ------------------------------------------------------------
cat("\n===============================================================\n")
## 
## ===============================================================
cat("         TOP 5 OFERTAS POTENCIALES - ZONA SUR                 \n")
##          TOP 5 OFERTAS POTENCIALES - ZONA SUR
cat("         Presupuesto máximo: $850M COP                        \n")
##          Presupuesto máximo: $850M COP
cat("===============================================================\n\n")
## ===============================================================
for (i in 1:nrow(top5)) {
  oferta <- top5[i, ]
  cat(sprintf("🏠 OFERTA #%d — ID: %d | Barrio: %s\n", i, oferta$id, oferta$barrio))
  cat(sprintf("   Área: %dm² | Estrato: %d | Hab: %d | Baños: %d | Parq: %d\n",
              oferta$areaconst, oferta$estrato, oferta$habitaciones,
              oferta$banios, oferta$parqueaderos))
  cat(sprintf("   Precio oferta:   $%.1f M\n", oferta$preciom))
  cat(sprintf("   Precio modelo:   $%.1f M\n", oferta$precio_estimado))
  cat(sprintf("   Diferencia:      %+.1f M (%+.1f%%)\n",
              oferta$diferencia_precio, oferta$pct_diferencia))
  cat(sprintf("   Margen crédito:  $%.1f M\n", oferta$margen_credito))
  cat(sprintf("   Score de valor:  %.2f\n", oferta$score_valor))

  if (oferta$pct_diferencia < -5) {
    cat("   OPORTUNIDAD: precio por debajo del mercado\n")
  } else if (oferta$pct_diferencia <= 5) {
    cat("   PRECIO JUSTO: alineado con el mercado\n")
  } else {
    cat("  SOBREPRECIO: evaluar negociación\n")
  }
  cat("---------------------------------------------------------------\n")
}
## 🏠 OFERTA #1 — ID: 8036 | Barrio: seminario
##    Área: 256m² | Estrato: 5 | Hab: 5 | Baños: 5 | Parq: 3
##    Precio oferta:   $530.0 M
##    Precio modelo:   $672.5 M
##    Diferencia:      -142.5 M (-21.2%)
##    Margen crédito:  $320.0 M
##    Score de valor:  40.01
##    OPORTUNIDAD: precio por debajo del mercado
## ---------------------------------------------------------------
# ------------------------------------------------------------
# PASO 7: TABLA RESUMEN
# ------------------------------------------------------------
resumen <- top5 %>%
  select(id, barrio, estrato, areaconst, preciom,
         precio_estimado, pct_diferencia, margen_credito, score_valor) %>%
  rename(
    "ID"          = id,
    "Barrio"      = barrio,
    "Estrato"     = estrato,
    "Área (m²)"   = areaconst,
    "Precio ($M)" = preciom,
    "Modelo ($M)" = precio_estimado,
    "Dif. (%)"    = pct_diferencia,
    "Margen ($M)" = margen_credito,
    "Score"       = score_valor
  )

print(resumen)
## # A tibble: 1 × 9
##      ID Barrio    Estrato `Área (m²)` `Precio ($M)` `Modelo ($M)` `Dif. (%)`
##   <dbl> <chr>       <dbl>       <dbl>         <dbl>         <dbl>      <dbl>
## 1  8036 seminario       5         256           530          672.      -21.2
## # ℹ 2 more variables: `Margen ($M)` <dbl>, Score <dbl>
# ------------------------------------------------------------
# PASO 8: MAPA LEAFLET CON TOP 5
# ------------------------------------------------------------
top5_geo <- top5 %>% filter(!is.na(latitud), !is.na(longitud))

leaflet(top5_geo) %>%
  addTiles() %>%
  addMarkers(
    lng   = ~longitud,
    lat   = ~latitud,
    popup = ~paste0(
      "<b>Oferta ID: ", id, "</b><br>",
      "Barrio: ", barrio, "<br>",
      "Área: ", areaconst, " m² | Estrato: ", estrato, "<br>",
      "Precio: $", preciom, "M<br>",
      "Modelo: $", round(precio_estimado, 1), "M<br>",
      "Diferencia: ", round(pct_diferencia, 1), "%<br>",
      "Margen crédito: $", margen_credito, "M"
    ),
    label = ~paste0("$", preciom, "M | ", barrio)
  ) %>%
  addLegend("bottomright",
            colors = "blue",
            labels = "Oferta potencial",
            title  = "Top 5 Ofertas ≤ $850M")

Conclusión — Resultado de Búsqueda Zona Sur: Una Sola Oferta

El algoritmo identificó únicamente 1 propiedad que cumple simultáneamente todos los criterios del perfil solicitado (apartamento, ~300m², 5 habitaciones, ≥3 baños, ≥3 parqueaderos, estrato 5-6, precio ≤ $850M) en la Zona Sur. Esto es un resultado relevante en sí mismo, pues refleja la escasa oferta disponible para este perfil específico en el mercado.

Análisis de la única oferta — ID 8036 | Barrio Seminario La propiedad presenta características sólidas: 256m², estrato 5, 5 habitaciones, 5 baños y 3 parqueaderos. Su precio de oferta es $530M, mientras que el modelo lo valora en $672.5M, generando una brecha de -$142.5M (-21.2%), lo que la clasifica como una oportunidad de mercado clara. El margen de crédito disponible es amplio ($320M), lo que otorga considerable seguridad financiera al comprador. Sin embargo, hay aspectos que merecen discusión: El área es inferior a la solicitada. El cliente buscaba 300m² y la oferta tiene 256m², una diferencia de 44m² (-14.7%) que está dentro de la tolerancia del ±20% aplicada en el filtro pero que debe ser considerada en la negociación. 5 baños supera el perfil solicitado. El cliente requería 3 baños y la propiedad ofrece 5, lo que agrega valor real no reflejado en el precio de oferta y puede justificar el interés por la propiedad.