Las ventas de bienes raíces en Cali se han visto disminuidas de manera significativa durante el último año, en parte por la coyuntura política y social del país, que ha llevado a las instituciones bancarias a ser más conservadoras en el otorgamiento de crédito hipotecario. Hace dos días, María recibió una carta de una compañía internacional que desea ubicar a dos de sus empleados (con sus respectivas familias) en la ciudad, y solicita asesoría para la compra de dos viviendas con las siguientes características:
| Característica | Vivienda 1 | Vivienda 2 |
|---|---|---|
| Tipo | Casa | Apartamento |
| Área construida (m2) | 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 |
el Objetivo es responder la solicitud mediante técnicas de modelación estadística (regresión lineal múltiple), analizando los dos casos y entregando un informe.
# install.packages("gitcreds")
# gitcreds::gitcreds_delete("https://github.com")
# devtools::install_github("centromagis/paqueteMODELOS", force = TRUE)
library(paqueteMODELOS)
library(dplyr)
library(ggplot2)
library(plotly)
library(leaflet)
library(corrplot)
library(mice)
library(car)
library(lmtest)
library(broom)
library(modelsummary)
library(knitr)
set.seed(1234)
data("vivienda")
str(vivienda)
## spc_tbl_ [8,322 × 13] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
## $ id : num [1:8322] 1147 1169 1350 5992 1212 ...
## $ zona : chr [1:8322] "Zona Oriente" "Zona Oriente" "Zona Oriente" "Zona Sur" ...
## $ piso : chr [1:8322] NA NA NA "02" ...
## $ estrato : num [1:8322] 3 3 3 4 5 5 4 5 5 5 ...
## $ preciom : num [1:8322] 250 320 350 400 260 240 220 310 320 780 ...
## $ areaconst : num [1:8322] 70 120 220 280 90 87 52 137 150 380 ...
## $ parqueaderos: num [1:8322] 1 1 2 3 1 1 2 2 2 2 ...
## $ banios : num [1:8322] 3 2 2 5 2 3 2 3 4 3 ...
## $ habitaciones: num [1:8322] 6 3 4 3 3 3 3 4 6 3 ...
## $ tipo : chr [1:8322] "Casa" "Casa" "Casa" "Casa" ...
## $ barrio : chr [1:8322] "20 de julio" "20 de julio" "20 de julio" "3 de julio" ...
## $ longitud : num [1:8322] -76.5 -76.5 -76.5 -76.5 -76.5 ...
## $ latitud : num [1:8322] 3.43 3.43 3.44 3.44 3.46 ...
## - attr(*, "spec")=List of 3
## ..$ cols :List of 13
## .. ..$ id : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ zona : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
## .. ..$ piso : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
## .. ..$ estrato : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ preciom : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ areaconst : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ parqueaderos: list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ banios : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ habitaciones: list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ tipo : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
## .. ..$ barrio : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
## .. ..$ longitud : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ latitud : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## ..$ default: list()
## .. ..- attr(*, "class")= chr [1:2] "collector_guess" "collector"
## ..$ delim : chr ";"
## ..- attr(*, "class")= chr "col_spec"
## - attr(*, "problems")=<externalptr>
Alizar una revision a la base de datos vivienda
encontramos información de oferta de propiedades residenciales en Cali
durante los últimos tres meses, con variables como:
ubicación - zona - barrio - coordenadas geográficas
características físicas - area construida - número de baños - habitaciones - parqueaderos - piso
otras variables - estrato - precio en millones
Antes de modelar, es necesario conocer el conjunto de datos disponible. Para ello se tuvo en cuenta lo siguiente:
En esta etapa no se modifican los datos, solo se documentan sus características.
dim(vivienda)
## [1] 8322 13
sapply(vivienda, class)
## id zona piso estrato preciom areaconst
## "numeric" "character" "character" "numeric" "numeric" "numeric"
## parqueaderos banios habitaciones tipo barrio longitud
## "numeric" "numeric" "numeric" "character" "character" "numeric"
## latitud
## "numeric"
La base cuenta con 8.322 registros y 13 variables.
Las variables id, estrato,
preciom, areaconst, parqueaderos,
banios, habitaciones, longitud y
latitud son numéricas; zona,
piso, tipo y barrio son de tipo
texto (categóricas/nominales, aunque piso y
estrato tienen un orden implícito).
Para el análisis se definen dos grupos de variables de interés:
num_vars <- c("estrato", "preciom", "areaconst", "parqueaderos", "banios", "habitaciones")
cat_vars <- c("zona", "tipo", "barrio")
summary(vivienda[, num_vars])
## estrato preciom areaconst parqueaderos
## Min. :3.000 Min. : 58.0 Min. : 30.0 Min. : 1.000
## 1st Qu.:4.000 1st Qu.: 220.0 1st Qu.: 80.0 1st Qu.: 1.000
## Median :5.000 Median : 330.0 Median : 123.0 Median : 2.000
## Mean :4.634 Mean : 433.9 Mean : 174.9 Mean : 1.835
## 3rd Qu.:5.000 3rd Qu.: 540.0 3rd Qu.: 229.0 3rd Qu.: 2.000
## Max. :6.000 Max. :1999.0 Max. :1745.0 Max. :10.000
## NA's :3 NA's :2 NA's :3 NA's :1605
## banios habitaciones
## Min. : 0.000 Min. : 0.000
## 1st Qu.: 2.000 1st Qu.: 3.000
## Median : 3.000 Median : 3.000
## Mean : 3.111 Mean : 3.605
## 3rd Qu.: 4.000 3rd Qu.: 4.000
## Max. :10.000 Max. :10.000
## NA's :3 NA's :3
data.frame(desv_estandar = sapply(vivienda[, num_vars], sd, na.rm = TRUE))
## desv_estandar
## estrato 1.029222
## preciom 328.647244
## areaconst 142.964126
## parqueaderos 1.124909
## banios 1.428210
## habitaciones 1.459537
El precio (preciom) y el área construida
(areaconst) son las variables con mayor dispersión
relativa, coherente con un mercado que va desde vivienda económica hasta
vivienda de lujo. El estrato se mueve entre 3 y 6
M <- cor(vivienda[, num_vars], use = "pairwise.complete.obs")
round(M, 2)
## estrato preciom areaconst parqueaderos banios habitaciones
## estrato 1.00 0.61 0.27 0.42 0.42 -0.07
## preciom 0.61 1.00 0.69 0.69 0.67 0.26
## areaconst 0.27 0.69 1.00 0.58 0.65 0.52
## parqueaderos 0.42 0.69 0.58 1.00 0.57 0.28
## banios 0.42 0.67 0.65 0.57 1.00 0.59
## habitaciones -0.07 0.26 0.52 0.28 0.59 1.00
corrplot(M, method = "color", addCoef.col = "black", type = "upper", tl.col = "black")
El precio se correlaciona de forma moderaday no llega a ser del todo alta con área construida, parqueaderos y baños (0.6-0.7), lo cual es consistente con el mercado y lo qye se esperaria: propiedades más grandes y con más comodidades tienden a ser mas caras. No se observan correlaciones muy altas (>0.9) por lo que no se esperaria redundancia
par(mfrow = c(2, 3))
for (v in num_vars) {
boxplot(vivienda[[v]], main = v, col = "#8FD3D3")
}
par(mfrow = c(1, 1))
data.frame(
variable = num_vars,
n_atipicos = sapply(num_vars, function(v) length(boxplot.stats(vivienda[[v]])$out))
)
## variable n_atipicos
## estrato estrato 0
## preciom preciom 552
## areaconst areaconst 382
## parqueaderos parqueaderos 567
## banios banios 72
## habitaciones habitaciones 888
Se identifican datos atípicos en todas las variables, excepto en el estrato que como se dijo antes va de 3 a6. En un mercado inmobiliario real estos valores no necesariamente son errores: corresponden a propiedades de lujo o de gran tamaño, por lo que no se eliminan, aunque se debe tener presente que estos valores extremos pueden afectar los supuestos del modelo de regresión
El siguiente grafico nos permite ver por variables la cantidad de registros (a la derecha) que tienen x cantidad (a la izquierda) de variables vacias en rojo, y en la parte inferior (eje X) viene siendo el total de vacias por variable.
colSums(is.na(vivienda))
## id zona piso estrato preciom areaconst
## 3 3 2638 3 2 3
## parqueaderos banios habitaciones tipo barrio longitud
## 1605 3 3 3 3 3
## latitud
## 3
md.pattern(vivienda[, c("zona", "piso", "estrato", "preciom", "areaconst",
"parqueaderos", "banios", "habitaciones", "tipo", "barrio")],
rotate.names = TRUE)
## preciom zona estrato areaconst banios habitaciones tipo barrio
## 4808 1 1 1 1 1 1 1 1
## 1909 1 1 1 1 1 1 1 1
## 876 1 1 1 1 1 1 1 1
## 726 1 1 1 1 1 1 1 1
## 1 1 0 0 0 0 0 0 0
## 2 0 0 0 0 0 0 0 0
## 2 3 3 3 3 3 3 3
## parqueaderos piso
## 4808 1 1 0
## 1909 1 0 1
## 876 0 1 1
## 726 0 0 2
## 1 0 0 9
## 2 0 0 10
## 1605 2638 4266
Se observan tres situaciones distintas de datos faltantes:
id, zona, estrato,
areaconst, banios, habitaciones,
tipo, barrio, longitud,
latitud). Representan un 0.04% de los datos.parqueaderos: 1.605 valores faltantes
(19.3%)piso: 2.638 valores faltantes (31.7%),
el porcentaje más alto de toda la base.p_zona <- ggplot(vivienda, aes(x = zona)) + geom_bar(fill = "#0D3B66") +
theme_minimal() + labs(title = "Oferta por zona", x = NULL, y = "Viviendas") +
theme(axis.text.x = element_text(angle = 30, hjust = 1))
p_tipo <- ggplot(vivienda, aes(x = tipo)) + geom_bar(fill = "#F4D35E") +
theme_minimal() + labs(title = "Oferta por tipo", x = NULL, y = "Viviendas")
p_estrato <- ggplot(vivienda, aes(x = factor(estrato))) + geom_bar(fill = "#EE6C4D") +
theme_minimal() + labs(title = "Oferta por estrato", x = "Estrato", y = "Viviendas")
gridExtra::grid.arrange(p_zona, p_tipo, p_estrato, ncol = 3)
La oferta está fuertemente concentrada en la Zona Sur (más de la mitad de los registros), predominan los apartamentos sobre las casas, y el estrato 5 es el más frecuente. La base no incluye viviendas de estratos 1 y 2, por lo que las conclusiones aplican al segmento medio y alto del mercado, que es justamente el segmento en el que se ubican las dos solicitudes
De acuerdo con lo observado en la etapa anterior, se define la siguiente secuencia de actividades:
zona y tipo a factor, lo que es
necesario para generar el modeloparqueaderos con la mediana, dado que su
ausencia no está asociada de forma clara al tipo de vivienda (no se
puede asumir que sea homogéneamente “cero parqueaderos”).piso sin imputar ya que no es una variable
requerida en el modelo de precio de esta actividad por lo que se
excluira del análisis cuantitativolongitud y latitud: a diferencia
de un análisis puramente multivariado
(PCA/conglomerados/correspondencia), en esta actividad se requiere
ubicar las viviendas en un mapa (filtro por zona, oferta
recomendada)core <- c("zona", "estrato", "preciom", "areaconst", "banios", "habitaciones",
"tipo", "barrio", "longitud", "latitud")
viv <- vivienda[rowSums(is.na(vivienda[, core])) == 0, ]
viv$zona <- factor(gsub("^Zona ", "", trimws(viv$zona)))
viv$tipo <- factor(viv$tipo)
viv$parqueaderos[is.na(viv$parqueaderos)] <- median(viv$parqueaderos, na.rm = TRUE)
dim(viv)
## [1] 8319 13
colSums(is.na(viv))
## id zona piso estrato preciom areaconst
## 0 0 2635 0 0 0
## parqueaderos banios habitaciones tipo barrio longitud
## 0 0 0 0 0 0
## latitud
## 0
La base final de trabajo queda con 8319 registros,
prácticamente sin pérdida de información frente a los 8.322 originales,
y sin datos faltantes en las variables que se usarán en el filtrado, la
exploración y el modelo (piso conserva sus datos faltantes,
pero no se usa como predictor).
zonaAntes de filtrar las bases por zona, se calcula el centroide
geográfico (promedio de longitud y latitud) de cada zona sobre la
totalidad de la base viv. Esto permite, más adelante,
verificar si los puntos de cada base filtrada están efectivamente
ubicados en su zona declarada o si existen inconsistencias entre la
etiqueta de zona y la ubicación geográfica real del
predio.
centroides <- viv %>%
group_by(zona) %>%
summarise(lon_prom = mean(longitud), lon_sd = sd(longitud),
lat_prom = mean(latitud), lat_sd = sd(latitud), n = n())
centroides
## # A tibble: 5 × 6
## zona lon_prom lon_sd lat_prom lat_sd n
## <fct> <dbl> <dbl> <dbl> <dbl> <int>
## 1 Centro -76.5 0.00896 3.44 0.0105 124
## 2 Norte -76.5 0.0160 3.46 0.0311 1920
## 3 Oeste -76.5 0.0121 3.44 0.0145 1198
## 4 Oriente -76.5 0.0160 3.43 0.0208 351
## 5 Sur -76.5 0.0143 3.39 0.0297 4726
Se observa que cada zona tiene un centroide geográfico claramente
distinto y una dispersión (lon_sd, lat_sd)
relativamente acotada, lo que sugiere que, en general, las zonas sí
corresponden a áreas geográficas diferenciadas de la ciudad. Esta
información se usa en las secciones siguientes para contrastar, para
cada base filtrada, si sus puntos están más cerca del centroide de su
propia zona que del centroide de cualquier otra zona.
Se solicita una casa en la Zona Norte, con área construida de 200 m², 1 parqueadero, 2 baños, 4 habitaciones y estrato 4 o 5, con un crédito preaprobado de $350 millones.
base1)Se filtra la base de trabajo para incluir únicamente las ofertas de casas de la Zona Norte.
base1 <- viv %>% filter(tipo == "Casa", zona == "Norte")
dim(base1)
## [1] 722 13
head(base1, 3)
## # A tibble: 3 × 13
## id zona piso estrato preciom areaconst parqueaderos banios habitaciones
## <dbl> <fct> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 1209 Norte 02 5 320 150 2 4 6
## 2 1592 Norte 02 5 780 380 2 3 3
## 3 4057 Norte 02 6 750 445 2 7 6
## # ℹ 4 more variables: tipo <fct>, barrio <chr>, longitud <dbl>, latitud <dbl>
Se verifica que el filtro haya quedado correctamente aplicado:
table(Tipo = base1$tipo)
## Tipo
## Apartamento Casa
## 0 722
table(Zona = base1$zona)
## Zona
## Centro Norte Oeste Oriente Sur
## 0 722 0 0 0
La base base1 queda con 722 registros,
todos con tipo = "Casa" y zona = "Norte", tal
como lo confirman las tablas anteriores.
base1norte_c <- centroides %>% filter(zona == "Norte")
otras_c <- centroides %>% filter(zona != "Norte")
base1$dist_norte <- sqrt((base1$longitud - norte_c$lon_prom)^2 + (base1$latitud - norte_c$lat_prom)^2)
base1$dist_otra_zona <- sapply(seq_len(nrow(base1)), function(i) {
min(sqrt((base1$longitud[i] - otras_c$lon_prom)^2 + (base1$latitud[i] - otras_c$lat_prom)^2))
})
base1$ubicacion <- ifelse(base1$dist_norte <= base1$dist_otra_zona,
"Coherente con Zona Norte", "Más cerca de otra zona")
table(base1$ubicacion)
##
## Coherente con Zona Norte Más cerca de otra zona
## 515 207
pal1 <- colorFactor(c("#0D3B66", "#EE6C4D"), domain = base1$ubicacion)
leaflet(base1) %>%
addTiles() %>%
addCircleMarkers(lng = ~longitud, lat = ~latitud, radius = 4, stroke = FALSE,
fillOpacity = 0.6, color = ~pal1(ubicacion),
popup = ~paste0("Barrio: ", barrio, "<br>Precio: $", preciom, "M",
"<br>Área: ", areaconst, " m2")) %>%
addLegend(position = "bottomright", pal = pal1, values = ~ubicacion, title = "Ubicación")
De los 722 registros de base1, 207
(28.7%) están geográficamente más cerca del centroide de otra
zona que del centroide de la propia Zona Norte. Esto no
significa necesariamente que el filtro esté mal aplicado (el
filtro por zona == "Norte" es correcto); lo que evidencia
es una inconsistencia entre la etiqueta comercial de zona y la
ubicación geográfica real del predio, esto puede llegar a pasar
por
Si bien para el análisis de esta solicitud se conservan todos los
registros de base1 se deja como notacion la importancia de
revizar la zona de la base segun la longitud/latitud
Se explora la relación entre el precio (preciom) y el
área construida, el estrato, el número de baños, el número de
habitaciones y la zona, usando gráficos interactivos del paquete
plotly.
base1)vars_mod <- c("preciom", "areaconst", "estrato", "banios", "habitaciones", "parqueaderos")
M1 <- cor(base1[, vars_mod])
round(M1, 2)
## preciom areaconst estrato banios habitaciones parqueaderos
## preciom 1.00 0.73 0.61 0.52 0.32 0.30
## areaconst 0.73 1.00 0.46 0.46 0.38 0.26
## estrato 0.61 0.46 1.00 0.41 0.11 0.20
## banios 0.52 0.46 0.41 1.00 0.58 0.29
## habitaciones 0.32 0.38 0.11 0.58 1.00 0.19
## parqueaderos 0.30 0.26 0.20 0.29 0.19 1.00
corrplot(M1, method = "color", addCoef.col = "black", type = "upper", tl.col = "black")
Dentro de las casas de la Zona Norte, el precio se correlaciona más
fuertemente con areaconst (0.73) y estrato
(0.61), de forma moderada con banios (0.52), y de forma más
débil con habitaciones (0.32) y parqueaderos
(0.3). Esto sugiere que el tamaño de la propiedad y el estrato
socioeconómico son los factores más asociados al precio, mientras que el
número de habitaciones aporta relativamente poca información adicional
una vez se conoce el área construida
g1 <- ggplot(base1, aes(x = areaconst, y = preciom, color = estrato,
text = paste0("Barrio: ", barrio, "<br>Baños: ", banios,
"<br>Habitaciones: ", habitaciones))) +
geom_point(alpha = 0.5) +
scale_color_gradient(low = "#F4D35E", high = "#0D3B66") +
theme_minimal() +
labs(title = "Precio vs. área construida (color = estrato)", x = "Área construida (m2)",
y = "Precio (millones)", color = "Estrato")
ggplotly(g1, tooltip = c("x", "y", "color", "text"))
g2 <- ggplot(base1, aes(x = factor(banios), y = preciom)) +
geom_boxplot(fill = "#8FD3D3") +
theme_minimal() +
labs(title = "Precio según número de baños", x = "Baños", y = "Precio (millones)")
ggplotly(g2)
g3 <- ggplot(base1, aes(x = factor(habitaciones), y = preciom)) +
geom_boxplot(fill = "#EE6C4D") +
theme_minimal() +
labs(title = "Precio según número de habitaciones", x = "Habitaciones", y = "Precio (millones)")
ggplotly(g3)
Se confirma visualmente la relación positiva entre precio y área construida , así como una relación positiva pero con más dispersión con baños y habitaciones: a partir de 5-6 habitaciones el precio mediano deja de crecer de forma clara, e incluso algunas casas muy grandes en número de habitaciones no son las más costosas, por lo que el efecto de habitaciones sobre el precio podría no ser tan claro en el modelo de regresión
Dado que base1 ya está en Zona Norte, se construye un
gráfico de contexto sobre todas las casas de la base
viv (todas las zonas) para dimensionar en qué
lugar del mercado se ubica la Zona Norte en términos de precio:
casas <- viv %>% filter(tipo == "Casa")
g4 <- ggplot(casas, aes(x = zona, y = preciom, fill = zona == "Norte")) +
geom_boxplot() +
scale_fill_manual(values = c("TRUE" = "#0D3B66", "FALSE" = "#8FD3D3"), guide = "none") +
theme_minimal() +
labs(title = "Precio de casas por zona (Zona Norte resaltada)", x = NULL, y = "Precio (millones)")
ggplotly(g4)
La Zona Norte se ubica en un nivel de precios intermedio para casas:
por debajo de Zona Oeste y Zona Sur, y por encima de Zona Centro y Zona
Oriente. Esto es coherente con lo que se busca: al no incluir
zona como variable dentro de base1 (por ser
constante), su efecto ya queda controlado por el propio filtro, y el
análisis se enfoca en cómo varían área, estrato, baños y habitaciones
dentro de esa zona.
Se estima un modelo de regresión lineal múltiple para el precio en función del á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\]
m1 <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = base1)
summary(m1)
##
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos +
## banios, data = base1)
##
## Residuals:
## Min 1Q Median 3Q Max
## -924.94 -77.71 -17.66 45.90 1081.29
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -251.05177 30.11848 -8.335 3.94e-16 ***
## areaconst 0.81090 0.04352 18.634 < 2e-16 ***
## estrato 84.61108 7.17727 11.789 < 2e-16 ***
## habitaciones 0.95948 4.10569 0.234 0.81529
## parqueaderos 16.55976 5.70396 2.903 0.00381 **
## banios 24.57669 5.35583 4.589 5.26e-06 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 158.2 on 716 degrees of freedom
## Multiple R-squared: 0.6548, Adjusted R-squared: 0.6524
## F-statistic: 271.6 on 5 and 716 DF, p-value: < 2.2e-16
Interpretacion de los coeficientes:
Bondad de ajuste: el modelo alcanza un R² de 0.655 (R² ajustado 0.652), es decir, explica cerca del 65% de la variabilidad del precio de las casas de la Zona Norte. Es un ajuste razonable para datos de corte transversal de precios de vivienda, pero deja un 35% de la variabilidad sin explicar, atribuible a factores no incluidos en la base: calidad de los acabados, antigüedad de la construcción, piso (excluido por su alto porcentaje de datos faltantes), vista, cercanía a vías principales o centros comerciales, y el barrio específico dentro de la zona (más de 400 barrios distintos en la base completa).
Para mejorar el modelo podría afinar los datos faltantes del
parqueadero o probar transformaciones (por ejemplo logarítmicas en
preciom y areaconst, dado que ambas variables
son de cola derecha, como se pudo ver en los datos atípicos).
par(mfrow = c(2, 2))
plot(m1)
par(mfrow = c(1, 1))
Linealidad: el gráfico de residuales vs. valores ajustados no muestra una curvatura sistemática marcada en el grueso de los datos, aunque se observa mayor dispersión de residuales conforme aumenta el precio ajustado, con varios valores muy alejados de cero en la cola de precios altos (coherente con las viviendas de lujo identificadas como atípicas en el entendimiento de los datos).
Normalidad de los residuales:
shapiro.test(residuals(m1))
##
## Shapiro-Wilk normality test
##
## data: residuals(m1)
## W = 0.83433, p-value < 2.2e-16
La prueba de Shapiro-Wilk rechaza la normalidad de los
residuales (p < 0.001), lo cual también es visible en el
gráfico Q-Q (colas pesadas hacia la derecha). Esto es consistente con la
asimetría positiva de preciom documentada desde el
entendimiento de los datos: unas pocas viviendas de muy alto valor
generan residuales grandes. Con un tamaño de muestra de 722
observaciones, los estimadores puntuales de los coeficientes siguen
siendo válidos (insesgados), pero los intervalos de confianza y las
pruebas de hipótesis basadas en normalidad deben interpretarse con
cautela.
Homocedasticidad (varianza constante de los residuales):
bptest(m1)
##
## studentized Breusch-Pagan test
##
## data: m1
## BP = 132.24, df = 5, p-value < 2.2e-16
La prueba de Breusch-Pagan rechaza la homocedasticidad (p < 0.001): la varianza del error crece con el nivel de precio, un patrón típico de datos de precios (los errores absolutos de predicción tienden a ser proporcionalmente más grandes en propiedades más costosas).
Multicolinealidad:
vif(m1)
## areaconst estrato habitaciones parqueaderos banios
## 1.523989 1.433058 1.621630 1.120670 1.918323
Todos los factores de inflación de varianza (VIF) están por debajo de 2, muy lejos del umbral de preocupación (5 o 10), por lo que no hay evidencia de multicolinealidad problemática entre los predictores, a pesar de las correlaciones moderadas observadas en el análisis exploratorio.
Independencia: no se evalúa formalmente autocorrelación (por ejemplo, Durbin-Watson), ya que las observaciones no siguen un orden temporal ni espacial secuencial; corresponden a una muestra transversal de ofertas de vivienda.
Qué se podría hacer para mejorar el cumplimiento de los
supuestos Transformar preciom y
areaconst a escala logarítmica para reducir la asimetría y
estabilizar la varianza; estimar errores estándar robustos a
heterocedasticidad; o tratar de forma separada el segmento de vivienda
de lujo (posiblemente con un modelo o un componente adicional), en lugar
de forzar un único modelo lineal sobre todo el rango de precios.
La vivienda 1 solicitada es una casa de 200 m² de área construida, 1 parqueadero, 2 baños y 4 habitaciones, en estrato 4 o 5. Como el estrato de la solicitud es ambiguo (“4 o 5”), se calcula la predicción para ambos escenarios:
nueva_v1 <- data.frame(
areaconst = c(200, 200),
estrato = c(4, 5),
habitaciones = c(4, 4),
parqueaderos = c(1, 1),
banios = c(2, 2)
)
pred_v1 <- predict(m1, newdata = nueva_v1, interval = "prediction")
tabla_pred1 <- cbind(estrato = nueva_v1$estrato, round(pred_v1, 1))
kable(tabla_pred1, col.names = c("Estrato", "Precio estimado", "Límite inferior (95%)", "Límite superior (95%)"))
| Estrato | Precio estimado | Límite inferior (95%) | Límite superior (95%) |
|---|---|---|---|
| 4 | 319.1 | 7.9 | 630.4 |
| 5 | 403.7 | 92.0 | 715.5 |
Con estrato 4, el precio esperado es de $319 millones, dentro del crédito preaprobado de $350 millones. Con estrato 5, el precio esperado sube a $404 millones, por encima del crédito disponible. Los intervalos de predicción del 95% son amplios (reflejo del R² de 0.65 y de la heterocedasticidad detectada), por lo que estas cifras deben usarse como una guía de orden de magnitud y no como un precio exacto de cierre. Dado el presupuesto de $350 millones, es más realista enfocar la búsqueda en casas de estrato 4 en la Zona Norte con las características solicitadas; una casa de estrato 5 con esa área tiende, en promedio, a superar el presupuesto.
Se buscan, dentro de base1, ofertas reales con precio
menor o igual a $350 millones, estrato 4 o 5, y características lo más
cercanas posible a lo solicitado (área 200 m², 1 parqueadero, 2 baños, 4
habitaciones). Para ello se calcula, para cada oferta que cumple el
presupuesto, una distancia (estandarizada) de su perfil de
características al perfil solicitado, y se seleccionan las más
cercanas:
feat <- c("areaconst", "parqueaderos", "banios", "habitaciones")
m_feat <- sapply(feat, function(v) mean(base1[[v]]))
s_feat <- sapply(feat, function(v) sd(base1[[v]]))
objetivo_v1 <- c(areaconst = 200, parqueaderos = 1, banios = 2, habitaciones = 4)
candidatas1 <- base1 %>% filter(preciom <= 350, estrato %in% c(4, 5))
Z1 <- scale(candidatas1[, feat], center = m_feat, scale = s_feat)
z_obj1 <- (objetivo_v1 - m_feat) / s_feat
candidatas1$distancia_perfil <- sqrt(rowSums(sweep(Z1, 2, z_obj1)^2))
ofertas_v1 <- candidatas1 %>%
arrange(distancia_perfil) %>%
head(5) %>%
select(id, barrio, estrato, preciom, areaconst, parqueaderos, banios, habitaciones,
longitud, latitud, distancia_perfil)
kable(ofertas_v1 %>% select(-longitud, -latitud), digits = 2,
col.names = c("ID", "Barrio", "Estrato", "Precio (M)", "Área (m2)",
"Parqueaderos", "Baños", "Habitaciones", "Distancia al perfil"))
| ID | Barrio | Estrato | Precio (M) | Área (m2) | Parqueaderos | Baños | Habitaciones | Distancia al perfil |
|---|---|---|---|---|---|---|---|---|
| 1666 | alamos | 4 | 275 | 120 | 1 | 2 | 4 | 0.48 |
| 3586 | la merced | 4 | 330 | 240 | 1 | 2 | 3 | 0.60 |
| 4779 | prados del norte | 5 | 280 | 140 | 1 | 2 | 3 | 0.65 |
| 1924 | vipasa | 4 | 320 | 264 | 1 | 2 | 3 | 0.67 |
| 94 | zona norte | 4 | 265 | 162 | 1 | 3 | 4 | 0.69 |
leaflet(ofertas_v1) %>%
addTiles() %>%
addMarkers(lng = ~longitud, lat = ~latitud,
popup = ~paste0("<b>Barrio:</b> ", barrio,
"<br><b>Precio:</b> $", preciom, "M",
"<br><b>Área:</b> ", areaconst, " m2",
"<br><b>Estrato:</b> ", estrato,
"<br><b>Parqueaderos:</b> ", parqueaderos,
"<br><b>Baños:</b> ", banios,
"<br><b>Habitaciones:</b> ", habitaciones))
Las 5 ofertas anteriores cumplen el presupuesto de $350 millones y se encuentran en la Zona Norte. Se observa un trade-off esperable: las opciones de menor área (alrededor de 120-160 m²) dejan más margen de precio frente al presupuesto, mientras que las que más se acercan a los 200 m² solicitados (por ejemplo, las ofertas en Vipasa y La Merced) usan casi todo el presupuesto disponible. Ninguna de las opciones recomendadas alcanza exactamente el número de habitaciones solicitado (4) junto con el área de 200 m² dentro del presupuesto; esto es consistente con el hallazgo del modelo de que, a este nivel de presupuesto, el mercado ofrece un intercambio entre área y número de habitaciones más que una combinación de ambos en su nivel máximo. Se recomienda a María presentar estas alternativas al cliente, priorizando según si su prioridad es el área construida o el número de habitaciones.
La compañía solicita un apartamento en la Zona Sur, con área construida de 300 m², 3 parqueaderos, 3 baños, 5 habitaciones y estrato 5 o 6, con un crédito preaprobado de $850 millones. Se repiten, para esta segunda solicitud, los pasos 1 a 6 aplicados a la primera.
base2)base2 <- viv %>% filter(tipo == "Apartamento", zona == "Sur")
dim(base2)
## [1] 2787 13
head(base2, 3)
## # A tibble: 3 × 13
## id zona piso estrato preciom areaconst parqueaderos banios habitaciones
## <dbl> <fct> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 5098 Sur 05 4 290 96 1 2 3
## 2 698 Sur 02 3 78 40 1 1 2
## 3 8199 Sur <NA> 6 875 194 2 5 3
## # ℹ 4 more variables: tipo <fct>, barrio <chr>, longitud <dbl>, latitud <dbl>
table(Tipo = base2$tipo)
## Tipo
## Apartamento Casa
## 2787 0
table(Zona = base2$zona)
## Zona
## Centro Norte Oeste Oriente Sur
## 0 0 0 0 2787
La base base2 queda con 2787 registros,
todos con tipo = "Apartamento" y
zona = "Sur".
base2sur_c <- centroides %>% filter(zona == "Sur")
otras_c2 <- centroides %>% filter(zona != "Sur")
base2$dist_sur <- sqrt((base2$longitud - sur_c$lon_prom)^2 + (base2$latitud - sur_c$lat_prom)^2)
base2$dist_otra_zona <- sapply(seq_len(nrow(base2)), function(i) {
min(sqrt((base2$longitud[i] - otras_c2$lon_prom)^2 + (base2$latitud[i] - otras_c2$lat_prom)^2))
})
base2$ubicacion <- ifelse(base2$dist_sur <= base2$dist_otra_zona,
"Coherente con Zona Sur", "Más cerca de otra zona")
table(base2$ubicacion)
##
## Coherente con Zona Sur Más cerca de otra zona
## 2294 493
pal2 <- colorFactor(c("#0D3B66", "#EE6C4D"), domain = base2$ubicacion)
leaflet(base2) %>%
addTiles() %>%
addCircleMarkers(lng = ~longitud, lat = ~latitud, radius = 4, stroke = FALSE,
fillOpacity = 0.5, color = ~pal2(ubicacion),
popup = ~paste0("Barrio: ", barrio, "<br>Precio: $", preciom, "M",
"<br>Área: ", areaconst, " m2")) %>%
addLegend(position = "bottomright", pal = pal2, values = ~ubicacion, title = "Ubicación")
De los 2787 registros de base2, 493
(17.7%) están geográficamente más cerca del centroide de otra
zona que del de la Zona Sur. La proporción es menor que en
base1, pero confirma nuevamente que la etiqueta comercial
de zona no es perfectamente consistente con la
geolocalización, probablemente por las mismas razones discutidas para la
Solicitud 1 (límites de zona difusos, digitación manual del campo
zona frente a una geolocalización independiente). Se
mantiene la base completa para el análisis, apoyándose en la variable
zona como criterio de negocio.
M2 <- cor(base2[, vars_mod])
round(M2, 2)
## preciom areaconst estrato banios habitaciones parqueaderos
## preciom 1.00 0.76 0.67 0.72 0.33 0.54
## areaconst 0.76 1.00 0.48 0.66 0.43 0.45
## estrato 0.67 0.48 1.00 0.57 0.21 0.30
## banios 0.72 0.66 0.57 1.00 0.51 0.41
## habitaciones 0.33 0.43 0.21 0.51 1.00 0.14
## parqueaderos 0.54 0.45 0.30 0.41 0.14 1.00
corrplot(M2, method = "color", addCoef.col = "black", type = "upper", tl.col = "black")
En los apartamentos de Zona Sur el precio se correlaciona fuertemente
con areaconst (0.76), banios (0.72) y
estrato (0.67), y de forma moderada con
parqueaderos (0.54), mientras que habitaciones
(0.33) es la de menor asociación lineal directa con el precio.
h1 <- ggplot(base2, aes(x = areaconst, y = preciom, color = estrato,
text = paste0("Barrio: ", barrio, "<br>Baños: ", banios,
"<br>Habitaciones: ", habitaciones))) +
geom_point(alpha = 0.4) +
scale_color_gradient(low = "#F4D35E", high = "#0D3B66") +
theme_minimal() +
labs(title = "Precio vs. área construida (color = estrato)", x = "Área construida (m2)",
y = "Precio (millones)", color = "Estrato")
ggplotly(h1, tooltip = c("x", "y", "color", "text"))
h2 <- ggplot(base2, aes(x = factor(banios), y = preciom)) +
geom_boxplot(fill = "#8FD3D3") +
theme_minimal() +
labs(title = "Precio según número de baños", x = "Baños", y = "Precio (millones)")
ggplotly(h2)
h3 <- ggplot(base2, aes(x = factor(habitaciones), y = preciom)) +
geom_boxplot(fill = "#EE6C4D") +
theme_minimal() +
labs(title = "Precio según número de habitaciones", x = "Habitaciones", y = "Precio (millones)")
ggplotly(h3)
aptos <- viv %>% filter(tipo == "Apartamento")
h4 <- ggplot(aptos, aes(x = zona, y = preciom, fill = zona == "Sur")) +
geom_boxplot() +
scale_fill_manual(values = c("TRUE" = "#0D3B66", "FALSE" = "#8FD3D3"), guide = "none") +
theme_minimal() +
labs(title = "Precio de apartamentos por zona (Zona Sur resaltada)", x = NULL, y = "Precio (millones)")
ggplotly(h4)
El precio crece de forma claracon el área y el número de baños, adicional las habitaciones si parecen tener una relacion directa y creciente con el precio. En cuanto al precio por zona, la Zona Sur se ubica en un nivel de precios bajo-medio para apartamentos, muy por debajo de la Zona Oeste, lo que es consistente con que la Zona Sur concentre el segmento más masivo de la oferta de apartamentos de la ciudad.
m2 <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = base2)
summary(m2)
##
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos +
## banios, data = base2)
##
## Residuals:
## Min 1Q Median 3Q Max
## -1233.80 -45.98 -2.05 42.25 927.76
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -314.61850 13.51614 -23.277 < 2e-16 ***
## areaconst 1.45139 0.04876 29.766 < 2e-16 ***
## estrato 68.93151 2.66560 25.860 < 2e-16 ***
## habitaciones -16.28835 3.41634 -4.768 1.96e-06 ***
## parqueaderos 51.59374 3.16010 16.327 < 2e-16 ***
## banios 49.11206 3.02770 16.221 < 2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 95.01 on 2781 degrees of freedom
## Multiple R-squared: 0.7544, Adjusted R-squared: 0.754
## F-statistic: 1709 on 5 and 2781 DF, p-value: < 2.2e-16
Interpretación de los coeficientes:
base1, lo que sugiere que en los apartamentos de Zona Sur
el área construida es un determinante todavía más importante del precio
(por unidad de área) que en las casas de Zona Norte.Bondad de ajuste: el modelo alcanza un R² de
0.754 (R² ajustado 0.754), superior al de base1,
explicando cerca del 75% de la variabilidad del precio
de los apartamentos de Zona Sur. El mejor ajuste, frente al modelo de
casas, es razonable: los apartamentos de una misma zona tienden a ser
más homogéneos entre sí (edificios con especificaciones estándar) que
las casas, que varían más en lote, número de pisos y remodelaciones.
Persiste, sin embargo, un 25% de variabilidad no
explicada, atribuible a factores como el piso del apartamento (excluido
de la base por datos faltantes, pero puede que unos pisos sean mas
apetecidos que otros, el año de construcción, etc. se pueden tomar las
mismas estrategias de mejora sugeridas para las casas incluir
barrio, recuperar piso, transformar
variables.
par(mfrow = c(2, 2))
plot(m2)
par(mfrow = c(1, 1))
shapiro.test(residuals(m2))
##
## Shapiro-Wilk normality test
##
## data: residuals(m2)
## W = 0.79864, p-value < 2.2e-16
bptest(m2)
##
## studentized Breusch-Pagan test
##
## data: m2
## BP = 929.2, df = 5, p-value < 2.2e-16
vif(m2)
## areaconst estrato habitaciones parqueaderos banios
## 2.027751 1.555176 1.441342 1.310740 2.470269
Los resultados son cualitativamente similares a los de
base1: la prueba de Shapiro-Wilk rechaza la
normalidad de los residuales (colas pesadas asociadas a los
apartamentos de mayor valor), y la prueba de Breusch-Pagan
rechaza la homocedasticidad (la dispersión de los
errores crece con el precio). Los VIF son todos inferiores a 2.5, por lo
que tampoco hay evidencia de multicolinealidad problemática. Al igual
que en base1, no se evalúa formalmente
independencia/autocorrelación por tratarse de datos transversales. Por
lo anterior posibles sugerencias de mejora son transformación
logarítmica, errores estándar robustos a heterocedasticidad, tratamiento
diferenciado del segmento de mayor valor
nueva_v2 <- data.frame(
areaconst = c(300, 300),
estrato = c(5, 6),
habitaciones = c(5, 5),
parqueaderos = c(3, 3),
banios = c(3, 3)
)
pred_v2 <- predict(m2, newdata = nueva_v2, interval = "prediction")
tabla_pred2 <- cbind(estrato = nueva_v2$estrato, round(pred_v2, 1))
kable(tabla_pred2, col.names = c("Estrato", "Precio estimado", "Límite inferior (95%)", "Límite superior (95%)"))
| Estrato | Precio estimado | Límite inferior (95%) | Límite superior (95%) |
|---|---|---|---|
| 5 | 686.1 | 498.7 | 873.5 |
| 6 | 755.1 | 567.6 | 942.5 |
Con estrato 5, el precio esperado es de $686 millones; con estrato 6, de $755 millones. Ambos escenarios están dentro del crédito preaprobado de $850 millones, con un margen de cerca de $164 millones (estrato 5) y $95 millones (estrato 6). Sin embargo, el límite superior del intervalo de predicción del 95% para estrato 6 ($942 millones) queda por encima del presupuesto, por lo que si el cliente prioriza el estrato 6 conviene dejar un margen de negociación.
Dado el amplio margen de presupuesto frente al precio esperado, la familia tiene margen para considerar tanto estrato 5 como estrato 6; se recomienda priorizar estrato 5 si no se quiere exceder el presupuesto, o dejar un colchón para negociar algo en estrato 6.
objetivo_v2 <- c(areaconst = 300, parqueaderos = 3, banios = 3, habitaciones = 5)
m_feat2 <- sapply(feat, function(v) mean(base2[[v]]))
s_feat2 <- sapply(feat, function(v) sd(base2[[v]]))
candidatas2 <- base2 %>% filter(preciom <= 850, estrato %in% c(5, 6))
Z2 <- scale(candidatas2[, feat], center = m_feat2, scale = s_feat2)
z_obj2 <- (objetivo_v2 - m_feat2) / s_feat2
candidatas2$distancia_perfil <- sqrt(rowSums(sweep(Z2, 2, z_obj2)^2))
ofertas_v2 <- candidatas2 %>%
arrange(distancia_perfil) %>%
head(5) %>%
select(id, barrio, estrato, preciom, areaconst, parqueaderos, banios, habitaciones,
longitud, latitud, distancia_perfil)
kable(ofertas_v2 %>% select(-longitud, -latitud), digits = 2,
col.names = c("ID", "Barrio", "Estrato", "Precio (M)", "Área (m2)",
"Parqueaderos", "Baños", "Habitaciones", "Distancia al perfil"))
| ID | Barrio | Estrato | Precio (M) | Área (m2) | Parqueaderos | Baños | Habitaciones | Distancia al perfil |
|---|---|---|---|---|---|---|---|---|
| 6175 | capri | 5 | 350 | 270.00 | 3 | 3 | 4 | 1.68 |
| 2308 | San Fernando | 5 | 350 | 258.00 | 2 | 4 | 5 | 2.03 |
| 4266 | el ingenio | 6 | 700 | 250.00 | 2 | 4 | 5 | 2.10 |
| 8036 | seminario | 5 | 530 | 256.00 | 3 | 5 | 5 | 2.30 |
| 8113 | cuarto de legua | 5 | 410 | 295.55 | 2 | 4 | 4 | 2.45 |
leaflet(ofertas_v2) %>%
addTiles() %>%
addMarkers(lng = ~longitud, lat = ~latitud,
popup = ~paste0("<b>Barrio:</b> ", barrio,
"<br><b>Precio:</b> $", preciom, "M",
"<br><b>Área:</b> ", areaconst, " m2",
"<br><b>Estrato:</b> ", estrato,
"<br><b>Parqueaderos:</b> ", parqueaderos,
"<br><b>Baños:</b> ", banios,
"<br><b>Habitaciones:</b> ", habitaciones))
Las 5 ofertas anteriores cumplen el presupuesto de $850 millones y están en la Zona Sur. En este caso el presupuesto es amplio frente al precio típico de la zona, por lo que hay más margen de elección: varias opciones alcanzan o superan los 3 parqueaderos y 3 baños solicitados, aunque ninguna llega a las 5 habitaciones dentro del presupuesto sin sacrificar otras características, en línea con el hallazgo del modelo de que, controlando por área, un número alto de habitaciones no es lo que más valoriza estas propiedades. Se recomienda recomienda entonces revisar entre las opciones o que se quiere priorizar sin pasar el presupuesto, por otra parte con los puntos mostrados en el mapa observamos que algunas de las opciones no parecen estar en la ozna sur, por lo que valdria la pena al igual que en casas, revisar esta etiqueta o si el error esta en la longitud/latitud.
modelsummary(list("Casas - Zona Norte" = m1, "Apartamentos - Zona Sur" = m2),
stars = TRUE, gof_omit = "Log.Lik|F|RMSE")
| Casas - Zona Norte | Apartamentos - Zona Sur | |
|---|---|---|
| + p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001 | ||
| (Intercept) | -251.052*** | -314.619*** |
| (30.118) | (13.516) | |
| areaconst | 0.811*** | 1.451*** |
| (0.044) | (0.049) | |
| estrato | 84.611*** | 68.932*** |
| (7.177) | (2.666) | |
| habitaciones | 0.959 | -16.288*** |
| (4.106) | (3.416) | |
| parqueaderos | 16.560** | 51.594*** |
| (5.704) | (3.160) | |
| banios | 24.577*** | 49.112*** |
| (5.356) | (3.028) | |
| Num.Obs. | 722 | 2787 |
| R2 | 0.655 | 0.754 |
| R2 Adj. | 0.652 | 0.754 |
| AIC | 9369.4 | 33300.9 |
| BIC | 9401.5 | 33342.4 |
piso (31.7% de datos faltantes) y
parqueaderos (19.3%), y revisar el proceso de asignación de
la variable zona, dado varios de los registros de ambas
bases (29% en Zona Norte, 18% en Zona Sur) tiene coordenadas geográficas
más cercanas a otra zona, lo que puede afectar la confiabilidad de
futuros filtros y análisis por zona.piso, y probar
transformaciones logarítmicas de precio y área construida, dado que
ambas variables presentan asimetría y heterocedasticidad asociada al
segmento de vivienda de alto valor.