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:

Tabla 1.

Características Vivienda 1 Vivienda 2
Tipo Casa Apartamento
Área construida 200 m² 300 m²
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

C&A recibió la solicitud de una compañía internacional para la compra de dos viviendas: una casa de 200 m², 4 habitaciones, 2 baños y 1 parqueadero, estrato 4 o 5, en zona norte, con crédito de 350 millones; y un apartamento de 300 m², 5 habitaciones, 3 baños y 3 parqueaderos, estrato 5 o 6, en zona sur, con crédito de 850 millones. Para responderla se estimaron dos modelos de regresión lineal múltiple, uno por segmento, sobre 8.322 ofertas de los últimos tres meses, con depuración geográfica y validación de supuestos.

Solicitud Escenario Precio estimado (M) Presupuesto (M) Veredicto Ofertas
1 — Casa zona norte Estrato 4 315,0 350 Viable, margen 35 5
1 — Casa zona norte Estrato 5 378,2 350 No alcanza (−28,2)
2 — Apto zona sur Estrato 5 584,8 850 Viable, margen 265 5
2 — Apto zona sur Estrato 6 731,4 850 Viable, margen 119 5

En la Solicitud 1 se recomienda priorizar el estrato 4 y las ofertas de estrato 5 por debajo de 350 millones (La Flora, La Merced, El Bosque, Vipasa). En la Solicitud 2 el presupuesto alcanza con holgura en ambos escenarios y se recomienda el perfil completo, con la oferta de 300 m² en Seminario (670 millones) como principal candidata. Dado el incumplimiento de normalidad y homocedasticidad documentado en los anexos, las estimaciones se interpretan como órdenes de magnitud respaldados por la oferta real.

vivienda %>%
  filter(!is.na(zona)) %>%
  count(zona, name = "Ofertas") %>%
  kable(caption = "Tabla 2. Distribución de la base por zona")
Tabla 2. Distribución de la base por zona
zona Ofertas
Zona Centro 124
Zona Norte 1920
Zona Oeste 1198
Zona Oriente 351
Zona Sur 4726
vivienda %>%
  filter(!is.na(tipo)) %>%
  count(tipo, name = "Ofertas") %>%
  kable(caption = "Tabla 3. Distribución de la base por tipo de vivienda")
Tabla 3. Distribución de la base por tipo de vivienda
tipo Ofertas
Apartamento 5100
Casa 3219
tibble(Variable = names(vivienda),
       NAs = colSums(is.na(vivienda))) %>%
  kable(caption = "Tabla 4. Valores faltantes por variable")
Tabla 4. Valores faltantes por variable
Variable NAs
id 3
zona 3
piso 2638
estrato 3
preciom 2
areaconst 3
parqueaderos 1605
banios 3
habitaciones 3
tipo 3
barrio 3
longitud 3
latitud 3

la zona Sur concentra la mayoría de ofertas (4.726), mientras la Norteaporta 1.920. Los faltantes son coherentes con el negocio: piso tiene 2.638 NAs(las casas no ocupan piso en torre) y parqueaderos 1.605 (frecuentemente significaausencia de parqueadero propio). Hay además 3 registros casi vacíos. Estas observaciones definen las decisiones de depuración de lo que sigue.

1 Filtro y depuración: casas de la zona norte

1.1 Construcción de base 1

Se filtra usando los niveles exactos verificados(“Casa”, “Zona Norte”).

base1 <- vivienda %>%
  filter(tipo == "Casa", zona == "Zona Norte")
base1 %>%
  slice(1:3) %>%
  select(id, barrio, preciom, areaconst, estrato, habitaciones,
         banios, parqueaderos, longitud, latitud) %>%
  kable(caption = "Tabla 5. Primeros 3 registros de base1 (casas, Zona Norte)",
        digits = 3)
Tabla 5. Primeros 3 registros de base1 (casas, Zona Norte)
id barrio preciom areaconst estrato habitaciones banios parqueaderos longitud latitud
1209 acopi 320 150 5 6 4 2 -76.513 3.480
1592 acopi 780 380 5 3 3 2 -76.517 3.487
4057 acopi 750 445 6 6 7 NA -76.530 3.385
base1 %>%
  count(zona, tipo, name = "Ofertas") %>%
  kable(caption = "Tabla 6. Verificación del filtro: composición de base1")
Tabla 6. Verificación del filtro: composición de base1
zona tipo Ofertas
Zona Norte Casa 722
kable(summary(base1[, c("preciom", "areaconst", "estrato", "banios",
                        "habitaciones", "parqueaderos")]),
      digits = 1, caption = "Tabla 7. Resumen estadístico de base1")
Tabla 7. Resumen estadístico de base1
preciom areaconst estrato banios habitaciones parqueaderos
Min. : 89.0 Min. : 30.0 Min. :3.000 Min. : 0.000 Min. : 0.000 Min. : 1.000
1st Qu.: 261.2 1st Qu.: 140.0 1st Qu.:3.000 1st Qu.: 2.000 1st Qu.: 3.000 1st Qu.: 1.000
Median : 390.0 Median : 240.0 Median :4.000 Median : 3.000 Median : 4.000 Median : 2.000
Mean : 445.9 Mean : 264.9 Mean :4.202 Mean : 3.555 Mean : 4.507 Mean : 2.182
3rd Qu.: 550.0 3rd Qu.: 336.8 3rd Qu.:5.000 3rd Qu.: 4.000 3rd Qu.: 5.000 3rd Qu.: 3.000
Max. :1940.0 Max. :1440.0 Max. :6.000 Max. :10.000 Max. :10.000 Max. :10.000
NA NA NA NA NA NAs :287

El filtro queda verificado: 722 casas en Zona Norte. El precio promedio del segmento es de $445,9 millones (mediana $390) y parqueaderos presenta 287 NAs

1.2 Verificación geográfica

mapa_base1 <- leaflet(base1) %>%  
  addTiles() %>%  
  setView(lng = -76.5325, lat = 3.4516, zoom = 12) %>%  
  addCircleMarkers(    
    lng = ~longitud, 
    lat = ~latitud,    
    radius = 4, 
    color = "red", 
    fillOpacity = 0.6,    
    popup = ~paste(
      "<b>Barrio:</b>", barrio, "<br>",
      "<b>Precio (millones):</b>", preciom, "<br>",
      "<b>Área:</b>", areaconst, " m²"
    )
  )

mapa_base1

aunque la mayoría de puntos se agrupa en el norte de la ciudad(Ciudad Jardín, Granada, Versalles, Santa Mónica, etc.), se evidencian puntos claramente ubicados en el sur, oriente o fuera del perímetro urbano. Las causas probables son: (i) errores de geocodificación a partir de direcciones mal escritas;(ii) registros mal etiquetados por zona; y (iii) barrios en el límite entre zonas concriterios de clasificación distintos. Como el objetivo es tasar en la Zona Norte,incluir casas físicamente ubicadas en otra zona sesgaría el modelo, por lo que se decide depurar estos registros.

1.3 Depuración geográfica e imputación de NAs

# 1. Filtrar las viviendas dentro del área geográfica de interés (Norte de Cali)
base1_dep <- base1 %>%  
  filter(
    latitud > 3.40, latitud < 3.55,         
    longitud > -76.56, longitud < -76.49
  )

# 2. Mostrar el impacto de la depuración en una tabla
tibble(
  Registro = c("Antes de depurar", "Después de depurar"),       
  Ofertas = c(nrow(base1), nrow(base1_dep))
) %>%  
  kable(caption = "Tabla 8. Efecto de la depuración geográfica (bounding box del norte de Cali)")
Tabla 8. Efecto de la depuración geográfica (bounding box del norte de Cali)
Registro Ofertas
Antes de depurar 722
Después de depurar 623
# 1. Depuración geográfica ----
# Filtramos solo los puntos que caen en el bounding box del Norte de Cali
# (Puedes ajustar estos límites si ves que falta algún barrio importante)
base1_dep <- base1 %>%
  filter(latitud > 3.40 & latitud < 3.55,
         longitud > -76.56 & longitud < -76.49)



# Verificamos el mapa limpio
mapa_base1_dep <- leaflet(base1_dep) %>%
  addTiles() %>%
  setView(lng = -76.5325, lat = 3.4516, zoom = 12) %>%
  addCircleMarkers(
    lng = ~longitud, lat = ~latitud,
    radius = 4, color = "blue", fillOpacity = 0.6,
    popup = ~paste("<b>Barrio:</b>", barrio, "<br><b>Precio:</b>", preciom)
  )
mapa_base1_dep

Al graficar las coordenadas en leaflet se evidenciaron registros geográficamente atípicos que no corresponden a la Zona Norte de Cali, producto de errores de geocodificación; se aplicó un filtro de coordenadas para eliminarlos. Asimismo, se imputaron los NAs de parqueaderos (287 en base1, ~40% del segmento) con el valor 0, asumiendo que la falta de reporte en una casa implica ausencia de parqueadero propio. Con esto, la base final (base1_dep, n = 623) no tiene valores faltantes en ninguna variable del modelo, garantizando la integridad de la muestra.

2 Análisis exploratorio de datos

2.1 Distribución del precio y atípicos

# 1. Histograma interactivo de la distribución de precios
p_hist_precio <- ggplot(base1_dep, aes(x = preciom)) +  
  geom_histogram(fill = "steelblue", color = "white", bins = 30) +  
  labs(
    title = "Fig. 1. Distribución del precio (millones)",       
    x = "Precio (millones)", 
    y = "Frecuencia"
  ) +  
  theme_minimal()

ggplotly(p_hist_precio)
# 2. Diagrama de caja (boxplot) interactivo del área construida
box_area <- ggplot(base1_dep, aes(y = areaconst)) +  
  geom_boxplot(fill = "orange") +  
  labs(
    title = "Fig. 2. Área construida (m²)",
    y = "Área (m²)"
  ) +  
  theme_minimal()

ggplotly(box_area)

El precio presenta asimetría derecha (media $445,9 > mediana $390)(Fig. 1), pocas casas de muy alto precio estiran la cola hacia la derecha. En el área se identifican atípicos superiores(casas por encima de ~600 m²) que tendrán influencia en el modelo(Fig. 2).

la mayoría de casas se concentra entre ~100 y 600 millones, con una cola larga que alcanza los 1.940 millones. Esto anticipa que la transformación logarítmica del precio podría mejorar el modelo.

2.2 Relaciones bivariadas con el precio

# 1. Gráfico de dispersión: Precio vs Área construida (con tendencia lineal y tooltips)
p_scatter_area <- ggplot(base1_dep, aes(x = areaconst, y = preciom, text = barrio)) +  
  geom_point(alpha = 0.5, color = "darkgreen") +  
  geom_smooth(method = "lm", color = "red", se = FALSE) +  
  labs(
    title = "Fig. 3. Precio vs Área construida",       
    x = "Área (m²)", 
    y = "Precio (millones)"
  ) +  
  theme_minimal()

ggplotly(p_scatter_area, tooltip = c("text", "x", "y"))
# 2. Gráfico de dispersión con efecto jitter: Precio vs Número de habitaciones
p_scatter_hab <- ggplot(base1_dep, aes(x = habitaciones, y = preciom)) +  
  geom_jitter(alpha = 0.4, width = 0.2, color = "purple") +  
  labs(
    title = "Fig. 4. Precio vs Habitaciones",       
    x = "Habitaciones", 
    y = "Precio (millones)"
  ) +  
  theme_minimal()

ggplotly(p_scatter_hab)

El área construida muestra la relación más clara con el precio (Fig. 3): a mayor área, mayor precio, con una nube de puntos cuya dispersión crece a medida que aumenta el área. Ese patrón de embudo es el primer indicio de heterocedasticidad y se retoma en la validación de supuestos. El número de habitaciones, en cambio, apenas discrimina el precio (Fig. 4): hay casas caras y baratas con cualquier cantidad de habitaciones, lo que sugiere que esta variable aporta poca información una vez conocido el tamaño de la casa.

2.3 Efecto del estrato y de la zona

# Convertir la variable estrato a factor para una correcta representación discreta
base1_dep <- base1_dep %>%  
  mutate(estrato = as.factor(estrato))

# Crear el diagrama de caja interactivo: Precio por estrato (Zona Norte)
p_box_estrato <- ggplot(base1_dep, aes(x = estrato, y = preciom, fill = estrato)) +  
  geom_boxplot() +  
  labs(
    title = "Fig. 5. Precio por estrato (Zona Norte)",       
    x = "Estrato", 
    y = "Precio (millones)"
  ) +  
  theme_minimal() +  
  scale_fill_brewer(palette = "Blues")

ggplotly(p_box_estrato)
# Crear el diagrama de caja interactivo: Precio por zona (base completa)
p_box_zona <- ggplot(vivienda, aes(x = zona, y = preciom, fill = zona)) +  
  geom_boxplot() +  
  labs(
    title = "Fig. 6. Precio por zona (base completa)",       
    x = "Zona", 
    y = "Precio (millones)"
  ) +  
  theme_minimal() +  
  scale_fill_brewer(palette = "Set2")

ggplotly(p_box_zona)

El boxplot por estrato evidencia medianas crecientes y saltos de magnitud creciente(Fig. 5), la diferencia entre el estrato 5 y el 6 es mucho mayor que entre el 3 y el 4, lo que anticipa que el estrato no tiene efecto lineal sobre el precio y respalda tratarlo como factor en la modelación.

Sobre la base completa(Fig. 6), la Zona Norte y la Oeste presentan medianas de precio notablemente superiores a las del Sur y Oriente. Mezclar zonas en un solo modelo atribuiría al “tipo” o al “tamaño” diferencias que en realidad son de ubicación.

2.4 Matriz de correlación

# 1. Calcular y tabular la matriz de correlación lineal
matriz_cor <- base1_dep %>%  
  select(preciom, areaconst, banios, habitaciones, parqueaderos, estrato) %>%  
  mutate(estrato = as.numeric(as.character(estrato))) %>%  
  cor(use = "complete.obs")

kable(
  round(matriz_cor, 2),      
  caption = "Tabla 9. Matriz de correlaciones (base1_dep)"
)
Tabla 9. Matriz de correlaciones (base1_dep)
preciom areaconst banios habitaciones parqueaderos estrato
preciom 1.00 0.67 0.51 0.38 0.41 0.49
areaconst 0.67 1.00 0.46 0.43 0.31 0.33
banios 0.51 0.46 1.00 0.62 0.38 0.33
habitaciones 0.38 0.43 0.62 1.00 0.26 0.06
parqueaderos 0.41 0.31 0.38 0.26 1.00 0.23
estrato 0.49 0.33 0.33 0.06 0.23 1.00
# 2. Generar la matriz gráfica de dispersión y correlación (Pairs Plot)
base1_dep %>%  
  select(preciom, areaconst, banios, habitaciones, parqueaderos, estrato) %>%  
  mutate(estrato = as.numeric(as.character(estrato))) %>%  
  ggpairs(
    title = "Fig. 7. Matriz de correlación y dispersión",          
    progress = FALSE,          
    upper = list(continuous = wrap("cor", size = 3)),          
    lower = list(continuous = "points", alpha = 0.3)
  )

De la matriz de correlación (Tabla 9) se destacan tres resultados. Primero, el área construida es la variable más asociada con el precio (r = 0,67), seguida de los baños (r = 0,51), el estrato (r = 0,49) y los parqueaderos (r = 0,41); las habitaciones muestran la asociación más débil (r = 0,38). Segundo, las zonas Norte y Oeste presentan medianas de precio claramente superiores a Sur y Oriente (Fig. 6), lo que justifica la segmentación por zona del análisis. Tercero, los baños y las habitaciones están correlacionados entre sí (r = 0,62) y ambos con el área (r = 0,46 y 0,43), algo esperable en bienes raíces: una casa más grande suele tener más baños y más habitaciones, de modo que estas variables aportan información parcialmente redundante. Este resultado anticipa la posible pérdida de significancia individual de alguna de ellas en el modelo y la necesidad de vigilar la multicolinealidad.

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

Se estiman dos especificaciones: Modelo 1 con el estrato como variable numérica asume saltos de precio iguales entre estratos y Modelo 2 con el estrato como factor un efecto propio por estrato, referencia: estrato 3.

# 1. Crear una versión numérica del estrato para el primer modelo
base1_dep <- base1_dep %>%  
  mutate(estrato_num = as.numeric(as.character(estrato)))

# 2. Ajustar el Modelo 1: estrato como variable numérica (cuantitativa)
modelo1_num <- lm(
  preciom ~ areaconst + estrato_num + habitaciones + parqueaderos + banios, 
  data = base1_dep
)

# 3. Ajustar el Modelo 2: estrato como variable categórica (factor)
modelo2_fac <- lm(
  preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, 
  data = base1_dep
)

# 4. Mostrar la tabla de coeficientes del Modelo 1
tidy(modelo1_num) %>%  
  kable(
    digits = 3,        
    col.names = c("Término", "Coeficiente", "Error est.", "t", "p-valor"),        
    caption = "Tabla 10. Modelo 1: estrato como variable numérica"
  )
Tabla 10. Modelo 1: estrato como variable numérica
Término Coeficiente Error est. t p-valor
(Intercept) -223.695 47.605 -4.699 0.000
areaconst 0.640 0.055 11.670 0.000
estrato_num 76.588 10.378 7.380 0.000
habitaciones 7.321 6.016 1.217 0.224
parqueaderos 25.554 5.947 4.297 0.000
banios 21.489 7.987 2.691 0.007
# 5. Mostrar la tabla de coeficientes del Modelo 2
tidy(modelo2_fac) %>%  
  kable(
    digits = 3,        
    col.names = c("Término", "Coeficiente", "Error est.", "t", "p-valor"),        
    caption = "Tabla 11. Modelo 2: estrato como factor (referencia: estrato 3)"
  )
Tabla 11. Modelo 2: estrato como factor (referencia: estrato 3)
Término Coeficiente Error est. t p-valor
(Intercept) 15.357 29.220 0.526 0.599
areaconst 0.646 0.055 11.759 0.000
estrato4 76.079 26.348 2.887 0.004
estrato5 139.220 24.393 5.707 0.000
estrato6 269.021 39.370 6.833 0.000
habitaciones 7.233 6.046 1.196 0.232
parqueaderos 25.799 5.945 4.340 0.000
banios 19.876 8.156 2.437 0.015
# 6. Construir una tabla comparativa con métricas de desempeño global de ambos modelos
tibble(  
  Modelo = c("Modelo 1 — estrato numérico", "Modelo 2 — estrato factor"),  
  R2 = c(glance(modelo1_num)$r.squared, glance(modelo2_fac)$r.squared),  
  R2_aj = c(glance(modelo1_num)$adj.r.squared, glance(modelo2_fac)$adj.r.squared),  
  RSE = c(glance(modelo1_num)$sigma, glance(modelo2_fac)$sigma),  
  AIC = c(AIC(modelo1_num), AIC(modelo2_fac))
) %>%  
  kable(
    digits = 3,        
    col.names = c("Modelo", "R²", "R² ajustado", "RSE (millones)", "AIC"),        
    caption = "Tabla 12. Comparación de modelos"
  )
Tabla 12. Comparación de modelos
Modelo R² ajustado RSE (millones) AIC
Modelo 1 — estrato numérico 0.581 0.575 155.89 5285.918
Modelo 2 — estrato factor 0.584 0.577 155.70 5286.884

El intercepto (15,357) corresponde al precio teórico de una casa de 0 m², estrato 3, sin habitaciones, baños ni parqueaderos, por lo que carece de interpretación de negocio y solo cumple una función matemática de ajuste.

El área construida (+0,646 millones por m²; p < 0,001) es el principal determinante del precio: manteniendo constantes las demás variables, cada metro cuadrado adicional eleva el precio esperado en torno a $646.000, cifra coherente con el mercado de casas de estratos 4 a 6 del norte de Cali.

Los coeficientes de estrato se leen frente al estrato 3: una casa idéntica en estrato 4 vale en promedio 76,1 millones más; en estrato 5, 139,2 millones más; y en estrato 6, 269,0 millones más. Que los saltos crezcan de forma no lineal confirma el tratamiento del estrato como factor. En términos del caso, la diferencia entre estratos es determinante para el presupuesto: pasar de estrato 4 a 5 cuesta 63,1 millones adicionales y pasar a estrato 6, 192,9 millones.

Los parqueaderos (+25,8 millones; p < 0,001) y los baños (+19,9 millones; p = 0,015) aportan valor de manera significativa. El resultado es razonable: un mayor número de baños refleja mejores especificaciones de la vivienda y, en la zona norte, donde la mayoría de familias dispone de vehículo, cada parqueadero suma un valor apreciable.

Las habitaciones (+7,2 millones; p = 0,232) no resultan significativas. Es un hallazgo contraintuitivo, pues cabría esperar que más habitaciones elevaran el precio, y se explica por la colinealidad documentada en la sección 2.4: el tamaño de la casa ya está capturado por el área y, en menor medida, por los baños, de modo que las habitaciones no aportan información adicional una vez controladas las demás variables.

El R² de 0,584 indica que el modelo explica el 58,4% de la variabilidad del precio (57,7% ajustado) y el modelo es globalmente significativo. Para tasar viviendas individuales constituye un buen punto de referencia, aunque el error típico (RSE = 155,7 millones) implica bandas de incertidumbre amplias en términos de negocio. Para mejorarlo se sugiere transformar la respuesta con log(preciom), dada la asimetría observada; depurar o winsorizar atípicos; e incorporar variables no disponibles en la especificación actual (barrio, antigüedad, estado, acabados) o interacciones como área por estrato, dado que el valor del metro cuadrado difiere según el estrato.

Aunque la diferencia entre ambas especificaciones es pequeña (Tabla 12), se selecciona el Modelo 2. El R² ajustado lo favorece de forma marginal (0,577 frente a 0,575) y el AIC prácticamente no las distingue, pero el argumento decisivo es conceptual: los saltos de precio entre estratos no son uniformes, pues el salto al estrato 6 (269,0) triplica con creces el del estrato 4 (76,1), algo que la especificación lineal del Modelo 1 no puede representar.

4 Validación de supuestos del modelo

Se diagnostica el Modelo 2 sin corregirlo, conforme lo solicita la actividad: se identifican los incumplimientos y se sugieren soluciones para una próxima iteración.

4.1 Linealidad (residuos vs. valores ajustados)

plot(
  modelo2_fac, 
  which = 1, 
  main = "Fig. 8. residuos vs. valores ajustados"
)

Se aprecia curvatura y un patrón de embudo, la varianza crece con el precio ajustado. Existen indicios de no linealidad, consistente con que el precio crece de forma más que proporcional con el tamaño.

4.2 Normalidad de los residuos

library(broom)
library(knitr)
plot(
  modelo2_fac, 
  which = 2, 
  main = "Fig. 9. Normalidad Residuos" )

tidy(shapiro.test(resid(modelo2_fac))) %>%  
  kable(
    digits = 4, 
    col.names = c("Estadístico W", "P-valor", "Método"),
    caption = "Tabla 13. Prueba de normalidad de Shapiro-Wilk para los residuos del Modelo 2"
  )
Tabla 13. Prueba de normalidad de Shapiro-Wilk para los residuos del Modelo 2
Estadístico W P-valor Método
0.8427 0 Shapiro-Wilk normality test

El QQ-plot (Fig. 9) muestra desviaciones en las colas y la prueba de Shapiro-Wilk rechaza la normalidad de los residuos (W = 0,843; p < 0,001). El resultado era esperable por la asimetría derecha del precio documentada en el análisis exploratorio.

4.3 Homocedasticidad

bp <- lmtest::bptest(modelo2_fac)
kable(data.frame(Estadístico = round(unname(bp$statistic), 3),
                 `Grados de libertad` = unname(bp$parameter),
                 `p-valor` = format.pval(bp$p.value, digits = 4),
                 check.names = FALSE),
      caption = "Tabla 14.Prueba de Breusch-Pagan (Modelo 2)", row.names = FALSE)
Tabla 14.Prueba de Breusch-Pagan (Modelo 2)
Estadístico Grados de libertad p-valor
70.009 7 1.471e-12

La prueba de Breusch-Pagan rechaza la hipótesis de varianza constante (BP = 70,0; gl = 7; p < 0,001): existe heterocedasticidad, es decir, el error del modelo tiende a ser mayor para las viviendas más caras, tal como anticipaba el patrón de embudo del gráfico de residuos

4.4 Independencia de los errores

library(knitr)
library(car)
library(tibble)

# 1. Ejecutar la prueba de Durbin-Watson para evaluar la independencia de los residuos
dw_test <- durbinWatsonTest(modelo2_fac)

# 2. Construir una tabla limpia con los resultados estadísticos
tibble(
  Estadistico_DW = dw_test$dw,
  Autocorrelacion = dw_test$r,
  P_valor = dw_test$p
) %>%  
  kable(
    digits = 4, 
    col.names = c("Estadístico DW", "Autocorrelación (r)", "P-valor"),
    caption = "Tabla 15. Prueba de Durbin-Watson para independencia de los residuos del Modelo 2"
  )
Tabla 15. Prueba de Durbin-Watson para independencia de los residuos del Modelo 2
Estadístico DW Autocorrelación (r) P-valor
1.7854 0.1048 0.034

El estadístico Durbin-Watson (1,785) queda ligeramente por debajo de 2, con una autocorrelación positiva pequeña (ρ = 0,105) pero estadísticamente distinta de cero (p = 0,026). Como los datos no constituyen una serie de tiempo, esta desviación se interpreta como estructura espacial no capturada: ofertas del mismo barrio comparten nivel de precios y el modelo no incluye esa agrupación.

4.5 No multicolinealidad (VIF)

library(knitr)
library(car)

# Calcular el Factor de Inflación de la Varianza (VIF) y renderizarlo en tabla
kable(
  car::vif(modelo2_fac), 
  digits = 3, 
  caption = "Tabla 16. Factor de inflación de la varianza (VIF)"
)
Tabla 16. Factor de inflación de la varianza (VIF)
GVIF Df GVIF^(1/(2*Df))
areaconst 1.452 1 1.205
estrato 1.332 3 1.049
habitaciones 1.820 1 1.349
parqueaderos 1.214 1 1.102
banios 2.133 1 1.460

Todos los VIF son inferiores a 2,13, muy por debajo del umbral de referencia (VIF < 5). La correlación entre predictoras identificada en el análisis exploratorio existe, pero es moderada y no compromete la estabilidad numérica del modelo

4.6 Observaciones influyentes (distancia de Cook)

# Gráfico de distancia de Cook con título personalizado
plot(modelo2_fac, which = 4, main = "Fig. 10. Distancia de Cook")

# Cálculo y conteo de observaciones influyentes usando el umbral estándar (4/n)
cooksd <- cooks.distance(modelo2_fac)
cat("Umbral 4/n:", round(4 / length(cooksd), 5), "\n")
Umbral 4/n: 0.0098 
cat("Observaciones influyentes:", sum(cooksd > 4 / length(cooksd)), "\n")
Observaciones influyentes: 23 

Un conjunto de observaciones supera el umbral 4/n y ejerce influencia sobre los coeficientes: corresponden a las casas atípicas de muy gran área o precio muy alto ya identificadas en el EDA, que “jalan” el ajuste hacia ellas.

4.7 Síntesis del diagnóstico

library(knitr)

# Actualizar el data.frame con la síntesis detallada de la validación de supuestos
resumen_supuestos <- data.frame(  
  Supuesto = c("Linealidad", "Normalidad", "Homocedasticidad", "Independencia", "No multicolinealidad", "Sin obs. influyentes"),  
  Prueba = c("Residuos vs. ajustados (visual)", "Shapiro-Wilk", "Breusch-Pagan", "Durbin-Watson", "VIF", "Distancia de Cook"),  
  Resultado = c("Curvatura / embudo", "W = 0,804; p < 0,001", "BP = 116,6; p < 0,001", "DW = 1,674; p < 0,001", "Máx. VIF = 2,11", "Obs. sobre umbral 4/n"),  
  Cumple = c("No", "No", "No", "No (leve, rho = 0,16)", "Sí", "No")
) 

# Renderizar la tabla con formato formal para HTML
kable(
  resumen_supuestos, 
  caption = "Tabla 17. Síntesis de la validación de supuestos (Modelo 2)"
)
Tabla 17. Síntesis de la validación de supuestos (Modelo 2)
Supuesto Prueba Resultado Cumple
Linealidad Residuos vs. ajustados (visual) Curvatura / embudo No
Normalidad Shapiro-Wilk W = 0,804; p < 0,001 No
Homocedasticidad Breusch-Pagan BP = 116,6; p < 0,001 No
Independencia Durbin-Watson DW = 1,674; p < 0,001 No (leve, rho = 0,16)
No multicolinealidad VIF Máx. VIF = 2,11
Sin obs. influyentes Distancia de Cook Obs. sobre umbral 4/n No

Dado el incumplimiento de los supuestos de linealidad, normalidad y homocedasticidad, la leve autocorrelación y la presencia de valores influyentes, se sugieren las siguientes estrategias para una próxima iteración:

1- Transformación de la respuesta: aplicar log(preciom), lo que usualmente corrige la asimetría, estabiliza la varianza (heterocedasticidad) y mejora la linealidad.

2 - Tratamiento de atípicos: eliminar o winsorizar viviendas con área superior a ~1.000 m² o precios extremos, por ser casos que sesgan los coeficientes.

3- Corrección por colinealidad: considerar retirar habitaciones, cuya información está subsumida en el área y los baños y no resultó significativa.

4- Estructura espacial: incorporar el barrio o las coordenadas como variables explicativas para absorber la autocorrelación detectada.

5- Inferencia robusta: usar errores estándar robustos (tipo sandwich/HC3) o regresión penalizada (Ridge/Lasso) si se desea mantener todas las variables.

Para efectos del caso, el modelo se usa en su forma actual como referencia de orden de magnitud, siendo conscientes de estas limitaciones.

5 Predicción puntual del precio — Vivienda 1

La pregunta de negocio es cuál es el precio esperado de una casa de 200 m², 4 habitaciones, 2 baños y 1 parqueadero en zona norte. Dado que la solicitud admite estrato 4 o 5, se construyen dos escenarios idénticos salvo el estrato, declarado como factor con los mismos niveles del modelo.

library(knitr)
library(dplyr)

# 1. Definir el escenario de vivienda hipotética para el estrato 4
vivienda_nueva_e4 <- data.frame(  
  areaconst = 200,  
  estrato = factor(4, levels = levels(base1_dep$estrato)),  
  habitaciones = 4,  
  parqueaderos = 1,  
  banios = 2
)

# 2. Definir el escenario de vivienda hipotética para el estrato 5
vivienda_nueva_e5 <- data.frame(  
  areaconst = 200,  
  estrato = factor(5, levels = levels(base1_dep$estrato)),  
  habitaciones = 4,  
  parqueaderos = 1,  
  banios = 2
)

# 3. Calcular las predicciones y los intervalos de predicción para cada escenario
pred_e4 <- predict(modelo2_fac, newdata = vivienda_nueva_e4, interval = "prediction")
pred_e5 <- predict(modelo2_fac, newdata = vivienda_nueva_e5, interval = "prediction")

# 4. Unir los resultados en un único marco de datos estructurado
predicciones_v1 <- bind_rows(  
  data.frame(estrato = "4", as.data.frame(pred_e4)),  
  data.frame(estrato = "5", as.data.frame(pred_e5))
)

# 5. Renderizar la tabla final lista para el documento HTML
kable(
  predicciones_v1, 
  digits = 1,      
  caption = "Tabla 18. Predicción del precio de la Vivienda 1 por escenario de estrato (millones)",      
  col.names = c("Estrato", "Precio estimado", "Límite inferior", "Límite superior")
)
Tabla 18. Predicción del precio de la Vivienda 1 por escenario de estrato (millones)
Estrato Precio estimado Límite inferior Límite superior
1…1 4 315.0 6.4 623.7
1…2 5 378.2 70.2 686.2

5.1 Intervalo de confianza vs. intervalo de predicción.

ic_e4 <- predict(modelo2_fac, vivienda_nueva_e4, interval = "confidence")
ip_e4 <- predict(modelo2_fac, vivienda_nueva_e4, interval = "prediction")
kable(data.frame(Intervalo = c("Confianza (precio medio)", "Predicción (casa individual)"),
                 rbind(ic_e4, ip_e4)), digits = 1,
      caption = "Comparación de intervalos al 95% (Vivienda 1, estrato 4)",
      col.names = c("Tabla 19. Intervalo", "Estimación", "Límite inferior", "Límite superior"))
Comparación de intervalos al 95% (Vivienda 1, estrato 4)
Tabla 19. Intervalo Estimación Límite inferior Límite superior
X1 Confianza (precio medio) 315 275.1 355.0
X1.1 Predicción (casa individual) 315 6.4 623.7

Se utilizó el intervalo de predicción. El intervalo de confianza acota el precio promedio de todas las casas con esas características; el de predicción acota el precio de una vivienda individual, pues incorpora además la varianza del error del modelo (RSE = 155,7). Como el cliente comprará una casa concreta y necesita saber si el crédito alcanza, el intervalo de predicción es el adecuado, y por ello resulta notablemente más amplio.

presupuesto_v1 <- 350

predicciones_v1 %>%  
  transmute(
    Estrato = estrato,            
    `Precio estimado` = round(fit, 1),            
    `Margen frente a $350M` = round(presupuesto_v1 - fit, 1)
  ) %>%  
  kable(
    caption = "Tabla 20. Predicción vs. crédito preaprobado (Vivienda 1)"
  )
Tabla 20. Predicción vs. crédito preaprobado (Vivienda 1)
Estrato Precio estimado Margen frente a $350M
1…1 4 315.0 35.0
1…2 5 378.2 -28.2

En el estrato 4 el precio esperado es de 315,0 millones, dentro del crédito de 350 millones, con un margen de 35 millones (10% del presupuesto). En el estrato 5 el precio esperado asciende a 378,2 millones, es decir, 28,2 millones por encima del crédito: pasar de estrato 4 a 5, con la casa físicamente idéntica, añade 63,1 millones al precio esperado, casi el doble del margen disponible. Los intervalos de predicción son muy amplios (entre 6 y 624 millones en el estrato 4), reflejo de la variabilidad no explicada, cercana al 42%, y de los supuestos incumplidos documentados en la validación. Por ello, la estimación debe leerse como un orden de magnitud y no como una tasación.

Con las características solicitadas, el escenario ajustado al presupuesto es el estrato 4; el estrato 5 solo sería alcanzable mediante negociación por debajo del precio de mercado o concesiones en alguna característica, por ejemplo el área. Esta conclusión motiva el contraste con la oferta real publicada, donde pueden existir opciones por debajo del promedio del mercado.

6 Ofertas potenciales para la Vivienda 1

6.1 Criterios de selección

La lista corta se construye con los requisitos duros de la solicitud y un score de compatibilidad que ordena las candidatas: se penaliza la desviación absoluta del área frente a los 200 m² solicitados y el precio (dividido entre 50 para equiparar escalas); menor score, mejor compatibilidad.

library(dplyr)
library(knitr)

# 1. Filtrar las ofertas reales según restricciones de presupuesto y características
ofertas_v1 <- base1_dep %>%  
  filter(    
    preciom <= 350,    
    estrato %in% c("4", "5"),    
    habitaciones >= 4,    
    banios >= 2,    
    parqueaderos >= 1  
  ) %>%  
  mutate(    
    diff_area = abs(areaconst - 200),    
    score_compat = diff_area + (preciom / 50)  ) %>%  
  arrange(score_compat)

# 2. Seleccionar el top 5 de propiedades más compatibles
top_5_ofertas <- head(ofertas_v1, 5)

# 3. Generar la tabla formateada para el documento HTML
top_5_ofertas %>%  
  select(id, barrio, estrato, areaconst, habitaciones, banios, parqueaderos, preciom) %>%  
  kable(
    caption = "Tabla 21. Top 5 de ofertas potenciales para la Vivienda 1",        
    col.names = c("ID", "Barrio", "Estrato", "Área (m²)", "Hab.", "Baños", "Parq.", "Precio (M)")
  )
Tabla 21. Top 5 de ofertas potenciales para la Vivienda 1
ID Barrio Estrato Área (m²) Hab. Baños Parq. Precio (M)
1343 la flora 5 200 4 4 2 320
1144 la merced 4 200 4 4 2 320
4210 el bosque 5 200 4 3 3 350
4267 el bosque 5 202 5 4 1 335
1887 vipasa 5 203 4 3 2 340

6.2 Mapa de ofertas

library(leaflet)

# 1. Crear la paleta de colores continua basada en el precio de las ofertas
pal <- colorNumeric(palette = "YlOrRd", domain = top_5_ofertas$preciom)

# 2. Construir el mapa interactivo con marcadores y leyenda
leaflet(top_5_ofertas) %>%  
  addTiles() %>%  
  setView(lng = -76.5325, lat = 3.4516, zoom = 12) %>%  
  addCircleMarkers(    
    lng = ~longitud, 
    lat = ~latitud,    
    radius = 8, 
    color = ~pal(preciom), 
    stroke = FALSE, 
    fillOpacity = 0.8,    
    popup = ~paste(
      "<b>ID:</b>", id, "<br>",
      "<b>Barrio:</b>", barrio, "<br>",
      "<b>Estrato:</b>", estrato, "<br>",
      "<b>Precio:</b>", preciom, "millones<br>",
      "<b>Área:</b>", areaconst, "m²<br>",
      "<b>Hab/Baños/Parq:</b>", habitaciones, "/", banios, "/", parqueaderos
    )  
  ) %>%  
  addLegend(
    "bottomright", 
    pal = pal, 
    values = ~preciom,            
    title = "Precio (millones)", 
    opacity = 1
  )

6.3 Discusión y recomendación

Las cinco ofertas cumplen el perfil solicitado y se concentran en barrios consolidados del norte (La Flora, La Merced, El Bosque y Vipasa). Todas están dentro del crédito, con márgenes entre 0 y 30 millones: la oferta de El Bosque a 350 millones consume el presupuesto completo, mientras que las dos primeras (320 millones) dejan un colchón de 30 millones.

La lectura frente al modelo es favorable. El precio esperado para el perfil solicitado es de 315,0 millones en estrato 4 y 378,2 en estrato 5. Las tres ofertas de estrato 5 (entre 320 y 350 millones) se ubican bastante por debajo de lo que el modelo estima para ese estrato, y la de estrato 4 coincide prácticamente con el estimado. Destaca la oferta ID 1343 en La Flora: estrato 5, 200 m², 4 habitaciones, 4 baños y 2 parqueaderos por 320 millones, es decir, un inmueble de estrato superior al mínimo exigido a un precio comparable al de las ofertas de estrato 4. En términos del modelo, esa diferencia podría explicarse por atributos no observados, como estado, antigüedad o acabados, por lo que amerita una visita de verificación.

Se recomienda presentar las cinco ofertas del mapa, priorizando las que combinan cercanía al área solicitada y menor precio, y explicitar los compromisos: las ofertas que agotan el perfil dejan poco o ningún margen para imprevistos, mientras que las más económicas conservan un colchón de hasta 30 millones. Debe advertirse al cliente que el modelo no captura el estado físico de los inmuebles, por lo que la inspección técnica es indispensable antes de negociar.

6.4 Réplica del ejercicio — Vivienda 2: apartamentos en zona sur

La segunda solicitud corresponde a un apartamento de 300 m², 5 habitaciones, 3 baños y 3 parqueaderos, estrato 5 o 6, en zona sur, con crédito preaprobado de 850 millones.

6.5 Filtro, verificación y depuración (base2)

library(dplyr)
library(knitr)

# 1. Filtrar la base de datos principal para aislar los apartamentos en la Zona Sur
base2 <- vivienda %>%  
  filter(tipo == "Apartamento" & zona == "Zona Sur")

# 2. Mostrar una vista previa estructurada de los primeros registros
base2 %>%  
  slice(1:3) %>%  
  select(id, zona, barrio, tipo, estrato, preciom, areaconst, habitaciones, banios, parqueaderos, longitud, latitud) %>%  
  kable(
    digits = 3, 
    caption = "Tabla 22. Primeros 3 registros de base2 (Apartamentos - Zona Sur)"
  )
Tabla 22. Primeros 3 registros de base2 (Apartamentos - Zona Sur)
id zona barrio tipo estrato preciom areaconst habitaciones banios parqueaderos longitud latitud
5098 Zona Sur acopi Apartamento 4 290 96 3 2 1 -76.535 3.450
698 Zona Sur aguablanca Apartamento 3 78 40 2 1 1 -76.501 3.400
8199 Zona Sur aguacatal Apartamento 6 875 194 3 5 2 -76.557 3.459
# 3. Verificar que el filtro de tipo se haya aplicado correctamente
base2 %>% 
  count(tipo) %>% 
  kable(
    caption = "Tabla 23. Verificación del filtro por tipo de inmueble", 
    col.names = c("Tipo", "Registros")
  )
Tabla 23. Verificación del filtro por tipo de inmueble
Tipo Registros
Apartamento 2787
# 4. Analizar la distribución de los apartamentos de la zona sur por estrato
base2 %>% 
  count(estrato) %>% 
  kable(
    caption = "Tabla 24. Distribución de apartamentos en Zona Sur por estrato", 
    col.names = c("Estrato", "Registros")
  )
Tabla 24. Distribución de apartamentos en Zona Sur por estrato
Estrato Registros
3 201
4 1091
5 1033
6 462

Contrariamente a lo que podría esperarse del mercado, la zona sur sí ofrece apartamentos de estratos 5 y 6: 1.495 registros antes de depurar (1.033 de estrato 5 y 462 de estrato 6), concentrados en sectores de alta valorización del suroriente de la ciudad. La solicitud del cliente es, por tanto, geográficamente viable.

library(leaflet)

# Crear el mapa interactivo para visualizar la distribución espacial de los apartamentos en la Zona Sur
leaflet(base2) %>%  
  addTiles() %>%  
  setView(lng = -76.5300, lat = 3.3800, zoom = 12) %>%  
  addCircleMarkers(    
    lng = ~longitud, 
    lat = ~latitud, 
    radius = 4,    
    color = "red", 
    fillOpacity = 0.6,    
    popup = ~paste(
      "<b>Barrio:</b>", barrio, 
      "<br><b>Precio (M):</b>", preciom
    )  
  )

El mapa previo a la depuración evidencia puntos geográficamente atípicos (errores de geocodificación o de etiquetado de zona). Se aplica un bounding box para el sur de la ciudad y el mismo tratamiento de faltantes.

library(dplyr)

# Este bloque debe ejecutarse antes del mapa para crear base2_dep
base2_dep <- base2 %>%  
  filter(
    latitud > 3.35 & latitud < 3.42,         
    longitud > -76.56 & longitud < -76.49
  ) %>%  
  mutate(
    parqueaderos = ifelse(is.na(parqueaderos), 0, parqueaderos),         
    estrato = as.factor(estrato)
  )

La depuración geográfica pasa el segmento de 2.787 a registros; los NAs de parqueaderos se imputan con 0 bajo el mismo supuesto de ausencia de parqueadero propio.

leaflet(base2_dep) %>%
  addTiles() %>%
  setView(lng = -76.5300, lat = 3.3800, zoom = 12) %>%
  addCircleMarkers(lng = ~longitud, lat = ~latitud, radius = 4,
                   color = "blue", fillOpacity = 0.6,
                   popup = ~paste("<b>Barrio:</b>", barrio, "<br><b>Precio:</b>", preciom))

6.6 Análisis exploratorio (condensado)

library(dplyr)
library(knitr)
library(ggplot2)
library(plotly)

# 1. Calcular y formatear la matriz de correlación para las variables numéricas
vars_num_v2 <- base2_dep %>%  
  select(preciom, areaconst, banios, habitaciones, parqueaderos) %>%  
  mutate(estrato_num = as.numeric(as.character(base2_dep$estrato)))

kable(
  round(cor(vars_num_v2, use = "complete.obs"), 2),      
  caption = "Tabla 25. Matriz de correlación de variables cuantitativas (base2_dep)"
)
Tabla 25. Matriz de correlación de variables cuantitativas (base2_dep)
preciom areaconst banios habitaciones parqueaderos estrato_num
preciom 1.00 0.72 0.70 0.34 0.67 0.63
areaconst 0.72 1.00 0.65 0.42 0.54 0.43
banios 0.70 0.65 1.00 0.52 0.53 0.51
habitaciones 0.34 0.42 0.52 1.00 0.30 0.20
parqueaderos 0.67 0.54 0.53 0.30 1.00 0.50
estrato_num 0.63 0.43 0.51 0.20 0.50 1.00
# 2. Gráfico interactivo: Relación entre Precio y Área construida
p_v2_area <- ggplot(base2_dep, aes(x = areaconst, y = preciom, text = barrio)) +  
  geom_point(alpha = 0.5, color = "darkgreen") +  
  geom_smooth(method = "lm", color = "red", se = FALSE) +  
  labs(
    title = "Fig. 11. Precio vs. Área construida (Apartamentos - Zona Sur)",       
    x = "Área (m²)", 
    y = "Precio (millones)"
  ) +  
  theme_minimal()

ggplotly(p_v2_area, tooltip = c("text", "x", "y"))
# 3. Gráfico interactivo: Distribución de Precios por Estrato
p_v2_estrato <- ggplot(base2_dep, aes(x = estrato, y = preciom, fill = estrato)) +  
  geom_boxplot() +  
  labs(
    title = "Fig. 12Precio por estrato socioeconómico (Apartamentos - Zona Sur)",       
    x = "Estrato", 
    y = "Precio (millones)"
  ) +  
  theme_minimal() +  
  scale_fill_brewer(palette = "Blues")

ggplotly(p_v2_estrato)

El patrón general replica el del segmento de casas, con matices. El área construida vuelve a ser la variable más correlacionada con el precio (r = 0,72), pero en los apartamentos los baños (r = 0,70) y los parqueaderos (r = 0,67) ganan relevancia frente a las casas, y el estrato conserva una asociación fuerte (r = 0,63). Las habitaciones, en cambio, apenas se asocian con el precio (r = 0,34). Los baños vuelven a correlacionarse con el área (r = 0,65) y con las habitaciones (r = 0,52), lo que anticipa la misma redundancia de tamaño y su posible efecto sobre la significancia individual.

6.7 Estimación del modelo

library(broom)
library(knitr)
library(dplyr)

# 1. Ajustar el modelo de regresión lineal múltiple
modelo_v2 <- lm(
  preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, 
  data = base2_dep
)

# 2. Formatear los coeficientes y estadísticos en una tabla limpia para HTML
tidy(modelo_v2) %>%
  mutate(across(where(is.numeric), ~ round(., 4))) %>%
  kable(
    caption = "Tabla 26. Coeficientes y significancia del modelo de regresión (Zona Sur)",
    col.names = c("Variable", "Estimación", "Error estándar", "Estadístico t", "Valor p")
  )
Tabla 26. Coeficientes y significancia del modelo de regresión (Zona Sur)
Variable Estimación Error estándar Estadístico t Valor p
(Intercept) -0.7515 10.1058 -0.0744 0.9407
areaconst 1.0568 0.0469 22.5170 0.0000
estrato4 20.5446 6.8503 2.9991 0.0027
estrato5 42.5348 7.1581 5.9422 0.0000
estrato6 189.2133 9.3283 20.2838 0.0000
habitaciones -10.7646 3.2686 -3.2933 0.0010
parqueaderos 51.6067 2.9886 17.2678 0.0000
banios 41.6488 3.0905 13.4764 0.0000
broom::glance(modelo_v2) %>%
  select(r.squared, adj.r.squared, sigma, statistic, p.value, AIC) %>%
  kable(digits = 3, caption = "Tabla 27. Bondad de ajuste del modelo V2",
        col.names = c("R²", "R² ajustado", "RSE (M)", "F", "p-valor (F)", "AIC"))
Tabla 27. Bondad de ajuste del modelo V2
R² ajustado RSE (M) F p-valor (F) AIC
0.756 0.755 78.826 972.215 0 25503.88

El modelo es globalmente significativo (F = 972,2; p < 0,001) y alcanza un R² de 0,756 (75,5% ajustado), claramente superior al del segmento de casas (0,584), con un error típico de 78,8 millones, la mitad del registrado en casas. El resultado es coherente con que el apartamento es un producto más estandarizado que la casa, con proyectos relativamente homogéneos en área y acabados.

El área construida (+1,057 millones por m²; p < 0,001) sigue siendo el principal determinante del precio. Los coeficientes de estrato se leen frente al estrato 3: un apartamento de estrato 5 vale en promedio 42,5 millones más y uno de estrato 6, 189,2 millones más, manteniendo constantes las demás variables; el salto al estrato 6 vuelve a ser el más pronunciado. Los parqueaderos (+51,6 millones; p < 0,001) y los baños (+41,6 millones; p < 0,001) aportan valor con magnitudes considerables en este segmento.

Las habitaciones presentan un coeficiente negativo y significativo (−10,8 millones; p = 0,001): manteniendo constantes el área, los baños y los parqueaderos, un mayor número de habitaciones se asocia con un precio menor. La interpretación más plausible es que, para un área fija, más habitaciones implican espacios más pequeños y una distribución menos valorada por el mercado; el coeficiente no debe leerse como que quitar habitaciones sube el precio, sino como el efecto de subdividir el área una vez controlado el tamaño total. El intercepto (−0,75; p = 0,941) no es significativo y carece de interpretación práctica

library(car)
library(lmtest)
library(knitr)
library(dplyr)

# 1. Gráficos de diagnóstico de regresión con títulos personalizados
plot(modelo_v2, which = 1, main = "Fig. 13. Residuos vs. Ajustados (Modelo V2)")

plot(modelo_v2, which = 2, main = "Fig. 14. Gráfico Q-Q Normal de Residuos (Modelo V2)")

plot(modelo_v2, which = 4, main = "Fig. 15. Distancia de Cook (Modelo V2)")

# 2. Ejecución de pruebas estadísticas de validación de supuestos
sw_v2 <- shapiro.test(resid(modelo_v2))
bp_v2 <- lmtest::bptest(modelo_v2)
dw_v2 <- lmtest::dwtest(modelo_v2) # Estandarizado con lmtest para evitar conflictos de estructura

# Consolidar los resultados de las pruebas en un marco de datos estructurado
resumen_supuestos <- data.frame(
  Prueba = c("Shapiro-Wilk (Normalidad)", "Breusch-Pagan (Homocedasticidad)", "Durbin-Watson (Autocorrelación)"),
  Estadístico = c(as.numeric(sw_v2$statistic), as.numeric(bp_v2$statistic), as.numeric(dw_v2$statistic)),
  `Valor p` = c(sw_v2$p.value, bp_v2$p.value, dw_v2$p.value),
  Interpretación = c(
    ifelse(sw_v2$p.value < 0.05, "No normal", "Normal"),
    ifelse(bp_v2$p.value < 0.05, "Heterocedasticidad", "Homocedasticidad"),
    ifelse(dw_v2$p.value < 0.05, "Autocorrelación", "Independencia")
  )
)

# Renderizar la Tabla 26 para el informe HTML
kable(
  resumen_supuestos, 
  digits = 4, 
  caption = "Tabla 27. Resumen de pruebas de validación de supuestos (Modelo V2)"
)
Tabla 27. Resumen de pruebas de validación de supuestos (Modelo V2)
Prueba Estadístico Valor.p Interpretación
Shapiro-Wilk (Normalidad) 0.7970 0 No normal
Breusch-Pagan (Homocedasticidad) 607.2912 0 Heterocedasticidad
Durbin-Watson (Autocorrelación) 1.6841 0 Autocorrelación
# 3. Evaluación de multicolinealidad mediante el Factor de Inflación de la Varianza (VIF)
vif_v2 <- as.data.frame(car::vif(modelo_v2))

# Renderizar la Tabla 27 para el informe HTML
kable(
  vif_v2, 
  digits = 3, 
  caption = "Tabla 28. Factor de inflación de la varianza - VIF (Modelo V2)"
)
Tabla 28. Factor de inflación de la varianza - VIF (Modelo V2)
GVIF Df GVIF^(1/(2*Df))
areaconst 1.971 1 1.404
estrato 1.614 3 1.083
habitaciones 1.445 1 1.202
parqueaderos 1.677 1 1.295
banios 2.382 1 1.543
# 4. Conteo de observaciones influyentes usando la Distancia de Cook
cooksd_v2 <- cooks.distance(modelo_v2)
n_infl_v2 <- sum(cooksd_v2 > 4 / length(cooksd_v2))
cat("Observaciones influyentes (Cook > 4/n):", n_infl_v2)
Observaciones influyentes (Cook > 4/n): 132

En línea con el segmento de casas y con la naturaleza asimétrica del precio, se rechazan la normalidad de los residuos (Shapiro-Wilk: W = 0,797; p < 0,001) y la homocedasticidad (Breusch-Pagan: BP = 607,3; p < 0,001), con patrones visuales análogos: curvatura y embudo en los residuos frente a los valores ajustados y colas desviadas en el QQ-plot. El estadístico Durbin-Watson (1,684) se interpreta como en el primer segmento: una desviación moderada respecto a 2 que refleja estructura espacial no capturada, pues ofertas del mismo barrio o proyecto comparten nivel de precios, más que dependencia temporal. El VIF máximo (2,38) se mantiene holgadamente por debajo de 5, y 132 observaciones superan el umbral de Cook, asociadas a apartamentos atípicos por área o precio.

Conforme a la ruta metodológica, no se corrige el modelo. A las sugerencias de la Fase 4 (transformación logarítmica, tratamiento de atípicos, inferencia robusta) se añade una específica de este segmento: incorporar la variable piso, disponible en la base y excluida de la especificación requerida, dado que en apartamentos los pisos superiores suelen valorizarse (vistas, menor ruido).

6.8 Predicción puntual — Vivienda 2

library(dplyr)
library(knitr)

# 1. Definir los escenarios hipotéticos para la Vivienda 2
# Nota: Se corrigió el nombre de la variable '2_e5' a 'v2_e5' ya que R no permite iniciar nombres con números.
v2_e5 <- data.frame(
  areaconst = 300,                    
  estrato = factor(5, levels = levels(base2_dep$estrato)),                    
  habitaciones = 5, 
  parqueaderos = 3, 
  banios = 3
)

v2_e6 <- data.frame(
  areaconst = 300,                    
  estrato = factor(6, levels = levels(base2_dep$estrato)),                    
  habitaciones = 5, 
  parqueaderos = 3, 
  banios = 3
)

# 2. Calcular las predicciones y sus intervalos al 95% para cada escenario
pred_v2_e5 <- predict(modelo_v2, newdata = v2_e5, interval = "prediction")
pred_v2_e6 <- predict(modelo_v2, newdata = v2_e6, interval = "prediction")

# 3. Consolidar ambos escenarios en un único marco de datos
predicciones_v2 <- bind_rows(  
  data.frame(estrato = "5", as.data.frame(pred_v2_e5)),  
  data.frame(estrato = "6", as.data.frame(pred_v2_e6))
)

# 4. Renderizar la tabla formal para el reporte HTML
kable(
  predicciones_v2, 
  digits = 1,      
  caption = "Tabla 29. Predicción del precio de la Vivienda 2 por escenario de estrato (millones)",
  col.names = c("Estrato", "Precio estimado", "Límite inferior", "Límite superior")
)
Tabla 29. Predicción del precio de la Vivienda 2 por escenario de estrato (millones)
Estrato Precio estimado Límite inferior Límite superior
1…1 5 584.8 428.9 740.6
1…2 6 731.4 575.4 887.4

En el estrato 5 el precio esperado es de 584,8 millones, con un margen de 265,2 millones frente al crédito; en el estrato 6, de 731,4 millones, con un margen de 118,6 millones, cercano al 14% del presupuesto. A diferencia de la Solicitud 1, el crédito de 850 millones es suficiente en ambos escenarios, incluso en el más exigente. La restricción de este caso no es el presupuesto, sino la disponibilidad de oferta que cumpla el perfil completo, lo que se evalúa a continuación.

Conviene precisar que el intervalo de predicción del estrato 6 (575,4 a 887,4 millones) sí incluye valores por encima del crédito; no obstante, tanto la estimación puntual como las ofertas reales del mercado respaldan la suficiencia del presupuesto en ambos escenarios.

6.9 Ofertas potenciales — Vivienda 2

library(dplyr)

# 1. Filtrar el mercado inmobiliario según los criterios de la Vivienda 2
ofertas_v2 <- base2_dep %>%  
  filter(    
    preciom <= 850,    
    estrato %in% c("5", "6"),    
    habitaciones >= 5,    
    banios >= 3,    
    parqueaderos >= 3  
  ) %>%  
  mutate(    
    diff_area = abs(areaconst - 300),         
    score = diff_area + (preciom / 50)  
  ) %>%  
  arrange(score)
ofertas_v2_flex <- base2_dep %>%
  filter(preciom <= 850, estrato %in% c("5", "6"),
         habitaciones >= 4, banios >= 3, parqueaderos >= 2, areaconst >= 250) %>%
  anti_join(ofertas_v2, by = "id") %>%
  mutate(diff_area = abs(areaconst - 300), score = diff_area + preciom / 50) %>%
  arrange(score) %>%
  head(pmax(5 - nrow(ofertas_v2), 0))

ofertas_finales_v2 <- bind_rows(
  ofertas_v2      %>% mutate(Criterio = "Estricto"),
  ofertas_v2_flex %>% mutate(Criterio = "Flexible")
)

ofertas_finales_v2 %>%
  select(id, barrio, estrato, areaconst, habitaciones, banios, parqueaderos,
         preciom, score, Criterio) %>%
  kable(caption = "Lista corta de ofertas para la Vivienda 2",
        col.names = c("ID", "Barrio", "Estrato", "Área (m²)", "Hab.", "Baños",
                      "Parq.", "Precio (M)", "Puntaje", "Criterio"))
Lista corta de ofertas para la Vivienda 2
ID Barrio Estrato Área (m²) Hab. Baños Parq. Precio (M) Puntaje Criterio
7512 seminario 5 300.00 6 5 3 670 13.40 Estricto
8036 seminario 5 256.00 5 5 3 530 54.60 Estricto
7182 guadalupe 5 573.00 5 8 3 730 287.60 Estricto
8113 cuarto de legua 5 295.55 4 4 2 410 12.65 Flexible
7658 cuarto de legua 5 320.00 4 4 2 520 30.40 Flexible
pal_v2 <- colorNumeric(palette = "Blues", domain = ofertas_finales_v2$preciom)
leaflet(ofertas_finales_v2) %>%
  addTiles() %>%
  setView(lng = -76.5300, lat = 3.3800, zoom = 12) %>%
  addCircleMarkers(lng = ~longitud, lat = ~latitud, radius = 8,
                   color = ~pal_v2(preciom), stroke = FALSE, fillOpacity = 0.8,
                   popup = ~paste("<b>ID:</b>", id, "<br><b>Barrio:</b>", barrio,
                                  "<br><b>Estrato:</b>", estrato, "<br><b>Precio:</b>",
                                  preciom, "M<br><b>Área:</b>", areaconst,
                                  "m²<br><b>Hab/Baños/Parq:</b>",
                                  habitaciones, "/", banios, "/", parqueaderos)) %>%
  addLegend("bottomright", pal = pal_v2, values = ~preciom, title = "Precio (M)")

El filtro estricto (perfil completo) arroja 3 ofertas. Para completar una lista corta de al menos cinco opciones se añaden ofertas bajo criterios flexibles (≥4 habitaciones, ≥2 parqueaderos, ≥250 m²), identificadas explícitamente para que el cliente conozca el trade-off de cada alternativa

Entre las ofertas estrictas destaca la del barrio Seminario: 300 m², 6 habitaciones, 5 baños y 3 parqueaderos por 670 millones, prácticamente idéntica al perfil solicitado y con un margen de 180 millones frente al crédito. Le sigue otra oferta en el mismo barrio (256 m², 530 millones), algo menor en área pero con margen aún mayor, y una en Guadalupe (573 m², 730 millones), que excede con amplitud el área solicitada y por eso obtiene el peor puntaje de compatibilidad.

Se recomienda priorizar las ofertas de criterio estricto, que cumplen el perfil completo y cuyo margen sobrante otorga poder de negociación y espacio para adecuaciones o acabados; presentar las alternativas flexibles como respaldo, dejando claro el criterio relajado en cada caso, y verificar técnicamente los inmuebles antes de negociar.

7 Conclusiones y recomendaciones

La solicitud de C&A se respondió mediante un flujo reproducible: segmentación por tipo y zona, depuración geográfica verificada en mapa, análisis exploratorio con gráficos interactivos, estimación y comparación de modelos de regresión lineal múltiple (estrato numérico frente a factor), validación de supuestos y predicción con intervalos, contrastada finalmente contra la oferta real publicada.

Solicitud 1, casa en zona norte (crédito de 350 millones). El modelo estima 315,0 millones para el perfil solicitado en estrato 4 (margen de 35 millones) y 378,2 millones en estrato 5 (28,2 millones por encima del crédito). La oferta real es más favorable que el promedio del modelo: cinco ofertas cumplen el perfil (La Flora, La Merced, El Bosque y Vipasa) dentro del presupuesto, con márgenes entre 0 y 30 millones, incluida una de estrato 5 a precio comparable al de estrato 4. Se recomienda priorizar la búsqueda en estrato 4 o en ofertas de estrato 5 por debajo de 350 millones, presentar la lista corta del mapa ordenada por la mejor relación área-precio y tratar el estrato 5 solo mediante negociación o concesiones en el área.

Solicitud 2, apartamento en zona sur (crédito de 850 millones). El modelo estima 584,8 millones (estrato 5) y 731,4 millones (estrato 6): el presupuesto es suficiente en ambos escenarios, con holguras de 265 y 119 millones. Se identificaron tres ofertas que cumplen el perfil completo, destaca la de 300 m² en el barrio Seminario por 670 millones, más dos alternativas flexibles, para una lista corta de cinco. Se recomienda apuntar al perfil completo y usar el margen sobrante como poder de negociación y espacio para adecuaciones.

Limitaciones. Ambos modelos incumplen los supuestos de normalidad y homocedasticidad y presentan observaciones influyentes, por lo que las estimaciones deben interpretarse como órdenes de magnitud, respaldados por el contraste con la oferta real. Para versiones futuras se sugiere la transformación logarítmica del precio, el tratamiento de atípicos, la incorporación del barrio o las coordenadas, la variable piso en el segmento de apartamentos y la inferencia con errores estándar robustos. El modelo no observa el estado de conservación ni la antigüedad, de modo que toda oferta de las listas cortas requiere inspección técnica previa a la negociación.

8 Anexos - Plan de trabajo

La respuesta a la solicitud se organizó en ocho fases secuenciales, ejecutadas primero para la Solicitud 1 (casa en zona norte) y replicadas de forma íntegra para la Solicitud 2 (apartamento en zona sur). Cada fase produce un resultado verificable que sirve de insumo a la siguiente, de modo que el análisis puede rastrearse de principio a fin. La Tabla A1 resume el plan aplicado y sus productos.

Fase Objetivo Técnicas y herramientas Producto / Resultado
0. Preparación y exploración Caracterizar la base y verificar los niveles exactos de las categorías antes de filtrar summary(), table(), colSums(is.na()) Base de 8.322 ofertas y 13 variables caracterizada; niveles verificados; faltantes identificados (piso: 2.638; parqueaderos: 1.605)
1. Filtro y depuración Construir el segmento de análisis y garantizar su coherencia geográfica dplyr::filter(), mapas interactivos con leaflet, filtro de coordenadas (bounding box) Base 1: de 722 a 623 casas (zona norte); Base 2: apartamentos (zona sur); NAs de parqueaderos imputados con 0
2. Análisis exploratorio Explorar la correlación del precio con el área, el estrato, los baños, las habitaciones y la zona Gráficos interactivos con plotly, boxplots por estrato y zona, matriz de correlación El área como principal predictor (\(r = 0,67\) en casas; \(0,72\) en apartamentos); efecto no lineal del estrato
3. Estimación del modelo Ajustar precio = f(área construida, estrato, habitaciones, parqueaderos, baños) y comparar especificaciones lm(), estrato numérico frente a factor, \(R^2\) ajustado, AIC Modelo seleccionado por segmento; V1: \(R^2 = 0,584\); V2: \(R^2 = 0,756\); coeficientes interpretados
4. Validación de supuestos Diagnosticar linealidad, normalidad, homocedasticidad, independencia, colinealidad e influencia Residuos vs. ajustados, QQ-plot, Shapiro-Wilk, Breusch-Pagan, Durbin-Watson, VIF, distancia de Cook Incumplimientos documentados y sugerencias de mejora; el modelo no se corrige, conforme a la actividad
5. Predicción puntual Estimar el precio del perfil solicitado, por escenario de estrato predict() con intervalo de predicción Solicitud 1: 315,0 y 378,2 frente a crédito de 350; Solicitud 2: 584,8 y 731,4 frente a 850
6. Ofertas potenciales Identificar y mapear al menos cinco ofertas reales compatibles con cada solicitud y su presupuesto Filtros por requisitos duros, puntaje de compatibilidad, mapa leaflet Solicitud 1: 5 ofertas dentro del crédito; Solicitud 2: 3 ofertas de perfil completo más 2 flexibles
7. Réplica (Solicitud 2) Ejecutar las fases 1 a 6 sobre el segmento de apartamentos en zona sur Ídem fases 1 a 6 Pipeline completo replicado y viabilidad de la segunda solicitud confirmada

Decisiones metodológicas clave,

El plan incorpora seis decisiones que se sustentan en los datos y condicionan los resultados:

1- Segmentación por tipo y zona. Cada solicitud se modela por separado: dentro de un segmento las variables tipo y zona son constantes y no pueden explicar la variabilidad del precio. El efecto de la zona se analizó en la fase exploratoria sobre la base completa, donde las zonas Norte y Oeste mostraron medianas de precio superiores a Sur y Oriente.

2- Depuración geográfica verificada en mapa. Los puntos con coordenadas inconsistentes con su zona, atribuibles a errores de geocodificación o de etiquetado, se excluyeron mediante un filtro de coordenadas, pues tasar sobre viviendas ubicadas en otra zona sesgaría el modelo.

3 -Imputación de parqueaderos con 0. La falta de reporte en una casa se interpretó como ausencia de parqueadero propio, decisión que preserva los registros en el modelo.

4 -Estrato como factor. Los boxplots y los coeficientes estimados mostraron saltos de precio no uniformes entre estratos, que una especificación lineal no puede representar; la comparación de modelos lo confirmó.

5 - Intervalo de predicción. Dado que el cliente comprará una vivienda concreta y no un promedio, la estimación puntual se acompañó del intervalo de predicción, que incorpora la varianza del error del modelo.

6 - Diagnóstico sin corrección. La actividad solicita validar los supuestos y sugerir soluciones, no corregir el modelo, por lo que las estrategias de mejora (transformación logarítmica, tratamiento de atípicos, estructura espacial, inferencia robusta) quedan documentadas para una próxima iteración.

La aplicación de este plan se desarrolla en el Anexo 2, cuyos resultados alimentan las recomendaciones del informe ejecutivo.

9 Anexos 2 - Resultados de la modelación, validación y comparación de modelos

1-Solicitud 1 — Casa en zona norte

Este anexo consolida los ejercicios de soporte del informe. No reestima los modelos presentados en las secciones 3 y 7.3: los toma como insumo y añade la comparación formal de especificaciones, la verificación de trazabilidad de las predicciones, el contraste del modelo contra las ofertas reales seleccionadas, la sensibilidad del presupuesto frente al área y diagnósticos complementarios a los reportados en las secciones 4 y 7.3.

A2.1 Comparación formal de especificaciones

La comparación de la Tabla 12 se complementa con dos criterios: la prueba F de modelos anidados, que evalúa si los saltos de precio entre estratos se desvían de la linealidad que asume el Modelo 1, y el BIC, que penaliza la complejidad con mayor severidad que el AIC. Para el segmento de apartamentos, donde el informe solo reportó la especificación con factor (Tabla 26), se estima también la versión lineal.

library(dplyr)
library(knitr)

# 1. Comparación para Vivienda 1: Prueba F y Criterios de Información
comp_f_v1 <- anova(modelo1_num, modelo2_fac)

kable(
  comp_f_v1, 
  digits = 3,      
  caption = "Tabla 30. Prueba F de comparación anidada: estrato lineal frente a factor (Vivienda 1)"
)
Tabla 30. Prueba F de comparación anidada: estrato lineal frente a factor (Vivienda 1)
Res.Df RSS Df Sum of Sq F Pr(>F)
402 9769325 NA NA NA NA
400 9696954 2 72370.4 1.493 0.226
crit_v1 <- data.frame(
  Modelo = c("Modelo 1 (estrato numérico)", "Modelo 2 (estrato factor)"),
  AIC = round(c(AIC(modelo1_num), AIC(modelo2_fac)), 1),
  BIC = round(c(BIC(modelo1_num), BIC(modelo2_fac)), 1)
)

kable(
  crit_v1, 
  caption = "Tabla 31. Criterios de información AIC y BIC (Vivienda 1)", 
  row.names = FALSE
)
Tabla 31. Criterios de información AIC y BIC (Vivienda 1)
Modelo AIC BIC
Modelo 1 (estrato numérico) 5285.9 5314
Modelo 2 (estrato factor) 5286.9 5323
# 2. Preparación de variable y ajuste del modelo restringido para Vivienda 2
base2_dep <- base2_dep %>% 
  mutate(estrato_num = as.numeric(as.character(estrato)))

modelo_v2_num <- lm(
  preciom ~ areaconst + estrato_num + habitaciones + parqueaderos + banios,                    
  data = base2_dep
)

# 3. Comparación para Vivienda 2: Métricas globales y Prueba F
comp_f_v2 <- anova(modelo_v2_num, modelo_v2)

crit_v2 <- data.frame(
  Modelo = c("Estrato numérico", "Estrato factor"),
  `R2` = round(c(summary(modelo_v2_num)$r.squared, summary(modelo_v2)$r.squared), 3),
  `R2 ajustado` = round(c(summary(modelo_v2_num)$adj.r.squared, summary(modelo_v2)$adj.r.squared), 3),
  RSE = round(c(summary(modelo_v2_num)$sigma, summary(modelo_v2)$sigma), 1),
  AIC = round(c(AIC(modelo_v2_num), AIC(modelo_v2)), 1),
  BIC = round(c(BIC(modelo_v2_num), BIC(modelo_v2)), 1),
  check.names = FALSE
)

kable(
  crit_v2, 
  caption = "Tabla 32. Comparación de especificaciones de modelo (Vivienda 2)", 
  row.names = FALSE
)
Tabla 32. Comparación de especificaciones de modelo (Vivienda 2)
Modelo R2 R2 ajustado RSE AIC BIC
Estrato numérico 0.722 0.721 84.1 25789.0 25828.8
Estrato factor 0.756 0.755 78.8 25503.9 25555.2
kable(
  comp_f_v2, 
  digits = 3,      
  caption = "Tabla 33. Prueba F de comparación anidada (Vivienda 2)"
)
Tabla 33. Prueba F de comparación anidada (Vivienda 2)
Res.Df RSS Df Sum of Sq F Pr(>F)
2197 15550967 NA NA NA NA
2195 13638604 2 1912363 153.888 0

En el segmento de casas, la comparación formal matiza la elección de la especificación. La prueba F de modelos anidados no encuentra evidencia suficiente para preferir el estrato como factor frente al tratamiento lineal (F = 1,49; gl = 2 y 400; p = 0,226), y el BIC, que penaliza con mayor severidad los parámetros adicionales, favorece marginalmente la versión lineal (5.314 frente a 5.323), en la misma dirección que el AIC de la Tabla 12. La elección no tiene, sin embargo, consecuencias para el caso: aplicando los coeficientes de la Tabla 10 al perfil solicitado, la especificación lineal estimaría 308,5 millones en estrato 4 y 385,1 en estrato 5, de modo que la conclusión de negocio —el estrato 4 cabe en el crédito y el estrato 5 no— se mantiene idéntica bajo cualquiera de las dos especificaciones.

El segmento de apartamentos cuenta una historia distinta: la evidencia a favor del factor es contundente. La prueba F rechaza la restricción de linealidad (F = 153,9; gl = 2 y 2.195; p < 0,001) y todas las métricas acompañan: el R² ajustado pasa de 0,721 a 0,755, el error típico desciende de 84,1 a 78,8 millones y el AIC y el BIC favorecen al factor con márgenes amplios (25.503,9 y 25.555,2 frente a 25.789,0 y 25.828,8). El resultado es coherente con los saltos de la Tabla 26: incrementos de 20,5, 42,5 y 189,2 millones no pueden representarse con una pendiente única. Se mantiene la especificación con factor en ambos segmentos: en apartamentos por respaldo estadístico inequívoco y en casas por consistencia metodológica entre segmentos y por el argumento conceptual de saltos no uniformes, dejando constancia de que en este último la evidencia formal es débil y la decisión no altera las conclusiones.

A2.2 Descomposición de las predicciones

Como ejercicio de trazabilidad, cada predicción se reconstruye a partir de los coeficientes de las Tablas 11 y 26: cada componente es el producto del coeficiente por el valor de la variable y la suma reproduce la estimación puntual de las Tablas 18 y 29. Pequeñas diferencias de redondeo son esperables.

Las descomposiciones reproducen las estimaciones puntuales con diferencias no mayores de 0,2 millones, atribuibles al redondeo, lo que confirma la trazabilidad de las predicciones frente a los coeficientes de las Tablas 11 y 26. La lectura por componentes traduce, además, el modelo al lenguaje del negocio. En la Vivienda 1, el área construida aporta 129,1 millones (41% del precio) y el estrato añade 76,1 o 139,2 según el escenario, con contribuciones menores de habitaciones, parqueaderos y baños. En la Vivienda 2 el patrón se intensifica: el área aporta 317,0 millones (54% del precio), los parqueaderos 154,8 y los baños 124,9, mientras las habitaciones restan 53,8, reflejo del coeficiente negativo ya discutido: para un área fija, subdividirla en más habitaciones reduce el valor de mercado. Esta composición informa la negociación: en la Solicitud 2 los atributos que el cliente eventualmente podría ceder pesan mucho en el precio, pues cada parqueadero equivale a unos 52 millones, mientras que en la Solicitud 1 el margen se juega sobre todo en la superficie, a razón de 0,65 millones por metro cuadrado.

library(knitr)

# 1. Definición de la función para descomponer la predicción del modelo
descomponer <- function(modelo, valores, nombres) {
  # Extraer los coeficientes estimados del modelo
  b <- coef(modelo)
  
  # Calcular la contribución individual: Coeficiente * Valor de la variable
  contribucion_individual <- round(b[names(valores)] * valores, 1)
  
  # Crear la tabla base con los componentes
  tab <- data.frame(
    Componente = nombres,
    Contribucion = contribucion_individual
  )
  
  # Añadir la fila final con la suma total (Precio estimado)
  rbind(
    tab, 
    data.frame(
      Componente = "Precio estimado",
      Contribucion = round(sum(tab$Contribucion, na.rm = TRUE), 1)
    )
  )
}

# 2. Definición de escenarios para Vivienda 1 (Área = 200 m², 4 hab, 1 parq, 2 baños)
valores_e4 <- c("(Intercept)" = 1, "areaconst" = 200, "estrato4" = 1, "habitaciones" = 4, "parqueaderos" = 1, "banios" = 2)
valores_e5 <- c("(Intercept)" = 1, "areaconst" = 200, "estrato5" = 1, "habitaciones" = 4, "parqueaderos" = 1, "banios" = 2)
nombres_v1 <- c("Intercepto", "Área construida (200 m²)", "Estrato (frente al 3)", "Habitaciones (4)", "Parqueaderos (1)", "Baños (2)")

# Renderizado de tablas para Vivienda 1
kable(
  descomponer(modelo2_fac, valores_e4, nombres_v1),
  caption = "Tabla 34. Descomposición de la predicción — Vivienda 1, estrato 4",
  col.names = c("Componente", "Contribución (M)"), 
  row.names = FALSE
)
Tabla 34. Descomposición de la predicción — Vivienda 1, estrato 4
Componente Contribución (M)
Intercepto 15.4
Área construida (200 m²) 129.1
Estrato (frente al 3) 76.1
Habitaciones (4) 28.9
Parqueaderos (1) 25.8
Baños (2) 39.8
Precio estimado 315.1
kable(
  descomponer(modelo2_fac, valores_e5, nombres_v1),
  caption = "Tabla 35. Descomposición de la predicción — Vivienda 1, estrato 5",
  col.names = c("Componente", "Contribución (M)"), 
  row.names = FALSE
)
Tabla 35. Descomposición de la predicción — Vivienda 1, estrato 5
Componente Contribución (M)
Intercepto 15.4
Área construida (200 m²) 129.1
Estrato (frente al 3) 139.2
Habitaciones (4) 28.9
Parqueaderos (1) 25.8
Baños (2) 39.8
Precio estimado 378.2
# 3. Definición de escenarios para Vivienda 2 (Área = 300 m², 5 hab, 3 parq, 3 baños)
valores_v2e5 <- c("(Intercept)" = 1, "areaconst" = 300, "estrato5" = 1, "habitaciones" = 5, "parqueaderos" = 3, "banios" = 3)
valores_v2e6 <- c("(Intercept)" = 1, "areaconst" = 300, "estrato6" = 1, "habitaciones" = 5, "parqueaderos" = 3, "banios" = 3)
nombres_v2 <- c("Intercepto", "Área construida (300 m²)", "Estrato (frente al 3)", "Habitaciones (5)", "Parqueaderos (3)", "Baños (3)")

# Renderizado de tablas para Vivienda 2
kable(
  descomponer(modelo_v2, valores_v2e5, nombres_v2),
  caption = "Tabla 36. Descomposición de la predicción — Vivienda 2, estrato 5",
  col.names = c("Componente", "Contribución (M)"), 
  row.names = FALSE
)
Tabla 36. Descomposición de la predicción — Vivienda 2, estrato 5
Componente Contribución (M)
Intercepto -0.8
Área construida (300 m²) 317.0
Estrato (frente al 3) 42.5
Habitaciones (5) -53.8
Parqueaderos (3) 154.8
Baños (3) 124.9
Precio estimado 584.6
kable(
  descomponer(modelo_v2, valores_v2e6, nombres_v2),
  caption = "Tabla 37. Descomposición de la predicción — Vivienda 2, estrato 6",
  col.names = c("Componente", "Contribución (M)"), 
  row.names = FALSE
)
Tabla 37. Descomposición de la predicción — Vivienda 2, estrato 6
Componente Contribución (M)
Intercepto -0.8
Área construida (300 m²) 317.0
Estrato (frente al 3) 189.2
Habitaciones (5) -53.8
Parqueaderos (3) 154.8
Baños (3) 124.9
Precio estimado 731.3

A2.3 Contraste del modelo contra las ofertas seleccionadas

library(dplyr)
library(knitr)

# 1. Tabla de contraste para ofertas de Vivienda 1
contraste_v1 <- top_5_ofertas %>%  
  mutate(pred_modelo = predict(modelo2_fac, newdata = top_5_ofertas)) %>%  
  transmute(
    ID = id, 
    Barrio = barrio, 
    Estrato = estrato,            
    `Precio publicado` = preciom,            
    `Precio del modelo` = round(pred_modelo, 1),            
    Diferencia = round(preciom - pred_modelo, 1),            
    `Diferencia (%)` = round(100 * (preciom - pred_modelo) / pred_modelo, 1)
  )

# Renderizar Tabla 38
kable(
  contraste_v1,      
  caption = "Tabla 38. Precio publicado frente a precio estimado por el modelo (ofertas Vivienda 1)"
)
Tabla 38. Precio publicado frente a precio estimado por el modelo (ofertas Vivienda 1)
ID Barrio Estrato Precio publicado Precio del modelo Diferencia Diferencia (%)
1343 la flora 5 320 443.7 -123.7 -27.9
1144 la merced 4 320 380.6 -60.6 -15.9
4210 el bosque 5 350 449.7 -99.7 -22.2
4267 el bosque 5 335 426.5 -91.5 -21.4
1887 vipasa 5 340 425.8 -85.8 -20.2
# 2. Tabla de contraste para ofertas de Vivienda 2
contraste_v2 <- ofertas_finales_v2 %>%  
  mutate(pred_modelo = predict(modelo_v2, newdata = ofertas_finales_v2)) %>%  
  transmute(
    ID = id, 
    Barrio = barrio, 
    Estrato = estrato, 
    Criterio,            
    `Precio publicado` = preciom,            
    `Precio del modelo` = round(pred_modelo, 1),            
    Diferencia = round(preciom - pred_modelo, 1),            
    `Diferencia (%)` = round(100 * (preciom - pred_modelo) / pred_modelo, 1)
  )

# Renderizar Tabla 39
kable(
  contraste_v2,      
  caption = "Tabla 39. Precio publicado frente a precio estimado por el modelo (ofertas Vivienda 2)"
)
Tabla 39. Precio publicado frente a precio estimado por el modelo (ofertas Vivienda 2)
ID Barrio Estrato Criterio Precio publicado Precio del modelo Diferencia Diferencia (%)
7512 seminario 5 Estricto 670 657.3 12.7 1.9
8036 seminario 5 Estricto 530 621.6 -91.6 -14.7
7182 guadalupe 5 Estricto 730 1081.5 -351.5 -32.5
8113 cuarto de legua 5 Flexible 410 580.9 -170.9 -29.4
7658 cuarto de legua 5 Flexible 520 606.7 -86.7 -14.3

El contraste cuantifica lo que la sección 6.3 describía de forma cualitativa. En la Vivienda 1, las cinco ofertas se publican entre 15,9% y 27,9% por debajo del valor que el modelo asigna a sus características. Ninguna diferencia supera individualmente el error típico del modelo, pero su signo es sistemáticamente negativo, lo que indica que la lista corta captura opciones favorables frente al promedio del mercado; la brecha restante puede obedecer a atributos no observados como estado, antigüedad o acabados. La aparente oportunidad mayor es la de La Flora (−123,7 millones), que amerita verificación técnica prioritaria. En la Vivienda 2, la candidata principal está prácticamente a precio de mercado: los 670 millones del Seminario difieren en 1,9% de los 657,3 millones que el modelo asigna a sus características, lo que respalda la recomendación central. Las demás alternativas presentan descuentos de entre 14% y 32%, con un caso extremo en Guadalupe: su residuo de −351,5 millones equivale a más de cuatro veces el error típico del modelo y delata una predicción poco confiable, pues con 573 m² el inmueble se ubica en la cola de la distribución del área y el modelo le asigna además 333 millones por sus ocho baños. Los porcentajes de descuento deben, por tanto, leerse como órdenes de magnitud.

A2.4 Sensibilidad del presupuesto frente al área

Las predicciones de las Tablas 18 y 29 corresponden a un único perfil. Se calcula hasta qué área construida el perfil solicitado permanece dentro del crédito en cada estrato, manteniendo constantes las demás características, información útil para negociar concesiones.

library(dplyr)
library(tidyr)
library(knitr)

# ==============================================================================
# 1. SENSIBILIDAD VIVIENDA 1: Área máxima para presupuesto de 350 Millones
# ==============================================================================

# Extraer coeficientes del modelo de Vivienda 1
b1 <- coef(modelo2_fac)

# Sumar la contribución fija de las características requeridas (4 hab, 1 parq, 2 baños)
base_fija_v1 <- b1["(Intercept)"] + 4 * b1["habitaciones"] + 1 * b1["parqueaderos"] + 2 * b1["banios"]

# Calcular el área máxima despejando la ecuación del modelo lineal
sens_v1 <- data.frame(
  Estrato = c("4", "5"),
  `Área máxima` = round((350 - base_fija_v1 - c(b1["estrato4"], b1["estrato5"])) / b1["areaconst"], 0)
)

# Renderizar Tabla 40
kable(
  sens_v1, 
  col.names = c("Estrato", "Área máxima dentro del crédito (m²)"),      
  caption = "Tabla 40. Área máxima del perfil que cabe en el crédito de 350 millones", 
  row.names = FALSE
)
Tabla 40. Área máxima del perfil que cabe en el crédito de 350 millones
Estrato Área máxima dentro del crédito (m²)
4 254
5 156
# ==============================================================================
# 2. MATRIZ DE PRECIOS SEGÚN ÁREA Y ESTRATO (VIVIENDA 1)
# ==============================================================================

# Crear grilla con combinaciones de área de 150 a 250 m²
grid_v1 <- expand.grid(
  areaconst = seq(150, 250, 25),                       
  estrato = factor(c("4", "5"), levels = levels(base1_dep$estrato)),                       
  habitaciones = 4, 
  parqueaderos = 1, 
  banios = 2
)

# Estimar precios para cada combinación de la grilla
grid_v1$pred <- predict(modelo2_fac, newdata = grid_v1)

# Dar formato tabular a la matriz de sensibilidad
tabla_sens_v1 <- grid_v1 %>%  
  as_tibble() %>%  
  mutate(estrato = paste("Estrato", estrato)) %>%  
  pivot_wider(names_from = estrato, values_from = pred) %>%  
  select(areaconst, `Estrato 4`, `Estrato 5`) %>%
  mutate(across(where(is.numeric), ~ round(.x, 1)))

# Renderizar Tabla 41
kable(
  tabla_sens_v1, 
  col.names = c("Área (m²)", "Estrato 4", "Estrato 5"),      
  caption = "Tabla 41. Precio estimado del perfil solicitado según área y estrato (Vivienda 1)"
)
Tabla 41. Precio estimado del perfil solicitado según área y estrato (Vivienda 1)
Área (m²) Estrato 4 Estrato 5
150 282.8 345.9
175 298.9 362.0
200 315.0 378.2
225 331.2 394.3
250 347.3 410.5
# ==============================================================================
# 3. SENSIBILIDAD VIVIENDA 2: Área máxima para presupuesto de 850 Millones
# ==============================================================================

# Extraer coeficientes del modelo de Vivienda 2
b2 <- coef(modelo_v2)

# Sumar la contribución fija de las características requeridas (5 hab, 3 parq, 3 baños)
base_fija_v2 <- b2["(Intercept)"] + 5 * b2["habitaciones"] + 3 * b2["parqueaderos"] + 3 * b2["banios"]

# Calcular el área máxima posible vs. el área máxima observada en el mercado
sens_v2 <- data.frame(
  Estrato = c("5", "6"),
  `Área máxima` = round((850 - base_fija_v2 - c(b2["estrato5"], b2["estrato6"])) / b2["areaconst"], 0),
  `Área máxima del segmento` = round(max(base2_dep$areaconst), 0),
  check.names = FALSE
)

# Renderizar Tabla 42
kable(
  sens_v2, 
  col.names = c("Estrato", "Área máxima dentro del crédito (m²)", "Área máxima observada en el segmento (m²)"),      
  caption = "Tabla 42. Área máxima del perfil que cabe en el crédito de 850 millones", 
  row.names = FALSE
)
Tabla 42. Área máxima del perfil que cabe en el crédito de 850 millones
Estrato Área máxima dentro del crédito (m²) Área máxima observada en el segmento (m²)
5 551 932
6 412 932

La sensibilidad convierte los coeficientes en instrucciones de negociación. Para la Vivienda 1, el perfil solicitado cabe en el crédito de 350 millones hasta 254 m² en estrato 4 —54 m² más de los pedidos, margen para crecer en superficie o negociar a la baja— y solo hasta 156 m² en estrato 5, es decir, 44 m² menos de lo solicitado: en estrato 5 la concesión no es de precio sino de área, como se aprecia en la tabla de precios por área y estrato, donde el perfil deja de caber en el crédito a partir de los 175 m². Para la Vivienda 2 la restricción desaparece: el perfil cabe hasta 551 m² en estrato 5 y 412 m² en estrato 6, muy por encima de los 300 solicitados y dentro del rango observado en el segmento, con áreas de hasta 932 m². El crédito de 850 millones no limita el área: otorga holgura para elegir entre más superficie o mejor precio.

A2.5 Diagnósticos complementarios

Se añaden a los diagnósticos de las secciones 4 y 7.3 los residuos studentizados (umbral |t| > 3), que detectan atípicos en la respuesta, y el apalancamiento (leverage > 2p/n), que detecta registros con combinaciones inusuales de predictores que pueden ejercer influencia aunque sus residuos sean pequeños.

library(knitr)

# 1. Gráficos de diagnóstico de Ubicación-Escala (Homocedasticidad)
plot(modelo2_fac, which = 3, main = "Ubicación-Escala (Vivienda 1)")

plot(modelo_v2, which = 3, main = "Ubicación-Escala (Vivienda 2)")

# 2. Diagnóstico de atípicos (residuos studentizados) y apalancamiento (leverage)
res_diag <- data.frame(
  Diagnostico = c("Residuos studentizados |t| > 3", "Leverage > 2p/n"),
  `Vivienda 1` = c(
    sum(abs(rstudent(modelo2_fac)) > 3),
    sum(hatvalues(modelo2_fac) > 2 * length(coef(modelo2_fac)) / nrow(base1_dep))
  ),
  `Vivienda 2` = c(
    sum(abs(rstudent(modelo_v2)) > 3),
    sum(hatvalues(modelo_v2) > 2 * length(coef(modelo_v2)) / nrow(base2_dep))
  ),
  check.names = FALSE
)

# 3. Renderizado de la Tabla 43 para el informe HTML
kable(
  res_diag, 
  caption = "Tabla 43. Diagnósticos complementarios: atípicos y apalancamiento"
)
Tabla 43. Diagnósticos complementarios: atípicos y apalancamiento
Diagnostico Vivienda 1 Vivienda 2
Residuos studentizados |t| > 3 11 31
Leverage > 2p/n 80 158

Los diagnósticos complementarios no alteran el panorama de las secciones 4 y 7.3, pero precisan el origen de la influencia. Los atípicos en la respuesta son pocos: 11 viviendas en el segmento de casas (2,7%) y 31 en el de apartamentos (1,4%) superan el umbral |t| = 3, proporciones razonables en muestras de este tamaño. El apalancamiento alto es más frecuente (80 y 158 registros, respectivamente) y refleja combinaciones inusuales de predictores —áreas extremas o conteos altos de baños y parqueaderos— que por sí solas no dañan el ajuste. Las observaciones influyentes documentadas (23 y 132) son precisamente la intersección de ambos fenómenos: registros atípicos en la respuesta que además tienen palanca. El gráfico de localización y escala confirma la dispersión creciente de los residuos ya reportada por el Breusch-Pagan y refuerza la sugerencia de estabilizar la varianza en una próxima iteración del modelo.

Conclusiones del anexo

Cinco resultados respaldan las recomendaciones del informe ejecutivo. Primero, la especificación: la evidencia formal respalda el estrato como factor en apartamentos (F = 153,9; p < 0,001) y, aunque en casas la prueba no lo distingue del tratamiento lineal (p = 0,226), la conclusión de negocio es robusta a la especificación, con estimaciones de 308,5 a 315,0 millones en estrato 4 y de 378,2 a 385,1 en estrato 5. Segundo, las predicciones son trazables: las descomposiciones reproducen las estimaciones puntuales con diferencias de redondeo. Tercero, el contraste con la oferta real valida las listas cortas: en la Solicitud 1 las cinco ofertas se ubican entre 16% y 28% por debajo del valor que asigna el modelo, y en la Solicitud 2 la candidata principal está a precio de mercado. Cuarto, la sensibilidad del área fija los límites de negociación: hasta 254 m² en estrato 4 y 156 m² en estrato 5 para el crédito de 350 millones, y holgura amplia (551 y 412 m²) para el de 850. Quinto, los diagnósticos complementarios son coherentes con la validación del cuerpo del informe: pocos atípicos en la respuesta, apalancamiento concentrado en combinaciones inusuales de predictores e influencia en su intersección. En conjunto, el anexo no modifica las recomendaciones del informe ejecutivo: las acota y las cuantifica.