Caso C&A

Enunciado

Maria comenzó como agente de bienes raíces en Cali hace 10 años. Después de laborar dos años para una empresa nacional, se traslado a Bogotá y trabajó para otra agencia de bienes raíces. Sus amigos y familiares la convencieron de que con su experiencia y conocimientos del negocio debía abrir su propia agencia. Terminó por adquirir la licencia de intermediario y al poco tiempo fundó su propia compañía, C&A (Casas y Apartamentos) en Cali. Santiago y Lina, dos vendedores de la empresa anterior aceptaron trabajar en la nueva compaña. En la actualidad ocho agentes de bienes raíces colaboran con ella en C&A.

Actualmente las ventas de bienes raíces en Cali se han visto disminuidas de manera significativa en lo corrido del año. Durante este periodo muchas instituciones bancarias de ahorro y vivienda están prestando grandes sumas de dinero para la industria y la construcción comercial y residencial. Cuando el efecto producto de las tensiones políticas y sociales disminuya, se espera que la actividad económica de este sector se reactive.

Hace dos días, María recibió una carta solicitando asesoría para la compra de dos viviendas por parte de una compañía internacional que desea ubicar a dos de sus empleados con sus familias en la ciudad. Las solicitudes incluyen las siguientes condiciones:

Características Vivienda 1 Vivienda 2
Tipo Casa Apartamento
área construida 200 300
parqueaderos 1 3
baños 2 3
habitaciones 4 5
estrato 4 o 5 5 o 6
zona Norte Sur
crédito preaprobado 350 millones 850 millones
library(tidyverse)
library(plotly)
library(leaflet)
library(janitor)
library(broom)
library(performance)
library(car)
library(lmtest)
library(sandwich)
library(modelsummary)
library(paqueteMODELOS)
library(RColorBrewer)
library(performance)
library(dplyr)
library(tibble)
library(purrr)
# Carga de datos
data("vivienda")

# Vista rapida
glimpse(vivienda)
## Rows: 8,322
## Columns: 13
## $ id           <dbl> 1147, 1169, 1350, 5992, 1212, 1724, 2326, 4386, 1209, 159…
## $ zona         <chr> "Zona Oriente", "Zona Oriente", "Zona Oriente", "Zona Sur…
## $ piso         <chr> NA, NA, NA, "02", "01", "01", "01", "01", "02", "02", "02…
## $ estrato      <dbl> 3, 3, 3, 4, 5, 5, 4, 5, 5, 5, 6, 4, 5, 6, 4, 5, 5, 4, 5, …
## $ preciom      <dbl> 250, 320, 350, 400, 260, 240, 220, 310, 320, 780, 750, 62…
## $ areaconst    <dbl> 70, 120, 220, 280, 90, 87, 52, 137, 150, 380, 445, 355, 2…
## $ parqueaderos <dbl> 1, 1, 2, 3, 1, 1, 2, 2, 2, 2, NA, 3, 2, 2, 1, 4, 2, 2, 2,…
## $ banios       <dbl> 3, 2, 2, 5, 2, 3, 2, 3, 4, 3, 7, 5, 6, 2, 4, 4, 4, 3, 2, …
## $ habitaciones <dbl> 6, 3, 4, 3, 3, 3, 3, 4, 6, 3, 6, 5, 6, 2, 5, 5, 4, 3, 3, …
## $ tipo         <chr> "Casa", "Casa", "Casa", "Casa", "Apartamento", "Apartamen…
## $ barrio       <chr> "20 de julio", "20 de julio", "20 de julio", "3 de julio"…
## $ longitud     <dbl> -76.51168, -76.51237, -76.51537, -76.54000, -76.51350, -7…
## $ latitud      <dbl> 3.43382, 3.43369, 3.43566, 3.43500, 3.45891, 3.36971, 3.4…
summary(vivienda$preciom)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max.    NA's 
##    58.0   220.0   330.0   433.9   540.0  1999.0       2

Escenario: Casa en zona Norte

1 Paso: Filtro y verificacion de la base

1.1 base1: Casas en zona norte

base1 <- vivienda %>%
  filter(tipo == "Casa", zona == "Zona Norte")

# Mostrar los primeros 3 registros
head(base1, 3)

1.2 Tablas de comprobacion

# Conteo por tipo y zona en la base completa
vivienda %>% count(tipo, zona)
# Conteo por tipo y zona en la base1
base1 %>% count(tipo, zona)

1.3 Resumen numerico de variables clave en base1

summary(select(base1, preciom, areaconst, parqueaderos, banios, habitaciones, estrato))
##     preciom         areaconst       parqueaderos        banios      
##  Min.   :  89.0   Min.   :  30.0   Min.   : 1.000   Min.   : 0.000  
##  1st Qu.: 261.2   1st Qu.: 140.0   1st Qu.: 1.000   1st Qu.: 2.000  
##  Median : 390.0   Median : 240.0   Median : 2.000   Median : 3.000  
##  Mean   : 445.9   Mean   : 264.9   Mean   : 2.182   Mean   : 3.555  
##  3rd Qu.: 550.0   3rd Qu.: 336.8   3rd Qu.: 3.000   3rd Qu.: 4.000  
##  Max.   :1940.0   Max.   :1440.0   Max.   :10.000   Max.   :10.000  
##                                    NA's   :287                      
##   habitaciones       estrato     
##  Min.   : 0.000   Min.   :3.000  
##  1st Qu.: 3.000   1st Qu.:3.000  
##  Median : 4.000   Median :4.000  
##  Mean   : 4.507   Mean   :4.202  
##  3rd Qu.: 5.000   3rd Qu.:5.000  
##  Max.   :10.000   Max.   :6.000  
## 

1.4 Mapa con los puntos de la base1

leaflet(base1) %>%
  addTiles() %>%
  addCircleMarkers(
    lng = ~longitud, lat = ~latitud,
    popup = ~paste0(
      "<b>Casa</b> - ", barrio, "<br>",
      "precio (M): ", round(preciom,1), "<br>",
      "area: ", areaconst, " m2<br>",
      "estrato: ", estrato, " | hab: ", habitaciones,
      " | banios: ", banios, " | parqueaderos: ", parqueaderos
    ),
    radius = 6, stroke = FALSE, fillOpacity = 0.8
  )

Al realizar el filtro de casas en la Zona Norte se observa en el mapa que los puntos no se concentran únicamente en esa zona, sino que aparecen dispersos por toda la ciudad. Esto sugiere que puede haber inconsistencias entre la variable categórica zona y las coordenadas geográficas. Para verificar si el problema ocurre en todas las zonas y no solo en la Zona Norte, se genera un mapa con todas las viviendas coloreadas según la zona registrada. Esto permite observar si las categorías presentan solapamientos y dispersión en toda la ciudad.

# Paleta de colores con tantas zonas como existan
zonas_unicas <- unique(vivienda$zona)
pal <- colorFactor(palette = brewer.pal(length(zonas_unicas), "Set1"),
                   domain = vivienda$zona)

leaflet(vivienda) %>%
  addTiles() %>%
  addCircleMarkers(
    lng = ~longitud, lat = ~latitud,
    color = ~pal(zona),
    popup = ~paste0(
      "<b>", tipo, "</b> - ", barrio, "<br>",
      "zona: ", zona, "<br>",
      "precio (M): ", round(preciom,1), "<br>",
      "area: ", areaconst, " m2<br>",
      "estrato: ", estrato, " | hab: ", habitaciones,
      " | banios: ", banios, " | parqueaderos: ", parqueaderos
    ),
    radius = 5, stroke = FALSE, fillOpacity = 0.7
  ) %>%
  addLegend("bottomright", pal = pal, values = ~zona,
            title = "Zonas", opacity = 1)
## Warning in validateCoords(lng, lat, funcName): Data contains 3 rows with either
## missing or invalid lat/lon values and will be ignored

El resultado evidencia que la dispersión ocurre en todas las categorías de zona, lo que indica que la variable zona probablemente fue asignada manualmente en el proceso de recolección de datos y no está vinculada directamente a las coordenadas geográficas (latitud y longitud). Esta forma de clasificación puede introducir inconsistencias y explicar por qué aparecen registros categorizados en una zona pero localizados espacialmente en otras.

En consecuencia, los análisis posteriores deben tener en cuenta que la variable zona corresponde a un atributo categórico nominal y no necesariamente a una representación geográfica precisa. Cualquier interpretación espacial debe basarse con mayor confianza en las coordenadas, mientras que la variable zona puede servir como descriptor socioeconómico o administrativo.

2 Paso. EDA interactivo con plotly

2.1 Conocimiento de los datos Casa - Norte

Para un mejor entendimiento de los datos con los que vamos a trabajar visualizamos histogramas que nos permita entender la distribución de las caracteristicas de las viviendas a analizar, para este caso, Casas ubicadas en la Zona norte.

# solo Casas en Zona Norte
casas_norte <- vivienda %>%
  filter(tipo == "Casa", zona == "Zona Norte") %>%
  tidyr::drop_na(preciom, areaconst, estrato, banios, habitaciones, parqueaderos)

# Histogramas interactivos
p <- plot_ly()

p <- p %>%
  add_histogram(x = ~casas_norte$preciom,      name = "Precio (M)",
                nbinsx = 35, marker = list(color = "#3182bd"),
                visible = TRUE,  showlegend = FALSE) %>%
  add_histogram(x = ~casas_norte$areaconst,    name = "Area (m2)",
                nbinsx = 35, marker = list(color = "#31a354"),
                visible = FALSE, showlegend = FALSE) %>%
  add_histogram(x = ~casas_norte$habitaciones, name = "Habitaciones",
                xbins = list(size = 1), marker = list(color = "#756bb1"),
                visible = FALSE, showlegend = FALSE) %>%
  add_histogram(x = ~casas_norte$banios,       name = "Banios",
                xbins = list(size = 1), marker = list(color = "#e6550d"),
                visible = FALSE, showlegend = FALSE) %>%
  add_histogram(x = ~casas_norte$parqueaderos, name = "Parqueaderos",
                xbins = list(size = 1), marker = list(color = "#2ca25f"),
                visible = FALSE, showlegend = FALSE) %>%
  add_histogram(x = ~casas_norte$estrato,      name = "Estrato",
                xbins = list(size = 1), marker = list(color = "#fd8d3c"),
                visible = FALSE, showlegend = FALSE) %>%
  layout(
    barmode = "overlay",
    title  = list(text = "Histograma — Precio (M)"),
    xaxis  = list(title = "precio (millones)", autorange = TRUE),
    yaxis  = list(title = "frecuencia"),
    updatemenus = list(list(
      type = "dropdown", direction = "down", x = 1.05, xanchor = "left", y = 1.15,
      buttons = list(
        list(label = "Precio (M)", method = "update",
             args = list(
               list(visible = c(TRUE,FALSE,FALSE,FALSE,FALSE,FALSE)),
               list(title = list(text = "Histograma — Precio (M)"),
                    xaxis = list(title = "precio (millones)", autorange = TRUE))
             )),
        list(label = "Area (m2)", method = "update",
             args = list(
               list(visible = c(FALSE,TRUE,FALSE,FALSE,FALSE,FALSE)),
               list(title = list(text = "Histograma — Area (m2)"),
                    xaxis = list(title = "area (m2)", autorange = TRUE))
             )),
        list(label = "Habitaciones", method = "update",
             args = list(
               list(visible = c(FALSE,FALSE,TRUE,FALSE,FALSE,FALSE)),
               list(title = list(text = "Histograma — Habitaciones"),
                    xaxis = list(title = "habitaciones", autorange = TRUE))
             )),
        list(label = "Banios", method = "update",
             args = list(
               list(visible = c(FALSE,FALSE,FALSE,TRUE,FALSE,FALSE)),
               list(title = list(text = "Histograma — Banios"),
                    xaxis = list(title = "banios", autorange = TRUE))
             )),
        list(label = "Parqueaderos", method = "update",
             args = list(
               list(visible = c(FALSE,FALSE,FALSE,FALSE,TRUE,FALSE)),
               list(title = list(text = "Histograma — Parqueaderos"),
                    xaxis = list(title = "parqueaderos", autorange = TRUE))
             )),
        list(label = "Estrato", method = "update",
             args = list(
               list(visible = c(FALSE,FALSE,FALSE,FALSE,FALSE,TRUE)),
               list(title = list(text = "Histograma — Estrato"),
                    xaxis = list(title = "estrato", autorange = TRUE))
             ))
      )
    ))
  )

p

Los histogramas nos permiten ver cómo se distribuyen las variables clave del mercado que nos interesa. En precio, la forma aparente es asimétrica hacia la derecha: hay una concentración en rangos medios y una “cola” de valores altos menos frecuentes. En área construida, la distribución parece seguir un patrón parecido, con muchas casas en metrajes intermedios y algunas pocas muy grandes; a simple vista, el umbral de 200 m² puede cumplirse con un alto porcentaje de las viviendas, considerando esta area como un minimo y no como un numero cerrado. En las variables discretas, habitaciones muestra picos claros en 3–4; baños suele concentrarse en 2–3; y parqueaderos en 1–2. Los niveles más altos existen, pero son menos comunes; una hipótesis natural es que dichos niveles “premium” vengan asociados a precios mayores. En estrato, se observan principalmente 4 y 5 dentro de esta zona, con menor presencia de otros niveles; esto sugiere una segmentación socioeconómica del Norte está predominantemente en este segmento.

2.2 Preparación de los datos

# Variables de interes (quitamos NAs)
df_eda <- vivienda %>%
  select(preciom, areaconst, estrato, banios, habitaciones, parqueaderos, zona) %>%
  drop_na()

df_eda$zona <- as.factor(df_eda$zona)

2.3 Matriz de correlación (numericas)

num_mat <- df_eda %>% select(-zona)
cor_mat <- cor(num_mat, use = "pairwise.complete.obs")

plot_ly(
  x = colnames(cor_mat),
  y = colnames(cor_mat),
  z = cor_mat,
  type = "heatmap",
  zmin = -1, zmax = 1,
  hovertemplate = "x=%{x}<br>y=%{y}<br>corr=%{z:.2f}<extra></extra>"
) %>%
  layout(title = "Matriz de correlacion (precio y predictores)",
         xaxis = list(title = ""), yaxis = list(title = ""))

2.4 Tabla de correlacion con preciom (ordenada)

preds <- c("areaconst","estrato","banios","habitaciones","parqueaderos")

cor_tb <- purrr::map_dfr(
  preds,
  ~tibble(variable = .x,
          corr = cor(df_eda$preciom, df_eda[[.x]], use = "pairwise.complete.obs"))
) %>% arrange(desc(abs(corr)))

cor_tb

La matriz de correlación entre preciom y los predictores numéricos (areaconst, estrato, banios, habitaciones, parqueaderos) muestra, en general, relaciones positivas: a mayor tamaño o mejor nivel socioeconómico, mayor precio esperado. En particular, se observan tres patrones relevantes:

  1. Parqueaderos lidera la asociación con el precio. Esto es consistente con el mercado: inmuebles con más cupos de parqueo suelen pertenecer a segmentos de mayor valor (proyectos premium, familias con varios vehículos), y a menudo vienen acompañados de más área construida y mejor dotación interna, lo que refuerza la señal de precio.

  2. Área construida y baños siguen muy de cerca, con correlaciones altas. Es razonable: más metros y mejor equipamiento elevan el valor de mercado.

  3. Estrato presenta correlación media, coherente con la idea de que el entorno socioeconómico influye en el precio, pero su efecto puede mezclarse con atributos del inmueble.

  4. Habitaciones muestra una correlación baja-media. En la práctica, el número de habitaciones crece con el área, pero no siempre “explica” tanto precio como m², baños o parqueaderos (p. ej., plantas abiertas vs. compartimentadas).

Implicaciones para el modelado

  1. Las correlaciones reportadas son bivariadas y no controlan por el resto de variables; por tanto, no implican causalidad. El análisis multivariado es el que permitirá aislar efectos.

  2. Se anticipa colinealidad entre variables de tamaño/amenidades (areaconst, banios, parqueaderos y, en menor medida, habitaciones). En el MRLM se evaluará con VIF y, si es necesario, se ajustará la especificación (p. ej., retirar o combinar predictores altamente redundantes).

  3. Dado que la dispersión del precio aumenta con el tamaño, podría existir heterocedasticidad; por ello se considerarán errores robustos (HC) en la inferencia y, de ser pertinente, transformaciones (p. ej., log(preciom)).

  4. Zona se incluirá como factor para capturar diferencias de nivel de precios entre segmentos del mercado; sin embargo, su interpretación será no geográfica estricta por las inconsistencias detectadas entre la etiqueta de zona y las coordenadas.

  5. Se revisarán atípicos e influencia (leverage y distancia de Cook) para evitar que pocos casos dominen la estimación.

  6. Para la etapa de predicción y comparación, se reportarán indicadores de desempeño (R², RMSE/MAE) y se usará un conjunto de prueba para validar capacidad predictiva.

2.5 Dispersores interactivos (precio vs cada predictor)

make_scatter_lm <- function(data, xvar, xlab){
  g <- ggplot(data, aes(x = .data[[xvar]], y = preciom, color = zona)) +
    geom_point(alpha = 0.7) +
    geom_smooth(
      data = data,
      mapping = aes(x = .data[[xvar]], y = preciom),
      method = "lm", se = TRUE, linewidth = 0.9, inherit.aes = FALSE
    ) +
    labs(title = paste("preciom vs", xlab),
         x = xlab, y = "precio (millones)") +
    theme_minimal()
  ggplotly(g, tooltip = c("x","y","color"))
}

make_scatter_lm(df_eda, "areaconst",    "area construida")
## `geom_smooth()` using formula = 'y ~ x'
make_scatter_lm(df_eda, "estrato",      "estrato")
## `geom_smooth()` using formula = 'y ~ x'
make_scatter_lm(df_eda, "banios",       "banios")
## `geom_smooth()` using formula = 'y ~ x'
make_scatter_lm(df_eda, "habitaciones", "habitaciones")
## `geom_smooth()` using formula = 'y ~ x'
make_scatter_lm(df_eda, "parqueaderos", "parqueaderos")
## `geom_smooth()` using formula = 'y ~ x'

Los dispersores interactivos (plotly) confirman las tendencias de la matriz:

  • Precio vs. área construida: nube ascendente con pendiente positiva. Si notas curvatura (precios creciendo más rápido en áreas grandes), podrías considerar transformar alguna variable (p. ej., log(preciom) o log(areaconst)) más adelante.

  • Precio vs. estrato: relación positiva y aproximadamente lineal. Dado que estrato es una escala ordinal, puede modelarse como numérica (tratando su orden) o como factor (si sospechas efectos no lineales por salto de estrato).

  • Precio vs. baños / habitaciones / parqueaderos: relaciones positivas pero con más dispersión. Esto sugiere que estas variables aportan señal, aunque su efecto puede solaparse con el de areaconst (posible colinealidad).

  • Atípicos y dispersión: se observan puntos alejados de la tendencia y una variabilidad del precio que crece con el tamaño (indicio de heterocedasticidad). Esto no invalida el EDA, pero lo anotamos para revisarlo en los supuestos del modelo.

2.6 Zona (categorica): Distribución de precio

df_eda1 <- vivienda %>%
  dplyr::filter(tipo == "Casa") %>%
  dplyr::select(preciom, areaconst, estrato, banios, habitaciones, parqueaderos, zona) %>%
  tidyr::drop_na()

# Boxplot de precio por zona
g_zona_casas <- ggplot(df_eda1, aes(x = zona, y = preciom, fill = zona)) +
  geom_boxplot(outlier.alpha = 0.3) +
  labs(title = "Distribucion de precio por zona (Casas - todas las zonas)",
       x = "zona", y = "precio (millones)", fill = "zona") +
  theme_minimal()
ggplotly(g_zona_casas)
resumen_zona_casas <- df_eda1 %>%
  dplyr::group_by(zona) %>%
  dplyr::summarise(
    n               = dplyr::n(),
    mediana_precio  = median(preciom,   na.rm = TRUE),
    iqr_precio      = IQR(preciom,      na.rm = TRUE),
    mediana_area    = median(areaconst, na.rm = TRUE),
    mediana_estrato = median(estrato,   na.rm = TRUE),
    .groups = "drop"
  ) %>%
  dplyr::arrange(dplyr::desc(mediana_precio))

resumen_zona_casas

El boxplot interactivo por zona evidencia diferencias sistemáticas en el nivel de precios entre zonas (medianas distintas y rangos intercuartílicos diferentes). Esto respalda incluir zona en el análisis como descriptor del mercado.

No obstante, como se mostró en el Paso 1, existe inconsistencia espacial: las coordenadas geográficas no siempre coinciden con la zona declarada. En consecuencia:

  • Trataremos zona como un atributo categórico informativo (socioeconómico/administrativo), no como segmentación geográfica exacta.

  • La interpretación será cuidadosa: diferencias por zona reflejan más bien perfiles del mercado que fronteras espaciales precisas.

  • Solo se muestra viviendas tipo Casa para evitar duplicidad de información cuando evaluemos el escenario 2 (apartamentos zona sur)

3 Paso. Estimación del modelo e interpretación

3.1 Ajustar el modelo

df_model <- vivienda %>%
  dplyr::select(preciom, areaconst, estrato, habitaciones, parqueaderos, banios) %>%
  tidyr::drop_na()


set.seed(123)
n   <- nrow(df_model)
idx <- sample(seq_len(n), size = floor(0.8*n))
train1 <- df_model[idx, ]
test1  <- df_model[-idx, ]


m1 <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios,
         data = train1)

summary(m1)  
## 
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos + 
##     banios, data = train1)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -1370.30   -87.24   -16.93    53.65  1102.78 
## 
## Coefficients:
##                Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  -372.46318   15.92499  -23.39   <2e-16 ***
## areaconst       0.82302    0.02423   33.96   <2e-16 ***
## estrato        95.57515    3.15103   30.33   <2e-16 ***
## habitaciones  -30.85371    2.52032  -12.24   <2e-16 ***
## parqueaderos   75.67827    2.87725   26.30   <2e-16 ***
## banios         61.73659    2.93055   21.07   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 175.6 on 5367 degrees of freedom
## Multiple R-squared:  0.7238, Adjusted R-squared:  0.7235 
## F-statistic:  2813 on 5 and 5367 DF,  p-value: < 2.2e-16
# Predicción y métricas en TEST (out-of-sample)
test1$pred  <- predict(m1, newdata = test1)
test1$resid <- test1$preciom - test1$pred
rmse1 <- sqrt(mean(test1$resid^2))
mae1  <- mean(abs(test1$resid))
r2_1  <- cor(test1$preciom, test1$pred)^2
cat(sprintf("CASAS — TEST: RMSE = %.2f | MAE = %.2f | R2 = %.3f\n", rmse1, mae1, r2_1))
## CASAS — TEST: RMSE = 183.32 | MAE = 119.20 | R2 = 0.710

3.2 Interpretacion

Especificación: preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios Muestra: 6.717 observaciones, Entrenamiento: 5373, Prueba: 1344 Unidades: preciom en millones de pesos; areaconst en m²; estrato escala 3–6; habitaciones, parqueaderos y banios son conteos.

Resumen de ajuste y significancia

  • Todos los coeficientes resultaron estadísticamente significativos al 1%

  • R² = 0.724 y R² ajustado = 0.724: el modelo explica ~72% de la variabilidad del precio.

  • F(5, 5367)= 2813, p < 0.001: el conjunto de predictores es significativo en bloque.

  • RMSE ≈ 175.6 (millones) en entrenamiento

Lectura de coeficientes (ceteris paribus):

  • Área construida (areaconst) = = 0.823 (t = 33.96; p < 0.001): por cada +1 m², el precio esperado aumenta ≈ 0.823 millones (≈ $823.000 COP), manteniendo constantes las demas variables.

  • Estrato (estrato) = 95.575 (t = 30.33; p < 0.001): +1 nivel de estrato se asocia con +95.6 millones en el precio esperado, reforzando el rol del entorno socioeconomico.

  • Parqueaderos (parqueaderos) = 75.678 (t = 26.30; p < 0.001): +1 cupo implica +75.7 millones.

  • Baños (banios) = 61.737 (t = 21.07; p < 0.001): +1 baño se asocia con +61.7 millones.

  • Habitaciones (habitaciones) = −30.854 (t = −12.24; p < 0.001): a igual metraje, mas habitaciones implican espacios mas subdivididos; puede reducir el precio frente a distribuciones mas amplias o mejor dotadas

Constante (Intercept) = –372.463 La constante representa el precio esperado cuando todas las variables valen cero; no es interpretable económicamente (m² = 0, sin baños, etc.).

Coherencia económica y lógica de signos Los predictores de tamaño y amenidades (m², baños, parqueaderos) así como el entorno (estrato) muestran el signo esperado y gran significancia. El signo negativo de “habitaciones” es coherente condicional a m² fijo. En suma, los resultados son lógicos con el funcionamiento del mercado: más área, mejor dotación y mejor entorno elevan la disposición a pagar.

Metricas en TEST: RMSE = 183.32, MAE = 119.20, R² = 0.710 En terminos de negocio, el RMSE ≈ 183 millones es el error tipico de prediccion out-of-sample en la escala original de precios. Es material para decisiones puntuales, pero razonable en un mercado naturalmente disperso. El R² de prueba (0.710) es cercano al de entrenamiento (~0.724), lo que sugiere buen equilibrio sesgo–varianza sin sobreajuste evidente.

Implicaciones y posibles mejoras:

  • Capturar diferencias de nivel de mercado (p. ej., incluir zona como factor y comparar).

  • Tratar no linealidades: evaluar log(preciom) o terminos flexibles en areaconst si los residuales sugieren curvatura.

  • Interacciones plausibles: areaconst × estrato, areaconst × banios.

  • Heterocedasticidad: reportar errores robustos (HC) y/o transformaciones si la dispersion crece con el tamaño.

  • Colinealidad: si VIF es alto, simplificar o usar regularizacion (ridge/lasso).

  • Validacion: mantener enfoque en metrica out-of-sample (RMSE/MAE/R² en test o CV) para priorizar capacidad predictiva real.

4 Paso. Validacion de supuestos

4.1 Panel de diagnostico general

performance::check_model(m1)

4.2 Diagnosticos del modelo

aug <- augment(m1)

# Residuos vs ajustados (linealidad y varianza)
ggplot(aug, aes(.fitted, .resid)) +
  geom_point(alpha = 0.6) +
  geom_hline(yintercept = 0, linewidth = 0.7) +
  geom_smooth(method = "loess", se = FALSE, linewidth = 0.9) +
  labs(title = "Residuos vs ajustados", x = "ajustados", y = "residuos") +
  theme_minimal()
## `geom_smooth()` using formula = 'y ~ x'

# QQ-plot (normalidad aproximada)
ggplot(aug, aes(sample = .std.resid)) +
  stat_qq() +
  stat_qq_line(linewidth = 0.9) +
  labs(title = "QQ-plot de residuos estandarizados", x = "teorico", y = "observado") +
  theme_minimal()

# Scale-Location (heterocedasticidad)
ggplot(aug, aes(.fitted, sqrt(abs(.std.resid)))) +
  geom_point(alpha = 0.6) +
  geom_smooth(method = "loess", se = FALSE, linewidth = 0.9) +
  labs(title = "Scale-Location", x = "ajustados", y = "sqrt(|residuos est|)") +
  theme_minimal()
## `geom_smooth()` using formula = 'y ~ x'

  • Residuos vs ajustados — Linealidad

La nube de residuos vs ajustados muestra curvatura leve en precios altos; la LOESS se aparta de la horizontal. Esto sugiere no linealidades suaves (especialmente con areaconst) y/o interacciones.

  • QQ-plot — Normalidad de residuos

El QQ-plot ajusta bien en el centro pero se desvía en las colas (colas algo pesadas). Con el tamaño muestral disponible, OLS se mantiene estable; aun así, reforzaremos la inferencia con errores robustos.

  • Scale–Location — Homocedasticidad

El gráfico Scale–Location muestra dispersión creciente con los ajustados (efecto abanico), indicio de heterocedasticidad. Esto impacta las errores estándar de OLS.

aug <- broom::augment(m1)

y_obs <- if (exists("df_model")) df_model$preciom else (aug$.fitted + aug$.resid)

df_dens <- dplyr::bind_rows(
  tibble::tibble(valor = y_obs,        serie = "observado"),
  tibble::tibble(valor = aug$.fitted,  serie = "ajustado")
)

ggplot(df_dens, aes(x = valor, color = serie)) +
  geom_density(linewidth = 1) +
  labs(title = "Densidad: observado vs ajustado",
       x = "precio (millones)", y = "densidad") +
  theme_minimal()

Las distribuciones coinciden bien en la zona central, lo que indica que el modelo captura adecuadamente el nivel medio del mercado. Se observan diferencias en las colas: subestima la frecuencia de precios muy bajos y no reproduce por completo la cola alta (inmuebles muy costosos). Este patrón es coherente con la heterocedasticidad y sugiere que, para mayor precisión en extremos, conviene considerar errores robustos e incorporar no linealidad (p. ej., log(preciom) o un termino flexible en areaconst).

4.3 Heterocedasticidad (Breusch-Pagan) y errores robustos

bp <- lmtest::bptest(m1)
bp
## 
##  studentized Breusch-Pagan test
## 
## data:  m1
## BP = 960.32, df = 5, p-value < 2.2e-16
# Tabla con errores robustos (HC1) si hay heterocedasticidad
if (bp$p.value < 0.05) {
  modelsummary(
    list("OLS (HC1)" = m1),
    estimate  = "{estimate}",
    statistic = c("SE(HC1) = {std.error}", "t = {statistic}", "p = {p.value}"),
    vcov      = sandwich::vcovHC(m1, type = "HC1"),
    gof_omit  = "AIC|BIC|Log.Lik",
    title     = "Tabla 2. Coeficientes con errores robustos (HC1)"
  )
}
Tabla 2. Coeficientes con errores robustos (HC1)
OLS (HC1)
(Intercept) -372.463
SE(HC1) = 17.609
t = -21.152
p = <0.001
areaconst 0.823
SE(HC1) = 0.061
t = 13.514
p = <0.001
estrato 95.575
SE(HC1) = 3.184
t = 30.020
p = <0.001
habitaciones -30.854
SE(HC1) = 3.515
t = -8.778
p = <0.001
parqueaderos 75.678
SE(HC1) = 4.828
t = 15.675
p = <0.001
banios 61.737
SE(HC1) = 3.932
t = 15.702
p = <0.001
Num.Obs. 5373
R2 0.724
R2 Adj. 0.724
RMSE 175.54
Std.Errors Custom

La prueba indica heterocedasticidad; por lo tanto, la inferencia se reporta con errores robustos (HC1) y, de ser necesario, se considerarán transformaciones (p. ej., log(preciom)) o ponderaciones.

4.4 Colinealidad (VIF)

vifs <- car::vif(m1)
vifs
##    areaconst      estrato habitaciones parqueaderos       banios 
##     2.184406     1.559678     2.072007     1.849471     2.876964

El panel de VIF ubica a todos los predictores por debajo de los umbrales habituales (≤ 5). No se evidencian problemas de multicolinealidad severa; las estimaciones son estables y las señales (signos) son coherentes con el negocio.

4.5 Influencia (Cook) y leverage

aug %>%
  mutate(id = row_number()) %>%
  arrange(desc(.cooksd)) %>%
  slice_head(n = 10)
ggplot(aug, aes(.hat, .std.resid^2)) +
  geom_point(alpha = 0.6) +
  labs(title = "Leverage vs residuos^2", x = "leverage (hat)", y = "residuos^2") +
  theme_minimal()

La nube leverage vs. residuos est. muestra pocos casos cercanos a los contornos; en conjunto, no se observan puntos con influencia extrema. Se recomienda monitorear estos casos al realizar predicciones puntuales de alto valor.

4.6 Indicadores de rendimiento

rmse <- sqrt(mean(test1$resid^2))
mae  <- mean(abs(test1$resid))
mape <- mean(abs(test1$resid / test1$preciom)) * 100

c(RMSE = rmse, MAE = mae, MAPE_pct = mape)
##     RMSE      MAE MAPE_pct 
## 183.3182 119.2004  26.5617

El modelo presenta:

  • MAE ~ 119 millones: error típico absoluto por inmueble. Es material para decisiones puntuales, por lo que conviene usar intervalos de predicción al recomendar opciones individuales.

  • RMSE > MAE (183 vs 119) sugiere errores grandes en algunos casos (colas pesadas), coherente con lo visto en los diagnósticos.

  • MAPE ~ 26.6% indica que, en promedio, la predicción se desvía cerca de un cuarto del valor real; el error relativo tiende a ser mayor en extremos (muy baratos o muy costosos).

4.7 Recomendaciones

  • Observamos que el error crece con el nivel de precio. Para mantener la validez de la inferencia: Reportar errores robustos (HC1) en las tablas de coeficientes.

  • Si se busca mayor precisión se puede considerar transformar el precio (p. ej., log(preciom)) o ponderar las observaciones (WLS) para dar menos peso a los casos con mayor variabilidad.

  • La relación entre precio y área parece tener curvatura en niveles altos. Para capturarla mejor se puede probar términos flexibles en areaconst (polinomio de grado bajo o splines).

  • Evaluar interacciones razonables (p. ej., areaconst × estrato), porque el “rendimiento” de los m² puede cambiar según el estrato.

  • Si los datos lo permiten, considerar factores que suelen explicar precio: antigüedad, calidad de acabados, amenidades (ascensor, piscina, seguridad).

5 Paso. Predicción del precio con las caracteristicas de la vivienda 1

presupuesto <- 350  # millones

# Vivienda 1: Casa, 200 m2, 1 parqueadero, 2 baños, 4 habitaciones
new_viv1 <- tibble::tibble(
  areaconst    = 200,
  estrato      = c(4, 5),   # dos escenarios
  habitaciones = 4,
  parqueaderos = 1,
  banios       = 2
)


pred_v1 <- dplyr::bind_cols(
  new_viv1,
  as.data.frame(predict(m1, newdata = new_viv1, interval = "prediction", level = 0.95))
)

pred_v1_tabla <- pred_v1 %>%
  dplyr::mutate(
    escenario = paste0("Estrato ", estrato),
    dentro_350M = fit <= presupuesto
  ) %>%
  dplyr::select(
    escenario, areaconst, habitaciones, banios, parqueaderos,
    pred_millones = fit, lwr, upr, dentro_350M
  )

knitr::kable(pred_v1_tabla, digits = 1,
             caption = "Prediccion Vivienda 1 (escenarios de estrato) con IC95% [millones]")
Table 5.1: Prediccion Vivienda 1 (escenarios de estrato) con IC95% [millones]
escenario areaconst habitaciones banios parqueaderos pred_millones lwr upr dentro_350M
Estrato 4 200 4 2 1 250.2 -94.3 594.6 TRUE
Estrato 5 200 4 2 1 345.8 1.3 690.2 TRUE

Se obtienen dos escenarios según el estrato:

Estrato 4: predicción puntual 250,2 millones con IC95% [0, 595] millones (el límite inferior negativo se interpreta como 0). Este escenario entra cómodamente en el crédito de 350 millones, y la amplitud del intervalo indica que hay inmuebles comparables que pueden moverse desde rangos muy bajos hasta casos más altos; aun así, el riesgo de exceder el presupuesto es bajo si priorizamos ofertas cercanas al centro de la distribución (≈ 230–320 millones).

Estrato 5: predicción puntual 345,8 millones con IC95% [1,3; 690] millones.La estimación cae en el borde del presupuesto y, en la práctica, es probable que varias opciones comparables lo superen.

Estos intervalos de predicción reflejan la variabilidad caso a caso, su amplitud es coherente con lo visto en la validación: heterocedasticidad y colas pesadas. Por ello, las cifras puntuales deben interpretarse como referencias y no como “precios exactos”.

Sensibilidad: Manteniendo lo demás constante, el modelo sugiere que:

+1 estrato ≈ +95,6 M,

+1 parqueadero ≈ +75,7 M,

+1 baño ≈ +61,7 M,

+10 m² ≈ +8,23 M,

+1 habitación ≈ –30,9 M (este valor es negativo porque, a igual metraje, más cuartos implican espacios más subdivididos y, en promedio, menor valoración).

6 Paso. Ofertas

# Se interpretan las especificaciones de la vivienda como un minimo, a excepcion del precio que es un tope maximo

ofertas1 <- vivienda %>%
  dplyr::filter(
    tipo == "Casa",
    zona == "Zona Norte",
    estrato %in% c(4, 5),
    areaconst >= 200,
    parqueaderos >= 1,
    banios >= 2,
    habitaciones >= 4,
    preciom <= 350
  ) %>%

  dplyr::mutate(pred_m1 = as.numeric(predict(m1, newdata = cur_data())),
                dentro_350M = preciom <= 350,
                dist_area   = abs(areaconst - 200)) %>%

  dplyr::arrange(dplyr::desc(dentro_350M), dist_area, preciom) %>%
  dplyr::slice_head(n = 5)
## Warning: There was 1 warning in `dplyr::mutate()`.
## ℹ In argument: `pred_m1 = as.numeric(predict(m1, newdata = cur_data()))`.
## Caused by warning:
## ! `cur_data()` was deprecated in dplyr 1.1.0.
## ℹ Please use `pick()` instead.
ofertas1_tabla <- ofertas1 %>%
  dplyr::transmute(
    barrio, zona, estrato,
    areaconst, habitaciones, banios, parqueaderos,
    precio_m   = round(preciom, 1),
    pred_m1    = round(pred_m1, 1),
    gap_modelo = round(preciom - pred_m1, 1),   # +: sobre el modelo; -: bajo el modelo
    dentro_350M
  )

ofertas1_tabla
leaflet(ofertas1) %>%
  addTiles() %>%
  addCircleMarkers(
    lng=~longitud, lat=~latitud,
    color = "forestgreen", radius=7, stroke=FALSE, fillOpacity=0.85,
    popup = ~paste0("<b>", barrio, "</b> (Estrato ", estrato, ")<br>",
                    "Area: ", areaconst, " m2 | Hab: ", habitaciones,
                    " | Banios: ", banios, " | Parq: ", parqueaderos, "<br>",
                    "<b>Precio:</b> ", round(preciom,1), " M<br>",
                    "<b>Pred. modelo:</b> ", round(pred_m1,1), " M")
  )

6.1 Analisis de las ofertas

Para valorar su atractivo relativo usamos el gap del modelo (precio observado – precio predicho): un gap negativo sugiere que la oferta está “barata” frente a lo que el mercado esperaría, según el modelo.

En La Flora (estrato 5, 200 m², 4 habitaciones, 4 baños y 2 parqueaderos por 320 M) el modelo predice 547,1 M, por lo que el gap es de –227,1 M, aproximadamente 41,5% por debajo de lo esperado. Además de ajustar exactamente el metraje, excede los mínimos en baños y parqueaderos, por lo que combina dotación sólida con un precio inusualmente bajo. Esta diferencia tan grande puede reflejar una oportunidad genuina, pero también amerita una revisión cuidadosa del estado físico del inmueble, costos de administración y entorno inmediato.

En El Bosque (estrato 5, 200 m², 4 habitaciones, 3 baños y 3 parqueaderos, precio 350 M), la predicción es 560,5 M y el gap asciende a –210,5 M (–37,6%). La gran ventaja competitiva está en los tres parqueaderos, valiosos para familias con dos vehículos o necesidades de visita; el punto menos favorable es que el precio se ubica exactamente en el tope del crédito, dejando cero margen para gastos de cierre o negociación (en caso de que se presenten), por lo que habría que considerar que todos los costos estan cubiertos por el monto del credito o dichos valores adicionales se cubririan con recursos propios.

La propuesta de Vipasa (estrato 5, 203 m², 4 habitaciones, 3 baños y 2 parqueaderos por 340 M) muestra un gap de –148,6 M sobre una predicción de 488,6 M, es decir, alrededor de –30,4%. Esta alternativa está 10 millones por debajo del presupuesto, lo que permite asumir gastos de negociación (en caso de que existan); es una opción sólida y balanceada para avanzar a visita.

En La Merced (estrato 4, 200 m², 4 habitaciones, 4 baños y 2 parqueaderos por 320 M), el modelo estima 449,2 M, con un gap de –129,2 M (–28,8%). A diferencia de las anteriores, esta oferta se ubica en estrato 4, alineada con uno de los escenarios de la solicitud y potencialmente con menores cargas de administración e impuestos que un estrato 5. Mantiene el metraje objetivo y una dotación amplia (4 baños, 2 parqueaderos), por lo que representa una alternativa muy consistente si se desea permanecer en estrato 4 sin sacrificar atributos.

Por último, El Bosque (estrato 5, 202 m², 5 habitaciones, 4 baños y 1 parqueadero, 335 M) presenta un gap de –108,0 M sobre una predicción de 443,0 M (–24,4%). Su fortaleza es el número de habitaciones (útil para familia numerosa o espacios de estudio/trabajo), aunque solo ofrece un parqueadero, que si bien cumple con el requisito solicitado por el cliente, la posiciona por debajo de las demás ofertas que ofrecen 2 parqueaderos, lo cual podría implicar costo adicional si se necesita un segundo cupo.

En conjunto, las cinco alternativas resultan atractivas frente al modelo (todas con gaps negativos importantes) y cumplen el presupuesto. Si el objetivo es maximizar valor relativo, La Flora sobresale por la magnitud del descuento y la dotación; si se prefiere estrato 4 por costos y perfil de entorno, La Merced es la mejor alineada; si se busca un equilibrio general con margen economico, Vipasa ofrece una relación precio–metraje–dotación muy conveniente; si la prioridad es el estacionamiento, El Bosque con 3 parqueaderos es la elección lógica (asumiendo la falta de margen para otros gastos); y para quienes requieren más dormitorios sin superar el crédito, el El Bosque de 5 habitaciones resulta competitivo, contemplando la posible renta de un parqueadero adicional.

Antes de decidir, conviene verificar estado físico de cada inmueble para confirmar que los grandes descuentos observados responden a una oportunidad y no a características no modeladas por el modelo (antigüedad, mantenimiento, exposición a ruido o ubicación específica dentro del barrio, además de costos de administración o vigilancia dependiendo del tipo de unidad residencial).

Escenario: Apartamento en zona Sur

7 Paso: Filtro y verificacion de la base

7.1 base2: Apartamentos en zona Sur

base2 <- vivienda %>%
  filter(tipo == "Apartamento", zona == "Zona Sur")

# Mostrar los primeros 3 registros
head(base2, 3)

7.2 Tablas de comprobacion

# Conteo por tipo y zona en la base completa
vivienda %>% count(tipo, zona)
# Conteo por tipo y zona en la base2 (Apartamento, Zona Sur)
base2 %>% count(tipo, zona)

7.3 Resumen numerico de variables clave en base1

summary(select(base2, preciom, areaconst, parqueaderos, banios, habitaciones, estrato))
##     preciom         areaconst       parqueaderos        banios     
##  Min.   :  75.0   Min.   : 40.00   Min.   : 1.000   Min.   :0.000  
##  1st Qu.: 175.0   1st Qu.: 65.00   1st Qu.: 1.000   1st Qu.:2.000  
##  Median : 245.0   Median : 85.00   Median : 1.000   Median :2.000  
##  Mean   : 297.3   Mean   : 97.47   Mean   : 1.415   Mean   :2.488  
##  3rd Qu.: 335.0   3rd Qu.:110.00   3rd Qu.: 2.000   3rd Qu.:3.000  
##  Max.   :1750.0   Max.   :932.00   Max.   :10.000   Max.   :8.000  
##                                    NA's   :406                     
##   habitaciones      estrato    
##  Min.   :0.000   Min.   :3.00  
##  1st Qu.:3.000   1st Qu.:4.00  
##  Median :3.000   Median :5.00  
##  Mean   :2.966   Mean   :4.63  
##  3rd Qu.:3.000   3rd Qu.:5.00  
##  Max.   :6.000   Max.   :6.00  
## 

7.4 Mapa con los puntos de la base1

leaflet(base2) %>%
  addTiles() %>%
  addCircleMarkers(
    lng = ~longitud, lat = ~latitud,
    popup = ~paste0(
      "<b>Apartamento</b> - ", barrio, "<br>",
      "precio (M): ", round(preciom, 1), "<br>",
      "area: ", areaconst, " m2<br>",
      "estrato: ", estrato, " | hab: ", habitaciones,
      " | banios: ", banios, " | parqueaderos: ", parqueaderos
    ),
    radius = 6, stroke = FALSE, fillOpacity = 0.8
  )

Al filtrar apartamentos en la Zona Sur, el mapa muestra que los puntos no se concentran exclusivamente en dicha zona, sino que aparecen dispersos por toda la ciudad. Esto sugiere posibles inconsistencias entre la variable categórica zona y las coordenadas geográficas registradas, como ya se habia observado en el escenario 1 donde se analizo un mapa con todas las zonas marcadas.

8 Paso. EDA interactivo con plotly

8.1 Conocimiento de los datos Casa - Norte

Para un mejor entendimiento de los datos con los que vamos a trabajar visualizamos histogramas que nos permita entender la distribución de las caracteristicas de las viviendas a analizar, para este escenario, Apartamentos ubicadas en la Zona Sur.

# solo apartamentos en zona sur
aptos_sur <- vivienda %>%
  filter(tipo == "Apartamento", zona == "Zona Sur") %>%
  tidyr::drop_na(preciom, areaconst, estrato, banios, habitaciones, parqueaderos)

# Histogramas
p <- plot_ly()

p <- p %>%
  add_histogram(x = ~aptos_sur$preciom,      name = "Precio (M)",
                nbinsx = 35, marker = list(color = "#3182bd"),
                visible = TRUE,  showlegend = FALSE) %>%
  add_histogram(x = ~aptos_sur$areaconst,    name = "Area (m2)",
                nbinsx = 35, marker = list(color = "#31a354"),
                visible = FALSE, showlegend = FALSE) %>%
  add_histogram(x = ~aptos_sur$habitaciones, name = "Habitaciones",
                xbins = list(size = 1), marker = list(color = "#756bb1"),
                visible = FALSE, showlegend = FALSE) %>%
  add_histogram(x = ~aptos_sur$banios,       name = "Banios",
                xbins = list(size = 1), marker = list(color = "#e6550d"),
                visible = FALSE, showlegend = FALSE) %>%
  add_histogram(x = ~aptos_sur$parqueaderos, name = "Parqueaderos",
                xbins = list(size = 1), marker = list(color = "#2ca25f"),
                visible = FALSE, showlegend = FALSE) %>%
  add_histogram(x = ~aptos_sur$estrato,      name = "Estrato",
                xbins = list(size = 1), marker = list(color = "#fd8d3c"),
                visible = FALSE, showlegend = FALSE) %>%
  layout(
    barmode = "overlay",
    title  = list(text = "Histograma — Precio (M) (Aptos Zona Sur)"),
    xaxis  = list(title = "precio (millones)", autorange = TRUE),
    yaxis  = list(title = "frecuencia"),
    updatemenus = list(list(
      type = "dropdown", direction = "down", x = 1.05, xanchor = "left", y = 1.15,
      buttons = list(
        list(label = "Precio (M)", method = "update",
             args = list(
               list(visible = c(TRUE,FALSE,FALSE,FALSE,FALSE,FALSE)),
               list(title = list(text = "Histograma — Precio (M) (Aptos Zona Sur)"),
                    xaxis = list(title = "precio (millones)", autorange = TRUE))
             )),
        list(label = "Area (m2)", method = "update",
             args = list(
               list(visible = c(FALSE,TRUE,FALSE,FALSE,FALSE,FALSE)),
               list(title = list(text = "Histograma — Area (m2) (Aptos Zona Sur)"),
                    xaxis = list(title = "area (m2)", autorange = TRUE))
             )),
        list(label = "Habitaciones", method = "update",
             args = list(
               list(visible = c(FALSE,FALSE,TRUE,FALSE,FALSE,FALSE)),
               list(title = list(text = "Histograma — Habitaciones (Aptos Zona Sur)"),
                    xaxis = list(title = "habitaciones", autorange = TRUE))
             )),
        list(label = "Banios", method = "update",
             args = list(
               list(visible = c(FALSE,FALSE,FALSE,TRUE,FALSE,FALSE)),
               list(title = list(text = "Histograma — Banios (Aptos Zona Sur)"),
                    xaxis = list(title = "banios", autorange = TRUE))
             )),
        list(label = "Parqueaderos", method = "update",
             args = list(
               list(visible = c(FALSE,FALSE,FALSE,FALSE,TRUE,FALSE)),
               list(title = list(text = "Histograma — Parqueaderos (Aptos Zona Sur)"),
                    xaxis = list(title = "parqueaderos", autorange = TRUE))
             )),
        list(label = "Estrato", method = "update",
             args = list(
               list(visible = c(FALSE,FALSE,FALSE,FALSE,FALSE,TRUE)),
               list(title = list(text = "Histograma — Estrato (Aptos Zona Sur)"),
                    xaxis = list(title = "estrato", autorange = TRUE))
             ))
      )
    ))
  )

p

Los histogramas permiten observar, de forma preliminar, cómo se distribuyen las variables clave del mercado objetivo. En precio, la forma aparente es asimétrica hacia la derecha: se concentra la masa en rangos medios y aparece una cola de valores altos menos frecuentes. En área construida, el patrón luce similar, con muchos apartamentos en metrajes intermedios y algunos pocos de gran tamaño; a simple vista, el umbral de 300 m² puede abordarse tratándolo como mínimo (no como número cerrado), lo que ampliaría el conjunto de opciones sin salirnos del perfil buscado.

En las variables discretas, habitaciones suele presentar picos en 3–4 y baños en 2–3, mientras que parqueaderos se concentra en 1–2; los niveles superiores existen pero son menos comunes. Una hipótesis razonable—que se verificará más adelante—es que esos niveles “premium” (más baños y parqueaderos) estén asociados a precios mayores. En cuanto a estrato, se aprecia una participación importante de 4–5 y presencia más acotada de 6; esto sugiere una segmentación socioeconómica donde los estratos 5–6 podrían concentrar los listados de mayor valor, algo que también contrastaremos con análisis bivariados y el modelo.

8.2 Preparación de los datos

df_eda2 <- vivienda %>%
  dplyr::filter(tipo == "Apartamento", zona == "Zona Sur") %>%
  dplyr::select(preciom, areaconst, estrato, banios, habitaciones, parqueaderos, zona) %>%
  tidyr::drop_na()

df_eda2$zona <- factor(df_eda2$zona)

8.3 Matriz de correlación (numericas)

num_mat2 <- df_eda2 %>% dplyr::select(-zona)
cor_mat2 <- cor(num_mat2, use = "pairwise.complete.obs")

plot_ly(
  x = colnames(cor_mat2),
  y = colnames(cor_mat2),
  z = cor_mat2,
  type = "heatmap",
  zmin = -1, zmax = 1,
  colorscale = "RdBu", reversescale = TRUE,
  hovertemplate = "x=%{x}<br>y=%{y}<br>corr=%{z:.2f}<extra></extra>"
) %>%
  layout(
    title = "Matriz de correlacion (Aptos Zona Sur: precio y predictores)",
    xaxis = list(title = ""), yaxis = list(title = "")  )

8.4 Tabla de correlacion con preciom (ordenada)

preds2 <- c("areaconst","estrato","banios","habitaciones","parqueaderos")

cor_tb2 <- purrr::map_dfr(
  preds2,
  ~ tibble::tibble(
      variable = .x,
      corr = cor(df_eda2$preciom, df_eda2[[.x]], use = "pairwise.complete.obs")
    )
) %>%
  dplyr::arrange(dplyr::desc(abs(corr)))

cor_tb2

La matriz muestra asociaciones positivas entre el precio y los predictores:

  • Área construida lidera, lo cual es coherente: en apartamentos, más m² suelen traducirse en mayor valor.

  • Baños aparece muy cerca del líder: más baños suelen indicar mejor dotación y confort, atributos que el mercado valora.

  • Parqueaderos también tiene correlación alta, pero ligeramente menor que m² y baños; sigue siendo un diferenciador relevante.

  • Estrato muestra una asociación media–alta, consistente con efectos del entorno (servicios, localización, percepción).

  • Habitaciones queda baja–media: a igual metraje, más cuartos pueden implicar espacios más subdivididos; por eso explica menos que m²/baños/parqueaderos.

Implicaciones para el modelado

  1. Las correlaciones reportadas son bivariadas y no controlan por el resto de variables; por tanto, no implican causalidad. El análisis multivariado es el que permitirá aislar efectos.

  2. Se anticipa colinealidad entre variables de tamaño/amenidades (areaconst, banios, parqueaderos y, en menor medida, habitaciones). En el MRLM se evaluará con VIF y, si es necesario, se ajustará la especificación (p. ej., retirar o combinar predictores altamente redundantes).

  3. Dado que la dispersión del precio aumenta con el tamaño, podría existir heterocedasticidad; por ello se considerarán errores robustos (HC) en la inferencia y, de ser pertinente, transformaciones (p. ej., log(preciom)).

  4. Zona se incluirá como factor para capturar diferencias de nivel de precios entre segmentos del mercado; sin embargo, su interpretación será no geográfica estricta por las inconsistencias detectadas entre la etiqueta de zona y las coordenadas.

  5. Se revisarán atípicos e influencia (leverage y distancia de Cook) para evitar que pocos casos dominen la estimación.

  6. Para la etapa de predicción y comparación, se reportarán indicadores de desempeño (R², RMSE/MAE) y se usará un conjunto de prueba para validar capacidad predictiva.

8.5 Dispersores interactivos (precio vs cada predictor)

make_scatter_lm2 <- function(data, xvar, xlab){
  g <- ggplot(data, aes(x = .data[[xvar]], y = preciom, color = factor(estrato))) +
    geom_point(alpha = 0.7) +
    geom_smooth(
      data = data,
      mapping = aes(x = .data[[xvar]], y = preciom),
      method = "lm", se = TRUE, linewidth = 0.9, inherit.aes = FALSE
    ) +
    labs(title = paste("preciom vs", xlab),
         x = xlab, y = "precio (millones)", color = "estrato") +
    theme_minimal()
  ggplotly(g, tooltip = c("x","y","color"))
}

make_scatter_lm2(df_eda2, "areaconst",    "area construida")
## `geom_smooth()` using formula = 'y ~ x'
make_scatter_lm2(df_eda2, "estrato",      "estrato")
## `geom_smooth()` using formula = 'y ~ x'
make_scatter_lm2(df_eda2, "banios",       "banios")
## `geom_smooth()` using formula = 'y ~ x'
make_scatter_lm2(df_eda2, "habitaciones", "habitaciones")
## `geom_smooth()` using formula = 'y ~ x'
make_scatter_lm2(df_eda2, "parqueaderos", "parqueaderos")
## `geom_smooth()` using formula = 'y ~ x'

Los dispersores interactivos (plotly) confirman las tendencias de la matriz:

  • Precio vs. área construida: nube ascendente con pendiente positiva. Si notas curvatura (precios creciendo más rápido en áreas grandes), podrías considerar transformar alguna variable (p. ej., log(preciom) o log(areaconst)) más adelante.

  • Precio vs. estrato: relación positiva y aproximadamente lineal. Dado que estrato es una escala ordinal, puede modelarse como numérica (tratando su orden) o como factor (si sospechas efectos no lineales por salto de estrato).

  • Precio vs. baños / habitaciones / parqueaderos: relaciones positivas pero con más dispersión. Esto sugiere que estas variables aportan señal, aunque su efecto puede solaparse con el de areaconst (posible colinealidad).

  • Atípicos y dispersión: se observan puntos alejados de la tendencia y una variabilidad del precio que crece con el tamaño (indicio de heterocedasticidad). Esto no invalida el EDA, pero lo anotamos para revisarlo en los supuestos del modelo.

8.6 Zona (categorica): Distribución de precio

df_eda_apts <- vivienda %>%
  dplyr::filter(tipo == "Apartamento") %>%
  dplyr::select(preciom, areaconst, estrato, banios, habitaciones, parqueaderos, zona) %>%
  tidyr::drop_na()

# Boxplot de precio por zona (Apartamentos - todas las zonas)
g_zona_apts <- ggplot(df_eda_apts, aes(x = zona, y = preciom, fill = zona)) +
  geom_boxplot(outlier.alpha = 0.3) +
  labs(title = "Distribucion de precio por zona (Apartamentos - todas las zonas)",
       x = "zona", y = "precio (millones)", fill = "zona") +
  theme_minimal()

ggplotly(g_zona_apts)
resumen_zona_apts <- df_eda_apts %>%
  dplyr::group_by(zona) %>%
  dplyr::summarise(
    n               = dplyr::n(),
    mediana_precio  = median(preciom,   na.rm = TRUE),
    iqr_precio      = IQR(preciom,      na.rm = TRUE),
    mediana_area    = median(areaconst, na.rm = TRUE),
    mediana_estrato = median(estrato,   na.rm = TRUE),
    .groups = "drop"
  ) %>%
  dplyr::arrange(dplyr::desc(mediana_precio))

resumen_zona_apts

El boxplot interactivo por zona evidencia diferencias sistemáticas en el nivel de precios entre zonas (medianas distintas y rangos intercuartílicos diferentes). Esto respalda incluir zona en el análisis como descriptor del mercado.

No obstante, como se mostró en el Escenario 1, existe inconsistencia espacial: las coordenadas geográficas no siempre coinciden con la zona declarada. En consecuencia:

  • Trataremos zona como un atributo categórico informativo (socioeconómico/administrativo), no como segmentación geográfica exacta.

  • La interpretación será cuidadosa: diferencias por zona reflejan más bien perfiles del mercado que fronteras espaciales precisas.

9 Paso. Estimación del modelo e interpretación

9.1 Ajustar el modelo

set.seed(123)


df_model2 <- vivienda %>%
  dplyr::filter(tipo == "Apartamento") %>%
  dplyr::select(preciom, areaconst, estrato, habitaciones, parqueaderos, banios) %>%
  tidyr::drop_na()

n2   <- nrow(df_model2)
idx2 <- sample(seq_len(n2), size = floor(0.8*n2))
train2 <- df_model2[idx2, ]
test2  <- df_model2[-idx2, ]


m2 <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios,
         data = train2)

summary(m2) 
## 
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos + 
##     banios, data = train2)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -1321.24   -54.94     0.58    46.72   991.41 
## 
## Coefficients:
##                Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  -242.14039   17.56781  -13.78   <2e-16 ***
## areaconst       2.31809    0.05604   41.37   <2e-16 ***
## estrato        51.88101    3.36063   15.44   <2e-16 ***
## habitaciones  -48.50845    4.20623  -11.53   <2e-16 ***
## parqueaderos   79.92232    4.49424   17.78   <2e-16 ***
## banios         48.82252    3.80041   12.85   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 135 on 3378 degrees of freedom
## Multiple R-squared:  0.7967, Adjusted R-squared:  0.7964 
## F-statistic:  2647 on 5 and 3378 DF,  p-value: < 2.2e-16
test2$pred  <- predict(m2, newdata = test2)
test2$resid <- test2$preciom - test2$pred

rmse2 <- sqrt(mean(test2$resid^2))
mae2  <- mean(abs(test2$resid))
r2_2  <- cor(test2$preciom, test2$pred)^2

cat(sprintf("APARTAMENTOS — TEST: RMSE = %.2f | MAE = %.2f | R2 = %.3f\n", rmse2, mae2, r2_2))
## APARTAMENTOS — TEST: RMSE = 151.30 | MAE = 86.96 | R2 = 0.729

9.2 Interpretacion

Especificación: preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios Muestra: 4.231 apartamentos, Entrenamiento: 3379, Prueba: 852 Unidades: preciom en millones de pesos; areaconst en m²; estrato escala 3–6; habitaciones, parqueaderos y banios son conteos.

Resumen de ajuste y significancia

  • Todos los coeficientes aparecen altamente significativos

  • R² = 0.797 y R² ajustado = 0.796: el modelo explica ~79.6% de la variabilidad del precio.

  • F(5, 3378)= 2647, p < 0.001: el conjunto de predictores es significativo en bloque.

  • RMSE ≈ 135 (millones): error típico de predicción en la escala original.

Lectura de coeficientes (ceteris paribus):

  • Área construida (areaconst) = 2.318: por cada +1 m², el precio esperado aumenta ≈ 2.318 millones, manteniendo constantes las demás variables. La magnitud es consistente con que, en apartamentos, el metraje “compra” tanto espacio habitable como calidades del proyecto.

  • Estrato (estrato) = 51.881: +1 nivel de estrato se asocia con +51 millones en el precio esperado. Refuerza que el entorno socioeconómico incide de manera sustancial en la valorización.

  • Parqueaderos (parqueaderos) = 99.922: +1 cupo de parqueadero implica +79.9 millones en el precio esperado. En edificios, la escasez/valor del cupo explica esta prima.

  • Baños (banios) = 48.823: +1 baño se asocia con +48.8 millones en el precio esperado. Capta la mejora en la dotación/comfort del inmueble.

  • Habitaciones (habitaciones) = –48.508: +1 habitación se asocia con –48.5 millones, manteniendo fija el área y el resto. Esta señal negativa es razonable en contexto multivariado: para el mismo metraje, más habitaciones implican espacios más subdivididos (áreas privadas más pequeñas), lo que puede reducir el precio frente a distribuciones más amplias o mejor equipadas. No es “menos cuartos = más barato” en general, sino “a igual m²”.

Constante (Intercept) = –242.14 La constante representa el precio esperado cuando todas las variables valen cero; no es interpretable económicamente (m² = 0, sin baños, etc.).

Coherencia económica y lógica de signos Los predictores de tamaño y amenidades (m², baños, parqueaderos) y el entorno (estrato) presentan el signo esperado y alta significancia. El signo negativo de habitaciones es coherente condicional a m² fijo: con el mismo metraje, más cuartos implican espacios más subdivididos y, en promedio, menor valoración. En suma, los resultados son consistentes con el mercado de apartamentos: más área, mejor dotación y mejor entorno elevan la disposición a pagar.

  • Colinealidad esperada. Dado el EDA, es plausible colinealidad entre areaconst, banios y parqueaderos (y en menor medida habitaciones). Se cuantificará con VIF; si fuera elevado, se considerará simplificar la especificación, combinar predictores o regularización ligera, manteniendo los efectos clave y la claridad del modelo.

Métricas en TEST: RMSE = 151.3, MAE = 87.0, R² = 0.729. El R² en prueba (0.729) es menor al de entrenamiento (0.797), lo que indica una ligera pérdida de ajuste pero dentro de lo esperado. El RMSE ≈ 151 millones representa el error típico de predicción out-of-sample en unidades de precio; es material para una compra individual, pero razonable en un mercado con alta dispersión. El MAE (≈ 87 millones) confirma un sesgo promedio moderado. En conjunto, el modelo conserva buena capacidad predictiva fuera de muestra.

Implicaciones y posibles mejoras:

  • Capturar diferencias de nivel de mercado: incluir zona como factor (aun con su inconsistencia geográfica, aporta señal socioeconómica) y comparar ajuste.

  • Tratar no linealidades: evaluar log(preciom) o términos flexibles en areaconst (p. ej., polinomio de bajo grado o spline) si los residuos sugieren curvatura.

  • Interacciones plausibles: probar areaconst × estrato (m² rinden distinto según el nivel del sector) o areaconst × banios.

  • Heterocedasticidad: reportar errores robustos (HC) y considerar transformaciones si la dispersión crece con el tamaño.

  • Colinealidad: si VIF es alto, simplificar (retirar predictores redundantes) o usar regularización (ridge/lasso) para estabilizar estimaciones.

  • Validación fuera de muestra: usar set de prueba o k-fold CV y monitorear RMSE/MAE/MAPE para priorizar capacidad predictiva real.

10 Paso. Validacion de supuestos

10.1 Panel de diagnostico general

performance::check_model(m2)

10.2 Diagnosticos del modelo

aug2 <- augment(m2)

# Residuos vs ajustados (linealidad y varianza)
ggplot(aug2, aes(.fitted, .resid)) +
  geom_point(alpha = 0.6) +
  geom_hline(yintercept = 0, linewidth = 0.7) +
  geom_smooth(method = "loess", se = FALSE, linewidth = 0.9) +
  labs(title = "Residuos vs ajustados", x = "ajustados", y = "residuos") +
  theme_minimal()
## `geom_smooth()` using formula = 'y ~ x'

# QQ-plot (normalidad aproximada)
ggplot(aug2, aes(sample = .std.resid)) +
  stat_qq() +
  stat_qq_line(linewidth = 0.9) +
  labs(title = "QQ-plot de residuos estandarizados", x = "teorico", y = "observado") +
  theme_minimal()

# Scale-Location (heterocedasticidad)
ggplot(aug2, aes(.fitted, sqrt(abs(.std.resid)))) +
  geom_point(alpha = 0.6) +
  geom_smooth(method = "loess", se = FALSE, linewidth = 0.9) +
  labs(title = "Scale-Location", x = "ajustados", y = "sqrt(|residuos est|)") +
  theme_minimal()
## `geom_smooth()` using formula = 'y ~ x'

  • Residuos vs ajustados — Linealidad

Se observa una curvatura marcada: la tendencia LOESS es ligeramente negativa en el tramo bajo, sube alrededor de valores medios y cae con fuerza en la cola alta de los ajustados. Esto sugiere no linealidades (especialmente con areaconst) y/o rendimientos decrecientes del metraje en precios elevados.

  • QQ-plot — Normalidad de residuos

El ajuste es razonable en la zona central, pero hay desvíos claros en ambas colas, con extremos más pesados (algunos outliers). Dado el tamaño muestral, las estimaciones OLS son estables; no obstante, para la inferencia conviene reportar errores robustos (HC) y acompañar predicciones con intervalos amplios, sobre todo en los rangos altos.

  • Scale–Location — Homocedasticidad

Aparece un efecto abanico: la dispersión aumenta a medida que crecen los valores ajustados. Es evidencia de heterocedasticidad, por lo que los errores estándar OLS pueden estar subestimados en la cola alta.

aug2 <- broom::augment(m2)

y_obs2 <- if (exists("df_model2")) df_model2$preciom else (aug2$.fitted + aug2$.resid)

df_dens2 <- dplyr::bind_rows(
  tibble::tibble(valor = y_obs2,        serie = "observado"),
  tibble::tibble(valor = aug2$.fitted,  serie = "ajustado")
)

ggplot(df_dens2, aes(x = valor, color = serie)) +
  geom_density(linewidth = 1) +
  labs(title = "Densidad: observado vs ajustado (Aptos)",
       x = "precio (millones)", y = "densidad") +
  theme_minimal()

Las distribuciones se superponen razonablemente en la zona central, lo que indica que el modelo está bien calibrado en el nivel medio del mercado. En los extremos aparecen desajustes: el modelo subrepresenta la masa de precios muy bajos y no reproduce completamente la cola alta (apartamentos de valor extraordinario). Este patrón es compatible con la heterocedasticidad y con no linealidades en el retorno del metraje a precios elevados.

10.3 Heterocedasticidad (Breusch-Pagan) y errores robustos

bp2 <- lmtest::bptest(m2)
bp2
## 
##  studentized Breusch-Pagan test
## 
## data:  m2
## BP = 886.56, df = 5, p-value < 2.2e-16
# Tabla con errores robustos (HC1) si hay heterocedasticidad
if (bp2$p.value < 0.05) {
  modelsummary(
    list("OLS (HC1 - Aptos)" = m2),
    estimate  = "{estimate}",
    statistic = c("SE(HC1) = {std.error}", "t = {statistic}", "p = {p.value}"),
    vcov      = sandwich::vcovHC(m2, type = "HC1"),
    gof_omit  = "AIC|BIC|Log.Lik",
    title     = "Tabla 2. Coeficientes con errores robustos (HC1)"
  )
}
Tabla 2. Coeficientes con errores robustos (HC1)
OLS (HC1 - Aptos)
(Intercept) -242.140
SE(HC1) = 26.374
t = -9.181
p = <0.001
areaconst 2.318
SE(HC1) = 0.174
t = 13.294
p = <0.001
estrato 51.881
SE(HC1) = 3.906
t = 13.283
p = <0.001
habitaciones -48.508
SE(HC1) = 7.806
t = -6.214
p = <0.001
parqueaderos 79.922
SE(HC1) = 11.385
t = 7.020
p = <0.001
banios 48.823
SE(HC1) = 5.538
t = 8.815
p = <0.001
Num.Obs. 3384
R2 0.797
R2 Adj. 0.796
RMSE 134.90
Std.Errors Custom

La prueba de Breusch–Pagan rechaza homocedasticidad (p < 0.05), por lo que la inferencia se reporta con HC1. Las SE aumentan, pero los signos y la significancia de los coeficientes se mantienen; R²/RMSE no cambian.

10.4 Colinealidad (VIF)

vifs2 <- car::vif(m2)
vifs2
##    areaconst      estrato habitaciones parqueaderos       banios 
##     2.802150     1.711215     1.425598     2.137138     2.964226

El panel de VIF ubica a todos los predictores por debajo de los umbrales habituales (≤ 5). No se evidencian problemas de multicolinealidad severa; las estimaciones son estables y las señales (signos) son coherentes con el negocio.

10.5 Influencia (Cook) y leverage

aug2 <- augment(m2)

aug2 %>%
  dplyr::mutate(id = dplyr::row_number()) %>%
  dplyr::arrange(dplyr::desc(.cooksd)) %>%
  dplyr::slice_head(n = 10)
ggplot(aug2, aes(.hat, (.std.resid)^2)) +
  geom_point(alpha = 0.6) +
  labs(title = "Leverage vs residuos^2 (Aptos)",
       x = "leverage (hat)", y = "residuos^2") +
  theme_minimal()

La nube leverage vs. residuos est. muestra pocos casos cercanos a los contornos; en conjunto, no se observan puntos con influencia extrema. Se recomienda monitorear estos casos al realizar predicciones puntuales de alto valor.

10.6 Indicadores de rendimiento

rmse2 <- sqrt(mean(test2$resid^2))
mae2  <- mean(abs(test2$resid))
mape2 <- mean(abs(test2$resid / test2$preciom)) * 100
r2_2  <- cor(test2$preciom, test2$pred)^2

c(RMSE = rmse2, MAE = mae2, MAPE_pct = mape2, R2_test = r2_2)
##        RMSE         MAE    MAPE_pct     R2_test 
## 151.3000955  86.9597163  24.2110867   0.7285892

El modelo presenta:

  • MAE ~ 86.9 M: error absoluto típico por inmueble. Es material para decisiones puntuales, así que conviene usar intervalos de predicción al recomendar casos individuales.

  • RMSE ≈ 151.3 M > MAE: indica algunos errores grandes (colas más pesadas), en línea con los diagnósticos de heterocedasticidad y desajustes en extremos.

  • MAPE ≈ 24.2%: en promedio, la predicción se desvía cerca de un cuarto del valor real. El error relativo tiende a aumentar en los extremos (muy baratos o muy costosos).

10.7 Recomendaciones

  • Errores robustos (HC1). Dado que el error crece con el nivel de precio, se puede reportar la inferencia con HC1 en las tablas de coeficientes para mantener la validez bajo heterocedasticidad.

  • Mayor precisión del ajuste. Se puede considerar transformar el precio (p. ej., log(preciom)) o aplicar ponderaciones (WLS) para reducir la influencia de los casos con mayor variabilidad.

  • No linealidad del metraje. La relación precio–área sugiere curvatura en niveles altos; se puede probar términos flexibles en areaconst (polinomio de bajo grado o splines) para capturar los rendimientos decrecientes.

  • Interacciones plausibles. Evaluar areaconst × estrato (el rendimiento de los m² puede variar por segmento) y mantener solo las que mejoren el ajuste sin inflar la colinealidad.

  • Variables relevantes del edificio (si existen). Incluir antigüedad, calidad de acabados y amenidades (ascensor, piscina, seguridad/portería, zonas comunes), que suelen explicar parte importante del precio en los apartamentos.

11 Paso. Predicción del precio con las caracteristicas de la vivienda 2

presupuesto2 <- 850  # millones

# Vivienda 2: Apartamento, 300 m2, 3 parqueaderos, 3 baños, 5 habitaciones
new_viv2 <- tibble::tibble(
  areaconst    = 300,
  estrato      = c(5, 6),  
  habitaciones = 5,
  parqueaderos = 3,
  banios       = 3
)


pred_v2 <- dplyr::bind_cols(
  new_viv2,
  as.data.frame(predict(m2, newdata = new_viv2, interval = "prediction", level = 0.95))
)


pred_v2_tabla <- pred_v2 %>%
  dplyr::mutate(
    escenario   = paste0("Estrato ", estrato),
    dentro_850M = fit <= presupuesto2
  ) %>%
  dplyr::select(
    escenario, areaconst, habitaciones, banios, parqueaderos,
    pred_millones = fit, lwr, upr, dentro_850M
  )

knitr::kable(
  pred_v2_tabla, digits = 1,
  caption = "Prediccion Vivienda 2 (escenarios de estrato) con IC95% [millones]"
)
Table 11.1: Prediccion Vivienda 2 (escenarios de estrato) con IC95% [millones]
escenario areaconst habitaciones banios parqueaderos pred_millones lwr upr dentro_850M
Estrato 5 300 5 3 3 856.4 590.7 1122.1 FALSE
Estrato 6 300 5 3 3 908.3 642.5 1174.0 FALSE

Se obtienen dos escenarios según el estrato:

Estrato 5: predicción de 856,4 con IC95% [590,7; 1122,1]. La estimación puntual queda dentro del presupuesto de 850 M, pero el límite superior lo supera; es decir, hay riesgo de encontrar comparables por encima del tope, aunque una parte relevante del rango cae por debajo de 850 M.

Estrato 6: predicción de 908.3 con IC95% [642,0; 1174,0]. La estimación puntual está por encima del presupuesto; aun así, el límite inferior sugiere que existen casos que podrían entrar, pero la probabilidad de exceder 850 M es alta.

Estos intervalos reflejan la variabilidad caso a caso y son coherentes con lo observado en diagnóstico (heterocedasticidad y colas). Por ello, trataremos las cifras puntuales como referencias, acompañadas de intervalos de predicción al recomendar unidades específicas.

Sensibilidad: Manteniendo lo demás constante, el modelo sugiere que:

+1 estrato ≈ +51,9 M,

+1 parqueadero ≈ +79.9 M,

+1 baño ≈ +48.8 M,

+10 m² ≈ +23.2 M,

+1 habitación ≈ –48.5 M (este valor es negativo porque, a igual metraje, más cuartos implican espacios más subdivididos y, en promedio, menor valoración).

12 Paso. Ofertas

# Se interpretan las especificaciones de la vivienda como un minimo, a excepcion del precio que es un tope maximo

ofertas2 <- vivienda %>%
  dplyr::filter(
    tipo == "Apartamento",
    zona == "Zona Sur",
    estrato %in% c(5, 6),
    areaconst    >= 300,
    parqueaderos >= 3,
    banios       >= 3,
    habitaciones >= 5,
    preciom      <= 850
  ) %>%
  dplyr::mutate(
    pred_m2     = as.numeric(predict(m2, newdata = dplyr::cur_data())),
    dentro_850M = preciom <= 850,
    dist_area   = abs(areaconst - 300)
  ) %>%
  dplyr::arrange(dplyr::desc(dentro_850M), dist_area, preciom) %>%
  dplyr::slice_head(n = 5)

# Tabla para el informe
ofertas2_tabla <- ofertas2 %>%
  dplyr::transmute(
    barrio, zona, estrato,
    areaconst, habitaciones, banios, parqueaderos,
    precio_m   = round(preciom, 1),
    pred_m2    = round(pred_m2, 1),
    gap_modelo = round(preciom - pred_m2, 1),  # +: sobre el modelo; -: bajo el modelo
    dentro_850M
  )

ofertas2_tabla
leaflet(ofertas2) %>%
  addTiles() %>%
  addCircleMarkers(
    lng = ~longitud, lat = ~latitud,
    color = "dodgerblue", radius = 7, stroke = FALSE, fillOpacity = 0.85,
    popup = ~paste0(
      "<b>", barrio, "</b> (Estrato ", estrato, ")<br>",
      "Area: ", areaconst, " m2 | Hab: ", habitaciones,
      " | Banios: ", banios, " | Parq: ", parqueaderos, "<br>",
      "<b>Precio:</b> ", round(preciom, 1), " M<br>",
      "<b>Pred. modelo:</b> ", round(pred_m2, 1), " M"
    )
  )

12.1 Analisis de las ofertas

Para valorar su atractivo relativo usamos el gap del modelo (precio observado – precio predicho): un gap negativo sugiere que la oferta está “barata” frente a lo que el mercado esperaría, según el modelo.

En el sector Seminario (estrato 5, 300 m², 6 hab, 5 baños, 3 parqueaderos; 670 M): Cumple exactamente el umbral de área (300 m²) y excede en dotación. El modelo predice 893,6 M, por lo que el gap es –223,6 M (≈ 25% por debajo del valor esperado). Queda bien dentro del presupuesto (850 M) y, por características, luce balanceado: metraje objetivo, tres parqueaderos y cinco baños. La diferencia negativa sugiere oportunidad; aun así, vale la pena confirmar estado del edificio/unidad (edad, mantenimiento, iluminación/ruido, vista) y costos de administración, para descartar que el “descuento” obedezca a factores no modelados.

La oferta del sector Guadalupe (estrato 5, 573 m², 5 hab, 8 baños, 3 parqueaderos; 730 M): Supera ampliamente el área mínima (573 m² es muy grande para la muestra) y ofrece una dotación inusual (ocho baños). El modelo estima 1.648,1 M, por lo que el gap es –918,1 M (≈ 56% por debajo del esperado): un “descuento” extraordinario. Esto puede ser una oportunidad excepcional si la unidad está en muy buen estado; en este caso, es imprescindible revisar conservación, accesibilidad (ascensor/es), cuota de administración, reglamento, servicios/zonas comunes, orientación y cualquier restricción legal que pueda explicar este bajo precio.

Ambas opciones entran en el presupuesto y muestran gaps muy negativos (precio “bajo” vs. modelo). Si se busca una alternativa más equilibrada y de menor riesgo, Seminario parece el primer candidato (ajuste exacto al requerimiento, buena dotación y gap moderado). Guadalupe podría representar una gran relación m²/precio, pero exige verificación técnica y financiera exhaustiva para confirmar que no hay factores que expliquen el precio (o que la predicción haya sobreestimado por el tamaño extremo). Recomendamos agendar visita, solicitar historial de administración y avalúo bancario comparativo; eso permitirá validar si el “descuento” observado es real y sostenible.

13 RESUMEN EJECUTIVO

Contexto. La empresa solicita ubicar dos viviendas en Cali con especificaciones mínimas y topes de crédito: Vivienda 1 (Casa, Zona Norte, tope 350 M) y Vivienda 2 (Apartamento, Zona Sur, tope 850 M). Enfoque. Se analizó la base reciente de mercado y se estimaron modelos de regresión múltiple para predecir precio a partir de área construida, estrato, baños, habitaciones y parqueaderos. Se usaron diagnósticos y errores robustos por heterocedasticidad.

13.1 Escenario 1 — Casa (Zona Norte, 200 m², 1 parq., 2 baños, 4 hab., estrato 4–5, tope 350 M)

Precio estimado

  • Estrato 4: 250,2 M (IC95% aprox. [0; 594,6]).

  • Estrato 5: 345,8 M (IC95% [1,3; 690,2]).

→ En estrato 4 el caso entra cómodamente; en estrato 5 queda en el borde del tope.

Ofertas sugeridas (5): todas por debajo del precio que “espera” el modelo (descuentos de ~24% a ~42%).

  • Destacan La Flora y El Bosque (3 parq.) por valor relativo y dotación; La Merced es la mejor alineada si se prefiere estrato 4; Vipasa ofrece buen equilibrio precio–m²–amenidades.

  • Recomendación: avanzar a visita y verificación de costos de administración/estado físico.

Calidad del modelo (casas): R² ≈ 0,71; RMSE ≈ 183 M. Usar intervalos de predicción al hacer recomendaciones puntuales.

13.2 Escenario 2 — Apartamento (Zona Sur, 300 m², 3 parq., 3 baños, 5 hab., estrato 5–6, tope 850 M)

Precio estimado

  • Estrato 5: 856,4 M (IC95% [590,7; 1122,1]) → dentro del tope, con riesgo de excederlo en algunos comparables.

  • Estrato 6: 882,9 M (IC95% [642,5; 1174,0]) → sobre el tope en la estimación puntual.

Ofertas sugeridas (2): ambas dentro del tope y con descuento frente al modelo.

  • Seminario (300 m², 670 M): gap –223,6 M (~–25%). Candidato prioritario por ajuste exacto al metraje y buena dotación.

  • Guadalupe (573 m², 730 M): gap –918,1 M (~–56%). Tamaño extremo; exige debida diligencia (edad, mantenimiento, amenities, cuota de administración) para confirmar oportunidad y descartar sobreestimación del modelo en la cola alta.

Calidad del modelo (apartamentos): R² ≈ 0,73; RMSE ≈ 151 M. Predicciones confiables para priorizar opciones; acompañar con intervalos en la cola alta.

13.3 Consideraciones transversales

  • Zona se usa como descriptor de mercado (no frontera geográfica estricta): se observaron inconsistencias entre etiqueta de zona y coordenadas.

  • Diagnóstico: no linealidad y heterocedasticidad (error crece con el precio).

13.4 Recomendaciones finales

  • Vivienda 1 (Casa Norte): proceder con visitas a las 3–5 alternativas identificadas, priorizando La Flora/Vipasa por valor relativo y La Merced si se prefiere estrato 4.

  • Vivienda 2 (Apto Sur): priorizar Seminario; evaluar Guadalupe con revisión técnica y financiera exhaustiva.

  • Acompañar cada propuesta con intervalos de predicción y una lista de chequeo (estado físico, antigüedad, administración, reglamento, ruido/iluminación, orientación, documentos).

  • En negociación, considerar márgenes derivados de los gaps frente al modelo (mayor gap negativo, mayor espacio potencial para cerrar).