Librerías

library(paqueteMODELOS)
library(knitr)
library(mice)
library(factoextra)
library(dplyr)
data("vivienda")
#str(vivienda)

Entendimiento y limpieza de datos

df <- vivienda
df$piso <- as.numeric(as.character(df$piso))
head(df, 5)
## # A tibble: 5 × 13
##      id zona     piso estrato preciom areaconst parqueaderos banios habitaciones
##   <dbl> <chr>   <dbl>   <dbl>   <dbl>     <dbl>        <dbl>  <dbl>        <dbl>
## 1  1147 Zona O…    NA       3     250        70            1      3            6
## 2  1169 Zona O…    NA       3     320       120            1      2            3
## 3  1350 Zona O…    NA       3     350       220            2      2            4
## 4  5992 Zona S…     2       4     400       280            3      5            3
## 5  1212 Zona N…     1       5     260        90            1      2            3
## # ℹ 4 more variables: tipo <chr>, barrio <chr>, longitud <dbl>, latitud <dbl>

Medidas de tendencia central

summary(df)
##        id           zona                piso           estrato     
##  Min.   :   1   Length:8322        Min.   : 1.000   Min.   :3.000  
##  1st Qu.:2080   Class :character   1st Qu.: 2.000   1st Qu.:4.000  
##  Median :4160   Mode  :character   Median : 3.000   Median :5.000  
##  Mean   :4160                      Mean   : 3.771   Mean   :4.634  
##  3rd Qu.:6240                      3rd Qu.: 5.000   3rd Qu.:5.000  
##  Max.   :8319                      Max.   :12.000   Max.   :6.000  
##  NA's   :3                         NA's   :2638     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

Revisando las medidas de tendencia central, se observa un insight importante referente al número de baños y habitaciones con valor igual a 0, lo que genera preguntarse ¿cómo es posible una casa/apartamento sin baños ni habitaciones?

colSums(df[, c("banios", "habitaciones")] == 0, na.rm = TRUE)
##       banios habitaciones 
##           45           66

Con un total de 8319 registros, las viviendas sin baños ni habitaciones representan el 0.54% y 0.79% respectivamente. Dado que los valores son muy pequeños, se procede a extraerlos del dataset, eliminando en total el 0.88% de los registros.

data <- df[!(df$banios == 0 | df$habitaciones == 0), ]

Se realiza un histograma para las variables de estudio con el objetivo de ver su distribución y ayudar a la comprensión de los datos

library(ggplot2)
library(tidyr)
library(dplyr)

df_long <- data %>%
  select(areaconst, banios, estrato, habitaciones, parqueaderos, piso, preciom) %>%
  pivot_longer(cols = everything(), 
               names_to = "variable", 
               values_to = "value")

ggplot(df_long, aes(x = value)) +
  geom_histogram(bins = 30, fill = "skyblue", color = "black") +
  facet_wrap(~ variable, scales = "free_x") +
  theme_minimal()
## Warning: Removed 4169 rows containing non-finite outside the scale range
## (`stat_bin()`).

Tratamiento de Missing Values

md.pattern(data,rotate.names = TRUE)

##      id zona estrato preciom areaconst banios habitaciones tipo barrio longitud
## 4787  1    1       1       1         1      1            1    1      1        1
## 1901  1    1       1       1         1      1            1    1      1        1
## 863   1    1       1       1         1      1            1    1      1        1
## 692   1    1       1       1         1      1            1    1      1        1
## 3     0    0       0       0         0      0            0    0      0        0
##       3    3       3       3         3      3            3    3      3        3
##      latitud parqueaderos piso     
## 4787       1            1    1    0
## 1901       1            1    0    1
## 863        1            0    1    1
## 692        1            0    0    2
## 3          0            0    0   13
##            3         1558 2596 4187

Reemplezando por media

Se identifica que parqueadero y piso, son las variables con mayor número de missing values. Dado que son variables de números enteros, se calcula media redondeada para reemplazar los missing values. Posterior a ello se verifican los resultados obtenidos.

# Back up de seguridad
data_clean1 <- data

# Calculo de la media aproximada
mean_piso <- round(mean(data$piso, na.rm = TRUE), 0)

# Replace NAs with the mean
data_clean1$piso[is.na(data_clean1$piso)] <- mean_piso

# Calculo de la media aproximada
mean_parking <- round(mean(data$parqueaderos, na.rm = TRUE),0)

# Replace NAs with the mean
data_clean1$parqueaderos[is.na(data_clean1$parqueaderos)] <- mean_parking

# Eliminar los registros con NA de columna id
data_clean1 <- data_clean1[!is.na(data_clean1$id), ]

# Verificar que no quedan NAs
colSums(is.na(data_clean1))
##           id         zona         piso      estrato      preciom    areaconst 
##            0            0            0            0            0            0 
## parqueaderos       banios habitaciones         tipo       barrio     longitud 
##            0            0            0            0            0            0 
##      latitud 
##            0

Eliminando missing values

Asimismo, también está la opción de eliminar los missing values del dataset. Adicional de los 3 valores de la columna id, que no pueden ser reemplazados por algún método estadístico. Nuevamente se verifican los resultados obtenidos.

# Selección de variables de interés para el análisis de pca

data_clean2 <- data %>%
  select(piso, preciom, areaconst, parqueaderos, banios, habitaciones)

data_clean2 <- na.omit(data_clean2)

# Verificar que no quedan NAs
colSums(is.na(data_clean2))
##         piso      preciom    areaconst parqueaderos       banios habitaciones 
##            0            0            0            0            0            0

Análisis de Componentes Principales

Metodo 1: Dataset ajustado con medias

Ajuste de escala

# Ajuste del dataset para dejar solo variables cuantitativas y obtención de escala
dataz = scale(data_clean1[,c(3,5:9)])
head(dataz) # primeros 6 registros
##            piso    preciom  areaconst parqueaderos      banios habitaciones
## [1,]  0.0734874 -0.5571698 -0.7309968   -0.8552405 -0.09041565    1.6579542
## [2,]  0.0734874 -0.3443558 -0.3796746   -0.8552405 -0.79819935   -0.4438323
## [3,]  0.0734874 -0.2531498  0.3229699    0.1335039 -0.79819935    0.2567632
## [4,] -0.8520384 -0.1011398  0.7445566    1.1222482  1.32515174   -0.4438323
## [5,] -1.3148013 -0.5267678 -0.5904679   -0.8552405 -0.79819935   -0.4438323
## [6,] -1.3148013 -0.5875718 -0.6115473   -0.8552405 -0.09041565   -0.4438323

PCA

prcomp(dataz)
## Standard deviations (1, .., p=6):
## [1] 1.7886541 1.0284987 0.8967369 0.6587619 0.5650832 0.4306779
## 
## Rotation (n x k) = (6 x 6):
##                     PC1         PC2         PC3        PC4         PC5
## piso         -0.1013236 -0.84999301  0.48910107  0.1235941  0.10727765
## preciom       0.4645859 -0.29991362 -0.24758344 -0.4324310 -0.09333790
## areaconst     0.4896222  0.03581693  0.03890712 -0.2570613  0.74128498
## parqueaderos  0.4034387 -0.21958594 -0.48675140  0.7379107 -0.01773786
## banios        0.4877129 -0.02307691  0.21963826 -0.2015360 -0.65398411
## habitaciones  0.3654271  0.37085082  0.64249586  0.3827773  0.04760531
##                      PC6
## piso         -0.03523615
## preciom       0.66121529
## areaconst    -0.37668232
## parqueaderos -0.08523772
## banios       -0.49502593
## habitaciones  0.40908072

En el siguiente gráfico con los resultados del PCA, se observa que el primer componente principal concentra el 53.7% de la variabilidad de la base de datos, mientras que los dos primeros en conjunto explican el 70.9%. Esto indica que una gran proporción de la variabilidad puede resumirse empleando únicamente el primer componente (CP1), el cual resulta de una combinación lineal de las variables originales

res.pca <- prcomp(dataz)
fviz_eig(res.pca, addlabels = TRUE)

Al analizar las variables en el plano de los componentes principales, es posible identificar la orientación y el significado de cada uno. En este caso, el primer componente principal se relaciona principalmente con las variables piso y precio, mientras que el segundo componente se asocia en mayor medida con areacons y banios, orientadas en la misma dirección, lo que evidencia una fuerte correlación entre ellas. Así, la Dimensión 2 podría interpretarse como un factor relacionado con el tamaño y la cantidad de baños, mientras que la Dimensión 1 estaría vinculada al valor y a la ubicación en altura o por el número de pisos (ya que el dataset no especifica)

fviz_pca_var(res.pca,
col.var = "contrib", # Color by contributions to the PC
gradient.cols = c("#A9CBB7","#E7B800",  "#034D94"),
repel = TRUE     # Avoid text overlapping
)

vivienda_num <- data_clean1 %>%
  select(piso, preciom, areaconst, parqueaderos, banios, habitaciones)

datos<- rbind(vivienda_num[100,], 
              vivienda_num[101,],
              vivienda_num[102,],
              vivienda_num[103,])

datos <- as.data.frame(datos)
rownames(datos) = c("vivienda 100","vivienda 101","vivienda 102","vivienda 103")
datos
##              piso preciom areaconst parqueaderos banios habitaciones
## vivienda 100    4     520       140            2      3            3
## vivienda 101    4     750       445            2      6            7
## vivienda 102    4     450       118            2      3            3
## vivienda 103    4     800       306            2      3            3
casos1 <- rbind(res.pca$x[400,1:2],res.pca$x[101,1:2]) # CP1
rownames(casos1) = c("400","600")
casos1 <- as.data.frame(casos1)

casos2 <- rbind(res.pca$x[102,1:2], res.pca$x[103,1:2]) # CP2
rownames(casos2) = c("1000","2000")
casos2 <- as.data.frame(casos2)

fviz_pca_ind(res.pca, col.ind = "#DEDEDE", gradient.cols = c("#00AFBB", "#E7B800", "#FC4E07")) +
geom_point(data = casos1, aes(x = PC1, y = PC2), color = "red", size = 3) +
geom_point(data = casos2, aes(x = PC1, y = PC2), color = "blue", size = 3)

fviz_pca_biplot(res.pca, 
repel = TRUE,
habillage = data_clean1$piso,
col.var = "#034A94", # Variables color
col.ind = c("#DEDEDE", "#034A94")  # Individuals color
)

Metodo 2: Dataset con faltantes eliminados

Se decide hacer otra iteración del modelo pero con el dataset con missing values eliminados

Ajuste de escala

data_clean2_numeric <- data_clean2 %>%
  dplyr::select_if(is.numeric) %>%
  scale()

head(data_clean2_numeric)
##            piso    preciom  areaconst parqueaderos     banios habitaciones
## [1,] -0.7061271 -0.1750766  0.7649053    1.0772608  1.3177459   -0.4388435
## [2,] -1.0810833 -0.6045496 -0.6119369   -0.7407711 -0.9099349   -0.4388435
## [3,] -1.0810833 -0.6659029 -0.6336765   -0.7407711 -0.1673747   -0.4388435
## [4,] -1.0810833 -0.7272561 -0.8873053    0.1682449 -0.9099349   -0.4388435
## [5,] -1.0810833 -0.4511664 -0.2713496    0.1682449 -0.1673747    0.3234003
## [6,] -0.7061271 -0.4204898 -0.1771446    0.1682449  0.5751856    1.8478880

PCA

En esta versión, el primer componenete toma mayor fuerza, aumentando 3.1 puntos, es decir una mejora del 6% aproximadamente. Para el componente 2 el incremento fue leve pasando de un 17,6% a un 17,8%. Finalmente el componente tres, que muestra una pequeña influencia, en esta caso disminuye 1.3 puntos

res.pca2 <- prcomp(data_clean2_numeric)
fviz_eig(res.pca2, addlabels = TRUE)

Un hallazgo relevante de este modelo corresponde a la orientación que adoptan las variables en el plano factorial. Se mantiene la alta contribución de las variables piso y preciom, junto con el conjunto de variables que se proyectan en la misma dirección, lo que evidencia una fuerte correlación entre ellas.

La dirección de cada vector refleja la relación existente entre las variables. Por ejemplo, la variable habitaciones presenta una orientación definida hacia el cuadrante positivo de la Dimensión 2, lo que sugiere que, a medida que aumenta el número de habitaciones, también se incrementa la varianza explicada en dicha dimensión.

En cuanto a las correlaciones, se identifica que baños, habitaciones y área construida mantienen una correlación positiva entre sí. Por otro lado, la variable precio se orienta en una dirección que la vincula con variables como parqueaderos, lo que sugiere que el valor de la propiedad podría estar influenciado por la disponibilidad de espacios de estacionamiento.

fviz_pca_var(res.pca2,
col.var = "contrib", # Color by contributions to the PC
gradient.cols = c("#A9CBB7","#E7B800",  "#034D94"),
repel = TRUE     # Avoid text overlapping
)

Para explicar el sentido de los ejes, se escogen cuatro casos extremos conformados por los siguientes clientes:

vivienda_num2 <- data_clean2 %>%
  select(piso, preciom, areaconst, parqueaderos, banios, habitaciones)

datos2<- rbind(vivienda_num2[100,], 
              vivienda_num2[390,],
              vivienda_num2[70,],
              vivienda_num2[530,])

datos2 <- as.data.frame(datos2)
rownames(datos2) = c("vivienda 100","vivienda 280","vivienda 10","vivienda 500")
datos2
##              piso preciom areaconst parqueaderos banios habitaciones
## vivienda 100    1     240       350            1      3            8
## vivienda 280    6     320       130            1      3            3
## vivienda 10    11     900       184            2      4            3
## vivienda 500    8     225        73            1      2            3
casos2_1 <- rbind(res.pca$x[100,1:2],res.pca$x[280,1:2]) # CP1
rownames(casos2_1) = c("100","280")
casos2_1 <- as.data.frame(casos2_1)

casos2_2 <- rbind(res.pca$x[200,1:2], res.pca$x[300,1:2]) # CP2
rownames(casos2_2) = c("10","500")
casos2_2 <- as.data.frame(casos2_2)

fviz_pca_ind(res.pca, col.ind = "#DEDEDE", gradient.cols = c("#00AFBB", "#E7B800", "#FC4E07")) +
geom_point(data = casos2_1, aes(x = PC1, y = PC2), color = "red", size = 3) +
geom_point(data = casos2_2, aes(x = PC1, y = PC2), color = "blue", size = 3)

fviz_pca_biplot(res.pca2, 
repel = TRUE,
habillage = data_clean2$habitaciones,
col.var = "#034A94", # Variables color
col.ind = c("#DEDEDE", "#034A94")  # Individuals color
)

Análisis de Conglomerados

Se realiza ejercicio utilizando dos métodos para el cálculo de distancias

Distancia Euclidiana

library(tidyverse)
## Warning: package 'tidyverse' was built under R version 4.4.3
## Warning: package 'tibble' was built under R version 4.4.3
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ forcats   1.0.0     ✔ readr     2.1.5
## ✔ lubridate 1.9.4     ✔ stringr   1.5.1
## ✔ purrr     1.0.2     ✔ tibble    3.3.0
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::combine() masks gridExtra::combine()
## ✖ dplyr::filter()  masks mice::filter(), stats::filter()
## ✖ dplyr::lag()     masks stats::lag()
## ✖ tibble::view()   masks summarytools::view()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
vivienda_clusters <- data_clean1 %>%
  select(piso, estrato, preciom, areaconst, parqueaderos, banios, habitaciones)

vivienda_clustersz = scale(vivienda_clusters)
vivienda_clustersz = as.data.frame(vivienda_clustersz)
#vivienda_clustersz
# distancia euclidiana

dist_emp <- dist(vivienda_clustersz, method = 'euclidean')

# Clúster jerárquico con el método complete
hc_emp <- hclust(dist_emp, method = 'complete')

# Determinamos a dónde pertenece cada observación
cluster_assigments <- cutree(hc_emp, k = 3)

# asignamos los clusters
assigned_cluster <- vivienda_clustersz %>% mutate(cluster = as.factor(cluster_assigments))

# Luego, crear el gráfico
ggplot(assigned_cluster, aes(x = preciom, y = areaconst, color = cluster)) +
  geom_point(size = 4) +
  geom_text(aes(label = cluster), vjust = -.8) + # Agregar etiquetas del clúster
  theme_classic()

## Distancia de Manhattan

Se realiza la clusterización utilizando otro tipo de cálculo de distancia, sin embargo los resultados fueron muy similiares

# distancia manhattan

dist_manh <- dist(vivienda_clustersz, method = 'euclidean')

# Clúster jerárquico con el método complete
hc_emp2 <- hclust(dist_manh, method = 'complete')

# Determinamos a dónde pertenece cada observación
cluster_assigments2 <- cutree(hc_emp2, k = 3)

# asignamos los clusters
assigned_cluster2 <- vivienda_clustersz %>% mutate(cluster = as.factor(cluster_assigments2))

# Luego, crear el gráfico
ggplot(assigned_cluster2, aes(x = preciom, y = areaconst, color = cluster)) +
  geom_point(size = 4) +
  geom_text(aes(label = cluster), vjust = -.8) + # Agregar etiquetas del clúster
  theme_classic()

Del análisis de conglomerados, se puede concluir:

  • El cluster 1: representa una correlación directa entre el área construida y el preciom.
  • El cluster 2: Este muestra una mayot concentración en áreas no tran grandes pero con precios más elevados. A diferencia del cluster 1, en el que se tenían en cuenta areas muy pequeñas
  • El cluster 3: Está más diferenciado de los dos clusters iniciales, y se caracteria por areas construidas medianas y grandes y por una varación en sus precios, posiblemente relacionada al número de pisos

Análisis de Correspondencia

dfviviendacategoria <- data_clean1 %>% dplyr::select(zona, estrato, tipo)
zonaestrato <- table(dfviviendacategoria$zona, dfviviendacategoria$estrato)
zonaestrato
##               
##                   3    4    5    6
##   Zona Centro   102   14    4    1
##   Zona Norte    566  394  758  170
##   Zona Oeste     52   83  286  767
##   Zona Oriente  334    8    2    1
##   Zona Sur      378 1607 1678 1038

Test chi cuadrado para las zonas y los estratos:

El resultado indica que se rechaza la hipótesis de independencia de las variables Zona~estrato (p-value: 0.00000000000000022).

chisq.test(zonaestrato)
## 
##  Pearson's Chi-squared test
## 
## data:  zonaestrato
## X-squared = 3801.8, df = 12, p-value < 2.2e-16

En el siguiente gráfico se muestra la relación entre el estrato socioeconómico y la zona de ubicación del inmueble. Los resultados indican que:

El estrato 6 se concentra principalmente en la zona oeste.

Los estratos 4 y 5 predominan en la zona sur.

El estrato 3 se localiza principalmente en las zonas centro y oriente.

Para evaluar el grado de representatividad del análisis, se calcularon los valores de la varianza acumulada a partir de los valores propios de la matriz de discrepancias.

library(FactoMineR)
resultados_ac <- CA(zonaestrato)

valores_prop <-resultados_ac$eig
valores_prop
##       eigenvalue percentage of variance cumulative percentage of variance
## dim 1 0.32216299              69.851179                          69.85118
## dim 2 0.12791612              27.734694                          97.58587
## dim 3 0.01113428               2.414127                         100.00000
fviz_screeplot(resultados_ac, addlabels = TRUE, ylim = c(0, 80))+ggtitle("")+
ylab("Porcentaje de varianza explicado") + xlab("Ejes")

Los resultados muestran que la primera componente principal explica aproximadamente el 70% de la variabilidad y que los dos primeros ejes factoriales, en conjunto, resumen el 97.6% de la información contenida en los datos.

A partir de todos los hallazgos, se concluye y recomienda a la inmobiliaria lo siguiente:

El análisis de componentes principales (PCA) reveló una marcada relación entre el precio de la vivienda y el piso en el que se encuentra el apartamento o el número de pisos que tiene la casa, un mayor detalle en esta variable del piso podría determinar mejor los factores que tienen un impacto significativo en el precio final de la propiedad.

Por otro lado, ll análisis de conglomerados identificó distintos segmentos de mercado, por lo que se sugiere al área de marketing diseñar estrategias específicas adaptadas a cada segmento identificado.