Una empresa inmobiliaria líder en una gran ciudad está buscando comprender en profundidad el mercado de viviendas urbanas para tomar decisiones estratégicas más informadas. La empresa posee una base de datos extensa que contiene información detallada sobre diversas propiedades residenciales disponibles en el mercado. Se requiere realizar un análisis holístico de estos datos para identificar patrones, relaciones y segmentaciones relevantes que permitan mejorar la toma de decisiones en cuanto a la compra, venta y valoración de propiedades.
Como primera acción se ejecuta la carga de la base de datos de vivienda:
library(paqueteDAT) #Esta es la librería donde está la Base de Datos
data(vivienda)
ls.str(vivienda)
## areaconst : num [1:8322] 70 120 220 280 90 87 52 137 150 380 ...
## banios : num [1:8322] 3 2 2 5 2 3 2 3 4 3 ...
## barrio : chr [1:8322] "20 de julio" "20 de julio" "20 de julio" "3 de julio" ...
## estrato : num [1:8322] 3 3 3 4 5 5 4 5 5 5 ...
## habitaciones : num [1:8322] 6 3 4 3 3 3 3 4 6 3 ...
## id : num [1:8322] 1147 1169 1350 5992 1212 ...
## latitud : num [1:8322] 3.43 3.43 3.44 3.44 3.46 ...
## longitud : num [1:8322] -76.5 -76.5 -76.5 -76.5 -76.5 ...
## parqueaderos : num [1:8322] 1 1 2 3 1 1 2 2 2 2 ...
## piso : chr [1:8322] NA NA NA "02" "01" "01" "01" "01" "02" "02" "02" "02" "02" ...
## preciom : num [1:8322] 250 320 350 400 260 240 220 310 320 780 ...
## tipo : chr [1:8322] "Casa" "Casa" "Casa" "Casa" "Apartamento" "Apartamento" ...
## zona : chr [1:8322] "Zona Oriente" "Zona Oriente" "Zona Oriente" "Zona Sur" ...
head(vivienda)
## id zona piso estrato preciom areaconst parqueaderos banios
## 1 1147 Zona Oriente <NA> 3 250 70 1 3
## 2 1169 Zona Oriente <NA> 3 320 120 1 2
## 3 1350 Zona Oriente <NA> 3 350 220 2 2
## 4 5992 Zona Sur 02 4 400 280 3 5
## 5 1212 Zona Norte 01 5 260 90 1 2
## 6 1724 Zona Norte 01 5 240 87 1 3
## habitaciones tipo barrio longitud latitud
## 1 6 Casa 20 de julio -76.51168 3.43382
## 2 3 Casa 20 de julio -76.51237 3.43369
## 3 4 Casa 20 de julio -76.51537 3.43566
## 4 3 Casa 3 de julio -76.54000 3.43500
## 5 3 Apartamento acopi -76.51350 3.45891
## 6 3 Apartamento acopi -76.51700 3.36971
tail(vivienda)
## id zona piso estrato preciom areaconst parqueaderos banios
## 8317 6417 Zona Sur <NA> 6 1800 400 3 6
## 8318 6998 Zona Sur <NA> 6 1000 189 3 5
## 8319 8139 Zona Sur <NA> 5 530 142 2 4
## 8320 NA <NA> <NA> NA NA NA NA NA
## 8321 NA <NA> <NA> NA NA NA NA NA
## 8322 NA <NA> <NA> NA 330 NA NA NA
## habitaciones tipo barrio longitud latitud
## 8317 5 Casa zona sur -76.54261 3.35809
## 8318 4 Apartamento zona sur -76.54666 3.44620
## 8319 4 Casa zona sur -76.55587 3.40889
## 8320 NA <NA> <NA> NA NA
## 8321 NA <NA> <NA> NA NA
## 8322 NA <NA> <NA> NA NA
summary(vivienda)
## id zona piso estrato
## Min. : 1 Length:8322 Length:8322 Min. :3.000
## 1st Qu.:2080 Class :character Class :character 1st Qu.:4.000
## Median :4160 Mode :character Mode :character Median :5.000
## Mean :4160 Mean :4.634
## 3rd Qu.:6240 3rd Qu.:5.000
## Max. :8319 Max. :6.000
## NA's :3 NA's :3
## preciom areaconst parqueaderos banios
## Min. : 58.0 Min. : 30.0 Min. : 1.000 Min. : 0.000
## 1st Qu.: 220.0 1st Qu.: 80.0 1st Qu.: 1.000 1st Qu.: 2.000
## Median : 330.0 Median : 123.0 Median : 2.000 Median : 3.000
## Mean : 433.9 Mean : 174.9 Mean : 1.835 Mean : 3.111
## 3rd Qu.: 540.0 3rd Qu.: 229.0 3rd Qu.: 2.000 3rd Qu.: 4.000
## Max. :1999.0 Max. :1745.0 Max. :10.000 Max. :10.000
## NA's :2 NA's :3 NA's :1605 NA's :3
## habitaciones tipo barrio longitud
## Min. : 0.000 Length:8322 Length:8322 Min. :-76.59
## 1st Qu.: 3.000 Class :character Class :character 1st Qu.:-76.54
## Median : 3.000 Mode :character Mode :character Median :-76.53
## Mean : 3.605 Mean :-76.53
## 3rd Qu.: 4.000 3rd Qu.:-76.52
## Max. :10.000 Max. :-76.46
## NA's :3 NA's :3
## latitud
## Min. :3.333
## 1st Qu.:3.381
## Median :3.416
## Mean :3.418
## 3rd Qu.:3.452
## Max. :3.498
## NA's :3
Para tener una idea inicial de la estructura de datos con la que vamos a trabajar, usamos las funciones ls.str() para visualizar la estructura de la BD, así como head() y tail() para visualizar los primeros y últimos 6 registros de la BD. Fortuitamente nos encontramos que los tres registros finales no tienen ningún dato, por lo que pueden descartarse, pero obviaremos este paso por el momento.
Reducir la dimensionalidad del conjunto de datos y visualizar la estructura de las variables en componentes principales para identificar características clave que influyen en la variación de precios y preferencias del mercado.
Para este análisis, utilizaremos las variables contínuas, omitiendo las variables categóricas. Ahora bien, se propondrá para un experimento posterior, analizar la variable “piso”, la cual aparece como variable de texto, pero podría impactar como atributo cuantificable, igual que número de habitaciones, o número de baños. El “piso” podría ser un elemento que afecte al precio o a la valoración de la vivienda, pero en un sentido diferente. En un edificio que cuente con ascensor, los pisos más altos tienen una mejor valoración, pero si el edificio no cuenta con ascensor, como es la generalidad en vivienda VIS, los apartamentos
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr 1.1.2 ✔ readr 2.1.4
## ✔ forcats 1.0.0 ✔ stringr 1.5.0
## ✔ ggplot2 3.4.2 ✔ tibble 3.2.1
## ✔ lubridate 1.9.2 ✔ tidyr 1.3.0
## ✔ purrr 1.0.2
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag() masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
vivienda_1 <- vivienda %>% mutate(piso_num = as.double(vivienda$piso))
summary(vivienda_1$piso_num)
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## 1.000 2.000 3.000 3.771 5.000 12.000 2638
Finalmente, creamos una variable de número con “piso”, pero encontramos que hay 2638 registros sin datos. Se presume que la mayoría podría estar en un piso 1, así que se construye un histograma con la respectiva tabla de frecuencias, pero se encuentra que de los datos existentes, la mayoría se encuentran en el piso 2. Véase:
ggplot() + geom_histogram(data = vivienda_1, aes(x=piso_num),fill = "blue", color = "black",
binwidth = 1) +
labs(x = "Piso", y = "Cantidad en piso",
title = "Piso donde se ubican las viviendas")+
theme (legend.position = "none")+
theme (panel.background = element_blank(),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank())
## Warning: Removed 2638 rows containing non-finite values (`stat_bin()`).
table(vivienda_1$piso_num)
##
## 1 2 3 4 5 6 7 8 9 10 11 12
## 860 1450 1097 607 567 245 204 211 146 130 84 83
summary(vivienda_1$parqueaderos)
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## 1.000 1.000 2.000 1.835 2.000 10.000 1605
Es decir, queda como opción, hacer un análisis sacrificando las 2638 observaciones de número de piso, a ver si se encuentra una asociación en el grupo. para el caso de parqueaderos, Se ordenó de forma descendente, dado qeu se considera que una vivienda tenga 10 parqueaderos puede ser “inusual”. Se encuentra de hecho un registro de una vivienda tipo apartamento de 190 millones y 50 metros cuadrados con 4 habitaciones, 2 baños y 10 parqueaderos (Registro ID=324) que se considera improbable. No obstante nos arriesgamos e imputamos el valor cero (0) en la variable parqueadero en los casos vacíos, asumienndo que cuando no se pudo recopilar información de esta variable es porque simplemente la vivienda no tenía parqueadero (y suele suceder)
De esta forma, se seleccionan las siguientes variables para el análisis “ACP”:
se descarta el “estrato socioeconómico” por tratarse de una variable categórica, que inclusive podría reemplazarse por 3: “Medio - Bajo” - 4: “Medio”, 5: “Medio-alto” y 6: “Alto”.
Tambien se descarta la información geográfica, al ser un tipo de dato en coordenadas que poco tiene que ver con el análisis en cuestión.
vivienda_1$parqueaderos[is.na(vivienda_1$parqueaderos)] <- 0
vivienda_2 = vivienda_1[,c(5,6,7,8,9,14)]
vivienda_2N = scale(vivienda_2)
head(vivienda_2N)
## preciom areaconst parqueaderos banios habitaciones piso_num
## [1,] -0.5595420 -0.7339949 -0.3870931 -0.07793773 1.6406840 NA
## [2,] -0.3465477 -0.3842568 -0.3870931 -0.77811479 -0.4147626 NA
## [3,] -0.2552644 0.3152194 0.4172487 -0.77811479 0.2703863 NA
## [4,] -0.1031256 0.7349051 1.2215906 1.32241640 -0.4147626 -0.6772734
## [5,] -0.5291143 -0.5940997 -0.3870931 -0.77811479 -0.4147626 -1.0597114
## [6,] -0.5899698 -0.6150839 -0.3870931 -0.07793773 -0.4147626 -1.0597114
Se verifican valores vacíos:
library(mice)
##
## Attaching package: 'mice'
## The following object is masked from 'package:stats':
##
## filter
## The following objects are masked from 'package:base':
##
## cbind, rbind
md.pattern(vivienda_2N)
## parqueaderos preciom areaconst banios habitaciones piso_num
## 5684 1 1 1 1 1 1 0
## 2635 1 1 1 1 1 0 1
## 1 1 1 0 0 0 0 4
## 2 1 0 0 0 0 0 5
## 0 2 3 3 3 2638 2649
Quedan como vacios los 2649 registros vacíos de la variable pisos
Corregimos:
vivienda_2N1 <- na.omit(vivienda_2N)
md.pattern(vivienda_2N1)
## /\ /\
## { `---' }
## { O O }
## ==> V <== No need for mice. This data set is completely observed.
## \ \|/ /
## `-----'
## preciom areaconst parqueaderos banios habitaciones piso_num
## 5684 1 1 1 1 1 1 0
## 0 0 0 0 0 0 0
Se hace el análisis de componentes principales:
prcomp(vivienda_2N1) # Componentes pricipales
## Standard deviations (1, .., p=6):
## [1] 1.7546971 1.0484126 0.8385355 0.5898904 0.5238349 0.4141620
##
## Rotation (n x k) = (6 x 6):
## PC1 PC2 PC3 PC4 PC5
## preciom 0.4714125 0.27258721 -0.24547821 -0.39943300 -0.1695749
## areaconst 0.4760412 -0.07766997 0.03437615 -0.52075212 0.5285691
## parqueaderos 0.4135081 0.29743030 -0.42926386 0.68067534 0.2916038
## banios 0.4912451 0.02409557 0.20867899 0.08280024 -0.7332009
## habitaciones 0.3548725 -0.37136685 0.65946127 0.31392764 0.2314667
## piso_num -0.1136213 0.83228894 0.52521917 -0.02334855 0.1251633
## PC6
## preciom -0.67446609
## areaconst 0.46433004
## parqueaderos 0.08903437
## banios 0.41244540
## habitaciones -0.38618082
## piso_num 0.04815749
viv_acp <- prcomp(vivienda_2N1) #Asignamos a una variable
# install.packages("devtools")
# library("devtools")
# install_github("kassambara/factoextra") - Esto por si "fviz_eig()" marca error
library("factoextra")
## Welcome! Want to learn more? See two factoextra-related books at https://goo.gl/ve3WBa
La distribución de cada uno de los componentes principales se grafica a continuación:
fviz_eig(viv_acp, addlabels = TRUE)
Los dos primeros componentes explican el 74% de la variación total de las variables. Ahora, veámos cómo contribuyen las variaciones de cada una de las dimensiones a las tres primeras componentes, que explican el 84% de todas las variaciones:
fviz_contrib(viv_acp, choice = "var", axes = 1)
fviz_contrib(viv_acp, choice = "var", axes = 2)
fviz_contrib(viv_acp, choice = "var", axes = 3)
Para la primera componente, los baños, el área construida, el precio y los parqueaderos son las variables de mayor impacto, mientras que las variaciones de número de piso son las mayores aportantes a la estructura de la segunda componente.
A continuación, presentamos el siguiente gráfico:
fviz_pca_var(viv_acp,
col.var = "contrib",
gradient.cols = c("orange", "purple"),
repel = TRUE)
Aquí podemos ver una fuerte asociación de los parqueaderos y el número de baños al precio de la vivienda. Absolutamente todas las variables analizadas se orientan en la misma dirección a excepción del número de piso, que se orienta de forma inversa a los otros determinantes.
A partir de las variables numéricas identificadas, de un total de seis dimensiones, se recomienda reducir a por lo menos tres dimensiones usando los tres componentes principales, que explican el 84% de la variación total de las seis dimensiones inicialmente estudiadas.
Los resultados de este análisis multivariado, muestran que alrededor del precio, la dimensión parqueaderos y número de baños y área construída en su orden, terminan siendo los atributos que van más de la mano con el precio. No obstante en este mercado, el número de habitaciones, termina siendo un atributo que va en la misma dirección pero que afecta en un grado diferente, y al parecer el número del piso no termina siendo un atributo a favor del precio.
Este resultado debe ser observado con cuidado por parte de la inmobiliaria. Al respecto, se recomendaría poder capturar más información como por ejemplo, si el apartamento cuenta o no con ascensor, o el costo asociado a la cuota de administración, en caso de estar integrado en un esquema de propiedad horizontal. Se presume que un piso alto en una estructura que no tenga acceso a ascensores puede impactar negativamente el precio, o posiblemente se enfrente a una cuota de administración que de igual forma lo desvalorice.
Por otro lado, que los parqueaderos o los baños sean atributos que se orienten mucho más al precio de venta de inmuebles nuevos, puede ser un indicador de un cambio en las preferencias de los consumidores, que valoran más este tipo de atributos, o de vivir en inmuebles más orientados a los visitantes, que disponer de inmuebles mejor equipados para sustentar o proyectar una familia grande (tener hijos que usen más habitaciones)
Agrupar las propiedades residenciales en segmentos homogéneos con características similares para entender las dinámicas y demandas específicas en diferentes partes de la ciudad y en diferentes estratos socioeconómicos.
Para el presente análisis de clustering, usaremos la misma data utilizada para el análisis de componentes principales, tratando de hallar una asociación entre el precio y el estrato socioeconómico inicialmente:
vivienda_clu1 <- vivienda_1[,c(5,6,7,8,9,14)]
vivienda_clu1 <- na.omit(vivienda_clu1)
head(vivienda_clu1)
## # A tibble: 6 × 6
## preciom areaconst parqueaderos banios habitaciones piso_num
## <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 400 280 3 5 3 2
## 2 260 90 1 2 3 1
## 3 240 87 1 3 3 1
## 4 220 52 2 2 3 1
## 5 310 137 2 3 4 1
## 6 320 150 2 4 6 2
vivienda_clu1z = scale(vivienda_clu1)
head(vivienda_clu1z)
## preciom areaconst parqueaderos banios habitaciones piso_num
## [1,] -0.07388895 0.8208022 1.2156117 1.37212934 -0.3961809 -0.6772734
## [2,] -0.51100887 -0.5699215 -0.4443371 -0.77917165 -0.3961809 -1.0597114
## [3,] -0.57345457 -0.5918803 -0.4443371 -0.06207132 -0.3961809 -1.0597114
## [4,] -0.63590027 -0.8480663 0.3856373 -0.77917165 -0.3961809 -1.0597114
## [5,] -0.35489461 -0.2259004 0.3856373 -0.06207132 0.3182519 -1.0597114
## [6,] -0.32367176 -0.1307456 0.3856373 0.65502901 1.7471176 -0.6772734
k_numbers <- numeric(10)
for (k in 1:10) {
kmeans_result <- kmeans(vivienda_clu1, centers = k, nstart = 10)
k_numbers [k] <- kmeans_result$tot.withinss
}
plot(1:10, k_numbers, type = "b", pch = 19, frame = FALSE,
xlab = "Numero de Clusters", ylab = "Inercia",
main = "Analisis de Codo")
k_opt <- 4
kmeans_result <- kmeans(vivienda_clu1, centers = k_opt, nstart = 10)
vivienda_clu1$cluster <- factor(kmeans_result$cluster)
cluster_summary <- aggregate(vivienda_clu1, by = list(Cluster = vivienda_clu1$cluster), FUN = mean)
## Warning in mean.default(X[[i]], ...): argument is not numeric or logical:
## returning NA
## Warning in mean.default(X[[i]], ...): argument is not numeric or logical:
## returning NA
## Warning in mean.default(X[[i]], ...): argument is not numeric or logical:
## returning NA
## Warning in mean.default(X[[i]], ...): argument is not numeric or logical:
## returning NA
print(cluster_summary)
## Cluster preciom areaconst parqueaderos banios habitaciones piso_num
## 1 1 423.8452 183.38200 1.7198151 3.466782 3.924899 3.630272
## 2 2 1368.7650 426.85920 3.6446991 5.197708 4.315186 3.922636
## 3 3 752.7706 303.60887 2.5116564 4.527607 4.330061 3.512883
## 4 4 209.1176 86.15295 0.8716386 2.165292 3.002868 3.914665
## cluster
## 1 NA
## 2 NA
## 3 NA
## 4 NA
ggplot(vivienda_clu1, aes(x = preciom, y = areaconst, color = cluster)) +
geom_point() +
labs(x = "Precio",
y = "Area Construida",
title = "Área construida y precio por cluster",
color = "Cluster") +
theme_minimal()
ggplot(vivienda_clu1, aes(x = preciom, y = banios, color = cluster)) +
geom_point() +
labs(x = "Precio",
y = "Número de Baños",
title = "Número de baños y precios por Cluster",
color = "Cluster") +
theme_minimal()
ggplot(vivienda_clu1, aes(x = preciom, y = parqueaderos, color = cluster)) +
geom_point() +
labs(x = "Precio",
y = "Parqueaderos",
title = "Número de parqueaderos y precio por cluster",
color = "Cluster") +
theme_minimal()
En conclusión, se segmentan 4 agrupaciones o segmentos en los que se pueden agrupar las viviendas. En cuanto a precios, se considera que los segmentos parten de la vivienda básica, a la vivienda media baja, y a las de zona alta y muy alta, que tienen aspectos dotacionales importantes.
Examinar la relación entre las variables categóricas (tipo de vivienda, zona y barrio) y las variables numéricas (precio, área construida, número de parqueaderos, baños, habitaciones) para identificar patrones de comportamiento del mercado inmobiliario.
A continuación elaboraremos en primer lugar, un análisis de relación entre las variables categóricas estrato socioecónomico y zona de la ciudad, en busca de una relación de correspondencia:
data(vivienda)
vivienda_c1 <- vivienda[, c(2,4)]
library(dplyr)
set.seed(1234)
md.pattern(vivienda_c1, rotate.names = TRUE)
## zona estrato
## 8319 1 1 0
## 3 0 0 2
## 3 3 6
library(mice)
library(FactoMineR)
vivienda_c1<- na.omit(vivienda_c1)
md.pattern(vivienda_c1, rotate.names = TRUE)
## /\ /\
## { `---' }
## { O O }
## ==> V <== No need for mice. This data set is completely observed.
## \ \|/ /
## `-----'
## zona estrato
## 8319 1 1 0
## 0 0 0
tabla_1 <- table(vivienda_c1$zona, vivienda_c1$estrato)
res_ac <- CA(tabla_1)
val_prop <-res_ac$eig
Tal y como se puede apreciar, se puede apreciar que en efecto, para un total de 8319 observaciones, se nota que existe una asociación entre el estrato socioeconómico y lka zona de la ciudad. En cuanto a la zona oeste, prevalece el estrato 4, mientras que los estratos 4 y 5 se reparten con mayor frecuencia entre las zonas sur y norte. Finalmente el estrato 3 se concentra más en las zonas centro y oriente.
library(factoextra)
fviz_screeplot(res_ac, addlabels = TRUE, ylim = c(0, 80))+ggtitle("")+
ylab("Porcentaje de varianza explicado") + xlab("Ejes")
Por otro lado se presenta el porcentaje de varianza explicado, que para la primera componente es del 70%.
Finalmente se propone analizar la distribucióin entre los precios y las zonas de la ciudad tal y como se presenta a continuación:
vivienda_c2 <- vivienda[, c(2,5)]
vivienda_c2 <- na.omit(vivienda_c2)
ggplot(vivienda_c2, aes(x =zona, y = preciom)) +
geom_boxplot() +
labs(title = "Precios por zona")+
theme (legend.position = "none")+
theme (panel.background = element_blank(),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank())
En efecto, la zona oriente, donde prevalece el estrato 6, es decir, la zona de ingresos “altos” presenta los precios de vivienda en promedio más altos, lo cual es coherente. Los precios de la zona sur son los segundos en promedio, tambien coherente con la prevalencia de estrato socioeconómicos 4 y 5. Los precios de las viviendas de la zona oriente son los más bajos. No solo por la prevalencia de estrato 3, sino porque es bien sabido que la zona oriente de la ciudad analizada en cuestión es la zona de más bajos ingresos de la ciudad.
Es importante señalar que el número de habitaciones actualmente parece no ser el elemento que orienta el precio o que motive las preferencias de compra de los habitantes de la ciudad; Parece que existen otro tipo de atributos a los que hay que poner más cuidado, como puede ser el número de parqueaderos o el número de baños. las preferencias de la poblacion pueden estarse inclinando a un tipo de vivienda donde se valoran atributos para recibir visitantes y no pensados en acoger a una familia grande a futuro.
La ciudad está plenamente segmentada en zonas de alto ingreso (Estrato 6 en zona oeste), zonas de ingresos medios (sur y norte) y zona de ingresos medio-bajos a bajos (zona oriente). Hay una correlación positiva marcada en cada una de estas zonas en cuanto a precio e ingresos, lo cual no da ningún tipo de sorpresas.
Se pueden analizar 4 agrupaciones de tipo de vivienda con diferentes características. Desde la vivienda más económica, con un total de 3 habitaciones hasta las de mayor performance con 4 o más de las mismas.
Importante seguir validando la calidad de la data. es inusual que existan casas con cero habitaciones, o casas de 1000 millones de pesos sin baños, o apartamentos de 50 metros cuadrados en estrato 4 con 10 parqueaderos.