Este es un analisis de compra de viviendas con las siguientes características:

tabla_viviendas <- tibble(
  Características = c(
    "Tipo",
    "área construida",
    "parqueaderos",
    "baños",
    "habitaciones",
    "estrato",
    "zona",
    "crédito preaprobado"
  ),
  `Vivienda 1` = c(
    "Casa",
    "200",
    "1",
    "2",
    "4",
    "4 o 5",
    "Norte",
    "350 millones"
  ),
  `Vivienda 2` = c(
    "Apartamento",
    "300",
    "3",
    "3",
    "5",
    "5 o 6",
    "Sur",
    "850 millones"
  )
)

knitr::kable(tabla_viviendas, caption = "Comparación de viviendas")
Comparación de viviendas
Características Vivienda 1 Vivienda 2
Tipo Casa Apartamento
área construida 200 300
parqueaderos 1 3
baños 2 3
habitaciones 4 5
estrato 4 o 5 5 o 6
zona Norte Sur
crédito preaprobado 350 millones 850 millones

Se responderá la solicitud del cliente mediante las tecnicas de modelacion. Por lo tanto este es un informe donde se analizarán los dos casos de vivienda.

Vivienda 1

Comenzaremos realizando el analisis de la Vivienda 1 (Casa, 200m2)

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

tabla_bonita(
  head(base1, 3),
  "Primeras 3 observaciones de viviendas tipo casa en Zona Norte"
)
Primeras 3 observaciones de viviendas tipo casa en 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 NA 7 6 Casa acopi -76.52950 3.38527

La base de datos se filtro para obtener viviendas tipo casa ubicadas en la zona norte de la Cali, las tablas que se muestran a continuacion permiten verificar que el filtro fue aplicado y corresponden a lo requerido

# Tablas que comprueban la consulta
tabla_bonita(
  base1 %>% count(tipo),
  "Distribución por tipo"
)
Distribución por tipo
tipo n
Casa 722
tabla_bonita(
  base1 %>% count(zona),
  "Distribución por zona"
)
Distribución por zona
zona n
Zona Norte 722
tabla_bonita(
  base1 %>% count(estrato) %>% arrange(desc(n)),
  "Distribución por estrato"
)
Distribución por estrato
estrato n
5 271
3 235
4 161
6 55
leaflet(base1) %>%
  addTiles() %>%
  addCircleMarkers(
    lng = ~longitud, lat = ~latitud,
    radius = 4, opacity = 0.8,
    popup = ~paste0("Precio: ", preciom, " M | Area: ", areaconst,
                    " | Estrato: ", estrato, " | Hab: ", habitaciones,
                    " | Baños: ", banios, " | Parq: ", parqueaderos)
  )

El mapa permite verificar la distribución geografica de las viviendas en donde se observa concentración en la zona norte de la ciudad y hay algunos puntos alejados dentro del filtro inicial

b1 <- base1 %>%
  select(preciom, areaconst, estrato, banios, habitaciones, parqueaderos) %>%
  drop_na(preciom, areaconst, estrato, banios, habitaciones, parqueaderos) %>%
  mutate(estrato = as.factor(estrato))

p1 <- plot_ly(
  data = b1, x = ~areaconst, y = ~preciom, color = ~estrato,
  type = "scatter", mode = "markers"
) %>%
  layout(title = "Vivienda 1: Precio vs Área (coloreado por estrato)",
         xaxis = list(title = "Área construida"),
         yaxis = list(title = "Precio (millones)"))
p1

El gráfico de dispersion nos muestra la relacion area vs precio directamente podemos ver una tendencia positiva que indica a mayor area aumenta el precio ademas el estrato tambien hace que las viviendas aumenten de precio.

p2 <- plot_ly(b1, x = ~estrato, y = ~preciom, type = "box") %>%
  layout(title = "Vivienda 1: Precio por estrato", xaxis = list(title="Estrato"), yaxis=list(title="Precio"))
p2

El box-plot nos permite analizar la distribución de precio segun el estrato en donde los estratos mas altos presentan precios un poco mas altos y consiste con lo natural del mercado inmobiliario

set.seed(123)
split1 <- initial_split(b1, prop = 0.70)
train1 <- training(split1)
test1  <- testing(split1)
m1 <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = train1)

# 1. Tabla de coeficientes
tabla_coeficientes <- broom::tidy(m1) %>%
  mutate(across(where(is.numeric), ~round(., 4))) %>%
  rename(
    Término = term,
    Estimación = estimate,
    `Error estándar` = std.error,
    `Estadístico t` = statistic,
    `Valor p` = p.value
  )

tabla_bonita(
  tabla_coeficientes,
  "Tabla 1. Coeficientes estimados del modelo de regresión lineal"
)
Tabla 1. Coeficientes estimados del modelo de regresión lineal
Término Estimación Error estándar Estadístico t Valor p
(Intercept) -20.6592 33.7099 -0.6129 0.5404
areaconst 0.7025 0.0624 11.2591 0.0000
estrato4 60.8866 30.5540 1.9928 0.0472
estrato5 149.7487 27.9055 5.3663 0.0000
estrato6 315.2393 47.7023 6.6085 0.0000
habitaciones 8.6590 6.8271 1.2683 0.2057
parqueaderos 22.8799 7.0416 3.2492 0.0013
banios 26.1028 9.3560 2.7900 0.0056
# 2. Tabla de métricas globales del modelo
tabla_metricas <- broom::glance(m1) %>%
  transmute(
    `R²` = round(r.squared, 4),
    `R² ajustado` = round(adj.r.squared, 4),
    `Error estándar residual` = round(sigma, 4),
    `Estadístico F` = round(statistic, 4),
    `Valor p del modelo` = signif(p.value, 4),
    AIC = round(AIC, 4),
    BIC = round(BIC, 4),
    `Número de observaciones` = nobs
  )

tabla_bonita(
  tabla_metricas,
  "Tabla 2. Métricas globales del modelo"
)
Tabla 2. Métricas globales del modelo
R² ajustado Error estándar residual Estadístico F Valor p del modelo AIC BIC Número de observaciones
0.6236 0.6147 158.3976 70.0457 0 3952.193 3985.646 304
# 3. Tabla resumen de residuales
tabla_residuales <- tibble(
  `Mínimo` = min(residuals(m1)),
  `Q1` = quantile(residuals(m1), 0.25),
  `Mediana` = median(residuals(m1)),
  `Media` = mean(residuals(m1)),
  `Q3` = quantile(residuals(m1), 0.75),
  `Máximo` = max(residuals(m1))
) %>%
  mutate(across(where(is.numeric), ~round(., 4)))

tabla_bonita(
  tabla_residuales,
  "Tabla 3. Resumen de residuales del modelo"
)
Tabla 3. Resumen de residuales del modelo
Mínimo Q1 Mediana Media Q3 Máximo
-834.8862 -85.5181 -14.5582 0 56.4292 931.1906

El modelo presenta un 𝑅2 de 0.6236, lo que indica que explica aproximadamente el 62.36% de la variabilidad del precio. El área construida, los estratos 5 y 6, el número de parqueaderos y el número de baños presentan efectos positivos y estadísticamente significativos sobre el precio del inmueble.

Realizamos un modelo de regresión lineal multiple para explicar el precio en funcion de: area, estrato, habitaciones, parqueaderos y baños.

El coeficiente areaconst indica el cambio esperado en el precio por cada m2

# Gráficos diagnósticos clásicos
par(mfrow=c(2,2))
plot(m1)

par(mfrow=c(1,1))

# Pruebas
shapiro.test(residuals(m1))     # normalidad (ojo: con n grande casi siempre rechaza)
bptest(m1)                      # heterocedasticidad
dwtest(m1)                      # autocorrelación (en datos no temporales puede no ser clave)
car::vif(m1)                    # multicolinealidad

El gráfico Q-Q permite evaluar la normalidad de los residuos. Aunque la prueba de Shapiro-Wilk puede rechazar la normalidad en muestras grandes, el análisis visual sugiere que los residuos se distribuyen aproximadamente de forma normal.

La prueba de Breusch-Pagan evalúa si la varianza de los residuos es constante. En caso de no rechazar la hipótesis nula, se concluye que no hay evidencia de heterocedasticidad.

El factor de inflación de varianza (VIF) se utilizó para evaluar multicolinealidad entre las variables explicativas. Valores de VIF inferiores a 5 sugieren que no existe un problema grave de colinealidad.

pred1 <- predict(m1, newdata = test1)

rmse1 <- rmse(test1$preciom, pred1)
mae1  <- mae(test1$preciom, pred1)
r2_1  <- cor(test1$preciom, pred1)^2

tabla_metricas_test <- tibble(
  RMSE = round(rmse1, 4),
  MAE = round(mae1, 4),
  `R² en test` = round(r2_1, 4)
)

tabla_bonita(
  tabla_metricas_test,
  "Tabla 4. Métricas de desempeño del modelo en el conjunto de prueba"
)
Tabla 4. Métricas de desempeño del modelo en el conjunto de prueba
RMSE MAE R² en test
151.9332 96.9536 0.5856
sol1_e4 <- tibble(
  areaconst = 200,
  estrato = factor("4", levels = levels(train1$estrato)),
  habitaciones = 4,
  parqueaderos = 1,
  banios = 2
)

sol1_e5 <- tibble(
  areaconst = 200,
  estrato = factor("5", levels = levels(train1$estrato)),
  habitaciones = 4,
  parqueaderos = 1,
  banios = 2
)

p_sol1_e4 <- predict(m1, sol1_e4)
p_sol1_e5 <- predict(m1, sol1_e5)

tabla_pred <- tibble(
  Escenario = c("Vivienda tipo en Estrato 4", "Vivienda tipo en Estrato 5"),
  `Precio estimado (millones)` = round(c(p_sol1_e4, p_sol1_e5), 2)
)

tabla_bonita(
  tabla_pred,
  "Tabla. Predicción del precio para una vivienda de 200 m² con 4 habitaciones, 1 parqueadero y 2 baños"
)
Tabla. Predicción del precio para una vivienda de 200 m² con 4 habitaciones, 1 parqueadero y 2 baños
Escenario Precio estimado (millones)
Vivienda tipo en Estrato 4 290.46
Vivienda tipo en Estrato 5 379.32
# Filtrado flexible  "200")
ofertas1 <- base1 %>%
  drop_na(preciom, areaconst, parqueaderos, banios, habitaciones, longitud, latitud) %>%
  filter(preciom <= 350) %>%
  filter(areaconst >= 180, areaconst <= 220) %>%
  filter(parqueaderos >= 1, banios >= 2, habitaciones >= 4) %>%
  arrange(preciom) %>%
  slice_head(n = 5)

tabla_ofertas1 <- ofertas1 %>%
  select(
    Precio = preciom,
    `Área construida` = areaconst,
    Estrato = estrato,
    Parqueaderos = parqueaderos,
    Baños = banios,
    Habitaciones = habitaciones,
    Barrio = barrio
  )

tabla_bonita(
  tabla_ofertas1,
  "Tabla 6. Mejores ofertas filtradas para la Vivienda 1"
)
Tabla 6. Mejores ofertas filtradas para la Vivienda 1
Precio Área construida Estrato Parqueaderos Baños Habitaciones Barrio
220 180 3 1 3 7 villa del prado
270 196 3 1 2 4 calima
280 180 3 1 4 5 zona norte
300 195 3 2 4 4 salomia
300 205 5 2 5 6 vipasa

Los resultados muestran que dentro del rango presupuestal considerado (≤ 350 millones), la mayoría de las propiedades disponibles con características similares corresponden a estrato 3. Esto sugiere que las viviendas de estrato 4 o superior con estas características tienden a ubicarse en rangos de precio más elevados.

leaflet(ofertas1) %>%
  addTiles() %>%
  addCircleMarkers(
    lng = ~longitud, lat = ~latitud,
    radius = 6,
    popup = ~paste0("Precio: ", preciom, " M | Barrio: ", barrio,
                    " | Area: ", areaconst, " | Estrato: ", estrato,
                    " | Hab: ", habitaciones, " | Baños: ", banios,
                    " | Parq: ", parqueaderos)
  )

En este caso lo mejor es que habria que validar el precio final, verificar si el cliente se ajusta le gusta realizando una visita presencial a cada apartamento que se ajuste.

Vivienda 2

Comenzaremos realizando las primeras observaciones del data set

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

tabla_bonita(
  head(base2, 3),
  "Tabla 7. Primeras 3 observaciones de apartamentos en Zona Sur"
)
Tabla 7. Primeras 3 observaciones de apartamentos en 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
leaflet(base2) %>%
  addTiles() %>%
  addCircleMarkers(
    lng = ~longitud,
    lat = ~latitud,
    radius = 4,
    opacity = 0.8,
    popup = ~paste0(
      "<b>Barrio:</b> ", barrio,
      "<br><b>Precio:</b> ", preciom, " M",
      "<br><b>Área:</b> ", areaconst,
      "<br><b>Estrato:</b> ", estrato,
      "<br><b>Habitaciones:</b> ", habitaciones,
      "<br><b>Baños:</b> ", banios,
      "<br><b>Parqueaderos:</b> ", parqueaderos,
      "<br><b>Longitud:</b> ", round(longitud, 5),
      "<br><b>Latitud:</b> ", round(latitud, 5)
    )
  )
b2 <- base2 %>%
  select(preciom, areaconst, estrato, banios, habitaciones, parqueaderos) %>%
  drop_na(preciom, areaconst, estrato, banios, habitaciones, parqueaderos) %>%
  mutate(estrato = as.factor(estrato))

p3 <- plot_ly(
  data = b2, x = ~areaconst, y = ~preciom, color = ~estrato,
  type = "scatter", mode = "markers"
) %>%
  layout(title = "Base 2: Precio vs Área (coloreado por estrato)",
         xaxis = list(title = "Área construida"),
         yaxis = list(title = "Precio (millones)"))
p3

Podemos ver que se parece mucho a la anterior grafica en donde tambien se puede ver una relacion positiva con el grafico de dispersion, por lo tanto realizaremos la estimación igual que con la anterior

set.seed(123)
split2 <- initial_split(b2, prop = 0.70)
train2 <- training(split2)
test2  <- testing(split2)
m2 <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = train2)
tabla_coeficientes_m2 <- broom::tidy(m2) %>%
  mutate(across(where(is.numeric), ~round(., 4))) %>%
  rename(
    Término = term,
    Estimación = estimate,
    `Error estándar` = std.error,
    `Estadístico t` = statistic,
    `Valor p` = p.value
  )

tabla_bonita(
  tabla_coeficientes_m2,
  "Tabla 14. Coeficientes estimados del modelo de regresión lineal para la Vivienda 2"
)
Tabla 14. Coeficientes estimados del modelo de regresión lineal para la Vivienda 2
Término Estimación Error estándar Estadístico t Valor p
(Intercept) -45.4440 15.6193 -2.9095 0.0037
areaconst 1.1049 0.0595 18.5729 0.0000
estrato4 31.2929 11.3072 2.7675 0.0057
estrato5 48.9390 11.3481 4.3125 0.0000
estrato6 202.7845 13.2763 15.2741 0.0000
habitaciones -9.9813 4.4993 -2.2184 0.0267
parqueaderos 78.4782 5.1639 15.1975 0.0000
banios 39.0443 3.9515 9.8809 0.0000
tabla_coeficientes_m2 <- broom::tidy(m2) %>%
  mutate(across(where(is.numeric), ~round(., 4))) %>%
  rename(
    Término = term,
    Estimación = estimate,
    `Error estándar` = std.error,
    `Estadístico t` = statistic,
    `Valor p` = p.value
  )

tabla_bonita(
  tabla_coeficientes_m2,
  "Tabla 14. Coeficientes estimados del modelo de regresión lineal para la Vivienda 2"
)
Tabla 14. Coeficientes estimados del modelo de regresión lineal para la Vivienda 2
Término Estimación Error estándar Estadístico t Valor p
(Intercept) -45.4440 15.6193 -2.9095 0.0037
areaconst 1.1049 0.0595 18.5729 0.0000
estrato4 31.2929 11.3072 2.7675 0.0057
estrato5 48.9390 11.3481 4.3125 0.0000
estrato6 202.7845 13.2763 15.2741 0.0000
habitaciones -9.9813 4.4993 -2.2184 0.0267
parqueaderos 78.4782 5.1639 15.1975 0.0000
banios 39.0443 3.9515 9.8809 0.0000

En el modelo estimado para la Vivienda 2, el área construida, el estrato, el número de parqueaderos y el número de baños presentan una relación positiva con el precio del inmueble. Esto significa que, manteniendo constantes las demás variables, un mayor valor en estas características se asocia con precios más altos. En contraste, el coeficiente de habitaciones aparece negativo, lo que puede deberse a la relación entre esta variable y otras características del inmueble, como el área construida o la distribución interna de la propiedad.

par(mfrow=c(2,2))
plot(m2)

par(mfrow=c(1,1))

shapiro.test(residuals(m2))
bptest(m2)
dwtest(m2)
car::vif(m2)
pred2 <- predict(m2, newdata = test2)

rmse2 <- rmse(test2$preciom, pred2)
mae2  <- mae(test2$preciom, pred2)
r2_2  <- cor(test2$preciom, pred2)^2

tabla_metricas_m2 <- tibble(
  Métrica = c("RMSE", "MAE", "R² en datos de prueba"),
  Valor = round(c(rmse2, mae2, r2_2), 4)
)

tabla_bonita(
  tabla_metricas_m2,
  "Tabla. Desempeño predictivo del modelo para la Vivienda 2"
)
Tabla. Desempeño predictivo del modelo para la Vivienda 2
Métrica Valor
RMSE 91.4014
MAE 54.9231
R² en datos de prueba 0.7826

El modelo presenta un R² de aproximadamente r round(r2_2,2) en el conjunto de prueba, lo que indica que cerca del r round(r2_2*100,1)% de la variabilidad del precio de los apartamentos es explicada por las variables incluidas en el modelo.

El RMSE muestra el error típico de predicción en millones de pesos, mientras que el MAE representa el error absoluto promedio del modelo. En conjunto, estos indicadores sugieren que el modelo tiene una capacidad predictiva moderada para estimar el precio de apartamentos con características similares.

sol2_e5 <- tibble(
  areaconst = 300,
  estrato = factor("5", levels = levels(train2$estrato)),
  habitaciones = 5,
  parqueaderos = 3,
  banios = 3
)

sol2_e6 <- tibble(
  areaconst = 300,
  estrato = factor("6", levels = levels(train2$estrato)),
  habitaciones = 5,
  parqueaderos = 3,
  banios = 3
)

p_sol2_e5 <- predict(m2, sol2_e5)
p_sol2_e6 <- predict(m2, sol2_e6)

tabla_pred_m2 <- tibble(
  Escenario = c("Apartamento estrato 5", "Apartamento estrato 6"),
  `Precio estimado (millones)` = round(c(p_sol2_e5, p_sol2_e6), 1)
)

tabla_bonita(
  tabla_pred_m2,
  "Tabla. Precio estimado para apartamentos con características del cliente"
)
Tabla. Precio estimado para apartamentos con características del cliente
Escenario Precio estimado (millones)
Apartamento estrato 5 637.6
Apartamento estrato 6 791.5
ofertas2 <- base2 %>%
  drop_na(preciom, areaconst, parqueaderos, banios, habitaciones, longitud, latitud) %>%
  filter(areaconst >= 300) %>%
  filter(parqueaderos >= 3, banios >= 3, habitaciones >= 5) %>%
  arrange(preciom) %>%
  slice_head(n = 5)

tabla_ofertas2 <- ofertas2 %>%
  select(
    Precio = preciom,
    `Área construida` = areaconst,
    Estrato = estrato,
    Parqueaderos = parqueaderos,
    Baños = banios,
    Habitaciones = habitaciones,
    Barrio = barrio
  )

tabla_bonita(
  tabla_ofertas2,
  "Tabla. Mejores ofertas encontradas para la Vivienda 2"
)
Tabla. Mejores ofertas encontradas para la Vivienda 2
Precio Área construida Estrato Parqueaderos Baños Habitaciones Barrio
370 300 3 3 6 5 melendez
670 300 5 3 5 6 seminario
730 573 5 3 8 5 guadalupe
1150 344 6 4 5 5 ciudad jardín
1150 464 6 4 6 5 ciudad jardín
leaflet(ofertas2) %>%
  addTiles() %>%
  addCircleMarkers(
    lng = ~longitud, lat = ~latitud,
    radius = 6,
    popup = ~paste0("Precio: ", preciom, " M | Barrio: ", barrio,
                    " | Area: ", areaconst, " | Estrato: ", estrato,
                    " | Hab: ", habitaciones, " | Baños: ", banios,
                    " | Parq: ", parqueaderos)
  )

La tabla presenta las mejores ofertas encontradas en el mercado que cumplen con las características solicitadas por el cliente.

Estas propiedades se ubican dentro del rango de precio considerado y presentan áreas construidas cercanas a los 300 m², además de contar con al menos tres parqueaderos, tres baños y cinco habitaciones.

Estas alternativas representan opciones potenciales para el cliente realice la visita presencial y tome una decision sobre el inmueble preferido

Conclusiones

El análisis realizado permitió estimar modelos de regresión para evaluar el precio de viviendas con características específicas solicitadas por el cliente.

En el caso de las casas ubicadas en la zona norte, el modelo mostró que variables como el área construida, el estrato y el número de parqueaderos tienen un impacto importante en el precio del inmueble.

Para los apartamentos ubicados en la zona sur, el modelo indica que el área construida, el estrato y la disponibilidad de parqueaderos también influyen significativamente en el valor del inmueble.

A partir de los modelos estimados y del análisis de las ofertas disponibles en el mercado, se identificaron varias alternativas que cumplen con las características solicitadas por el cliente y que se encuentran dentro del presupuesto disponible.

Estos resultados permiten orientar la toma de decisiones del comprador, proporcionando una estimación objetiva del valor de las propiedades según sus características principales. Hay que tener en cuenta que los datos del mapa en principio estaban atomizados por todo cali y no exactamente filtrados por zonas en donde era muy dificil verificar el precio con un filtrado inicial y esta regresión nos permitio identificarlos con precision.