library(paqueteMODELOS); data("vivienda")

1 Primera solicitud

En esta sección se presenta el análisis correspondiente a la primera solicitud realizada por la compañía internacional interesada en adquirir una vivienda en la ciudad de Cali para alojar a uno de sus empleados. De acuerdo con las condiciones establecidas, se trata de una casa ubicada en la zona Norte, con un área construida de 200 m², cuatro habitaciones, dos baños, un parqueadero y perteneciente a un estrato socioeconómico 4 o 5. Asimismo, la familia cuenta con un crédito preaprobado de 350 millones de pesos para la adquisición del inmueble. A partir de estas características, se desarrolla el análisis exploratorio y predictivo que permitirá determinar si el crédito disponible resulta suficiente frente a los precios estimados en el mercado y, de igual manera, ofrecer recomendaciones sobre la viabilidad de la compra.

library(gt)

tabla_viv1 <- data.frame(
  Características = c("Tipo", "área construida", "parqueaderos",
                      "baños", "habitaciones", "estrato",
                      "zona", "crédito preaprobado"),
  `Vivienda 1` = c("Casa", "200", "1", "2", "4",
                   "4 o 5", "Norte", "350 millones")
)

tabla_viv1 %>%
  gt() %>%
  tab_header(title = md("**Características de la Vivienda 1**"))
Características de la Vivienda 1
Características Vivienda.1
Tipo Casa
área construida 200
parqueaderos 1
baños 2
habitaciones 4
estrato 4 o 5
zona Norte
crédito preaprobado 350 millones

A continuación, se presenta el primer filtro aplicado a la base de datos, en el cual se incluyen únicamente las ofertas correspondientes a casas ubicadas en la Zona Norte de la ciudad. Para comprobar la validez de la consulta, se muestran los primeros tres registros de la base resultante, así como tablas de conteo que permiten verificar la clasificación de las viviendas por zona y tipo. Además, se incorpora un mapa con la ubicación de las ofertas seleccionadas, lo que facilita identificar si los puntos geográficos se concentran efectivamente en la zona correspondiente o si aparecen registros en otras áreas de la ciudad, discutiendo las posibles razones de estas discrepancias.

# LIMPIEZA Y PREPRACIÓN DE LOS DATOS
library(dplyr)
library(tidyr)
library(stringr)


# Tipos consistentes
vivienda <- vivienda %>%
  mutate(
    # numéricas
    areaconst    = as.numeric(areaconst),
    banios       = as.numeric(banios),
    habitaciones = as.numeric(habitaciones),
    preciom      = as.numeric(preciom),
    longitud     = as.numeric(longitud),
    latitud      = as.numeric(latitud),
    # categóricas
    tipo    = as.factor(tipo),
    zona    = as.factor(zona),
    barrio  = as.factor(barrio),
    estrato = as.factor(estrato),
    # piso a entero (valores no numéricos -> NA)
    piso = suppressWarnings(as.integer(piso))
  )

# Normalizar strings vacíos -> NA (por si hay columnas character)
vivienda <- vivienda %>%
  mutate(across(where(is.character), ~ na_if(str_squish(.), "")))

# Selección de variables + reglas mínimas de calidad
vivienda_limpia <- vivienda %>%
  dplyr::select(tipo, areaconst, parqueaderos, banios, habitaciones,
                estrato, zona, preciom, piso, barrio, longitud, latitud) %>%
  tidyr::drop_na(preciom, areaconst, estrato, zona) %>%
  dplyr::filter(preciom > 0, areaconst > 0) %>%
  dplyr::filter(!is.na(longitud), !is.na(latitud)) %>%
  mutate(
    parqueaderos = if_else(is.na(parqueaderos), 0L, as.integer(parqueaderos)),
    estrato_num  = suppressWarnings(as.numeric(as.character(estrato))),
    precio_m2    = preciom / areaconst
  ) %>%
  distinct()

# Winsorización 1%–99% en precio_m2
q01 <- quantile(vivienda_limpia$precio_m2, 0.01, na.rm = TRUE)
q99 <- quantile(vivienda_limpia$precio_m2, 0.99, na.rm = TRUE)
vivienda_limpia <- vivienda_limpia %>%
  mutate(precio_m2_wins = pmin(pmax(precio_m2, q01), q99))

# ── 2) Base para primera solicitud: Casas en Zona Norte ────────────────────────
base1 <- vivienda_limpia %>%
  dplyr::filter(.data$tipo == "Casa") %>%
  dplyr::filter(stringr::str_detect(.data$zona,
           stringr::regex("^Zona\\s*Norte$", ignore_case = TRUE)))

# Chequeos cortos (opcionales)
# head(base1, 3)
# base1 %>% count(zona, sort = TRUE)
# base1 %>% count(tipo, zona, sort = TRUE)
# --- Cargar paquetes en un chunk de setup antes ---
library(dplyr)
library(stringr)
library(tidyr)
library(gt)

# --- Crear base1 con Casas de Zona Norte ---
base1 <- vivienda_limpia %>%
  dplyr::filter(.data$tipo == "Casa") %>%
  dplyr::filter(stringr::str_detect(.data$zona,
                                    stringr::regex("^Zona\\s*Norte$", ignore_case = TRUE))) %>%
  tidyr::drop_na(zona)

# --- Verificaciones con tablas formateadas ---

# Primeros 3 registros
base1 %>%
  head(3) %>%
  gt() %>%
  tab_header(
    title = md("**Tabla 1. Primeros 3 registros de `base1` (Casas Zona Norte)**")
  )
Tabla 1. Primeros 3 registros de base1 (Casas Zona Norte)
tipo areaconst parqueaderos banios habitaciones estrato zona preciom piso barrio longitud latitud estrato_num precio_m2 precio_m2_wins
Casa 150 2 4 6 5 Zona Norte 320 2 acopi -76.51341 3.47968 5 2.133333 2.133333
Casa 380 2 3 3 5 Zona Norte 780 2 acopi -76.51674 3.48721 5 2.052632 2.052632
Casa 445 0 7 6 6 Zona Norte 750 2 acopi -76.52950 3.38527 6 1.685393 1.685393
# Conteo por zona
base1 %>%
  count(zona, sort = TRUE) %>%
  gt() %>%
  tab_header(
    title = md("**Tabla 2. Número de registros por zona en `base1`**")
  )
Tabla 2. Número de registros por zona en base1
zona n
Zona Norte 717
# Conteo por tipo y zona
base1 %>%
  count(tipo, zona, sort = TRUE) %>%
  gt() %>%
  tab_header(
    title = md("**Tabla 3. Número de registros por tipo y zona en `base1`**")
  )
Tabla 3. Número de registros por tipo y zona en base1
tipo zona n
Casa Zona Norte 717

Tras aplicar los filtros a la base de datos se construyó base1, conformada únicamente por casas de la Zona Norte. La verificación inicial muestra que los tres primeros registros cumplen con estas condiciones y que en total se cuentan 717 observaciones, todas clasificadas como casas en dicha zona. Esto confirma que el filtrado fue correcto y que la base resultante concentra únicamente la información necesaria para el análisis de la primera solicitud.

1.1 Distribución geográfica

library(leaflet)

leaflet(base1) %>%
  addTiles() %>%
  addCircleMarkers(
    ~longitud, ~latitud,
    popup = ~paste0("<b>", tipo, " - ", zona, "</b><br>",
                    "Barrio: ", barrio, "<br>",
                    "Precio: ", preciom, " M<br>",
                    "Área: ", areaconst, " m²<br>",
                    "Parq: ", parqueaderos, 
                    " | Baños: ", banios, 
                    " | Hab: ", habitaciones),
    radius = 6, stroke = FALSE, fillOpacity = 0.8
  )

Aunque el filtro de datos selecciona únicamente las viviendas clasificadas como Casas de Zona Norte, al visualizar los puntos en el mapa se observa que no todos se concentran estrictamente en el norte de la ciudad. Algunos aparecen en zonas más centrales o incluso hacia el sur. Esto puede deberse a varias razones:

  • Diferencias entre la clasificación comercial y la división geográfica oficial de la ciudad.

  • Posibles errores de geocodificación en las coordenadas de algunos registros.

  • Inclusión de barrios limítrofes que, según ciertas agencias, forman parte del “Norte” aunque geográficamente se ubiquen en áreas intermedias.

Ante esta situación, sería pertinente dialogar con los expertos inmobiliarios de C&A para revisar los límites que ellos reconocen como “Zona Norte” o “Zona Sur”, y comprender si existen criterios comerciales o excepciones en la clasificación. Esto permitiría ajustar la delimitación de manera más consistente.

Por el momento, se trabajará con la información tal cual aparece en la base de datos. Sin embargo, a futuro se podría contemplar una delimitación espacial basada en coordenadas, que permita marcar como valores atípicos o incongruentes aquellas observaciones que no estén dentro de un polígono definido como “Zona Norte”. Este sería un paso útil en análisis posteriores para garantizar mayor coherencia entre la variable categórica zona y la localización geográfica real.

1.2 Análisis exploratorio de datos (EDA)

En esta sección se realiza un análisis exploratorio enfocado en la variable respuesta, precio de la casa (preciom), en función de las variables: área construida (areaconst), estrato, número de baños, número de habitaciones y zona. Se emplean gráficos interactivos con el paquete plotly, lo que permite visualizar de forma dinámica las relaciones entre las variables y facilitar la interpretación de patrones en los datos.

library(plotly)
library(dplyr)
library(tidyr)
casas <- vivienda_limpia %>%
  filter(tipo == "Casa") %>%
  select(preciom, areaconst, estrato, banios, habitaciones, zona) %>%
  drop_na()

1.2.1 Matriz de Correlaciones

# --- Correlaciones numéricas (referencia) ---
matriz_cor <- round(cor(casas %>% select(where(is.numeric))), 2)

matriz_cor %>%
  as.data.frame() %>%
  tibble::rownames_to_column(var = "Variable") %>%
  gt() %>%
  tab_header(
    title = md("**Tabla 4. Matriz de correlación de variables numéricas (Casas)**")
  ) %>%
  # aplicar colores a todas las columnas numéricas
  data_color(
    columns = -Variable,
    colors = scales::col_numeric(
      palette = c("red", "white", "blue"), # azul = negativo, blanco = 0, rojo = positivo
      domain = c(-1, 1)
    )
  )
## Warning: Since gt v0.9.0, the `colors` argument has been deprecated.
## • Please use the `fn` argument instead.
## This warning is displayed once every 8 hours.
Tabla 4. Matriz de correlación de variables numéricas (Casas)
Variable preciom areaconst banios habitaciones
preciom 1.00 0.65 0.56 0.10
areaconst 0.65 1.00 0.49 0.29
banios 0.56 0.49 1.00 0.48
habitaciones 0.10 0.29 0.48 1.00

Los resultados de la matriz de correlación refuerzan las observaciones anteriores. El precio se relaciona principalmente con el área construida (r = 0,65) y el número de baños (r = 0,56), mientras que las habitaciones tienen una relación marginal (r = 0,10). Asimismo, área y baños también están correlacionados (r = 0,49), lo cual refleja que a mayor tamaño, suele incrementarse el número de baños. Este panorama confirma la relevancia del área, el estrato y los baños como predictores del valor de la vivienda, y la menor importancia relativa de las habitaciones.

1.2.2 Precio vs Área Construida

# Precio vs Área (color por zona)
plot_ly(casas, x=~areaconst, y=~preciom, color=~zona,
        type="scatter", mode="markers") %>%
  plotly::layout(title="CASAS: Precio vs Área por zona",
                 xaxis=list(title="Área (m²)"),
                 yaxis=list(title="Precio (M)"))

El análisis muestra una relación positiva entre el área construida y el precio de las viviendas: a mayor tamaño, mayor valor en el mercado. La correlación de 0,65 confirma esta tendencia moderada-alta, aunque persiste una dispersión considerable, dado que casas con áreas similares pueden diferir notablemente en precio según su ubicación o estrato. En términos de distribución geográfica, se observa una concentración de registros en las zonas Sur y Norte, mientras que en las zonas Centro y Oriente el número de observaciones es menor.

1.2.3 Precio por Estrato

# Precio por estrato (boxplot)
plot_ly(casas, x=~as.factor(estrato), y=~preciom, color=~as.factor(estrato),
        type="box") %>%
  plotly::layout(title="CASAS: Precio por estrato",
                 xaxis=list(title="Estrato"),
                 yaxis=list(title="Precio (M)"))

La variable estrato evidencia un patrón claramente ascendente: las viviendas de estratos superiores presentan precios más altos y mayor dispersión. En particular, el estrato 3 concentra inmuebles de menor valor, mientras que el estrato 6 registra las medianas más elevadas y un rango intercuartílico considerablemente mayor. Este comportamiento refleja cómo el estrato socioeconómico actúa como un diferenciador clave en la valoración de las viviendas.

1.2.4 Precio según Número de Baños

# Precio vs número de baños (boxplot)
plot_ly(casas, x=~banios, y=~preciom, color=~zona, type="box") %>%
  plotly::layout(title="CASAS: Precio según número de baños",
                 xaxis=list(title="Baños"),
                 yaxis=list(title="Precio (M)"))

Existe una relación positiva entre el número de baños y el precio de las casas. Aquellas con más de cuatro baños tienden a superar los 1.000 millones, lo que confirma que esta característica añade valor al inmueble. La correlación de 0,56 entre ambas variables refuerza este hallazgo. No obstante, se detectan excepciones: algunas viviendas con pocos baños muestran precios elevados, lo que puede explicarse por factores como la zona o el área construida.

1.2.5 Precio según Número de Habitaciones

# Precio vs número de habitaciones (boxplot)
plot_ly(casas, x=~habitaciones, y=~preciom, color=~zona, type="box") %>%
  plotly::layout(title="CASAS: Precio según número de habitaciones",
                 xaxis=list(title="Habitaciones"),
                 yaxis=list(title="Precio (M)"))

En contraste con los baños, el número de habitaciones no guarda una relación clara con el precio. La correlación es baja (0,10), lo que indica que, por sí sola, esta variable no explica las variaciones en el valor de mercado. Se observa que incluso viviendas con muchas habitaciones (entre ocho y diez) no necesariamente alcanzan precios altos, lo que sugiere que el área y el estrato pesan más en la determinación del precio final.

1.3 Modelo de regresión lineal múltiple

En esta etapa se realizó la preparación de los datos, filtrando únicamente las viviendas clasificadas como casas y depurando los registros con información incompleta. Se generaron variables numéricas y categóricas consistentes (como el estrato en formato numérico y la zona como factor), con el fin de contar con una base limpia y adecuada para el ajuste del modelo de regresión lineal múltiple.

# -------- 3.1 Preparación de datos --------
library(dplyr)
library(tidyr)
library(gt)

# Filtrar y limpiar datos de casas
casas_df <- vivienda_limpia %>%
  dplyr::filter(tipo == "Casa") %>%              # Filtrar solo casas
  tidyr::drop_na(preciom, areaconst, estrato, 
                 habitaciones, parqueaderos, 
                 banios, zona) %>%               # Eliminar NA en variables clave
  mutate(
    estrato_num = as.numeric(estrato),           # Convertir estrato a numérico
    zona = factor(zona)                          # Asegurar zona como factor
  )

# Vista previa formateada (primeros 6 registros)
casas_df %>%
  head(6) %>%
  gt() %>%
  tab_header(
    title = md("**Tabla X. Vista previa de los datos de Casas (base limpia)**")
  )
Tabla X. Vista previa de los datos de Casas (base limpia)
tipo areaconst parqueaderos banios habitaciones estrato zona preciom piso barrio longitud latitud estrato_num precio_m2 precio_m2_wins
Casa 70 1 3 6 3 Zona Oriente 250 NA 20 de julio -76.51168 3.43382 1 3.571429 3.571429
Casa 120 1 2 3 3 Zona Oriente 320 NA 20 de julio -76.51237 3.43369 1 2.666667 2.666667
Casa 220 2 2 4 3 Zona Oriente 350 NA 20 de julio -76.51537 3.43566 1 1.590909 1.590909
Casa 280 3 5 3 4 Zona Sur 400 2 3 de julio -76.54000 3.43500 2 1.428571 1.428571
Casa 150 2 4 6 5 Zona Norte 320 2 acopi -76.51341 3.47968 3 2.133333 2.133333
Casa 380 2 3 3 5 Zona Norte 780 2 acopi -76.51674 3.48721 3 2.052632 2.052632
# -------- 3.2 Estimación por MCO --------
mod_casas <- lm(preciom ~ areaconst + estrato_num + habitaciones + parqueaderos + banios + zona,
                data = casas_df)

# Resumen del modelo
summary(mod_casas)               
## 
## Call:
## lm(formula = preciom ~ areaconst + estrato_num + habitaciones + 
##     parqueaderos + banios + zona, data = casas_df)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -1317.53  -114.86   -23.96    71.35  1188.99 
## 
## Coefficients:
##                    Estimate Std. Error t value Pr(>|t|)    
## (Intercept)       -39.97030   23.42247  -1.706  0.08801 .  
## areaconst           0.80184    0.02558  31.348  < 2e-16 ***
## estrato_num       114.78591    4.84287  23.702  < 2e-16 ***
## habitaciones      -14.40638    2.59282  -5.556 2.98e-08 ***
## parqueaderos       40.56096    2.80314  14.470  < 2e-16 ***
## banios             38.93029    3.32707  11.701  < 2e-16 ***
## zonaZona Norte   -105.78405   22.08973  -4.789 1.75e-06 ***
## zonaZona Oeste     -3.84196   26.55571  -0.145  0.88498    
## zonaZona Oriente  -70.60017   23.53059  -3.000  0.00272 ** 
## zonaZona Sur      -84.44637   21.92996  -3.851  0.00012 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 202.7 on 3190 degrees of freedom
## Multiple R-squared:  0.682,  Adjusted R-squared:  0.6811 
## F-statistic: 760.3 on 9 and 3190 DF,  p-value: < 2.2e-16
# ANOVA marginal
car::Anova(mod_casas, type = 2)  
## Anova Table (Type II tests)
## 
## Response: preciom
##                 Sum Sq   Df F value    Pr(>F)    
## areaconst     40396511    1 982.726 < 2.2e-16 ***
## estrato_num   23093124    1 561.787 < 2.2e-16 ***
## habitaciones   1269040    1  30.872 2.983e-08 ***
## parqueaderos   8606697    1 209.375 < 2.2e-16 ***
## banios         5628110    1 136.915 < 2.2e-16 ***
## zona           2131626    4  12.964 1.793e-10 ***
## Residuals    131129948 3190                      
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

1.3.1 Interpretación del modelo de regresión lineal múltiple

El modelo estimado explica un 68,2% de la variabilidad en el precio de las casas (R² = 0.682), lo cual representa un ajuste sólido. Todas las variables incluidas resultan globalmente significativas (p < 0.001 en el test F).

Área construida (β = 0.80, p < 0.001): por cada metro cuadrado adicional, el precio de la vivienda aumenta en promedio 0,8 millones de pesos, manteniendo constantes las demás variables.

Estrato socioeconómico (β = 114.8, p < 0.001): cada incremento de un nivel en el estrato se asocia con un aumento de aproximadamente 115 millones de pesos en el precio.

Habitaciones (β = –14.4, p < 0.001): controlando por área, más habitaciones reducen el precio promedio, lo cual refleja colinealidad: casas con muchas habitaciones en espacios reducidos tienden a ser menos valorizadas.

Parqueaderos (β = 40.6, p < 0.001) y Baños (β = 38.9, p < 0.001): cada parqueadero y cada baño adicional incrementan el precio en torno a 40 y 39 millones respectivamente.

Zona: en comparación con la categoría base (probablemente Zona Centro), vivir en Norte, Oriente o Sur se asocia a precios más bajos, siendo la penalización mayor en la Zona Norte (–105 millones) y en la Zona Sur (–84 millones). La Zona Oeste no mostró diferencias significativas respecto a la base (p = 0.88).

El análisis ANOVA confirma que las variables más influyentes en el precio son área construida, estrato socioeconómico, parqueaderos y baños, mientras que el número de habitaciones aporta de forma marginal y el efecto de la zona depende de la localización específica.

En conclusión: el valor de la vivienda se explica principalmente por el tamaño, el estrato y los atributos de confort (baños y parqueaderos), mientras que el efecto de la zona es heterogéneo y en algunos casos negativo.

# -------- 3.3 Diagnósticos --------
library(car)
library(lmtest)
library(gt)
library(broom)

# --- Multicolinealidad (VIF) ---
vif_vals <- car::vif(mod_casas)

# Convertir a data frame para gt
vif_tbl <- tibble::tibble(
  Variable = names(vif_vals),
  VIF = round(as.numeric(vif_vals), 2)
)

vif_tbl %>%
  gt() %>%
  tab_header(
    title = md("**Tabla X. Factores de inflación de la varianza (VIF)**")
  )
Tabla X. Factores de inflación de la varianza (VIF)
VIF
1.50
2.18
1.64
1.58
2.13
1.50
1.00
1.00
1.00
1.00
1.00
4.00
1.23
1.48
1.28
1.26
1.46
1.05
# --- Heterocedasticidad (Breusch-Pagan) ---
bp_test <- lmtest::bptest(mod_casas)

# Pasar resultado a tibble para formatear
bp_tbl <- broom::tidy(bp_test)

bp_tbl %>%
  gt() %>%
  tab_header(
    title = md("**Tabla X. Prueba de Breusch-Pagan para heterocedasticidad**")
  ) %>%
  fmt_number(columns = c(statistic, p.value), decimals = 4)
Tabla X. Prueba de Breusch-Pagan para heterocedasticidad
statistic p.value parameter method
399.5891 0.0000 9 studentized Breusch-Pagan test
# --- Gráficos de supuestos
par(mfrow = c(1, 2))
plot(mod_casas, which = 1)  # Residuos vs ajustados (linealidad y homocedasticidad)
plot(mod_casas, which = 2)  # QQ-plot de residuos (aprox. normalidad)

par(mfrow = c(1, 1))

1.3.2 Diagnósticos del modelo

  • Multicolinealidad (VIF)

Los valores de VIF para todas las variables están entre 1.2 y 2.2, muy por debajo del umbral crítico (10, o incluso 5 como criterio conservador). Esto indica que no existe un problema grave de multicolinealidad entre los predictores: cada variable aporta información distinta al modelo.

  • Heterocedasticidad (Breusch-Pagan)

El test de Breusch-Pagan arroja un estadístico BP = 399.6, p < 0.001, lo que lleva a rechazar la hipótesis nula de homocedasticidad. En consecuencia, los residuos presentan heterocedasticidad: la varianza del error no es constante a lo largo de los valores ajustados.

  • Residuos vs Ajustados (gráfico izquierda)

El diagrama muestra un patrón en forma de abanico, con mayor dispersión a medida que aumentan los valores ajustados. Esto confirma la heterocedasticidad detectada en la prueba estadística. Además, se observan algunos puntos atípicos (ej. observaciones 1401, 1439, 1404) que influyen de manera importante en los residuos.

  • Normalidad de los residuos (QQ-plot, derecha)

El QQ-plot refleja una desviación notable en las colas: los residuos no siguen perfectamente una distribución normal. Hay observaciones extremas (outliers) que se apartan de la línea teórica. Aunque la normalidad no es crítica para la estimación de coeficientes (sí lo es para inferencia clásica), conviene tenerlo presente.

En conclusión:

No hay problemas de colinealidad.

Sí existe heterocedasticidad → se justifica el uso de errores estándar robustos.

Los residuos presentan outliers y ligera desviación de la normalidad, lo que sugiere considerar transformaciones o modelos alternativos en análisis futuros.

1.3.3 Interpretación de la tabla de coeficientes con errores estándar robustos

# -------- 3.4 Tabla de coeficientes con SE robustos --------

coefs_rob <- lmtest::coeftest(mod_casas, vcov = sandwich::vcovHC(mod_casas, type = "HC3"))

tab_coef <- broom::tidy(coefs_rob) %>%
  dplyr::mutate(
    term = dplyr::recode(
      as.character(term),
      "(Intercept)" = "Intercepto",
      "areaconst"   = "Área construida (m²)",
      "estrato_num" = "Estrato",
      "habitaciones"= "Habitaciones",
      "parqueaderos"= "Parqueaderos",
      "banios"      = "Baños",
      .default = term
    )
  ) %>%
  dplyr::rename(
    `Término` = term,
    `Estimación` = estimate,
    `Error Std. (rob.)` = std.error,
    `t (rob.)` = statistic,
    `p-valor` = p.value
  )

# -------- Mostrar tabla con estilo --------
# install.packages("kableExtra")   # <- ya instalada (comentado)
# library(kableExtra)              # <- ya cargada (comentado)

tab_coef %>%
  dplyr::mutate(dplyr::across(where(is.numeric), ~round(.x, 3))) %>%  # redondeo a 3 decimales
  kableExtra::kable("html", align = "lcccc",
                    caption = "Coeficientes del modelo con errores estándar robustos (HC3)") %>%
  kableExtra::kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover"))
Coeficientes del modelo con errores estándar robustos (HC3)
Término Estimación Error Std. (rob.) t (rob.) p-valor
Intercepto -39.970 17.291 -2.312 0.021
Área construida (m²) 0.802 0.054 14.964 0.000
Estrato 114.786 5.381 21.331 0.000
Habitaciones -14.406 2.919 -4.936 0.000
Parqueaderos 40.561 4.050 10.014 0.000
Baños 38.930 4.110 9.472 0.000
zonaZona Norte -105.784 14.123 -7.490 0.000
zonaZona Oeste -3.842 24.725 -0.155 0.877
zonaZona Oriente -70.600 14.162 -4.985 0.000
zonaZona Sur -84.446 13.906 -6.072 0.000

El modelo confirma que las variables área construida, estrato, parqueaderos y baños son determinantes significativos del precio de las viviendas, incluso tras corregir por heterocedasticidad. En promedio:

  • Cada m² adicional aumenta el precio en 0,8 millones de pesos.

  • Subir un nivel de estrato incrementa el valor en 115 millones.

  • Cada parqueadero y cada baño añaden alrededor de 40 y 39 millones, respectivamente.

  • El efecto de las habitaciones es negativo (–14 millones por unidad), lo que refleja que, manteniendo constantes el área y otros factores, un mayor número de habitaciones no necesariamente se traduce en un mayor precio.

  • En cuanto a la zona, en comparación con la categoría base (Zona Centro), Norte, Oriente y Sur presentan precios significativamente menores, mientras que la Zona Oeste no muestra diferencias relevantes.

1.3.4 Predicción para el Caso 1

# -------- 3.5 Predicción para el Caso 1 --------

#install.packages("gt")
library(gt)
new_casa <- tidyr::expand_grid(
  areaconst = 200,
  estrato_num = c(4,5),
  habitaciones = 4,
  parqueaderos = 1,
  banios = 2,
  zona = factor("Zona Norte", levels = levels(casas_df$zona))
)

pred_casa <- cbind(new_casa,
                   predict(mod_casas, newdata = new_casa, interval = "prediction")) %>%
  rename(pred = fit, li = lwr, ls = upr)

# Mostrar tabla con predicción e intervalo
pred_casa %>%
  gt() %>%
  fmt_number(columns = c(pred, li, ls), decimals = 1) %>%
  tab_header(
    title = md("**Predicción de precio – Casa Zona Norte (200 m², 4 hab., 2 baños, 1 parqueadero)**"),
    subtitle = "Comparar con crédito: 350 M"
  )
Predicción de precio – Casa Zona Norte (200 m², 4 hab., 2 baños, 1 parqueadero)
Comparar con crédito: 350 M
areaconst estrato_num habitaciones parqueaderos banios zona pred li ls
200 4 4 1 2 Zona Norte 534.6 136.1 933.0
200 5 4 1 2 Zona Norte 649.3 250.3 1,048.4

Con base en el modelo estimado, se proyecta que una casa ubicada en la Zona Norte, con 200 m² de área construida, 4 habitaciones, 2 baños y 1 parqueadero, tendría un precio esperado de aproximadamente 535 millones de pesos en estrato 4 y 649 millones en estrato 5. En ambos casos, el valor se encuentra por encima del crédito preaprobado de 350 millones, lo cual indica que la oferta disponible excede la capacidad de financiamiento establecida.

1.3.5 Ofertas potenciales en la Zona Norte

# ====== 6. Ofertas potenciales (<= 350M) y mapa ======
library(dplyr)
library(tidyr)
library(stringr)
library(leaflet)
library(scales)
library(gt)

# Especificación de la Vivienda 1
target <- list(
  zona          = "Zona Norte",
  area          = 200,
  hab           = 4,
  ban           = 2,
  parq          = 1,
  estratos_ok   = c(4, 5),
  precio_max    = 350
)

# 1) Filtro inicial estricto (tolerancias razonables)
filtrar_ofertas <- function(df, area_tol = 0.20, estratos = target$estratos_ok) {
  df %>%
    filter(
      tipo == "Casa",
      zona == target$zona,
      !is.na(latitud), !is.na(longitud),
      !is.na(areaconst), !is.na(habitaciones), !is.na(banios), !is.na(parqueaderos),
      !is.na(preciom), preciom <= target$precio_max,
      # tolerancia sobre área (±20% por defecto)
      areaconst >= target$area * (1 - area_tol),
      areaconst <= target$area * (1 + area_tol),
      # preferimos estratos 4-5
      as.numeric(estrato) %in% estratos
    )
}

candidatas <- filtrar_ofertas(vivienda_limpia, area_tol = 0.20, estratos = target$estratos_ok)

# Si no alcanza 5, relajamos criterios gradualmente
if (nrow(candidatas) < 5) {
  candidatas <- filtrar_ofertas(vivienda_limpia, area_tol = 0.30, estratos = c(3,4,5))
}
if (nrow(candidatas) < 5) {
  # Ampliar también a parques >= 0 y permitir zona norte con distintas mayúsculas (por si acaso)
  candidatas <- vivienda_limpia %>%
    filter(
      tipo == "Casa",
      str_detect(zona, regex("Zona Norte", ignore_case = TRUE)),
      !is.na(latitud), !is.na(longitud),
      !is.na(areaconst), !is.na(habitaciones), !is.na(banios), !is.na(parqueaderos),
      !is.na(preciom), preciom <= target$precio_max,
      areaconst >= target$area * 0.70,
      areaconst <= target$area * 1.30,
      as.numeric(estrato) %in% c(3,4,5)
    )
}

# 2) Scoring de similitud a la Vivienda 1 (menor = mejor)
#    ponderamos cercanía de área (40%), habitaciones (20%), baños (20%), parqueaderos (20%)
rankeadas <- candidatas %>%
  mutate(
    # diferencias relativas / absolutas
    score_area = abs(areaconst - target$area) / target$area,
    score_hab  = abs(habitaciones - target$hab),
    score_ban  = abs(banios - target$ban),
    score_parq = abs(parqueaderos - target$parq),
    # pequeña penalización si estrato distinto a 4-5
    pen_estrato = ifelse(as.numeric(estrato) %in% target$estratos_ok, 0, 0.25),

    # score total (ajusta pesos si lo deseas)
    score_total = 0.40*score_area + 0.20*score_hab + 0.20*score_ban + 0.20*score_parq + pen_estrato
  ) %>%
  arrange(score_total, preciom)

# 3) Seleccionar las 5 mejores
ofertas_top5 <- rankeadas %>%
  slice_head(n = 5) %>%
  mutate(
    precio_fmt = label_number(big.mark = ".", decimal.mark = ",", suffix = " M")(preciom),
    area_fmt   = paste0(round(areaconst,1), " m²")
  )

# 4) Tabla ejecutiva de ofertas
ofertas_top5 %>%
  select(
    barrio, zona, estrato, preciom, areaconst, habitaciones, banios, parqueaderos, score_total
  ) %>%
  mutate(
    `Precio (M)` = round(preciom, 1),
    `Área (m²)`  = round(areaconst, 1),
    `Score similitud` = round(score_total, 3)
  ) %>%
  select(
    barrio, zona, estrato, `Precio (M)`, `Área (m²)`, habitaciones, banios, parqueaderos, `Score similitud`
  ) %>%
  gt() %>%
  tab_header(
    title = md("**Ofertas potenciales (≤ 350 M) – Zona Norte**"),
    subtitle = "Ordenadas por mayor similitud con Vivienda 1"
  )
Ofertas potenciales (≤ 350 M) – Zona Norte
Ordenadas por mayor similitud con Vivienda 1
barrio zona estrato Precio (M) Área (m²) habitaciones banios parqueaderos Score similitud
la merced Zona Norte 5 350 216 4 2 2 0.482
la flora Zona Norte 5 320 160 4 3 1 0.530
prados del norte Zona Norte 5 300 146 4 3 1 0.558
prados del norte Zona Norte 5 280 140 3 2 1 0.570
la flora Zona Norte 5 320 140 4 3 1 0.570
# 5) Mapa interactivo (Leaflet)
#    Popups con info clave
make_popup <- function(df_row) {
  paste0(
    "<b>", ifelse(!is.na(df_row$barrio), df_row$barrio, "Sin barrio"), "</b><br/>",
    df_row$zona, " | Estrato ", df_row$estrato, "<br/>",
    "Precio: <b>", df_row$precio_fmt, "</b><br/>",
    "Área: ", df_row$area_fmt, " | Hab: ", df_row$habitaciones,
    " | Baños: ", df_row$banios, " | Parq: ", df_row$parqueaderos, "<br/>",
    "Score similitud: ", round(df_row$score_total, 3)
  )
}

pal <- colorNumeric(palette = "viridis", domain = ofertas_top5$preciom)

leaflet(ofertas_top5) %>%
  addTiles() %>%
  addCircleMarkers(
    lng = ~longitud, lat = ~latitud,
    radius = 7,
    stroke = TRUE, weight = 1, fillOpacity = 0.85,
    color = ~pal(preciom),
    label = ~paste0(barrio, " (", round(preciom,1), " M)"),
    popup = ~vapply(seq_len(nrow(ofertas_top5)), function(i) make_popup(ofertas_top5[i, ]), character(1))
  ) %>%
  addLegend("bottomright", pal = pal, values = ~preciom,
            title = "Precio (M)", opacity = 1)

Tras aplicar filtros y un sistema de similitud respecto a la Vivienda 1, se identificaron cinco alternativas de compra en la Zona Norte con precios iguales o inferiores a 350 millones. Estas propiedades presentan características cercanas al perfil buscado (200 m², 4 habitaciones, 2 baños, 1 parqueadero, estrato 4–5), y fueron ordenadas según su grado de similitud con el caso de referencia.

El mapa interactivo permite visualizar su localización exacta en la ciudad, mientras que la tabla ejecutiva resume las principales especificaciones de cada oferta. Destacan opciones en los barrios La Merced, La Flora y Prados del Norte, con áreas entre 140 y 216 m² y precios en el rango de 280–350 millones.

2 Segunda solicitud

Para la segunda solicitud, la compañía requiere la adquisición de un apartamento en la zona Sur de Cali, con un área construida de 300 m², cinco habitaciones, tres baños, tres parqueaderos y perteneciente a un estrato socioeconómico 5 o 6. La familia dispone de un crédito preaprobado de 850 millones de pesos, y el análisis busca establecer si dicho presupuesto resulta suficiente frente a los precios de mercado, así como identificar las alternativas más viables de acuerdo con estas características.

library(gt)

tabla_viv2 <- data.frame(
  Características = c("Tipo", "área construida", "parqueaderos",
                      "baños", "habitaciones", "estrato",
                      "zona", "crédito preaprobado"),
  `Vivienda 2` = c("Apartamento", "300", "3", "3", "5",
                   "5 o 6", "Sur", "850 millones")
)

tabla_viv2 %>%
  gt() %>%
  tab_header(title = md("**Características de la Vivienda 2**"))
Características de la Vivienda 2
Características Vivienda.2
Tipo Apartamento
área construida 300
parqueaderos 3
baños 3
habitaciones 5
estrato 5 o 6
zona Sur
crédito preaprobado 850 millones
# --- Crear base2 con Apartamentos de Zona Sur ---
base2 <- vivienda_limpia %>%
  dplyr::filter(.data$tipo == "Apartamento") %>%
  dplyr::filter(stringr::str_detect(
    .data$zona,
    stringr::regex("^Zona\\s*Sur$", ignore_case = TRUE)
  )) %>%
  tidyr::drop_na(zona)

# --- Verificaciones con tablas formateadas ---

# Primeros 3 registros
base2 %>%
  head(3) %>%
  gt::gt() %>%
  gt::tab_header(
    title = gt::md("**Tabla 1b. Primeros 3 registros de `base2` (Apartamentos Zona Sur)**")
  )
Tabla 1b. Primeros 3 registros de base2 (Apartamentos Zona Sur)
tipo areaconst parqueaderos banios habitaciones estrato zona preciom piso barrio longitud latitud estrato_num precio_m2 precio_m2_wins
Apartamento 96 1 2 3 4 Zona Sur 290 5 acopi -76.53464 3.44987 4 3.020833 3.020833
Apartamento 40 1 1 2 3 Zona Sur 78 2 aguablanca -76.50100 3.40000 3 1.950000 1.950000
Apartamento 194 2 5 3 6 Zona Sur 875 NA aguacatal -76.55700 3.45900 6 4.510309 4.510309
# Conteo por zona
base2 %>%
  dplyr::count(zona, sort = TRUE) %>%
  gt::gt() %>%
  gt::tab_header(
    title = gt::md("**Tabla 2b. Número de registros por zona en `base2`**")
  )
Tabla 2b. Número de registros por zona en base2
zona n
Zona Sur 2760
# Conteo por tipo y zona
base2 %>%
  dplyr::count(tipo, zona, sort = TRUE) %>%
  gt::gt() %>%
  gt::tab_header(
    title = gt::md("**Tabla 3b. Número de registros por tipo y zona en `base2`**")
  )
Tabla 3b. Número de registros por tipo y zona en base2
tipo zona n
Apartamento Zona Sur 2760

2.1 Distribución geográfica

# --- Mapa interactivo de Apartamentos en Zona Sur (base2) ---
library(leaflet)

leaflet(base2) %>%
  addTiles() %>%
  addCircleMarkers(
    ~longitud, ~latitud,
    popup = ~paste0("<b>", tipo, " - ", zona, "</b><br>",
                    "Barrio: ", barrio, "<br>",
                    "Precio: ", preciom, " M<br>",
                    "Área: ", areaconst, " m²<br>",
                    "Parq: ", parqueaderos, 
                    " | Baños: ", banios, 
                    " | Hab: ", habitaciones),
    radius = 6, stroke = FALSE, fillOpacity = 0.8
  )

El mapa interactivo muestra la localización de los apartamentos filtrados en la Zona Sur de la ciudad. Se observa una alta concentración de puntos en la franja sur-occidental, particularmente hacia sectores de expansión residencial que se extienden desde el límite sur con Jamundí hasta las inmediaciones del área urbana consolidada.

La distribución espacial evidencia que gran parte de la oferta se encuentra efectivamente dentro de los límites esperados de la Zona Sur, aunque algunos puntos aparecen dispersos hacia áreas aledañas, lo cual puede deberse a dos factores principales:

Errores en la geocodificación de las direcciones, que ubican algunos registros fuera de la delimitación estricta.

Definiciones comerciales de “zona” que no siempre coinciden con divisiones administrativas oficiales, lo que puede generar discrepancias en los mapas.

En conjunto, la visualización permite corroborar la existencia de una oferta significativa de apartamentos en la zona solicitada, confirmando que la base2 constituye un insumo adecuado para los análisis descriptivos y predictivos relacionados con la segunda solicitud de vivienda.

2.2 Análisis exploratorio de datos (EDA) – Apartamentos

En esta sección se presenta el análisis exploratorio de la variable respuesta, precio de la vivienda (preciom), para el segmento de apartamentos, en función del área construida (areaconst), estrato, número de baños, número de habitaciones y zona. Se utilizan gráficos interactivos con el paquete plotly, lo que permite examinar de manera dinámica las relaciones entre las variables y comparar comportamientos entre zonas, con énfasis en la Zona Sur por corresponder a la segunda solicitud. Este EDA servirá como base para la especificación del modelo y la posterior predicción del precio bajo las características requeridas.

library(plotly)
library(dplyr)
library(tidyr)

apartamentos <- vivienda_limpia %>%
  filter(tipo == "Apartamento") %>%
  select(preciom, areaconst, estrato, banios, habitaciones, zona) %>%
  drop_na()

2.2.1 Matriz de Correlaciones

# --- Correlaciones numéricas (referencia) - Apartamentos ---
matriz_cor_apto <- apartamentos %>%
  select(where(is.numeric)) %>%
  cor(use = "pairwise.complete.obs") %>%
  round(2)

matriz_cor_apto %>%
  as.data.frame() %>%
  tibble::rownames_to_column(var = "Variable") %>%
  gt() %>%
  tab_header(
    title = md("**Tabla X. Matriz de correlación de variables numéricas (Apartamentos)**")
  ) %>%
  fmt_number(columns = -Variable, decimals = 2) %>%
  data_color(
    columns = -Variable,
    colors = scales::col_numeric(
      palette = c("blue", "white", "red"),  # azul = negativo, blanco = 0, rojo = positivo
      domain = c(-1, 1)
    )
  )
Tabla X. Matriz de correlación de variables numéricas (Apartamentos)
Variable preciom areaconst banios habitaciones
preciom 1.00 0.83 0.74 0.30
areaconst 0.83 1.00 0.73 0.41
banios 0.74 0.73 1.00 0.50
habitaciones 0.30 0.41 0.50 1.00

La matriz de correlaciones para los apartamentos muestra que el precio se relaciona de manera muy fuerte con el área construida (0.83) y con el número de baños (0.74), lo que indica que estas son las variables más determinantes en la formación del valor de mercado. En contraste, la correlación con el número de habitaciones (0.30) es baja, lo que sugiere que esta característica por sí sola no explica de manera significativa el precio. En conjunto, los resultados reflejan que el tamaño del apartamento y su dotación de baños tienen un mayor peso en la fijación de precios que la cantidad de habitaciones.

2.2.2 Precio vs Área Construida

# Precio vs Área (color por zona) - Apartamentos
plot_ly(apartamentos, x = ~areaconst, y = ~preciom, color = ~zona,
        type = "scatter", mode = "markers") %>%
  plotly::layout(
    title = "APARTAMENTOS: Precio vs Área por zona",
    xaxis = list(title = "Área (m²)"),
    yaxis = list(title = "Precio (M)")
  )

La gráfica de dispersión muestra una relación positiva clara entre el área construida y el precio de los apartamentos, lo que confirma que a mayor tamaño tiende a registrarse un mayor valor de mercado. Sin embargo, la nube de puntos evidencia una gran dispersión, especialmente en áreas intermedias (100–300 m²), donde apartamentos de tamaño similar presentan precios muy distintos según la zona de ubicación. Esto refleja que, además del área, factores como la localización geográfica inciden de manera importante en la valorización. En particular, se observan apartamentos de alto precio en la Zona Oeste, mientras que en la Zona Sur los valores tienden a concentrarse en un rango medio, incluso para áreas construidas mayores.

2.2.3 Precio por Estrato

# Precio por estrato (boxplot) - Apartamentos
plot_ly(apartamentos, 
        x = ~as.factor(estrato), 
        y = ~preciom, 
        color = ~as.factor(estrato),
        type = "box") %>%
  plotly::layout(
    title = "APARTAMENTOS: Precio por estrato",
    xaxis = list(title = "Estrato"),
    yaxis = list(title = "Precio (M)")
  )

El boxplot muestra una relación clara entre el estrato socioeconómico y el precio de los apartamentos: a medida que aumenta el estrato, los precios tienden a ser más altos. En estratos 3 y 4, los precios se concentran en rangos bajos y medios, con poca dispersión, lo que indica que la mayoría de las ofertas en estos niveles se mantienen relativamente homogéneas. En cambio, los estratos 5 y 6 presentan precios medianos más elevados y una mayor dispersión, reflejando tanto la diversidad de oferta como la presencia de apartamentos de alto valor. En particular, el estrato 6 concentra las propiedades con precios más altos, incluso superando los 1.500 millones en algunos casos. En síntesis, el gráfico confirma que el estrato es un fuerte diferenciador de precios en el mercado de apartamentos, siendo un factor clave en la segmentación de la oferta.

2.2.4 Precio según Número de Baños

# Precio vs número de baños (boxplot) - Apartamentos
plot_ly(apartamentos, 
        x = ~banios, 
        y = ~preciom, 
        color = ~zona, 
        type = "box") %>%
  plotly::layout(
    title = "APARTAMENTOS: Precio según número de baños",
    xaxis = list(title = "Baños"),
    yaxis = list(title = "Precio (M)")
  )

El gráfico de cajas muestra que el precio de los apartamentos aumenta a medida que crece el número de baños, lo cual confirma la relación positiva ya observada en la matriz de correlaciones. En los apartamentos con 2 a 4 baños se concentra la mayor parte de la oferta, presentando precios que van desde niveles medios hasta inmuebles de alto valor. En los casos de 5 baños o más, aunque la muestra es menor, los precios se ubican en rangos considerablemente más altos, reflejando unidades residenciales de lujo o de gran tamaño. La dispersión dentro de cada categoría indica que, aun con el mismo número de baños, el precio varía según otros factores como el área construida y la zona de ubicación, siendo estas variables complementarias en la explicación del valor de mercado.

2.2.5 Precio según Número de Habitaciones

# Precio vs número de habitaciones (boxplot) - Apartamentos
plot_ly(apartamentos, 
        x = ~habitaciones, 
        y = ~preciom, 
        color = ~zona, 
        type = "box") %>%
  plotly::layout(
    title = "APARTAMENTOS: Precio según número de habitaciones",
    xaxis = list(title = "Habitaciones"),
    yaxis = list(title = "Precio (M)")
  )

El gráfico de cajas evidencia que el precio de los apartamentos tiende a aumentar conforme crece el número de habitaciones, aunque la relación no es tan marcada ni consistente como en el caso del área construida o el número de baños. Para apartamentos de 2 a 4 habitaciones se concentra la mayor parte de la oferta, con precios que van desde rangos medios hasta inmuebles de alto valor. En unidades con 5 o más habitaciones se observan precios más elevados, pero también una gran dispersión, lo que refleja que la variable habitaciones por sí sola no determina el precio, sino que interactúa con otros factores como la ubicación y el tamaño total del apartamento. En síntesis, el número de habitaciones influye en la valorización, pero su efecto es más débil y heterogéneo en comparación con otras variables estructurales.

2.3 Modelo de regresión lineal múltiple

En esta etapa se llevó a cabo la preparación de los datos, considerando únicamente las viviendas clasificadas como apartamentos y eliminando los registros con información incompleta. Se generaron variables numéricas y categóricas consistentes (como el estrato transformado a formato numérico y la zona definida como factor), de manera que se dispusiera de una base depurada y adecuada para el ajuste del modelo de regresión lineal múltiple.

# -------- 3.1 Preparación de datos (APARTAMENTOS) --------


# Filtrar y limpiar datos de apartamentos
apartamentos_df <- vivienda_limpia %>%
  dplyr::filter(tipo == "Apartamento") %>%       # Filtrar solo apartamentos
  tidyr::drop_na(preciom, areaconst, estrato,
                 habitaciones, parqueaderos,
                 banios, zona) %>%               # Eliminar NA en variables clave
  mutate(
    estrato_num = as.numeric(estrato),           # Convertir estrato a numérico
    zona = factor(zona)                          # Asegurar zona como factor
  )

# Vista previa formateada (primeros 6 registros)
apartamentos_df %>%
  head(6) %>%
  gt() %>%
  tab_header(
    title = md("**Tabla X. Vista previa de los datos de Apartamentos (base limpia)**")
  )
Tabla X. Vista previa de los datos de Apartamentos (base limpia)
tipo areaconst parqueaderos banios habitaciones estrato zona preciom piso barrio longitud latitud estrato_num precio_m2 precio_m2_wins
Apartamento 90 1 2 3 5 Zona Norte 260 1 acopi -76.51350 3.45891 3 2.888889 2.888889
Apartamento 87 1 3 3 5 Zona Norte 240 1 acopi -76.51700 3.36971 3 2.758621 2.758621
Apartamento 52 2 2 3 4 Zona Norte 220 1 acopi -76.51974 3.42627 2 4.230769 4.230769
Apartamento 137 2 3 4 5 Zona Norte 310 1 acopi -76.53105 3.38296 3 2.262774 2.262774
Apartamento 98 2 2 2 6 Zona Norte 520 2 acopi -76.54999 3.43505 4 5.306122 5.306122
Apartamento 108 2 3 3 4 Zona Norte 320 3 acopi -76.53638 3.40770 2 2.962963 2.962963
# -------- 3.2 Estimación por MCO (APARTAMENTOS) --------
mod_apart <- lm(preciom ~ areaconst + estrato_num + habitaciones + parqueaderos + banios + zona,
                data = apartamentos_df)

# Resumen del modelo
summary(mod_apart)
## 
## Call:
## lm(formula = preciom ~ areaconst + estrato_num + habitaciones + 
##     parqueaderos + banios + zona, data = apartamentos_df)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -1728.49   -52.33    -0.50    45.17   985.68 
## 
## Coefficients:
##                    Estimate Std. Error t value Pr(>|t|)    
## (Intercept)      -105.70321   28.15586  -3.754 0.000176 ***
## areaconst           2.03649    0.04223  48.226  < 2e-16 ***
## estrato_num        43.61882    2.72435  16.011  < 2e-16 ***
## habitaciones      -33.56196    3.24818 -10.333  < 2e-16 ***
## parqueaderos       52.32387    2.91669  17.939  < 2e-16 ***
## banios             51.91020    2.96824  17.489  < 2e-16 ***
## zonaZona Norte     14.34334   27.05959   0.530 0.596090    
## zonaZona Oeste     89.57782   27.43431   3.265 0.001101 ** 
## zonaZona Oriente    5.84467   31.56656   0.185 0.853116    
## zonaZona Sur       -3.03863   26.98230  -0.113 0.910340    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 130.8 on 5052 degrees of freedom
## Multiple R-squared:  0.7968, Adjusted R-squared:  0.7964 
## F-statistic:  2201 on 9 and 5052 DF,  p-value: < 2.2e-16
# ANOVA marginal (Type II)
car::Anova(mod_apart, type = 2)
## Anova Table (Type II tests)
## 
## Response: preciom
##                Sum Sq   Df F value    Pr(>F)    
## areaconst    39821229    1 2325.78 < 2.2e-16 ***
## estrato_num   4389043    1  256.34 < 2.2e-16 ***
## habitaciones  1827938    1  106.76 < 2.2e-16 ***
## parqueaderos  5510157    1  321.82 < 2.2e-16 ***
## banios        5236661    1  305.85 < 2.2e-16 ***
## zona          4956349    4   72.37 < 2.2e-16 ***
## Residuals    86498690 5052                      
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

2.3.1 Interpretación del modelo de regresión lineal múltiple

El modelo ajustado presenta un alto poder explicativo, con un 𝑅 2 R 2 de 0.797 (ajustado 0.796), lo que indica que cerca del 80 % de la variabilidad en el precio de los apartamentos puede ser explicada por las variables incluidas. El estadístico F es altamente significativo (p < 0.001), confirmando la validez global del modelo.

En cuanto a los coeficientes:

  • Área construida (β = 2.04, p < 0.001): es el principal determinante del precio. Por cada metro cuadrado adicional, el valor del apartamento aumenta en promedio en 2 millones de pesos, manteniendo constantes las demás variables.

  • Estrato socioeconómico (β = 43.62, p < 0.001): cada incremento en el estrato se asocia con un aumento aproximado de 43 millones en el precio, lo que refleja la fuerte segmentación del mercado según nivel socioeconómico.

  • Habitaciones (β = –33.56, p < 0.001): muestra un efecto negativo inesperado; controlando por área y otras variables, un mayor número de habitaciones reduce el precio estimado. Esto puede deberse a que, en apartamentos de la misma área, más habitaciones implican espacios más reducidos, percibidos como de menor valor.

  • Parqueaderos (β = 52.32, p < 0.001) y Baños (β = 51.91, p < 0.001): ambos tienen efectos positivos y significativos, indicando que cada parqueadero o baño adicional incrementa el precio en más de 50 millones de pesos.

  • Zona: el efecto global de la localización es estadísticamente significativo (ANOVA p < 0.001). No obstante, al comparar con la categoría base (no indicada explícitamente, probablemente “Centro”), solo la Zona Oeste (β = 89.58, p = 0.001) muestra un incremento sustancial y significativo en el precio; las demás zonas no difieren significativamente.

El modelo confirma que el precio de los apartamentos depende principalmente del área construida, el estrato, el número de parqueaderos y de baños, mientras que la variable habitaciones adquiere un efecto negativo al controlar por área. La localización también influye, destacándose la Zona Oeste con precios superiores. En conjunto, los resultados son lógicos y coherentes con el comportamiento del mercado, aunque la relación negativa de habitaciones merece análisis adicional como posible indicador de densidad interna de espacios.

# -------- 3.3 Diagnósticos (APARTAMENTOS) --------
library(car)
library(lmtest)
library(gt)
library(broom)
library(tibble)
## 
## Adjuntando el paquete: 'tibble'
## The following object is masked from 'package:summarytools':
## 
##     view
# --- Multicolinealidad (VIF) ---
vif_vals_ap <- car::vif(mod_apart)

vif_tbl_ap <- tibble(
  Variable = names(vif_vals_ap),
  VIF = round(as.numeric(vif_vals_ap), 2)
)

vif_tbl_ap %>%
  gt() %>%
  tab_header(title = md("**Tabla X. Factores de inflación de la varianza (VIF) – Apartamentos**"))
Tabla X. Factores de inflación de la varianza (VIF) – Apartamentos
VIF
2.55
2.10
1.43
2.03
2.99
1.47
1.00
1.00
1.00
1.00
1.00
4.00
1.60
1.45
1.20
1.43
1.73
1.05
# --- Heterocedasticidad (Breusch–Pagan) ---
bp_test_ap <- lmtest::bptest(mod_apart)
bp_tbl_ap  <- broom::tidy(bp_test_ap)

bp_tbl_ap %>%
  gt() %>%
  tab_header(title = md("**Tabla X. Prueba de Breusch–Pagan – Apartamentos**")) %>%
  fmt_number(columns = c(statistic, p.value), decimals = 4)
Tabla X. Prueba de Breusch–Pagan – Apartamentos
statistic p.value parameter method
1,441.1942 0.0000 9 studentized Breusch-Pagan test
# --- Gráficos de supuestos (sí, aquí están) ---
par(mfrow = c(1, 2))
plot(mod_apart, which = 1)  # Residuos vs. ajustados (linealidad y varianza constante)
plot(mod_apart, which = 2)  # QQ-plot de residuos (aprox. normalidad)

par(mfrow = c(1, 1))

2.3.2 Diagnósticos del modelo

  • Multicolinealidad (VIF) Los valores de VIF para todas las variables se encuentran entre 1.0 y 4.0, muy por debajo del umbral crítico (10, o incluso 5 en criterios más conservadores). Esto indica que no existe un problema grave de multicolinealidad entre los predictores: cada variable aporta información diferenciada al modelo.

  • Heterocedasticidad (Breusch–Pagan) El test de Breusch–Pagan arroja un estadístico BP = 1441.2, p < 0.001, por lo que se rechaza la hipótesis nula de homocedasticidad. En consecuencia, los residuos presentan heterocedasticidad, es decir, la varianza del error no es constante a lo largo de los valores ajustados. Esto implica que los errores estándar pueden estar sesgados; por ello es recomendable emplear errores estándar robustos (HC3) para obtener inferencias más confiables.

  • Residuos vs Ajustados (izquierda) El gráfico de residuos contra valores ajustados muestra un patrón de abanico, con mayor dispersión en valores altos, lo cual confirma la heterocedasticidad. También se identifican algunos outliers que influyen de manera importante en la variabilidad de los residuos.

  • Normalidad de los residuos (QQ-plot, derecha) El QQ-plot refleja una desviación en las colas respecto a la línea teórica, evidenciando que los residuos no siguen completamente una distribución normal. Existen observaciones extremas que se apartan de manera marcada. Aunque la normalidad no es estrictamente necesaria para la estimación de coeficientes, sí afecta a la inferencia clásica y conviene considerarlo en análisis posteriores.

En conclusión: No hay problemas de colinealidad. Sí existe heterocedasticidad → se justifica el uso de errores estándar robustos. Los residuos presentan outliers y cierta desviación de la normalidad, lo que sugiere evaluar transformaciones de variables o modelos alternativos en estudios futuros.

2.3.3 Interpretación de la tabla de coeficientes con errores estándar robustos

# -------- 3.4 Tabla de coeficientes con SE robustos (APARTAMENTOS) --------

# Coeficientes con errores estándar robustos (HC3)
coefs_rob_ap <- lmtest::coeftest(mod_apart,
                                 vcov = sandwich::vcovHC(mod_apart, type = "HC3"))

tab_coef_ap <- broom::tidy(coefs_rob_ap) %>%
  dplyr::mutate(
    term = dplyr::recode(
      as.character(term),
      "(Intercept)" = "Intercepto",
      "areaconst"   = "Área construida (m²)",
      "estrato_num" = "Estrato",
      "habitaciones"= "Habitaciones",
      "parqueaderos"= "Parqueaderos",
      "banios"      = "Baños",
      .default = term   # dejará las dummies de zona como "zonaZona Norte", etc.
    )
  ) %>%
  dplyr::rename(
    `Término` = term,
    `Estimación` = estimate,
    `Error Std. (rob.)` = std.error,
    `t (rob.)` = statistic,
    `p-valor` = p.value
  )

# -------- Mostrar tabla con estilo --------
# install.packages("kableExtra")   # <- ya instalada (comentado)
# library(kableExtra)              # <- ya cargada (comentado)

tab_coef_ap %>%
  dplyr::mutate(dplyr::across(where(is.numeric), ~ round(.x, 3))) %>%  # redondeo a 3 decimales
  kableExtra::kable("html", align = "lcccc",
                    caption = "Coeficientes del modelo (APARTAMENTOS) con errores estándar robustos (HC3)") %>%
  kableExtra::kable_styling(full_width = FALSE,
                            bootstrap_options = c("striped", "hover"))
Coeficientes del modelo (APARTAMENTOS) con errores estándar robustos (HC3)
Término Estimación Error Std. (rob.) t (rob.) p-valor
Intercepto -105.703 20.410 -5.179 0.000
Área construida (m²) 2.036 0.203 10.042 0.000
Estrato 43.619 2.998 14.548 0.000
Habitaciones -33.562 5.453 -6.154 0.000
Parqueaderos 52.324 5.781 9.051 0.000
Baños 51.910 6.125 8.475 0.000
zonaZona Norte 14.343 16.858 0.851 0.395
zonaZona Oeste 89.578 17.751 5.046 0.000
zonaZona Oriente 5.845 24.001 0.244 0.808
zonaZona Sur -3.039 17.576 -0.173 0.863

La estimación del modelo para apartamentos confirma la importancia de varias variables estructurales en la explicación del precio. El área construida es el factor más determinante: cada metro cuadrado adicional incrementa el valor en aproximadamente 2 millones de pesos, manteniendo constantes las demás variables. El estrato socioeconómico también resulta altamente significativo, con un aumento cercano a 43 millones por cada nivel adicional. De forma similar, el número de parqueaderos y baños tiene un efecto positivo relevante (más de 50 millones cada uno), lo que refleja su peso en la valorización. Por el contrario, el número de habitaciones muestra un efecto negativo: controlando por área, más cuartos reducen el precio, posiblemente por asociarse a espacios más pequeños por unidad. En cuanto a la localización, únicamente la Zona Oeste se diferencia significativamente, con un sobreprecio estimado de casi 90 millones frente a la zona base, mientras que las demás zonas no presentan efectos estadísticamente distintos. En conjunto, el modelo es coherente con el funcionamiento del mercado: el valor de los apartamentos está determinado principalmente por el área, la calidad socioeconómica, los baños y parqueaderos, mientras que la zona sólo marca una diferencia clara en el caso de la Zona Oeste.

2.3.4 Predicción para el Caso 2

# -------- 3.5 Predicción para el Caso 2 (APARTAMENTO) --------
library(gt)

new_apart <- tidyr::expand_grid(
  areaconst   = 300,
  estrato_num = c(5, 6),
  habitaciones = 5,
  parqueaderos = 3,
  banios       = 3,
  zona = factor("Zona Sur", levels = levels(apartamentos_df$zona))
)

pred_apart <- cbind(
  new_apart,
  predict(mod_apart, newdata = new_apart, interval = "prediction")
) %>%
  dplyr::rename(pred = fit, li = lwr, ls = upr)

# Mostrar tabla con predicción e intervalo
pred_apart %>%
  gt() %>%
  fmt_number(columns = c(pred, li, ls), decimals = 1) %>%
  tab_header(
    title = md("**Predicción de precio – Apartamento Zona Sur (300 m², 5 hab., 3 baños, 3 parqueaderos)**"),
    subtitle = "Comparar con crédito: 850 M"
  )
Predicción de precio – Apartamento Zona Sur (300 m², 5 hab., 3 baños, 3 parqueaderos)
Comparar con crédito: 850 M
areaconst estrato_num habitaciones parqueaderos banios zona pred li ls
300 5 5 3 3 Zona Sur 865.2 607.8 1,122.6
300 6 5 3 3 Zona Sur 908.8 651.2 1,166.5

La predicción para un apartamento en la Zona Sur con 300 m², 5 habitaciones, 3 baños y 3 parqueaderos muestra un valor estimado de 865 millones de pesos para estrato 5 y de 909 millones de pesos para estrato 6. En ambos casos, los precios se ubican ligeramente por encima del crédito preaprobado de 850 millones, lo que sugiere que el presupuesto es ajustado y podría no ser suficiente en escenarios menos favorables.

El intervalo de predicción al 95 % es amplio (aproximadamente entre 608 y 1,123 millones para estrato 5, y entre 651 y 1,167 millones para estrato 6), lo que refleja la variabilidad natural del mercado: es posible encontrar apartamentos con las mismas características tanto por debajo como bastante por encima del crédito disponible.

En conclusión, aunque el crédito de 850 millones podría cubrir parte de la oferta, existe un riesgo significativo de que se quede corto, sobre todo si se buscan unidades en sectores más valorizados de la Zona Sur o con acabados de alta gama.

2.3.5 Ofertas potenciales en la Zona Sur

# Especificación de la Vivienda 2 (Apartamento)
target_apto <- list(
  zona        = "Zona Sur",
  area        = 300,
  hab         = 5,
  ban         = 3,
  parq        = 3,
  estratos_ok = c(5, 6),
  precio_max  = 850
)

# 1) Filtro inicial (tolerancias razonables)
filtrar_ofertas_apto <- function(df, area_tol = 0.20, estratos = target_apto$estratos_ok) {
  df %>%
    filter(
      tipo == "Apartamento",
      # aceptar "Zona Sur" con posibles variaciones de mayúsculas/espacios
      str_detect(zona, regex("^\\s*Zona\\s*Sur\\s*$", ignore_case = TRUE)),
      !is.na(latitud), !is.na(longitud),
      !is.na(areaconst), !is.na(habitaciones), !is.na(banios), !is.na(parqueaderos),
      !is.na(preciom), preciom <= target_apto$precio_max,
      # tolerancia sobre área
      areaconst >= target_apto$area * (1 - area_tol),
      areaconst <= target_apto$area * (1 + area_tol),
      # estratos preferidos 5–6
      as.numeric(estrato) %in% estratos
    )
}

candidatas_apto <- filtrar_ofertas_apto(vivienda_limpia, area_tol = 0.20, estratos = target_apto$estratos_ok)

# Si no alcanza 5, relajar criterios gradualmente
if (nrow(candidatas_apto) < 5) {
  candidatas_apto <- filtrar_ofertas_apto(vivienda_limpia, area_tol = 0.30, estratos = c(4,5,6))
}
if (nrow(candidatas_apto) < 5) {
  # Ampliar zona (cualquier "sur"), tolerancia de área 30%, estratos 4–6
  candidatas_apto <- vivienda_limpia %>%
    filter(
      tipo == "Apartamento",
      str_detect(zona, regex("sur", ignore_case = TRUE)),
      !is.na(latitud), !is.na(longitud),
      !is.na(areaconst), !is.na(habitaciones), !is.na(banios), !is.na(parqueaderos),
      !is.na(preciom), preciom <= target_apto$precio_max,
      areaconst >= target_apto$area * 0.70,
      areaconst <= target_apto$area * 1.30,
      as.numeric(estrato) %in% c(4,5,6)
    )
}

# 2) Scoring de similitud a la Vivienda 2 (menor = mejor)
#    Pesos: área (40%), habitaciones (20%), baños (20%), parqueaderos (20%)
rankeadas_apto <- candidatas_apto %>%
  mutate(
    score_area = abs(areaconst - target_apto$area) / target_apto$area,
    score_hab  = abs(habitaciones - target_apto$hab),
    score_ban  = abs(banios - target_apto$ban),
    score_parq = abs(parqueaderos - target_apto$parq),
    pen_estrato = ifelse(as.numeric(estrato) %in% target_apto$estratos_ok, 0, 0.25),
    score_total = 0.40*score_area + 0.20*score_hab + 0.20*score_ban + 0.20*score_parq + pen_estrato
  ) %>%
  arrange(score_total, preciom)

# 3) Seleccionar las 5 mejores
ofertas_top5_apto <- rankeadas_apto %>%
  slice_head(n = 5) %>%
  mutate(
    precio_fmt = label_number(big.mark = ".", decimal.mark = ",", suffix = " M")(preciom),
    area_fmt   = paste0(round(areaconst, 1), " m²")
  )

# 4) Tabla ejecutiva de ofertas
ofertas_top5_apto %>%
  select(barrio, zona, estrato, preciom, areaconst, habitaciones, banios, parqueaderos, score_total) %>%
  mutate(
    `Precio (M)`      = round(preciom, 1),
    `Área (m²)`       = round(areaconst, 1),
    `Score similitud` = round(score_total, 3)
  ) %>%
  select(barrio, zona, estrato, `Precio (M)`, `Área (m²)`,
         habitaciones, banios, parqueaderos, `Score similitud`) %>%
  gt() %>%
  tab_header(
    title = md("**Ofertas potenciales (≤ 850 M) – Apartamentos Zona Sur**"),
    subtitle = "Ordenadas por mayor similitud con Vivienda 2"
  )
Ofertas potenciales (≤ 850 M) – Apartamentos Zona Sur
Ordenadas por mayor similitud con Vivienda 2
barrio zona estrato Precio (M) Área (m²) habitaciones banios parqueaderos Score similitud
el ingenio Zona Sur 6 700 250 5 4 2 0.717
ciudad jardín Zona Sur 6 695 227 3 3 3 0.747
pance Zona Sur 6 850 352 3 3 4 0.919
mayapan las vegas Zona Sur 6 655 241 3 3 2 0.929
santa teresita Zona Sur 6 850 222 3 3 2 0.954
# 5) Mapa interactivo (Leaflet)
make_popup_apto <- function(df_row) {
  paste0(
    "<b>", ifelse(!is.na(df_row$barrio), df_row$barrio, "Sin barrio"), "</b><br/>",
    df_row$zona, " | Estrato ", df_row$estrato, "<br/>",
    "Precio: <b>", df_row$precio_fmt, "</b><br/>",
    "Área: ", df_row$area_fmt, " | Hab: ", df_row$habitaciones,
    " | Baños: ", df_row$banios, " | Parq: ", df_row$parqueaderos, "<br/>",
    "Score similitud: ", round(df_row$score_total, 3)
  )
}

pal_apto <- colorNumeric(palette = "viridis", domain = ofertas_top5_apto$preciom)

leaflet(ofertas_top5_apto) %>%
  addTiles() %>%
  addCircleMarkers(
    lng = ~longitud, lat = ~latitud,
    radius = 7,
    stroke = TRUE, weight = 1, fillOpacity = 0.85,
    color = ~pal_apto(preciom),
    label = ~paste0(barrio, " (", round(preciom, 1), " M)"),
    popup = ~vapply(seq_len(nrow(ofertas_top5_apto)),
                    function(i) make_popup_apto(ofertas_top5_apto[i, ]), character(1))
  ) %>%
  addLegend("bottomright", pal = pal_apto, values = ~preciom,
            title = "Precio (M)", opacity = 1)

Las ofertas identificadas en la Zona Sur con precios inferiores o iguales a 850 millones corresponden principalmente a barrios de estrato 6 como El Ingenio, Ciudad Jardín, Pance, Mayapán Las Vegas y Santa Teresita.

En términos de similitud con la Vivienda 2 (apartamento de 300 m², 5 habitaciones, 3 baños y 3 parqueaderos), se observa lo siguiente:

  • El Ingenio (700 M, 250 m²) presenta la mejor correspondencia, con 5 habitaciones y 4 baños, ajustándose bien a las condiciones aunque con un área algo menor y un parqueadero menos.

  • Ciudad Jardín (695 M, 227 m²) es una opción económica dentro del rango, aunque su menor número de habitaciones (3) lo aleja parcialmente de las características solicitadas.

  • Pance (850 M, 352 m²) cumple con el área esperada e incluso la supera, pero ofrece menos habitaciones (3), lo que afecta su similitud a pesar de estar exactamente en el límite del crédito.

  • Mayapán Las Vegas (655 M, 241 m²) representa una alternativa más asequible, aunque con menor área y habitaciones, y un parqueadero menos.

  • Santa Teresita (850 M, 222 m²) es la opción con menor área dentro del grupo y también con menos habitaciones (3), lo que la ubica como menos alineada con el perfil solicitado pese a estar en el rango de precio.

En conclusión, las opciones más atractivas son El Ingenio y, en segundo lugar, Ciudad Jardín, por combinar precios ajustados y mayor cercanía a las características requeridas. Las demás opciones cumplen parcialmente, pero sacrifican espacio en habitaciones o área, aun cuando se mantienen en estrato alto y dentro del crédito máximo disponible.