Introducción

Este informe presenta un análisis detallado de la oferta inmobiliaria caleña, con el objetivo de identificar y proponer alternativas de vivienda en respuesta a dos solicitudes específicas. Para ello, se emplea una base de datos que contiene características clave de los inmuebles como la ubicación, el tamaño, el precio, el estrato socioeconómico, el número de habitaciones, baños, parqueaderos y el tipo de vivienda. Sobre esta información se aplican técnicas de ciencia de datos para la limpieza, el análisis exploratorio y el modelado, lo que permite obtener predicciones del precio de los inmuebles. Finalmente, se incluyen tablas y mapas interactivos que facilitan la visualización de las ofertas seleccionadas y respaldan la toma de decisiones de acuerdo con las necesidades del cliente.

# Instalar y cargar el paquete
if (!require("paqueteMODELOS")) {
  remotes::install_github("centromagis/paqueteMODELOS")
}

# Cargar datos
data("vivienda")

Preprocesamiento de los datos

Para la preparación y limpieza de la base de datos primero hacemos una inspección inicial de la estructura del dataset.

# Tabla con estructura de los datos
Estructura <- data.frame(
Variable = names(vivienda),
Tipo = sapply(vivienda, function(x) paste(class(x), collapse=",")),
Ejemplo = sapply(vivienda, function(x) paste(utils::head(x, 2), collapse = ", "))
)

knitr::kable(Estructura, caption = "Estructura del dataset original")
Estructura del dataset original
Variable Tipo Ejemplo
id id numeric 1147, 1169
zona zona character Zona Oriente, Zona Oriente
piso piso character NA, NA
estrato estrato numeric 3, 3
preciom preciom numeric 250, 320
areaconst areaconst numeric 70, 120
parqueaderos parqueaderos numeric 1, 1
banios banios numeric 3, 2
habitaciones habitaciones numeric 6, 3
tipo tipo character Casa, Casa
barrio barrio character 20 de julio, 20 de julio
longitud longitud numeric -76.51168, -76.51237
latitud latitud numeric 3.43382, 3.43369

Seguido, convertimos a factor las variables categóricas clave (zona, tipo, barrio, estrato) para evitar interpretarlas como numéricas por error en analisis posteriores. Además, eliminamos registros duplicados, eliminamos variables no necesarias para el análisis como la columna id e identificamos los valores faltantes en cada variable.

# Identificar los duplicados
num_duplicados <- sum(duplicated(vivienda))
removed_pct <- 100 * num_duplicados / nrow(vivienda)

knitr::kable(
  data.frame(
    Registros = nrow(vivienda),
    Duplicados = num_duplicados
  ),
  caption = "Conteo de duplicados"
)
Conteo de duplicados
Registros Duplicados
8322 1
# Eliminar duplicados conservando la primera aparición
ev_before <- nrow(vivienda)
vivienda <- dplyr::distinct(vivienda)
ev_after <- nrow(vivienda)

# Eliminar la variable id
vivienda <- vivienda %>% select(-id)

# Conteo y % de NAs por columna
na_tbl <- vivienda %>%
  dplyr::summarise(dplyr::across(dplyr::everything(), ~ sum(is.na(.)))) %>%
  tidyr::pivot_longer(dplyr::everything(), names_to = "Variable", values_to = "NA_n") %>%
  dplyr::mutate(Total = nrow(vivienda), NA_pct = round(100 * NA_n / Total, 2)) %>%
  dplyr::arrange(dplyr::desc(NA_pct))

knitr::kable(na_tbl, caption = "Valores faltantes por variable")
Valores faltantes por variable
Variable NA_n Total NA_pct
piso 2637 8321 31.69
parqueaderos 1604 8321 19.28
zona 2 8321 0.02
estrato 2 8321 0.02
areaconst 2 8321 0.02
banios 2 8321 0.02
habitaciones 2 8321 0.02
tipo 2 8321 0.02
barrio 2 8321 0.02
longitud 2 8321 0.02
latitud 2 8321 0.02
preciom 1 8321 0.01

Primero, se eliminan las NAs de variables con pocos valores faltantes. En el caso de La variable piso, se decide excluirla del análisis debido a que no presenta una definición clara y consistente de esta en el dataset: en apartamentos, el valor parece indicar el piso en el que está ubicada la vivienda, mientras que en casas podría referirse al número total de pisos (con valores atipicos > 4) o a otra característica no especificada. Esta ambigüedad, junto a su numerosa cantidad de faltantes (31.69%) impide su uso confiable en los análisis posteriores. Por otro laso, para tratar la variable parqueaderos se utiliza una imputación mediante MICE.

# Eliminar las NAs de variables con pocos valores faltantes
columnas_limpiar <- c("zona", "estrato", "preciom", "areaconst", "banios", "latitud","longitud", "habitaciones", "tipo")

vivienda_limpia1 <- vivienda %>%
  filter(if_all(all_of(columnas_limpiar), ~ !is.na(.)))

# Eliminar la variable piso
vivienda_limpia2 <- vivienda_limpia1 %>% select(-piso)

# Seleccionar variables relevantes para la imputación
vars_imputacion <- c("parqueaderos", "areaconst", "preciom", "habitaciones", "banios")

# Imputación con MICE
set.seed(123)
mice_data <- mice(vivienda_limpia2[, vars_imputacion], 
                  m = 1,              # número de imputaciones
                  method = "pmm",     # predictive mean matching
                  maxit = 5,          # iteraciones
                  seed = 123)
## 
##  iter imp variable
##   1   1  parqueaderos
##   2   1  parqueaderos
##   3   1  parqueaderos
##   4   1  parqueaderos
##   5   1  parqueaderos
vivienda_mice <- vivienda_limpia2
vivienda_mice[, vars_imputacion] <- complete(mice_data)
vivienda_mice$parqueaderos <- round(vivienda_mice$parqueaderos, 0)

Finalmente, para el tratamiento de outliers, se obtiene un analisis inicial por variable y se analiza si los atípicos de variables como habitaciones, baños y parqueaderos se asocian con propiedades de alta gama (mayor área/precio), pues si esto se cumple, se considera no eliminarlos para no perder información relevante del mercado alto.

# Función para detectar outliers y generar tabla
detectar_outliers_tabla <- function(df) {
  num_vars <- df[, sapply(df, is.numeric), drop = FALSE]
  
  resumen <- data.frame(Variable = character(), N_outliers = integer())
  
  for (var in names(num_vars)) {
    datos <- num_vars[[var]]
    
    Q1 <- quantile(datos, 0.25, na.rm = TRUE)
    Q3 <- quantile(datos, 0.75, na.rm = TRUE)
    IQR_val <- Q3 - Q1
    
    lim_inf <- Q1 - 1.5 * IQR_val
    lim_sup <- Q3 + 1.5 * IQR_val
    
    outliers <- sum(datos < lim_inf | datos > lim_sup, na.rm = TRUE)
    
    resumen <- rbind(resumen, data.frame(Variable = var, N_outliers = outliers))
  }
  
  # Ordenar de mayor a menor cantidad de outliers
  resumen[order(-resumen$N_outliers), , drop = FALSE]
}

# Mostrar tabla
tabla_outliers <- detectar_outliers_tabla(vivienda_mice)

knitr::kable(
  tabla_outliers,
  caption = "Conteo de outliers por variable"
)
Conteo de outliers por variable
Variable N_outliers
6 habitaciones 888
4 parqueaderos 630
2 preciom 552
3 areaconst 382
7 longitud 130
5 banios 72
1 estrato 0
8 latitud 0
# Función para obtener índices de outliers
get_outlier_idx <- function(x) {
  Q1 <- quantile(x, 0.25, na.rm = TRUE)
  Q3 <- quantile(x, 0.75, na.rm = TRUE)
  IQR_val <- Q3 - Q1
  lower <- Q1 - 1.5 * IQR_val
  upper <- Q3 + 1.5 * IQR_val
  which(x < lower | x > upper)
}

# Variables a evaluar
vars_check <- c("habitaciones", "banios", "parqueaderos")

# Usar el dataset imputado con MICE
df <- vivienda_mice

# Detectar y comparar
for (var in vars_check) {
  
  idx_outliers <- get_outlier_idx(df[[var]])
  
  cat("Variable:", var)
  cat("Número de outliers:", length(idx_outliers))
  
  # Revisar si coinciden con precios altos o área alta
  summary_precio <- summary(df$preciom[idx_outliers])
  summary_area   <- summary(df$areaconst[idx_outliers])
  
  cat("Resumen PrecioM en outliers:")
  print(summary_precio)
  
  cat("Resumen AreaConst en outliers:")
  print(summary_area)
  
  # Porcentaje de outliers que están en el 30% superior de precio o área
  p_precio <- mean(df$preciom[idx_outliers] > quantile(df$preciom, 0.7, na.rm = TRUE))
  p_area   <- mean(df$areaconst[idx_outliers] > quantile(df$areaconst, 0.7, na.rm = TRUE))
  
  cat(sprintf("Porcentaje de outliers con PrecioM alto (>70%%): %.1f%%\n", 100 * p_precio))
  cat(sprintf("Porcentaje de outliers con AreaConst alta (>70%%): %.1f%%\n", 100 * p_area))
}
## Variable: habitacionesNúmero de outliers: 888Resumen PrecioM en outliers:   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##    89.0   310.0   425.0   531.8   650.0  1940.0 
## Resumen AreaConst en outliers:   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##    35.0   200.0   296.0   321.2   410.0  1600.0 
## Porcentaje de outliers con PrecioM alto (>70%): 42.1%
## Porcentaje de outliers con AreaConst alta (>70%): 73.2%
## Variable: baniosNúmero de outliers: 72Resumen PrecioM en outliers:   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   175.0   586.2   740.0   902.8  1300.0  1940.0 
## Resumen AreaConst en outliers:   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   128.0   323.8   450.0   458.1   537.8   910.0 
## Porcentaje de outliers con PrecioM alto (>70%): 86.1%
## Porcentaje de outliers con AreaConst alta (>70%): 95.8%
## Variable: parqueaderosNúmero de outliers: 630Resumen PrecioM en outliers:   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##     190     720     950    1018    1300    1999 
## Resumen AreaConst en outliers:   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##    50.0   285.2   356.0   412.1   494.2  1586.0 
## Porcentaje de outliers con PrecioM alto (>70%): 91.7%
## Porcentaje de outliers con AreaConst alta (>70%): 93.0%

Ya que una proporción sustancial de outliers cae en los tramos superiores de precio y área, se sugiere que representan viviendas de alta gama y deben conservarse. El dataset final se guarda como vivienda_final, quedando con las siguientes dimensiones:

# Dataset final
vivienda_final <- vivienda_mice

# Resumen final
knitr::kable(
data.frame(
Registros = nrow(vivienda_final),
Variables = ncol(vivienda_final)
), caption = "Dimensiones del dataset final")
Dimensiones del dataset final
Registros Variables
8319 11

Análisis exploratorio de los datos

Para el análisis inicial, estudiamos la correlación de la variable precio con el resto de variables numéricas.

# Correlación entre precio y variables numéricas
vars <- vivienda_final %>%
  select(preciom, areaconst, banios, habitaciones, parqueaderos)

ct <- psych::corr.test(vars, use = "pairwise", method = "pearson")

corrplot(ct$r,
         method = "color",
         type = "lower",
         addCoef.col = "black",
         tl.col = "black",
         number.cex = .7,
         p.mat = ct$p,
         sig.level = 0.05,
         insig = "blank")

El gráfico nos muestra una alta correlación positiva significativa entre la variable precio con el area construida, el número de baños y los parqueaderos, mostrando que estas son caracteristicas determinantes para explicar la dinámica de precios en el mercado inmobiliario analizado.

p_violin <- plot_ly(
  vivienda_final,
  x = ~estrato,
  y = ~preciom,
  type = "violin",
  points = "all",                # muestra puntos
  box = list(visible = TRUE),    # añade boxplot dentro
  meanline = list(visible = TRUE)
) %>%
  plotly::layout(
    title = "Precio por estrato",
    xaxis = list(title = "Estrato"),
    yaxis = list(title = "Precio")
  )

p_violin

El gráfico muestra que el precio aumenta claramente con el estrato: las medianas y cuartiles son más altos a medida que sube el estrato. También, se presenta una mayor dispersión (violines más anchos y colas largas) en estratos altos, donde aparecen outliers de precio elevado. Aun así, se observa cierto solapamiento entre estratos vecinos, lo que sugiere que el estrato no explica por sí solo el precio y pueden influir, como se vio en la matriz de correlación, otros factores como el área, el número de baños y parqueaderos, entre otros.

# Boxplot interactivo Precio vs Zona
p_zona <- plot_ly(
  data = vivienda_final,
  x = ~zona,
  y = ~preciom,
  type = "box",
  boxpoints = "all",
  jitter = 0.35,
  pointpos = -1.6,
  marker = list(size = 5, opacity = 0.35),
  hovertemplate = "Zona: %{x}<br>Precio: $%{y:,.0f}<extra></extra>",
  color = ~zona,
  showlegend = FALSE
) %>%
  layout(
    title = "Distribución del precio por zona",
    xaxis = list(title = "Zona"),
    yaxis = list(title = "Precio", tickformat = ",.0f")
  )

p_zona

El gráfico muestra que la zona es un factor clave en la determinación del precio de la vivienda. Mientras que la Zona Oriente concentra los valores más bajos, las Zonas Norte y Oeste presentan precios más elevados y una amplia dispersión, lo que sugiere que pueden coexistir en la misma zona, viviendas de diferentes segmentos (propiedades de lujo y propiedades más económicas). Por su parte, la Zona Centro y Zona Sur exhiben precios intermedios, con el Centro más homogéneo y el Sur más heterogéneo.

Filtrado y mapeo de la base de datos

Se filtra la base de datos según las caracteristicas de Zona y Tipo de cada solicitud. A continuación se presentan algunos ejemplos para cada solicitud y un mapa interactivo que nos ayuda a visualizar la ubicación de las viviendas obtenidas en cada filtro:

# Filtrar datos
base1 <- vivienda_final %>% 
  filter(tipo == "Casa" & zona == "Zona Norte")

# Mostrar tabla
kable(
  head(base1, 3),
  caption = "Resultados Solicitud 1"
)
Resultados Solicitud 1
zona estrato preciom areaconst parqueaderos banios habitaciones tipo barrio longitud latitud
Zona Norte 5 320 150 2 4 6 Casa acopi -76.51341 3.47968
Zona Norte 5 780 380 2 3 3 Casa acopi -76.51674 3.48721
Zona Norte 6 750 445 3 7 6 Casa acopi -76.52950 3.38527
# Filtrar datos
base2 <- vivienda_final %>% 
  filter(tipo == "Apartamento" & zona == "Zona Sur")

# Mostrar tabla
kable(
  head(base1, 3),
  caption = "Resultados Solicitud 2"
)
Resultados Solicitud 2
zona estrato preciom areaconst parqueaderos banios habitaciones tipo barrio longitud latitud
Zona Norte 5 320 150 2 4 6 Casa acopi -76.51341 3.47968
Zona Norte 5 780 380 2 3 3 Casa acopi -76.51674 3.48721
Zona Norte 6 750 445 3 7 6 Casa acopi -76.52950 3.38527
# Filtrar coordenadas válidas
base1_map <- base1 %>% filter(!is.na(latitud), !is.na(longitud))
base2_map <- base2 %>% filter(!is.na(latitud), !is.na(longitud))

# Crear mapa interactivo
leaflet() %>%
  addProviderTiles(providers$CartoDB.Positron) %>%

  # Solicitud 1
  addCircleMarkers(
    data = base1_map,
    lng = ~longitud, lat = ~latitud,
    radius = 6, stroke = FALSE, fillOpacity = 0.75,
    color = "#1f77b4",
    label = ~paste0(
      "Zona: ", zona,
      "<br>Tipo: ", tipo,
      "<br>Precio: $", comma(preciom)
    ),
    group = "Resultados Solicitud 1"
  ) %>%

  # Solicitud 2
  addCircleMarkers(
    data = base2_map,
    lng = ~longitud, lat = ~latitud,
    radius = 6, stroke = FALSE, fillOpacity = 0.75,
    color = "#ff7f50",
    label = ~paste0(
      "Zona: ", zona,
      "<br>Tipo: ", tipo,
      "<br>Precio: $", comma(preciom)
    ),
    group = "Resultados Solicitud 2"
  ) %>%

  # Control de capas
  addLayersControl(
    overlayGroups = c("Resultados Solicitud 1", "Resultados Solicitud 2"),
    options = layersControlOptions(collapsed = FALSE)
  ) %>%

  # Leyenda
  addLegend(
    position = "bottomright",
    colors = c("#1f77b4", "#ff7f50"),
    labels = c("Resultados Solicitud 1", "Resultados Solicitud 2"),
    title = "Solicitudes"
  )

El mapa confirma una mayor concentración de inmuebles en el Norte para la Solicitud 1 y en el Sur para la Solicitud 2, lo que concuerda con los filtros aplicados; sin embargo, también se observan puntos fuera de las zonas esperadas, lo que puede atribuirse a problemas de calidad en los datos como errores de geocodificación, inconsistencias en la variable zona, registros ubicados en límites administrativos o errores de digitación. Debido a esto, en análisis futuros se recomienda realizar una revision de la base de datos que permitra validar la correspondencia espacial antes de realizar análisis más detallados.

Modelo de Regresión Lineal Multiple

Se ajusta un modelo de regresión lineal multiple para la predicción del precio de las viviendas usando como variables predictoras el area construida, el estrato, el número de habitaciones, parqueaderos y baños .

# Division de datos en Train/Test (70/30)
n <- nrow(vivienda_final)
idx_train <- sample(seq_len(n), size = floor(0.7 * n))
train <- vivienda_final[idx_train, ]
test  <- vivienda_final[-idx_train, ]


# Ajuste del modelo

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


# Coeficientes (tabla)

tabla_resultados <- tidy(modelo) %>%
  mutate(
    estimate   = round(estimate, 2),
    std.error  = round(std.error, 2),
    statistic  = round(statistic, 2),
    p.value    = formatC(p.value, format = "f", digits = 6)
  ) %>%
  rename(
    Variable         = term,
    Coeficiente      = estimate,
    `Error Estándar` = std.error,
    `Estadístico t`  = statistic,
    `Valor p`        = p.value
  )

kable(
  tabla_resultados,
  caption = "Resultados del modelo de regresión lineal múltiple (entrenamiento)"
)
Resultados del modelo de regresión lineal múltiple (entrenamiento)
Variable Coeficiente Error Estándar Estadístico t Valor p
(Intercept) -353.82 13.25 -26.71 0.000000
areaconst 0.86 0.02 37.98 0.000000
estrato 94.46 2.73 34.63 0.000000
habitaciones -25.49 2.21 -11.54 0.000000
parqueaderos 75.10 2.73 27.46 0.000000
banios 51.23 2.59 19.75 0.000000
# Métricas de ajuste

tabla_metricas <- glance(modelo) %>%
  mutate(
    r.squared     = round(r.squared, 4),
    adj.r.squared = round(adj.r.squared, 4)
  ) %>%
  select(r.squared, adj.r.squared) %>%
  rename(
    `` = r.squared,
    `R² Ajustado` = adj.r.squared
  )

kable(
  tabla_metricas,
  caption = "Métricas de ajuste del modelo"
)
Métricas de ajuste del modelo
R² Ajustado
0.738 0.7378
# Predicciones en datos de prueba
pred_test <- predict(modelo, newdata = test)

# Indicadores de rendimiento en datos de prueba
rmse_val <- rmse(actual = test$preciom, predicted = pred_test)
mae_val  <- mae(actual = test$preciom, predicted = pred_test)
mape_val <- mape(actual = test$preciom, predicted = pred_test) * 100  # en %

tabla_desempeno <- data.frame(
  RMSE = round(rmse_val, 3),
  MAE  = round(mae_val, 3),
  MAPE = paste0(round(mape_val, 2), " %")
)

kable(
  tabla_desempeno,
  caption = "Desempeño predictivo en el set de prueba"
)
Desempeño predictivo en el set de prueba
RMSE MAE MAPE
174.6 108.693 27.42 %

El modelo ajustado muestra que todas las variables incluidas son estadísticamente significativas en la explicación del precio de la vivienda. El coeficiente de área construida indica que por cada metro cuadrado adicional, el precio aumenta en promedio 0.86 millones de pesos, lo cual confirma la importancia del tamaño en la valorización del inmueble. En el caso del estrato, el numero de parqueaderos y de baños, se observa que un incremento de una unidad se asocia con un aumento de 94.46, 75.10 y 51.23 millones en el precio, respectivamente, lo que refleja la fuerte influencia del nivel socioeconómico, la ubicación y las caracteristicas que agregan comodidad al inmueble. Por otro lado, el número de habitaciones presenta un coeficiente negativo (–25.49 millones), lo cual puede explicarse porque, al controlar por área construida y número de baños, las habitaciones adicionales no implican necesariamente un mayor valor de mercado. En otras palabras, el modelo sugiere que más cuartos no siempre se traducen en un precio más alto si no están acompañados de mayor área, baños o parqueaderos.

El modelo presenta un R² de 0.7338, lo que significa que aproximadamente el 73.4% de la variabilidad del precio de la vivienda se explica por las variables incluidas para el análisis. El R² ajustado, que penaliza por el número de variables, es prácticamente igual, lo que indica que todas las variables aportan de manera consistente al modelo y no hay sobreajuste evidente.

La métricas obtenidas en el set de prueba muestran un MAE de 108.7 millones, lo que significa que, en promedio, las predicciones del modelo se desvían del valor real de la vivienda en alrededor de 109 millones. El MAPE de 27.4% indica que el error relativo de predicción es cercano a un tercio del precio real, lo cual refleja un ajuste moderado, es decir, el modelo logra capturar tendencias generales del precio, pero presenta limitaciones para predecir con alta precisión casos individuales.

A continuación se evaluan los supuestos del modelo:

Supuesto de linealidad y homocedasticidad

# Gráfico residuos vs ajustados
plot(modelo, which = 1)

# Prueba de homocedasticidad
library(lmtest)
bptest(modelo)
## 
##  studentized Breusch-Pagan test
## 
## data:  modelo
## BP = 940.28, df = 5, p-value < 2.2e-16

El gráfico de residuos vs valores ajustados muestra cierta curvatura y un aumento en la dispersión a medida que crece el valor predicho, lo que indica problemas de linealidad y heterocedasticidad. Esto se confirma con la prueba de Breusch-Pagan (p-valor < 0.05), que rechaza la hipótesis de homocedasticidad.

Supuesto de normalidad de los residuos

# Extraer residuos del modelo
residuos <- resid(modelo)

# QQ plot
plot(modelo, which = 2)

# Prueba de Kolmogorov-Smirnov para normalidad
ks.test(scale(residuos), "pnorm")
## 
##  Asymptotic one-sample Kolmogorov-Smirnov test
## 
## data:  scale(residuos)
## D = 0.13628, p-value < 2.2e-16
## alternative hypothesis: two-sided

El gráfico Q-Q evidencia desviaciones sistemáticas de la línea diagonal, particularmente en las colas de la distribución, lo cual sugiere el incumplimiento del supuesto de normalidad de los residuos. Este hallazgo se corrobora estadísticamente mediante la prueba de Kolmogorov-Smirnov (p-valor < 0.05), que lleva al rechazo de la hipótesis nula de normalidad en la distribución de los residuos.

Supuesto de no autocorrelación de los errores

dwtest(modelo)
## 
##  Durbin-Watson test
## 
## data:  modelo
## DW = 1.9904, p-value = 0.3562
## alternative hypothesis: true autocorrelation is greater than 0

El estadístico Durbin-Watson obtenido fue DW = 1.9904 con un p-valor = 0.3562, lo que indica que no existe evidencia de autocorrelación en los residuos. Por lo tanto, se cumple el supuesto de independencia, y los errores pueden considerarse aleatorios, sin patrones de correlación.

Supuesto de multicolinealidad

vif(modelo)
##    areaconst      estrato habitaciones parqueaderos       banios 
##     2.121480     1.622302     2.025966     1.855657     2.852366

Todos los valores de de VIF son menore a 5, lo que indica que no existe multicolinealidad significativa entre las variables y que cada una aporta informacion distinta al modelo.

Recomendaciones generales

Dado que el modelo presenta problemas de linealidad, heterocedasticidad y falta de normalidad en los residuos, se recomienda considerar transformaciones de la variable dependiente (por ejemplo, aplicar logaritmo al precio), utilizar errores estándar robustos para mitigar la heterocedasticidad o explorar modelos alternativos más flexibles, como la regresión robusta o los modelos generalizados (GLM), que permitan captar relaciones no lineales y mejorar la validez de las inferencias.

Predicción para la primera solicitud

Utilizando el modelo previamente ajustado, se hace la predicción según las caracteristicas solicitadas:

  • Tipo: Casa

  • Area construida: 200 m2

  • Parqueaderos: 1

  • Baños: 2

  • Habitaciones: 4

  • Estrato: 4 o 5

  • Zona: Norte

  • Precio: 350 millones

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

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

pred1 <- predict(modelo, newdata = vivienda1)
pred2 <- predict(modelo, newdata = vivienda2)

cat("Precio predicho para la vivienda con estrato 4:", pred1, "millones de pesos")
## Precio predicho para la vivienda con estrato 4: 271.3301 millones de pesos
cat("Precio predicho para la vivienda con estrato 5:", pred2, "millones de pesos")
## Precio predicho para la vivienda con estrato 5: 365.7948 millones de pesos

Con un crédito preaprobado de 350 millones, únicamente la alternativa con estrato 4 se ajusta al presupuesto. En consecuencia, se sugieren cinco candidatas priorizando aquellas con valores más cercanos al precio, área construida, al número de habitaciones por considerarse las características más esenciales en la decisión de compra.

AREA_OBJ  <- 200
HAB_OBJ   <- 4
BAN_OBJ   <- 2
PARK_OBJ  <- 1
ESTR_OBJ  <- 4
BUDGET    <- 350

pred_ref <- predict(modelo, newdata = data.frame(
  areaconst   = AREA_OBJ,
  estrato           = ESTR_OBJ,
  habitaciones  = HAB_OBJ,
  parqueaderos      = PARK_OBJ,
  banios         = BAN_OBJ
))

# Candidatas: Casa, Zona Norte y dentro del presupuesto
candidatas <- vivienda_final %>%
  filter(
    tipo  == "Casa",
    zona  == "Zona Norte",
    !is.na(preciom),
    preciom <= BUDGET
  ) %>%
  mutate(
    # Puntuación de cercanía a la solicitud
   score = 
  0.35*abs(preciom - as.numeric(pred_ref) +
  0.35*abs(areaconst - AREA_OBJ) +
  0.20*abs(habitaciones - HAB_OBJ) +
  0.05*abs(banios - BAN_OBJ) +
  0.05*abs(parqueaderos - PARK_OBJ))
  ) %>%
  arrange(score)

# Tomar 5 ofertas
ofertas_top <- candidatas %>% slice_head(n = 5)

# Tabla resumen de las ofertas
kable(
  ofertas_top %>% 
    select(preciom, areaconst, estrato, habitaciones, banios, parqueaderos, zona, tipo),
  caption = "Top 5 ofertas potenciales para la solicitud 1"
)
Top 5 ofertas potenciales para la solicitud 1
preciom areaconst estrato habitaciones banios parqueaderos zona tipo
270 196 3 4 2 1 Zona Norte Casa
230 82 3 6 6 2 Zona Norte Casa
250 135 3 4 3 1 Zona Norte Casa
190 435 3 0 0 1 Zona Norte Casa
253 140 4 4 3 2 Zona Norte Casa
# Mapa interactivo con ofertas
leaflet(ofertas_top) %>%
  addTiles() %>%
  addCircleMarkers(
    lng = ~longitud, lat = ~latitud,
    popup = ~paste(
      "Precio:", preciom, "M<br>",
      "Área:", areaconst, "m²<br>",
      "Hab/Baños/Parq:", habitaciones, "/", banios, "/", parqueaderos, "<br>",
      "Estrato:", estrato, "<br>",
      "Zona:", zona
    )
  )

Las cinco casas seleccionadas cumplen con el límite de 350 millones de pesos y se encuentran en la Zona Norte, de acuerdo con la solicitud. Dos de las opciones corresponden a estrato 4, que es el que mejor se ajusta al crédito disponible, mientras que el resto pertenecen a estrato 3, lo que ofrece alternativas con diferente nivel socioeconómico. En cuanto a las características físicas, todas cuentan con entre 4 y 6 habitaciones, 2 a 6 baños y al menos un parqueadero, condiciones coherentes con lo solicitado por el cliente (con una oferta atipica con 0 habitaciones y baños). Estas ofertas ofrecen opciones viables que permiten al cliente analizar con detalle los inmuebles para elegir lo que mejor balancee precio, tamaño y ubicación.

Predicción para la segunda solicitud

Utilizando el modelo previamente ajustado, se hace la predicción según las caracteristicas solicitadas:

  • Tipo: Apartamento

  • Area construida: 300 m2

  • Parqueaderos: 3

  • Baños: 3

  • Habitaciones: 5

  • Estrato: 5 o 6

  • Zona: Sur

  • Precio: 850 millones

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

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

pred3 <- predict(modelo, newdata = vivienda3)
pred4 <- predict(modelo, newdata = vivienda4)

cat("Precio predicho para la vivienda con estrato 5:", pred3, "millones de pesos")
## Precio predicho para la vivienda con estrato 5: 627.5852 millones de pesos
cat("Precio predicho para la vivienda con estrato 6:", pred4, "millones de pesos")
## Precio predicho para la vivienda con estrato 6: 722.0499 millones de pesos

Al igual que con la solicitud anterior, se hacen obtienen las mejores 5 ofertas que se muestran a continuación:

AREA_OBJ2  <- 300
HAB_OBJ2   <- 5
BAN_OBJ2   <- 3
PARK_OBJ2  <- 3
ESTR_OBJ2  <- 5
BUDGET2    <- 850

pred_ref <- predict(modelo, newdata = data.frame(
  areaconst = AREA_OBJ2,
  estrato = ESTR_OBJ2,
  habitaciones = HAB_OBJ2,
  parqueaderos = PARK_OBJ2,
  banios = BAN_OBJ2
))

# Candidatas: Casa, Zona Norte y dentro del presupuesto
candidatas2 <- vivienda_final %>%
  filter(
    tipo  == "Apartamento",
    zona  == "Zona Sur",
    !is.na(preciom),
    preciom <= BUDGET2
  ) %>%
  mutate(
    # Puntuación de cercanía a la solicitud
   score = 
  0.35*abs(preciom - as.numeric(pred_ref)) +
  0.35*abs(areaconst - AREA_OBJ2) +
  0.20*abs(habitaciones - HAB_OBJ2) +
  0.05*abs(banios - BAN_OBJ2) +
  0.05*abs(parqueaderos - PARK_OBJ2)
  ) %>%
  arrange(score)

# Tomar 5 ofertas
ofertas_top2 <- candidatas2 %>% slice_head(n = 5)

# Tabla resumen de las ofertas
kable(
  ofertas_top2 %>% 
    select(preciom, areaconst, estrato, habitaciones, banios, parqueaderos, zona, tipo),
  caption = "Top 5 ofertas potenciales para la solicitud 2"
)
Top 5 ofertas potenciales para la solicitud 2
preciom areaconst estrato habitaciones banios parqueaderos zona tipo
670 300 5 6 5 3 Zona Sur Apartamento
650 275 5 5 5 2 Zona Sur Apartamento
650 249 5 4 4 2 Zona Sur Apartamento
655 241 6 3 3 2 Zona Sur Apartamento
630 210 5 3 2 2 Zona Sur Apartamento
# Mapa interactivo con ofertas
leaflet(ofertas_top2) %>%
  addTiles() %>%
  addCircleMarkers(
    lng = ~longitud, lat = ~latitud,
    popup = ~paste(
      "Precio:", preciom, "M<br>",
      "Área:", areaconst, "m²<br>",
      "Hab/Baños/Parq:", habitaciones, "/", banios, "/", parqueaderos, "<br>",
      "Estrato:", estrato, "<br>",
      "Zona:", zona
    )
  )

Los cinco apartamentos seleccionados cumplen con los requisitos centrales de la solicitud, es decir, se ajustan al presupuesto disponible, se ubican en la Zona Sur y presentan áreas construidas y estratos acordes a lo solicitado. En cuanto a otras características como el número de habitaciones, baños y parqueaderos, las ofertas muestran una amplia variedad: algunas opciones ofrecen espacios más amplios con mayor cantidad de habitaciones y baños, mientras que otras presentan configuraciones más reducidas pero igualmente funcionales, acompañadas de menores exigencias en parqueaderos, lo que puede traducirse en ahorro en el costo total. Esto brinda al cliente la posibilidad de comparar alternativas y seleccionar la vivienda que mejor equilibre sus necesidades de espacio, comodidades y presupuesto.