# 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)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.
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.
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.
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.
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.
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"))| 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 |
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
## Registros después de eliminar filas con 12 o más valores faltantes: 8319
## Registros eliminados: 3
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
## NA en parqueaderos después de imputar: 0
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
## Registros después de excluir valores inválidos: 8243
## Registros excluidos: 76
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
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.
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
## Barrios únicos después de normalizar: 404
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
## base2 (Apartamentos, Zona Sur): 2777 registros
knitr::kable(head(base1, 3),
caption = tab_cap("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)"))| 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 |
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"))| Base | Tipo | Zona | Registros |
|---|---|---|---|
| base1 (Casas, Zona Norte) | Casa | Zona Norte | 700 |
| base2 (Apartamentos, Zona Sur) | Apartamento | Zona Sur | 2777 |
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.
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
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_finalFigura 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.
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).
| 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 |
| 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.
| 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 |
| 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
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
##
## 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
| 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:
areaconst: para un área fija, más
habitaciones suele implicar cuartos más pequeños y no necesariamente
mayor valor.| Vivienda | R² | 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.
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
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)| 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 |
| 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 |
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.
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)| 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.
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)| 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:
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)| 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.