# Datos
library(paqueteMODELOS)  # contiene el dataset "vivienda"
library(dplyr) # Manipulación de datos
library(ggplot2)
library(plotly)          # gráficos interactivos
library(leaflet)         # mapas interactivos 
library(car)             # VIF, pruebas de homocedasticidad, etc.
library(lmtest)          # bptest (Breusch-Pagan), dwtest (Durbin-Watson)
library(knitr)
library(kableExtra)      # tablas con mejor formato
library(stringr)

1 Resumen ejecutivo

Este resumen presenta, en términos de negocio, los hallazgos y la recomendación para las dos solicitudes de vivienda. El desarrollo técnico completo —código, pruebas estadísticas y validaciones— se encuentra en la sección de Anexos técnicos, más adelante en este mismo documento.

1.1 Vivienda 1 — Casa en Zona Norte (crédito preaprobado: $350 millones)

Para una casa con las características solicitadas (200 m² construidos, estrato 4 o 5, 4 habitaciones, 1 parqueadero y 2 baños), nuestro modelo estima un precio de mercado de entre $318 millones y $396 millones, según el estrato exacto. Ambos escenarios están dentro del presupuesto aprobado, aunque el estrato 5 deja un margen de negociación más estrecho.

Identificamos 43 ofertas reales en el mercado actual que cumplen el presupuesto y se ajustan razonablemente a lo solicitado. Recomendamos revisar las 5 mejores opciones (detalladas más adelante, con su ubicación en el mapa), priorizando las de estrato 4 por ofrecer mayor margen de negociación frente al crédito aprobado.

1.2 Vivienda 2 — Apartamento en Zona Sur (crédito preaprobado: $850 millones)

Para un apartamento con las características solicitadas (300 m² construidos, estrato 5 o 6, 5 habitaciones, 3 parqueaderos y 3 baños), el modelo estima un precio de mercado de entre $698 millones y $756 millones, según el estrato.

Identificamos 12 ofertas reales dentro del presupuesto y las características solicitadas. Recomendamos priorizar estrato 5, por dejar mayor margen frente al crédito aprobado de $850 millones.

1.3 Recomendación general

En ambos casos el área construida es el factor que más influye en el precio de la vivienda, seguido del estrato — el número de habitaciones, por sí solo, influye poco. Recomendamos a María enfocar la búsqueda en opciones dentro de los rangos de precio señalados arriba, y usar el listado de ofertas de los Anexos como punto de partida para agendar visitas con ambas familias.

2 Anexos técnicos

3 Introducción

Este análisis se apoya en la base de datos vivienda del paquete paqueteMODELOS, la cual ya fue objeto de un análisis exploratorio previo en el marco de un ejercicio de Análisis de Componentes Principales, en el que se identificaron y trataron diversas inconsistencias en los datos: valores faltantes concentrados principalmente en las variables piso y parqueaderos, así como registros con valores inválidos (banios == 0 y habitaciones == 0) atribuibles a errores de captura durante el proceso de web scraping. En dicho ejercicio, el interés estaba puesto en resumir la estructura de correlación entre las variables cuantitativas del mercado, por lo que las coordenadas geográficas (longitud, latitud) fueron excluidas al no considerarse relevantes para ese objetivo.

El enfoque del presente análisis es distinto y, en dos aspectos, complementa directamente lo excluido previamente. Primero, el objetivo ya no es exploratorio ni de reducción de dimensionalidad, sino predictivo: se busca estimar un modelo de regresión lineal múltiple que explique el precio de la vivienda (preciom) en función de sus características físicas (área construida, estrato, número de habitaciones, parqueaderos y baños), y utilizar dicho modelo para predecir el precio de las viviendas solicitadas y sugerir ofertas alternativas viables. Segundo, el componente geográfico —ausente en el análisis anterior— cobra ahora un rol central, pues se requiere verificar la ubicación de las ofertas mediante mapas interactivos y, eventualmente, presentar las alternativas recomendadas georreferenciadas.

Dado que la calidad de los datos no depende del objetivo del análisis, se retoma en gran medida el proceso de limpieza y preparación desarrollado previamente (eliminación de registros con exceso de valores faltantes, imputación de parqueaderos y exclusión de valores inválidos en banios y habitaciones), ajustándolo donde corresponde a los requerimientos de este nuevo enfoque. A partir de esta base depurada, el dataset se segmenta según las condiciones de cada solicitud —casas en zona norte para la primera vivienda, apartamentos en zona sur para la segunda— y sobre cada segmento se desarrolla el análisis exploratorio, la estimación del modelo, la validación de sus supuestos, la predicción del precio solicitado y la identificación de ofertas alternativas dentro del presupuesto disponible.

4 Carga y exploración inicial de los datos

tabla_estructura <- data.frame(
  Variable = names(vivienda),
  Tipo = sapply(vivienda, class),
  Ejemplo = sapply(vivienda, function(x) as.character(head(x[!is.na(x)], 1))),
  Valores_faltantes = sapply(vivienda, function(x) sum(is.na(x))),
  row.names = NULL
)

knitr::kable(tabla_estructura,
             caption = tab_cap("Estructura del dataset: tipo de dato, ejemplo y valores faltantes por variable"),
             col.names = c("Variable", "Tipo de dato", "Ejemplo", "Valores faltantes"))
Tabla 1. Estructura del dataset: tipo de dato, ejemplo y valores faltantes por variable
Variable Tipo de dato Ejemplo Valores faltantes
id numeric 1147 3
zona character Zona Oriente 3
piso character 02 2638
estrato numeric 3 3
preciom numeric 250 2
areaconst numeric 70 3
parqueaderos numeric 1 1605
banios numeric 3 3
habitaciones numeric 6 3
tipo character Casa 3
barrio character 20 de julio 3
longitud numeric -76.51168 3
latitud numeric 3.43382 3

5 Preparación de los datos

5.1 Eliminación de filas con 12 o más valores faltantes

Un registro con 12 o más valores faltantes (de un total de 13 columnas) no aporta información utilizable para el análisis, ya que prácticamente todo el registro estaría compuesto por datos ausentes. Por esta razón, antes de proceder con la imputación de parqueaderos y la exclusión de valores inválidos en banios y habitaciones, se eliminan del dataset las filas que presentan 12 o más valores faltantes.

n_antes <- nrow(vivienda)

vivienda <- vivienda %>%
  filter(rowSums(is.na(.)) < 12)

n_despues <- nrow(vivienda)

cat("Registros antes de eliminar filas con 12 o más valores faltantes:", n_antes, "\n")
## Registros antes de eliminar filas con 12 o más valores faltantes: 8322
cat("Registros después de eliminar filas con 12 o más valores faltantes:", n_despues, "\n")
## Registros después de eliminar filas con 12 o más valores faltantes: 8319
cat("Registros eliminados:", n_antes - n_despues, "\n")
## Registros eliminados: 3

5.2 Imputación de datos faltantes en parqueaderos

La variable parqueaderos es una de las variables explicativas del modelo de regresión que se estimará más adelante, por lo que resulta clave dejarla completa antes de filtrar por zona y tipo de vivienda. Dado el volumen de valores faltantes, se optó por imputar en lugar de excluir las filas correspondientes, bajo el supuesto de que un NA en esta variable representa información no reportada por el anunciante y no necesariamente la ausencia de parqueadero en el inmueble. La imputación se realiza usando la mediana de parqueaderos calculada por grupos definidos por tipo de vivienda y estrato, con el fin de capturar de mejor manera la heterogeneidad del mercado inmobiliario según estas dos características. Esta imputación se realiza sobre el dataset completo, antes de filtrar por zona, para aprovechar un mayor número de observaciones por grupo al calcular la mediana.

na_antes <- sum(is.na(vivienda$parqueaderos))

vivienda <- vivienda %>%
  group_by(tipo, estrato) %>%
  mutate(parqueaderos = ifelse(is.na(parqueaderos),
                                median(parqueaderos, na.rm = TRUE),
                                parqueaderos)) %>%
  ungroup()

na_despues <- sum(is.na(vivienda$parqueaderos))

if (na_despues > 0) {
  vivienda$parqueaderos[is.na(vivienda$parqueaderos)] <- median(vivienda$parqueaderos, na.rm = TRUE)
}

cat("NA en parqueaderos antes de imputar:", na_antes, "\n")
## NA en parqueaderos antes de imputar: 1602
cat("NA en parqueaderos después de imputar:", na_despues, "\n")
## NA en parqueaderos después de imputar: 0

5.3 Exclusión de registros con valores inválidos en baños, habitaciones y estrato faltante

Las variables banios y habitaciones registran un mínimo de 0, un valor sin sentido para una vivienda habitable, ya que toda propiedad residencial cuenta por definición con al menos un baño y una habitación. Estos casos probablemente corresponden a errores de captura y, dado que banios y habitaciones son predictoras directas del modelo de precio (paso 3), se excluyen estos registros en lugar de imputarlos, ya que no existe un supuesto razonable que permita reconstruir el valor real a partir de otras variables. Adicionalmente, se excluyen los registros sin estrato reportado, ya que esta variable también es predictora directa del modelo y tampoco existe un supuesto razonable que permita imputarla sin introducir sesgo.

n_antes <- nrow(vivienda)

vivienda <- vivienda %>%
  filter(banios > 0, habitaciones > 0, !is.na(estrato))

n_despues <- nrow(vivienda)

cat("Registros antes de excluir valores inválidos:", n_antes, "\n")
## Registros antes de excluir valores inválidos: 8319
cat("Registros después de excluir valores inválidos:", n_despues, "\n")
## Registros después de excluir valores inválidos: 8243
cat("Registros excluidos:", n_antes - n_despues, "\n")
## Registros excluidos: 76

5.4 Verificación final

Se verifica que las variables clave para el modelo de regresión y la segmentación por solicitud (preciom, areaconst, parqueaderos, banios, habitaciones, zona, tipo, longitud, latitud) queden libres de valores faltantes tras la preparación.

vars_clave <- c("preciom", "areaconst", "parqueaderos", "banios",
                 "habitaciones", "zona", "tipo", "longitud", "latitud", "estrato")

sapply(vivienda[, vars_clave], function(x) sum(is.na(x)))
##      preciom    areaconst parqueaderos       banios habitaciones         zona 
##            0            0            0            0            0            0 
##         tipo     longitud      latitud      estrato 
##            0            0            0            0

5.5 Normalización de nombres de barrio

La variable barrio presenta inconsistencias de captura que generan categorías duplicadas para el mismo barrio: variaciones de mayúsculas y minúsculas ("La Flora" vs. "la flora"), espacios adicionales, y corrupción de caracteres especiales (tildes y la letra “ñ”) atribuible a un problema de codificación de texto (encoding) en el proceso de origen de los datos. Esta falta de normalización, si no se corrige, infla artificialmente el número de barrios únicos y distorsiona cualquier análisis posterior que agrupe información por barrio —incluyendo la identificación de inconsistencias en la variable zona y la búsqueda de ofertas alternativas en las secciones siguientes—. Por esta razón, se normaliza barrio antes de proceder con el filtro por solicitud.

6 Revisa todas las columnas de texto del dataset

n_barrios_antes <- n_distinct(vivienda$barrio)

vivienda <- vivienda %>%
  mutate(
    barrio = str_trim(barrio),
    barrio = str_to_lower(barrio),
    barrio = iconv(barrio, from = "UTF-8", to = "UTF-8", sub = ""),  # elimina bytes corruptos/inválidos
    barrio = str_squish(barrio)
  )

n_barrios_despues <- n_distinct(vivienda$barrio)

cat("Barrios únicos antes de normalizar:", n_barrios_antes, "\n")
## Barrios únicos antes de normalizar: 433
cat("Barrios únicos después de normalizar:", n_barrios_despues, "\n")
## Barrios únicos después de normalizar: 404

7 Filtro de la base de datos por solicitud

Se construyen dos subconjuntos de datos a partir del dataset vivienda ya depurado, correspondientes a las características de cada una de las solicitudes recibidas: base1 reúne las ofertas de casas ubicadas en la zona norte de la ciudad (solicitud con crédito preaprobado de 350 millones de pesos), mientras que base2 reúne las ofertas de apartamentos en la zona sur (solicitud con crédito preaprobado de 850 millones de pesos).

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

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

cat("base1 (Casas, Zona Norte):", nrow(base1), "registros\n")
## base1 (Casas, Zona Norte): 700 registros
cat("base2 (Apartamentos, Zona Sur):", nrow(base2), "registros\n")
## base2 (Apartamentos, Zona Sur): 2777 registros

7.1 Primeros registros

knitr::kable(head(base1, 3),
             caption = tab_cap("Primeros 3 registros de base1 (Casas, Zona Norte)"))
Tabla 2. Primeros 3 registros de base1 (Casas, Zona Norte)
id zona piso estrato preciom areaconst parqueaderos banios habitaciones tipo barrio longitud latitud
1209 Zona Norte 02 5 320 150 2 4 6 Casa acopi -76.51341 3.47968
1592 Zona Norte 02 5 780 380 2 3 3 Casa acopi -76.51674 3.48721
4057 Zona Norte 02 6 750 445 3 7 6 Casa acopi -76.52950 3.38527
knitr::kable(head(base2, 3),
             caption = tab_cap("Primeros 3 registros de base2 (Apartamentos, Zona Sur)"))
Tabla 3. Primeros 3 registros de base2 (Apartamentos, Zona Sur)
id zona piso estrato preciom areaconst parqueaderos banios habitaciones tipo barrio longitud latitud
5098 Zona Sur 05 4 290 96 1 2 3 Apartamento acopi -76.53464 3.44987
698 Zona Sur 02 3 78 40 1 1 2 Apartamento aguablanca -76.50100 3.40000
8199 Zona Sur NA 6 875 194 2 5 3 Apartamento aguacatal -76.55700 3.45900

7.2 Tablas de verificación del filtro

Se verifica que cada base contenga únicamente los valores esperados de tipo y zona, como comprobación de que el filtro se aplicó correctamente.

verificacion_combinada <- bind_rows(
  base1 %>% count(Base = "base1 (Casas, Zona Norte)", tipo, zona),
  base2 %>% count(Base = "base2 (Apartamentos, Zona Sur)", tipo, zona)
)

knitr::kable(verificacion_combinada,
             caption = tab_cap("Verificación de filtro en base1 y base2: combinaciones de tipo y zona"),
             col.names = c("Base", "Tipo", "Zona", "Registros"))
Tabla 4. Verificación de filtro en base1 y base2: combinaciones de tipo y zona
Base Tipo Zona Registros
base1 (Casas, Zona Norte) Casa Zona Norte 700
base2 (Apartamentos, Zona Sur) Apartamento Zona Sur 2777

7.3 Verificación geográfica del filtro

fig_num_mapa_base1 <- fig_n

leaflet(base1) %>%
  addProviderTiles(providers$Esri.WorldGrayCanvas) %>%  # gratis, sin API key
  addMarkers(
    lng = ~longitud,
    lat = ~latitud,
    popup = ~paste0("Barrio: ", barrio,
                     "<br>Precio: ", preciom, " millones",
                     "<br>Área: ", areaconst, " m²"),
    clusterOptions = markerClusterOptions()  # agrupa marcadores cercanos -> mucho más liviano
  )

Figura 1. Ubicación geográfica de las ofertas en base1 (Casas, Zona Norte)

fig_num_mapa_base2 <- fig_n

leaflet(base2) %>%
  addProviderTiles(providers$Esri.WorldGrayCanvas) %>%  # gratis, sin API key
  addMarkers(
    lng = ~longitud,
    lat = ~latitud,
    popup = ~paste0("Barrio: ", barrio,
                     "<br>Precio: ", preciom, " millones",
                     "<br>Área: ", areaconst, " m²"),
    clusterOptions = markerClusterOptions()  # agrupa marcadores cercanos -> mucho más liviano
  )

Figura 2. Ubicación geográfica de las ofertas en base2 (Apartamentos, Zona Sur)

Las Figuras 0 y 1 permiten verificar visualmente si los puntos filtrados se ubican efectivamente dentro de la zona esperada de la ciudad. En ambos casos se observa que la mayoría de los puntos sí se concentra en el sector geográfico correspondiente, lo que confirma que el filtro por zona es, en términos generales, funcional. Sin embargo, en los dos mapas es visible también un número considerable de puntos ubicados en el extremo opuesto de la ciudad al esperado —viviendas etiquetadas como Zona Norte que aparecen geográficamente al sur, y viceversa—, un patrón que no corresponde a casos aislados sino a una proporción notoria del total de cada base. Esta observación motivó un análisis más detallado de la relación entre zona, barrio y las coordenadas geográficas, cuyos resultados y posibles causas se discuten a continuación.

7.4 Inconsistencias geográficas en base1 y base2

Se investigó la relación entre la variable zona, el barrio declarado y las coordenadas geográficas (longitud, latitud), a partir de puntos observados fuera del área esperada en los mapas de verificación.

# Zona moda (mayoritaria) por barrio, sobre el dataset completo normalizado
moda_zona_barrio <- vivienda %>%
  count(barrio, zona, sort = TRUE) %>%
  group_by(barrio) %>%
  slice_max(n, n = 1, with_ties = FALSE) %>%
  ungroup() %>%
  select(barrio, zona_moda = zona)

# Registros cuya zona declarada no coincide con la moda de su barrio
base1_check <- base1 %>%
  left_join(moda_zona_barrio, by = "barrio") %>%
  mutate(zona_no_coincide_moda = zona != zona_moda)

base2_check <- base2 %>%
  left_join(moda_zona_barrio, by = "barrio") %>%
  mutate(zona_no_coincide_moda = zona != zona_moda)

sum(base1_check$zona_no_coincide_moda); nrow(base1)
sum(base2_check$zona_no_coincide_moda); nrow(base2)

# Caso ilustrativo: Valle del Lili
vivienda %>% filter(barrio == "valle del lili") %>% count(zona)
# Evidencia de baja precisión geográfica: coordenadas compartidas
# por múltiples registros distintos
n_distinct(vivienda %>% select(longitud, latitud))
nrow(vivienda)

coordenadas_repetidas <- vivienda %>%
  count(longitud, latitud, sort = TRUE) %>%
  head(20)

# La coordenada más repetida: ¿a qué barrio/zona corresponde?
vivienda %>%
  filter(longitud == coordenadas_repetidas$longitud[1],
         latitud == coordenadas_repetidas$latitud[1]) %>%
  count(barrio, zona, sort = TRUE)
fig_num_distribucion_latitud <- fig_n

referencia_lat <- vivienda %>%
  filter(barrio %in% c("la flora", "valle del lili")) %>%
  group_by(barrio) %>%
  summarise(lat_mediana = median(latitud, na.rm = TRUE), .groups = "drop")

ggplot(base1, aes(x = latitud)) +
  geom_histogram(bins = 40, fill = "steelblue") +
  geom_vline(xintercept = referencia_lat$lat_mediana[referencia_lat$barrio == "la flora"],
             color = "blue", linetype = "dashed", linewidth = 1) +
  geom_vline(xintercept = referencia_lat$lat_mediana[referencia_lat$barrio == "valle del lili"],
             color = "red", linetype = "dashed", linewidth = 1) +
  labs(title = "Distribución de latitud en base1 (Casas, Zona Norte)",
       subtitle = "Línea azul = mediana de 'la flora' (norte); línea roja = mediana de 'valle del lili' (sur)",
       x = "Latitud", y = "Frecuencia")
Figura 3. Distribución de latitud en base1 frente a barrios de referencia del norte y del sur

Figura 3. Distribución de latitud en base1 frente a barrios de referencia del norte y del sur

La verificación geográfica de base1 y base2 reveló una desconexión entre la variable zona y las coordenadas geográficas (longitud, latitud) que va más allá de errores puntuales de captura. Si bien la inconsistencia directa entre barrio y zona es baja —por ejemplo, de 1,008 registros del barrio Valle del Lili, 1,004 (99.6%) están correctamente etiquetados como Zona Sur y solo 4 aparecen como Zona Norte—, la distribución espacial de base1 se encuentra sistemáticamente corrida hacia el sur de lo esperado para casas de “Zona Norte”, sin que un grupo identificable de barrios explique la totalidad del patrón (Figura 2). A esto se suma que el dataset completo contiene apenas 5,862 combinaciones únicas de coordenadas para 8,243 registros, con casos de hasta 106 propiedades distintas compartiendo exactamente la misma latitud y longitud, además de valores inválidos en el campo barrio (como “Cali” o “Zona Norte” en lugar de un nombre real de barrio). En conjunto, esta evidencia sugiere que zona/barrio y las coordenadas geográficas provienen de procesos de captura independientes durante el proceso de web scraping: el primero corresponde a una clasificación de texto libre asignada por el anunciante o el portal, mientras que las coordenadas probablemente se heredan de un mapa aproximado embebido en la página del anuncio (referido al sector o barrio, no a la dirección exacta de la vivienda), sin que ambas fuentes hayan sido validadas de forma cruzada antes de la publicación de los datos. Por esta razón, se recomienda no utilizar longitud/latitud como criterio de verificación fina de la zona declarada, limitando su uso en este informe a fines ilustrativos de ubicación aproximada, y se sugiere para trabajos futuros contrastar ambas variables contra una fuente oficial de geocodificación.

comparacion <- bind_rows(
  base1 %>% mutate(Base = "Zona Norte (Casas)"),
  base2 %>% mutate(Base = "Zona Sur (Apartamentos)")
)

color_norte <- "#8DA0CB"
color_sur   <- "#FC8D62"

# --- Fila 1: Área construida ---
p1_norte <- plot_ly(base1, x = ~areaconst, y = ~preciom, type = "scattergl", mode = "markers",
                     marker = list(color = color_norte, opacity = 0.6, size = 6),
                     name = "Zona Norte") %>%
  layout(xaxis = list(title = "Área (m²)"), yaxis = list(title = "Precio (millones)"))

p1_sur <- plot_ly(base2, x = ~areaconst, y = ~preciom, type = "scattergl", mode = "markers",
                   marker = list(color = color_sur, opacity = 0.6, size = 6),
                   name = "Zona Sur") %>%
  layout(xaxis = list(title = "Área (m²)"), yaxis = list(title = "Precio (millones)"))

# --- Fila 2: Estrato ---
p2_norte <- plot_ly(base1, x = ~factor(estrato), y = ~preciom, type = "box",
                     marker = list(color = color_norte), line = list(color = color_norte),
                     name = "Zona Norte") %>%
  layout(xaxis = list(title = "Estrato"), yaxis = list(title = "Precio (millones)"))

p2_sur <- plot_ly(base2, x = ~factor(estrato), y = ~preciom, type = "box",
                   marker = list(color = color_sur), line = list(color = color_sur),
                   name = "Zona Sur") %>%
  layout(xaxis = list(title = "Estrato"), yaxis = list(title = "Precio (millones)"))

# --- Fila 3: Habitaciones ---
p3_norte <- plot_ly(base1, x = ~factor(habitaciones), y = ~preciom, type = "box",
                     marker = list(color = color_norte), line = list(color = color_norte),
                     name = "Zona Norte") %>%
  layout(xaxis = list(title = "Habitaciones"), yaxis = list(title = "Precio (millones)"))

p3_sur <- plot_ly(base2, x = ~factor(habitaciones), y = ~preciom, type = "box",
                   marker = list(color = color_sur), line = list(color = color_sur),
                   name = "Zona Sur") %>%
  layout(xaxis = list(title = "Habitaciones"), yaxis = list(title = "Precio (millones)"))

# --- Fila 4: Baños ---
p4_norte <- plot_ly(base1, x = ~factor(banios), y = ~preciom, type = "box",
                     marker = list(color = color_norte), line = list(color = color_norte),
                     name = "Zona Norte") %>%
  layout(xaxis = list(title = "Baños"), yaxis = list(title = "Precio (millones)"))

p4_sur <- plot_ly(base2, x = ~factor(banios), y = ~preciom, type = "box",
                   marker = list(color = color_sur), line = list(color = color_sur),
                   name = "Zona Sur") %>%
  layout(xaxis = list(title = "Baños"), yaxis = list(title = "Precio (millones)"))

p_final <- subplot(
  p1_norte, p1_sur,
  p2_norte, p2_sur,
  p3_norte, p3_sur,
  p4_norte, p4_sur,
  nrows = 4, shareY = TRUE, titleX = TRUE, titleY = TRUE,
  margin = 0.06
) %>%
  layout(
    title = list(
      text = "Precio vs. Área, Estrato, Habitaciones y Baños — Zona Norte vs. Zona Sur",
      y = 0.99
    ),
    margin = list(t = 90),
    showlegend = FALSE,
    annotations = list(
      list(x = 0.18, y = 1.06, xref = "paper", yref = "paper", showarrow = FALSE,
           text = "<b>Zona Norte</b>", font = list(size = 13)),
      list(x = 0.82, y = 1.06, xref = "paper", yref = "paper", showarrow = FALSE,
           text = "<b>Zona Sur</b>", font = list(size = 13))
    )
  ) %>%
  config(displayModeBar = FALSE)  # oculta la barra de zoom/cámara que se encima arriba

# Forzar tamaño fijo en vez de "fill" (evita el bug de htmlwidgets 1.6.x
# en documentos con tema clásico tipo flatly)
p_final$sizingPolicy$browser$fill <- FALSE
p_final$width  <- 950
p_final$height <- 850

p_final

Figura 4. Comparación de precio entre Zona Norte (base1) y Zona Sur (base2) por variable

El área construida es la que mejor explica el precio de forma consistente en ambas zonas; el estrato también aporta señal clara y escalonada. El número de baños muestra un patrón creciente similar al del estrato —viviendas con más baños tienden a ubicarse en precios más altos en ambas zonas—, aunque menos marcado; las habitaciones, en cambio, son la variable más ruidosa y menos predictiva por sí sola, especialmente en Zona Norte. Esto es información valiosa de cara a construir el modelo de regresión múltiple: es esperable que areaconst, estrato y banios tengan coeficientes más robustos que habitaciones.

7.5 Matrices de correlación

Con el fin de complementar la comparación visual anterior, se calcula la matriz de correlación de Pearson entre el precio y las variables numéricas disponibles (areaconst, estrato, banios, habitaciones, parqueaderos), por separado para base1 (Zona Norte) y base2 (Zona Sur).

Tabla 5. Matriz de correlación de Pearson — base1 (Casas, Zona Norte)
preciom areaconst estrato banios habitaciones parqueaderos
preciom 1.000 0.730 0.617 0.568 0.375 0.465
areaconst 0.730 1.000 0.465 0.515 0.449 0.386
estrato 0.617 0.465 1.000 0.434 0.096 0.458
banios 0.568 0.515 0.434 1.000 0.607 0.401
habitaciones 0.375 0.449 0.096 0.607 1.000 0.231
parqueaderos 0.465 0.386 0.458 0.401 0.231 1.000
Tabla 6. Matriz de correlación de Pearson — base2 (Apartamentos, Zona Sur)
preciom areaconst estrato banios habitaciones parqueaderos
preciom 1.000 0.757 0.673 0.733 0.344 0.714
areaconst 0.757 1.000 0.483 0.687 0.450 0.597
estrato 0.673 0.483 1.000 0.572 0.212 0.525
banios 0.733 0.687 0.572 1.000 0.522 0.603
habitaciones 0.344 0.450 0.212 0.522 1.000 0.296
parqueaderos 0.714 0.597 0.525 0.603 0.296 1.000

Dado que estrato, banios, habitaciones y parqueaderos son variables numéricas discretas (no continuas) y algunas de ellas ordinales, se complementa el análisis con la correlación de Spearman, que mide relación monótona a partir de rangos en lugar de valores crudos, y que resulta más robusta a valores atípicos y más apropiada para variables ordinales como estrato.

Tabla 7. Matriz de correlación de Spearman — base1 (Casas, Zona Norte)
preciom areaconst estrato banios habitaciones parqueaderos
preciom 1.000 0.820 0.717 0.645 0.435 0.596
areaconst 0.820 1.000 0.554 0.594 0.513 0.491
estrato 0.717 0.554 1.000 0.465 0.166 0.604
banios 0.645 0.594 0.465 1.000 0.597 0.455
habitaciones 0.435 0.513 0.166 0.597 1.000 0.248
parqueaderos 0.596 0.491 0.604 0.455 0.248 1.000
Tabla 8. Matriz de correlación de Spearman — base2 (Apartamentos, Zona Sur)
preciom areaconst estrato banios habitaciones parqueaderos
preciom 1.000 0.866 0.752 0.706 0.392 0.678
areaconst 0.866 1.000 0.637 0.759 0.525 0.658
estrato 0.752 0.637 1.000 0.572 0.237 0.558
banios 0.706 0.759 0.572 1.000 0.502 0.593
habitaciones 0.392 0.525 0.237 0.502 1.000 0.306
parqueaderos 0.678 0.658 0.558 0.593 0.306 1.000

Interpretación: la correlación más fuerte con el precio es la del área construida, seguida delestrato y del número de baños; la correlación con el número de habitaciones es baja , lo que sugiere que, para casas, tener más cuartos no está tan asociado al precio como tener más área o mejor estrato (es común que casas con muchos cuartos pequeños no sean necesariamente más costosas). El gráfico anterior confirma una relación positiva y aproximadamente lineal entre área y precio, y muestra que los puntos de estrato alto tienden a ubicarse en la parte superior del gráfico(mayor precio para la misma área). Las matrices de Spearman muestran el mismo patrón general que las de Pearson —área construida como predictor más fuerte, habitaciones como el más débil, y baños/parqueaderos con mayor peso relativo en apartamentos (base2) que en casas (base1)—, sin diferencias grandes entre ambos métodos; esto sugiere que las relaciones entre estas variables y el precio son razonablemente lineales y no solo monótonas, lo cual da algo más de respaldo al uso de un modelo de regresión lineal en la siguiente sección. # Modelos de regresión lineal múltiple — Vivienda 1 y Vivienda 2

7.6 Estimación de los modelos

Se estima un modelo de regresión lineal múltiple sobre cada base (base1: casas, Zona Norte; base2: apartamentos, Zona Sur), utilizando en ambos casos como variables explicativas el área construida, el estrato, el número de habitaciones, el número de parqueaderos y el número de baños:

\[\text{preciom} = \beta_0 + \beta_1\,\text{areaconst} + \beta_2\,\text{estrato} + \beta_3\,\text{habitaciones} + \beta_4\,\text{parqueaderos} + \beta_5\,\text{banios} + \varepsilon\]

modelo_base1 <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios,
                    data = base1)
modelo_base2 <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios,
                    data = base2)

resumen_base1 <- summary(modelo_base1)
resumen_base2 <- summary(modelo_base2)

resumen_base1
## 
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos + 
##     banios, data = base1)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -894.38  -75.00  -18.45   44.29 1101.47 
## 
## Coefficients:
##                Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  -237.38679   31.89342  -7.443 2.92e-13 ***
## areaconst       0.76519    0.04615  16.580  < 2e-16 ***
## estrato        78.72606    7.75979  10.145  < 2e-16 ***
## habitaciones    3.58463    4.77684   0.750  0.45326    
## parqueaderos   17.81350    5.61309   3.174  0.00157 ** 
## banios         27.51162    5.90916   4.656 3.87e-06 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 156.9 on 694 degrees of freedom
## Multiple R-squared:  0.6589, Adjusted R-squared:  0.6564 
## F-statistic: 268.1 on 5 and 694 DF,  p-value: < 2.2e-16
resumen_base2
## 
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos + 
##     banios, data = base2)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -1120.55   -38.38    -2.81    38.27   922.91 
## 
## Coefficients:
##                Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  -259.27683   13.16926 -19.688  < 2e-16 ***
## areaconst       1.32548    0.05008  26.466  < 2e-16 ***
## estrato        57.44579    2.68557  21.391  < 2e-16 ***
## habitaciones  -19.52241    3.45457  -5.651 1.76e-08 ***
## parqueaderos   77.66743    3.90002  19.915  < 2e-16 ***
## banios         45.71708    3.10613  14.718  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 92.83 on 2771 degrees of freedom
## Multiple R-squared:  0.7639, Adjusted R-squared:  0.7635 
## F-statistic:  1793 on 5 and 2771 DF,  p-value: < 2.2e-16
Tabla 9. Coeficientes estimados de los modelos de regresión — Vivienda 1 (base1) y Vivienda 2 (base2)
Vivienda Variable Coeficiente Error estándar Valor t p-valor
Vivienda 1 (Casas, Zona Norte) (Intercept) -237.3868 31.8934 -7.4431 0.0000
areaconst 0.7652 0.0462 16.5798 0.0000
estrato 78.7261 7.7598 10.1454 0.0000
habitaciones 3.5846 4.7768 0.7504 0.4533
parqueaderos 17.8135 5.6131 3.1736 0.0016
banios 27.5116 5.9092 4.6558 0.0000
Vivienda 2 (Apartamentos, Zona Sur) (Intercept) -259.2768 13.1693 -19.6880 0.0000
areaconst 1.3255 0.0501 26.4664 0.0000
estrato 57.4458 2.6856 21.3905 0.0000
habitaciones -19.5224 3.4546 -5.6512 0.0000
parqueaderos 77.6674 3.9000 19.9146 0.0000
banios 45.7171 3.1061 14.7184 0.0000

Interpretación comparada de los coeficientes:

  • Área construida: por cada m² adicional, el precio esperado aumenta en promedio 0.765 millones en Vivienda 1 (casas) y 1.325 millones en Vivienda 2 (apartamentos), manteniendo las demás variables constantes (p-valores <0.001 y <0.001, respectivamente). En ambos casos es el efecto más robusto y esperado: a mayor tamaño construido, mayor precio.
  • Estrato: subir un estrato socioeconómico se asocia con un cambio de 78.7 millones en Vivienda 1 y de 57.4 millones en Vivienda 2, manteniendo todo lo demás constante. En ambos segmentos el estrato captura de forma indirecta infraestructura del sector, seguridad y valorización de la zona.
  • Número de habitaciones: el coeficiente es 3.58 en Vivienda 1 (no estadísticamente significativo) y -19.52 en Vivienda 2 (estadísticamente significativo). En ambos casos es razonable que el efecto sea débil frente a areaconst: para un área fija, más habitaciones suele implicar cuartos más pequeños y no necesariamente mayor valor.
  • Parqueaderos: cada parqueadero adicional aporta en promedio 17.8 millones en Vivienda 1 y 77.7 millones en Vivienda 2, coherente con que el parqueadero es un atributo de comodidad escaso y valorado en ambos segmentos del mercado.
  • Baños: cada baño adicional se asocia con un cambio de 27.5 millones en Vivienda 1 y de 45.7 millones en Vivienda 2, también coherente con la intuición de que más baños reflejan mayor comodidad y mejor especificación de la vivienda.

7.7 Ajuste de los modelos

Tabla 10. Ajuste comparado de los modelos de regresión — Vivienda 1 y Vivienda 2
Vivienda R² ajustado
Vivienda 1 (Casas, Zona Norte) 0.659 0.656
Vivienda 2 (Apartamentos, Zona Sur) 0.764 0.763

El modelo de Vivienda 1 explica cerca del 66% de la variabilidad del precio, mientras que el de Vivienda 2 explica cerca del 76%. Ambos son ajustes razonables para datos transversales de precios inmobiliarios —donde factores no observados como acabados, antigüedad o vistas también influyen en el precio—, pero dejan un margen importante sin explicar. Las mismas estrategias de mejora aplican a los dos modelos: (i) incorporar el barrio como variable categórica (dummies), (ii) trabajar con \(\log(\text{preciom})\) dado el sesgo a la derecha típico de precios de vivienda, y (iii) explorar términos de interacción como área × estrato.

7.8 Validación de supuestos

par(mfrow = c(2, 2))
plot(modelo_base1, which = 1, main = "Vivienda 1 — Residuales vs. Ajustados")
plot(modelo_base1, which = 2, main = "Vivienda 1 — QQ-plot")
plot(modelo_base2, which = 1, main = "Vivienda 2 — Residuales vs. Ajustados")
plot(modelo_base2, which = 2, main = "Vivienda 2 — QQ-plot")
Figura 5. Gráficos de diagnóstico de los modelos — Vivienda 1 (fila superior) y Vivienda 2 (fila inferior): residuales vs. ajustados y QQ-plot

Figura 5. Gráficos de diagnóstico de los modelos — Vivienda 1 (fila superior) y Vivienda 2 (fila inferior): residuales vs. ajustados y QQ-plot

par(mfrow = c(1, 1))
shapiro_base1 <- shapiro.test(residuals(modelo_base1))
bp_base1      <- lmtest::bptest(modelo_base1)
dw_base1      <- lmtest::dwtest(modelo_base1)
vif_base1     <- car::vif(modelo_base1)

shapiro_base2 <- shapiro.test(residuals(modelo_base2))
bp_base2      <- lmtest::bptest(modelo_base2)
dw_base2      <- lmtest::dwtest(modelo_base2)
vif_base2     <- car::vif(modelo_base2)
Tabla 11. Pruebas de supuestos comparadas — normalidad, homocedasticidad e independencia
Vivienda Shapiro-Wilk W p-valor Breusch-Pagan p-valor Durbin-Watson p-valor
Vivienda 1 (base1) 0.830 0 126.851 0 1.639 0
Vivienda 2 (base2) 0.784 0 898.599 0 1.550 0
Tabla 12. Factores de inflación de varianza (VIF) — Vivienda 1 y Vivienda 2
Vivienda Variable VIF
areaconst…1 Vivienda 1 (base1) areaconst 1.677
Vivienda 1 (base1) estrato 1.658
Vivienda 1 (base1) habitaciones 1.838
Vivienda 1 (base1) parqueaderos 1.378
Vivienda 1 (base1) banios 2.135
areaconst…6 Vivienda 2 (base2) areaconst 2.178
Vivienda 2 (base2) estrato 1.646
Vivienda 2 (base2) habitaciones 1.440
Vivienda 2 (base2) parqueaderos 1.858
Vivienda 2 (base2) banios 2.678
  • Normalidad de residuales (Shapiro-Wilk): la hipótesis nula de esta prueba es que los residuales siguen una distribución normal, por lo que un p-valor por debajo de 0.05 lleva a rechazarla. En Vivienda 1 se rechaza la normalidad de los residuales (p < 0.05). En Vivienda 2 también se rechaza la normalidad de los residuales (p < 0.05). En ambos casos los QQ-plots muestran el mismo patrón: ajuste razonable en la parte central pero colas más pesadas que las de una normal, típico de precios inmobiliarios con propiedades atípicamente costosas. Sugerencia para ambos modelos: modelar \(\log(\text{preciom})\).
  • Homocedasticidad (Breusch-Pagan): la hipótesis nula es varianza constante del error. En Vivienda 1 se rechaza la homocedasticidad: hay evidencia de heterocedasticidad. En Vivienda 2 también se rechaza la homocedasticidad. Esto es coherente con la forma de “cono” observada en los gráficos de “Residuales vs. Ajustados” de ambos modelos: el error crece con el precio ajustado, es decir, los modelos son menos precisos prediciendo viviendas caras que económicas. Sugerencia: errores estándar robustos (HC) o transformar la variable respuesta.
  • Independencia (Durbin-Watson): la hipótesis nula es ausencia de autocorrelación entre residuales consecutivos según el orden de las filas. Los valores del estadístico DW en ambos modelos (1.639 en Vivienda 1, 1.55 en Vivienda 2) están cercanos a 2, por lo que la magnitud de cualquier autocorrelación detectada es pequeña en términos prácticos. Además, ambas bases son datos de corte transversal sin orden temporal o espacial inherente —el resultado depende del orden arbitrario en que quedaron las filas—, por lo que un p-valor significativo no debe interpretarse como evidencia relevante de violación del supuesto de independencia, a diferencia de un verdadero análisis de series de tiempo.
  • Multicolinealidad (VIF): en ambos modelos todos los valores de VIF están muy por debajo del umbral de alerta de 5, por lo que no hay evidencia de multicolinealidad relevante entre los predictores. En los dos casos el predictor con mayor VIF es banios, moderadamente correlacionado con área construida y habitaciones, pero sin generar redundancia estadística problemática.

Nota: siguiendo el alcance de este informe, no se corrigen los supuestos aquí, pero se documentan las alternativas de mejora anteriores (log-precio, errores robustos, inclusión de barrio como variable) para ambos modelos.

7.9 Predicción del precio para las solicitudes

La solicitud de Vivienda 1 corresponde a una casa en Zona Norte con área de 200 m², 1 parqueadero, 2 baños, 4 habitaciones, y estrato 4 o 5. La solicitud de Vivienda 2 corresponde a un apartamento en Zona Sur con área de 300 m², 3 parqueaderos, 3 baños, 5 habitaciones, y estrato 5 o 6 (en ambos casos se evalúan los dos estratos dado que la solicitud no especifica uno único).

nueva_vivienda1 <- data.frame(
  areaconst     = 200,
  estrato       = c(4, 5),
  habitaciones  = 4,
  parqueaderos  = 1,
  banios        = 2
)

nueva_vivienda2 <- data.frame(
  areaconst     = 300,
  estrato       = c(5, 6),
  habitaciones  = 5,
  parqueaderos  = 3,
  banios        = 3
)

pred_base1 <- predict(modelo_base1, newdata = nueva_vivienda1, interval = "prediction")
pred_base2 <- predict(modelo_base2, newdata = nueva_vivienda2, interval = "prediction")

tabla_pred_base1 <- cbind(Vivienda = "Vivienda 1 (Casa, Z. Norte)", nueva_vivienda1, round(pred_base1, 1))
tabla_pred_base2 <- cbind(Vivienda = "Vivienda 2 (Apto, Z. Sur)", nueva_vivienda2, round(pred_base2, 1))

tabla_pred_comparada <- bind_rows(tabla_pred_base1, tabla_pred_base2)
Tabla 13. Precio estimado (millones $) para las solicitudes de Vivienda 1 y Vivienda 2, según estrato
Vivienda Área (m²) Estrato Habitaciones Parqueaderos Baños Precio estimado Límite inferior Límite superior
1…1 Vivienda 1 (Casa, Z. Norte) 200 4 4 1 2 317.7 9.0 626.5
Vivienda 1 (Casa, Z. Norte) 200 5 4 1 2 396.5 87.1 705.8
1…3 Vivienda 2 (Apto, Z. Sur) 300 5 5 3 3 698.1 514.9 881.3
Vivienda 2 (Apto, Z. Sur) 300 6 5 3 3 755.6 572.4 938.8

Para Vivienda 1, el modelo predice un precio esperado de 318 millones en estrato 4 y de 396 millones en estrato 5; comparado con el crédito preaprobado de $350 millones, el estrato 4 deja un margen razonable, mientras que el estrato 5 se acerca más al límite del crédito. Para Vivienda 2, el modelo predice un precio esperado de 698 millones en estrato 5 y de 756 millones en estrato 6, frente a un crédito preaprobado de $850 millones; en ambos casos los intervalos de predicción son amplios, consistente con el \(R^2\) moderado de los dos modelos.

7.10 Ofertas potenciales dentro del presupuesto

7.10.1 Vivienda 1 (Casas, Zona Norte)

Se buscan ofertas reales dentro de base1 que respeten el techo de $350 millones y se ajusten a un rango razonable alrededor del área y el número de habitaciones solicitados.

ofertas_base1 <- base1 %>%
  filter(
    preciom <= 350,
    estrato %in% c(4, 5),
    areaconst >= 150, areaconst <= 260,
    habitaciones >= 3, habitaciones <= 5
  ) %>%
  arrange(desc(preciom))

n_ofertas_base1 <- nrow(ofertas_base1)
Tabla 14. Cinco ofertas potenciales para Vivienda 1 (casas, Zona Norte, presupuesto ≤ $350M)
Barrio Estrato Precio (millones) Área (m²) Habitaciones Parqueaderos Baños
el bosque 5 350 200 4 3 3
el bosque 5 350 203 5 2 2
la flora 5 350 160 3 2 3
la flora 5 350 158 3 2 4
la flora 5 350 190 3 1 3
leaflet(top_ofertas_base1) %>%
  addProviderTiles(providers$Esri.WorldGrayCanvas) %>%
  addMarkers(
    lng = ~longitud, lat = ~latitud,
    popup = ~paste0("<b>Barrio: </b>", barrio,
                     "<br><b>Precio: </b>", preciom, " millones",
                     "<br><b>Área: </b>", areaconst, " m²",
                     "<br><b>Estrato: </b>", estrato,
                     "<br><b>Habitaciones: </b>", habitaciones,
                     "<br><b>Parqueaderos: </b>", parqueaderos,
                     "<br><b>Baños: </b>", banios),
    label = ~paste0("$", preciom, "M — ", barrio)
  )

Figura 6. Ubicación geográfica de las cinco ofertas potenciales recomendadas para Vivienda 1

Se identificaron 43 ofertas dentro de base1 que cumplen simultáneamente el presupuesto y un rango razonable de área y habitaciones alrededor de lo solicitado. Las cinco opciones de mayor precio —las más cercanas al techo de presupuesto, generalmente asociadas a mejores acabados o ubicaciones— se listan en la tabla y se georreferencian en el mapa anterior. Al revisarlas, conviene discutir explícitamente:

  • Dispersión geográfica: si las cinco ofertas se concentran en pocos barrios o están repartidas por distintos sectores de la Zona Norte, lo cual afecta la decisión final según preferencias de cercanía a trabajo, vías principales o servicios.
  • Relación estrato–presupuesto: dado que el modelo sugiere que estrato 5 se acerca más al límite del crédito preaprobado, se recomienda priorizar las ofertas de estrato 4 dentro de esta lista, ya que ofrecen mayor margen de negociación sin sacrificar significativamente el área o el número de habitaciones solicitado.
  • Coherencia con la predicción: vale la pena verificar si el precio real de estas ofertas cae dentro (o cerca) del intervalo de predicción calculado en la sección anterior; ofertas con precios muy por debajo de lo esperado por el modelo podrían indicar oportunidades particularmente buenas, mientras que precios muy por encima ameritan revisar si hay atributos no capturados por el modelo (remodelaciones, vista, etc.) que justifiquen el sobreprecio.

7.10.2 Vivienda 2 (Apartamentos, Zona Sur)

Se buscan ofertas reales dentro de base2 que respeten el techo de $850 millones y se ajusten a un rango razonable alrededor del área y el número de habitaciones solicitados.

ofertas_base2 <- base2 %>%
  filter(
    preciom <= 850,
    estrato %in% c(5, 6),
    areaconst >= 250, areaconst <= 350,
    habitaciones >= 4, habitaciones <= 6
  ) %>%
  arrange(desc(preciom))

n_ofertas_base2 <- nrow(ofertas_base2)
Tabla 15. Cinco ofertas potenciales para Vivienda 2 (apartamentos, Zona Sur, presupuesto ≤ $850M)
Barrio Estrato Precio (millones) Área (m²) Habitaciones Parqueaderos Baños
el ingenio 6 700 250 5 2 4
seminario 5 670 300 6 3 5
ciudadela pasoancho 5 650 275 5 2 5
seminario 5 530 256 5 3 5
cuarto de legua 5 520 320 4 2 4
leaflet(top_ofertas_base2) %>%
  addProviderTiles(providers$Esri.WorldGrayCanvas) %>%
  addMarkers(
    lng = ~longitud, lat = ~latitud,
    popup = ~paste0("<b>Barrio: </b>", barrio,
                     "<br><b>Precio: </b>", preciom, " millones",
                     "<br><b>Área: </b>", areaconst, " m²",
                     "<br><b>Estrato: </b>", estrato,
                     "<br><b>Habitaciones: </b>", habitaciones,
                     "<br><b>Parqueaderos: </b>", parqueaderos,
                     "<br><b>Baños: </b>", banios),
    label = ~paste0("$", preciom, "M — ", barrio)
  )

Figura 7. Ubicación geográfica de las cinco ofertas potenciales recomendadas para Vivienda 2

Se identificaron 12 ofertas dentro de base2 que cumplen simultáneamente el presupuesto y un rango razonable de área y habitaciones alrededor de lo solicitado. Las cinco opciones de mayor precio se listan en la tabla y se georreferencian en el mapa anterior. Al revisarlas, conviene discutir los mismos tres puntos que para Vivienda 1: dispersión geográfica de las ofertas, relación estrato–presupuesto (priorizando estrato 5 por dejar mayor margen frente al crédito de $850M que estrato 6), y coherencia del precio real frente al intervalo de predicción calculado arriba.