Problema

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.

Desarrollo del ejercicio

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.

1. Análisis de Componentes Principales:

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.

Desarrollo:

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”:

  • preciom
  • areaconst
  • banios
  • habitaciones
  • piso_num
  • Parqueadero

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.

Resultados generales y conclusiones: Análisis de Componentes Principales

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)

2. Análisis de Conglomerados:

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.

3. Análisis de Correspondencia:

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.

Informe final