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")
| 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.
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"
)
| 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"
)
| tipo | n |
|---|---|
| Casa | 722 |
tabla_bonita(
base1 %>% count(zona),
"Distribución por zona"
)
| zona | n |
|---|---|
| Zona Norte | 722 |
tabla_bonita(
base1 %>% count(estrato) %>% arrange(desc(n)),
"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"
)
| 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"
)
| R² | 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"
)
| 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"
)
| 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"
)
| 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"
)
| 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.
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"
)
| 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"
)
| 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"
)
| 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"
)
| 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"
)
| 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"
)
| 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
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.