library(ggplot2) # Para gráficos
library(dplyr) # Para manipulación de datos
## 
## Adjuntando el paquete: 'dplyr'
## The following object is masked from 'package:gridExtra':
## 
##     combine
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
library(cluster)   # Para clustering
library(factoextra) # Para visualización
## Warning: package 'factoextra' was built under R version 4.4.2
## Welcome! Want to learn more? See two factoextra-related books at https://goo.gl/ve3WBa
library(tidyr)
library(tinytex)
library(GGally)

Análisis exploratorio

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>

Primeramente, para conocer la data que se realiza un resumen de estadísticas descriptivas. De esta manera se puede observar:

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

Se tienen 8,322 observaciones y las siguientes variables:

Análisis de componentes principales

# Variables numéricas relevantes
vars_numericas <- c("estrato", "preciom", "areaconst", "parqueaderos",
                    "banios", "habitaciones", "longitud", "latitud")

# Nuevo data frame con las variables seleccionadas
df_numeric <- vivienda[, vars_numericas]

# Se eliminan filas con NA
df_numeric <- na.omit(df_numeric)

head(df_numeric)
## # A tibble: 6 × 8
##   estrato preciom areaconst parqueaderos banios habitaciones longitud latitud
##     <dbl>   <dbl>     <dbl>        <dbl>  <dbl>        <dbl>    <dbl>   <dbl>
## 1       3     250        70            1      3            6    -76.5    3.43
## 2       3     320       120            1      2            3    -76.5    3.43
## 3       3     350       220            2      2            4    -76.5    3.44
## 4       4     400       280            3      5            3    -76.5    3.44
## 5       5     260        90            1      2            3    -76.5    3.46
## 6       5     240        87            1      3            3    -76.5    3.37
# Ejecutar el PCA con centrado y escalado
pca_result <- prcomp(df_numeric, center = TRUE, scale. = TRUE)

# Resumen del PCA para ver la varianza explicada
summary(pca_result)
## Importance of components:
##                           PC1    PC2    PC3     PC4     PC5     PC6     PC7
## Standard deviation     1.9007 1.2022 0.9675 0.86369 0.68670 0.59739 0.49409
## Proportion of Variance 0.4516 0.1807 0.1170 0.09324 0.05895 0.04461 0.03052
## Cumulative Proportion  0.4516 0.6322 0.7492 0.84246 0.90141 0.94602 0.97653
##                            PC8
## Standard deviation     0.43327
## Proportion of Variance 0.02347
## Cumulative Proportion  1.00000
fviz_eig(pca_result, addlabels = TRUE, ylim = c(0, 60)) +
  ggtitle("Scree Plot: Varianza Explicada por Componentes")

fviz_pca_biplot(pca_result,
                geom.ind = "point",  # Representa las observaciones como puntos
                col.ind = "blue",    # Color para las observaciones
                repel = TRUE,        # Evita que las etiquetas se solapen
                title = "Biplot de PCA")

fviz_pca_var(pca_result,
             col.var = "contrib",            # Colorea según la contribución
             gradient.cols = c("blue", "yellow", "red"),
             repel = TRUE,                   # Evita solapamientos en las etiquetas
             title = "Contribución de Variables en el PCA")

El precio (preciom) está fuertemente influenciado por características estructurales como área construida, cantidad de baños, parqueaderos y estrato. Las variables de localización (latitud y longitud) contribuyen menos a la variabilidad principal del dataset, pero aún pueden ser importantes en Dim2. El PCA ha reducido la dimensionalidad a dos componentes que explican el 63.3% de la variabilidad (45.2% + 18.1%), lo que sugiere que estas dimensiones capturan gran parte de la estructura del mercado inmobiliario.

Análisis de conglomerados

Escalar variables para evitar que algunas dominen la agrupación

# Escalar las variables para normalizarlas (importante en clustering)
df_scaled <- scale(df_numeric)

# Verificar estructura
summary(df_scaled)
##     estrato           preciom          areaconst        parqueaderos    
##  Min.   :-1.9279   Min.   :-1.2264   Min.   :-1.0488   Min.   :-0.7425  
##  1st Qu.:-0.8747   1st Qu.:-0.6593   1st Qu.:-0.6602   1st Qu.:-0.7425  
##  Median : 0.1786   Median :-0.3399   Median :-0.3549   Median : 0.1465  
##  Mean   : 0.0000   Mean   : 0.0000   Mean   : 0.0000   Mean   : 0.0000  
##  3rd Qu.: 1.2319   3rd Qu.: 0.3317   3rd Qu.: 0.3599   3rd Qu.: 0.1465  
##  Max.   : 1.2319   Max.   : 4.5669   Max.   :10.8526   Max.   : 7.2582  
##      banios         habitaciones        longitud           latitud        
##  Min.   :-2.3587   Min.   :-2.6466   Min.   :-3.57020   Min.   :-1.91557  
##  1st Qu.:-0.9096   1st Qu.:-0.4477   1st Qu.:-0.74209   1st Qu.:-0.84733  
##  Median :-0.1851   Median :-0.4477   Median :-0.08256   Median :-0.07056  
##  Mean   : 0.0000   Mean   : 0.0000   Mean   : 0.00000   Mean   : 0.00000  
##  3rd Qu.: 0.5394   3rd Qu.: 0.2852   3rd Qu.: 0.60741   3rd Qu.: 0.84611  
##  Max.   : 4.8866   Max.   : 4.6831   Max.   : 4.11210   Max.   : 1.93093

Luego se determina el número óptimo de clústeres

# Método de Silhouette
fviz_nbclust(df_scaled, kmeans, method = "silhouette") +
  ggtitle("Número óptimo de Clústeres - Silhouette")

Una vez determinado el número óptimo de clústeres (en este caso, k = 2), aplicamos K-Means.

# Aplicar K-Means con k óptimo 
set.seed(123)  # Asegurar reproducibilidad
k <- 2  #  número óptimo encontrado
km_res <- kmeans(df_scaled, centers = k, nstart = 25)

# Agregar los clusters al data frame original
df_numeric$cluster <- as.factor(km_res$cluster)

# Ver resumen de cada grupo
aggregate(df_numeric, by = list(df_numeric$cluster), 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
##   Group.1  estrato  preciom areaconst parqueaderos   banios habitaciones
## 1       1 4.506104 296.6242  116.5592     1.353385 2.545616     3.191121
## 2       2 5.490958 819.7012  312.6555     2.816456 4.701175     4.465642
##    longitud  latitud cluster
## 1 -76.52690 3.418279      NA
## 2 -76.53791 3.408386      NA

Visualización de los conglomerados

fviz_cluster(km_res, data = df_scaled,
             ellipse.type = "norm", geom = "point",
             palette = "jco", ggtheme = theme_minimal()) +
  ggtitle("Visualización de Clústeres en Componentes Principales")

ggplot(df_numeric, aes(x = longitud, y = latitud, color = cluster)) +
  geom_point(alpha = 0.6) +
  scale_color_manual(values = c("red", "blue", "green", "purple")) +
  theme_minimal() +
  ggtitle("Distribución Geoespacial de los Clústeres")

En cuanto al cluster 1 las viviendas son de menor precio, área construida y comodidades (menos baños, habitaciones y parqueaderos) mientras el cluster 2 tiene viviendas de mayor estrato y mejores caracteristicas. En cuanto a la ubicación geografica no hay diferencias significativas por lo que se podría decir que la ubicación no es un diferencial importante para el precio.

Análisis de correspondencia

# Seleccionar solo las variables categóricas
df_correspondencia <- vivienda %>% select(tipo, zona, barrio)

# Eliminar valores nulos
df_correspondencia <- na.omit(df_correspondencia)

# Crear una tabla de contingencia entre 'tipo de vivienda' y 'zona'
tabla_contingencia <- table(df_correspondencia$tipo, df_correspondencia$zona)
# Mostrar la tabla
print(tabla_contingencia)
##              
##               Zona Centro Zona Norte Zona Oeste Zona Oriente Zona Sur
##   Apartamento          24       1198       1029           62     2787
##   Casa                100        722        169          289     1939