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.
Se inicia con la instalación y carga de paquetes, que contienen la base de datos “vivienda_faltantes”. Se cuenta con un total de 8,322 registros y 13 variables.
# Cargar los datos
data("vivienda")
# Visión general de los datos
glimpse(vivienda)
## Rows: 8,322
## Columns: 13
## $ id <dbl> 1147, 1169, 1350, 5992, 1212, 1724, 2326, 4386, 1209, 159…
## $ zona <chr> "Zona Oriente", "Zona Oriente", "Zona Oriente", "Zona Sur…
## $ piso <chr> NA, NA, NA, "02", "01", "01", "01", "01", "02", "02", "02…
## $ estrato <dbl> 3, 3, 3, 4, 5, 5, 4, 5, 5, 5, 6, 4, 5, 6, 4, 5, 5, 4, 5, …
## $ preciom <dbl> 250, 320, 350, 400, 260, 240, 220, 310, 320, 780, 750, 62…
## $ areaconst <dbl> 70, 120, 220, 280, 90, 87, 52, 137, 150, 380, 445, 355, 2…
## $ parqueaderos <dbl> 1, 1, 2, 3, 1, 1, 2, 2, 2, 2, NA, 3, 2, 2, 1, 4, 2, 2, 2,…
## $ banios <dbl> 3, 2, 2, 5, 2, 3, 2, 3, 4, 3, 7, 5, 6, 2, 4, 4, 4, 3, 2, …
## $ habitaciones <dbl> 6, 3, 4, 3, 3, 3, 3, 4, 6, 3, 6, 5, 6, 2, 5, 5, 4, 3, 3, …
## $ tipo <chr> "Casa", "Casa", "Casa", "Casa", "Apartamento", "Apartamen…
## $ barrio <chr> "20 de julio", "20 de julio", "20 de julio", "3 de julio"…
## $ longitud <dbl> -76.51168, -76.51237, -76.51537, -76.54000, -76.51350, -7…
## $ latitud <dbl> 3.43382, 3.43369, 3.43566, 3.43500, 3.45891, 3.36971, 3.4…
Se inicia identificando por cada variable el total de datos faltantes
md.pattern(vivienda,rotate.names = TRUE)
## preciom id zona estrato areaconst banios habitaciones tipo barrio longitud
## 4808 1 1 1 1 1 1 1 1 1 1
## 1909 1 1 1 1 1 1 1 1 1 1
## 876 1 1 1 1 1 1 1 1 1 1
## 726 1 1 1 1 1 1 1 1 1 1
## 1 1 0 0 0 0 0 0 0 0 0
## 2 0 0 0 0 0 0 0 0 0 0
## 2 3 3 3 3 3 3 3 3 3
## latitud parqueaderos piso
## 4808 1 1 1 0
## 1909 1 1 0 1
## 876 1 0 1 1
## 726 1 0 0 2
## 1 0 0 0 12
## 2 0 0 0 13
## 3 1605 2638 4275
Siendo la variable “id” un identificador unico, se procede a eliminar duplicados y NA de la variable “id”, obteniendo que las variables “piso” y “parqueaderos” son las unicas con datos faltantes a tratar
# Contar el total de registros duplicados basados en la variable "id"
total_duplicados_id <- sum(duplicated(vivienda$id))
# Eliminar registros duplicados basados en la variable "id"
id_sin_dup <- distinct(vivienda, id, .keep_all = TRUE)
#Eliminar registros faltantes (NA) variable "id"
id_sin_dup_na <- id_sin_dup %>% filter(!is.na(id))
faltantes <- colSums(is.na(id_sin_dup_na)) %>% as.data.frame()
faltantes
bd_sin_dup <- id_sin_dup_na #Base sin "id" duplicados
Se revisan las variables nominales “zona” - “tipo”
# Revisión variables nominales
table(bd_sin_dup$zona)
##
## Zona Centro Zona Norte Zona Oeste Zona Oriente Zona Sur
## 124 1920 1198 351 4726
table(bd_sin_dup$tipo)
##
## Apartamento Casa
## 5100 3219
Finalmente, se hace un analisis sobre las variables con datos faltantes:
plot_missing(bd_sin_dup)
# Tabla pisos_tipo
bd_sin_dup$piso <- as.numeric(bd_sin_dup$piso)
pisos_tipo <- table1(~ piso| tipo, data=bd_sin_dup)
kable(pisos_tipo, caption = "Distribución de pisos por tipo")
| Apartamento | Casa | Overall | |
|---|---|---|---|
| (N=5100) | (N=3219) | (N=8319) | |
| piso | |||
| Mean (SD) | 4.63 (2.81) | 2.14 (0.860) | 3.77 (2.61) |
| Median [Min, Max] | 4.00 [1.00, 12.0] | 2.00 [1.00, 10.0] | 3.00 [1.00, 12.0] |
| Missing | 1381 (27.1%) | 1254 (39.0%) | 2635 (31.7%) |
# Tabla parqueadero_tipo
parqueadero_tipo <- table1(~ parqueaderos | tipo, data=bd_sin_dup)
kable(parqueadero_tipo, caption = "Distribución de parqueaderos por tipo")
| Apartamento | Casa | Overall | |
|---|---|---|---|
| (N=5100) | (N=3219) | (N=8319) | |
| parqueaderos | |||
| Mean (SD) | 1.57 (0.743) | 2.29 (1.47) | 1.84 (1.12) |
| Median [Min, Max] | 1.00 [1.00, 10.0] | 2.00 [1.00, 10.0] | 2.00 [1.00, 10.0] |
| Missing | 869 (17.0%) | 733 (22.8%) | 1602 (19.3%) |
Se realiza una imputación basada en la mediana, donde se empleó la mediana calculada para cada tipo de vivienda para completar los valores faltantes en las variables correspondientes a ese tipo de vivienda.
# Calcular medianas por tipo de vivienda
medianas <- bd_sin_dup %>%
group_by(tipo) %>%
summarise(mediana_piso = median(piso, na.rm = TRUE),
mediana_parqueaderos = median(parqueaderos, na.rm = TRUE))
medianas
# Completar valores faltantes usando la mediana correspondiente
bd_clean <- bd_sin_dup %>%
left_join(medianas, by = "tipo") %>% # Unir las medianas calculadas al dataframe original
mutate(piso = if_else(is.na(piso), mediana_piso, piso),
parqueaderos = if_else(is.na(parqueaderos), mediana_parqueaderos,
parqueaderos)) %>%
select(-mediana_piso, -mediana_parqueaderos) # Eliminar columnas auxiliares
colSums(is.na(bd_clean))
## 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
glimpse(bd_clean)
## Rows: 8,319
## Columns: 13
## $ id <dbl> 1147, 1169, 1350, 5992, 1212, 1724, 2326, 4386, 1209, 159…
## $ zona <chr> "Zona Oriente", "Zona Oriente", "Zona Oriente", "Zona Sur…
## $ piso <dbl> 2, 2, 2, 2, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, …
## $ estrato <dbl> 3, 3, 3, 4, 5, 5, 4, 5, 5, 5, 6, 4, 5, 6, 4, 5, 5, 4, 5, …
## $ preciom <dbl> 250, 320, 350, 400, 260, 240, 220, 310, 320, 780, 750, 62…
## $ areaconst <dbl> 70, 120, 220, 280, 90, 87, 52, 137, 150, 380, 445, 355, 2…
## $ parqueaderos <dbl> 1, 1, 2, 3, 1, 1, 2, 2, 2, 2, 2, 3, 2, 2, 1, 4, 2, 2, 2, …
## $ banios <dbl> 3, 2, 2, 5, 2, 3, 2, 3, 4, 3, 7, 5, 6, 2, 4, 4, 4, 3, 2, …
## $ habitaciones <dbl> 6, 3, 4, 3, 3, 3, 3, 4, 6, 3, 6, 5, 6, 2, 5, 5, 4, 3, 3, …
## $ tipo <chr> "Casa", "Casa", "Casa", "Casa", "Apartamento", "Apartamen…
## $ barrio <chr> "20 de julio", "20 de julio", "20 de julio", "3 de julio"…
## $ longitud <dbl> -76.51168, -76.51237, -76.51537, -76.54000, -76.51350, -7…
## $ latitud <dbl> 3.43382, 3.43369, 3.43566, 3.43500, 3.45891, 3.36971, 3.4…
Durante esta etapa se lleva a cabo un analisis más detallado de las variables, mediante métodos que permiten resumir y presentar los datos de manera informativa, empleando medidas de tendencia central, dispersión, frecuencias y de distribución para comprender la naturaleza de los datos en profundidad y la identificación de posibles patrones o tendencias.
variables <- bd_clean[, c("preciom", "areaconst", "banios", "habitaciones",
"piso","parqueaderos")]
summarytools::descr(variables)
## Descriptive Statistics
## variables
## N: 8319
##
## areaconst banios habitaciones parqueaderos piso preciom
## ----------------- ----------- --------- -------------- -------------- --------- ---------
## Mean 174.93 3.11 3.61 1.76 3.54 433.90
## Std.Dev 142.96 1.43 1.46 1.04 2.26 328.67
## Min 30.00 0.00 0.00 1.00 1.00 58.00
## Q1 80.00 2.00 3.00 1.00 2.00 220.00
## Median 123.00 3.00 3.00 2.00 3.00 330.00
## Q3 229.00 4.00 4.00 2.00 4.00 540.00
## Max 1745.00 10.00 10.00 10.00 12.00 1999.00
## MAD 84.51 1.48 1.48 1.48 1.48 207.56
## IQR 149.00 2.00 1.00 1.00 2.00 320.00
## CV 0.82 0.46 0.40 0.59 0.64 0.76
## Skewness 2.69 0.93 1.63 2.50 1.59 1.85
## SE.Skewness 0.03 0.03 0.03 0.03 0.03 0.03
## Kurtosis 12.91 1.13 3.98 9.98 2.62 3.67
## N.Valid 8319.00 8319.00 8319.00 8319.00 8319.00 8319.00
## Pct.Valid 100.00 100.00 100.00 100.00 100.00 100.00
plot_histogram(variables)
De acuerdo con el análisis, se logra evidenciar que:
summarytools::freq(bd_clean$zona)
## Frequencies
## bd_clean$zona
## Type: Character
##
## Freq % Valid % Valid Cum. % Total % Total Cum.
## ------------------ ------ --------- -------------- --------- --------------
## Zona Centro 124 1.49 1.49 1.49 1.49
## Zona Norte 1920 23.08 24.57 23.08 24.57
## Zona Oeste 1198 14.40 38.97 14.40 38.97
## Zona Oriente 351 4.22 43.19 4.22 43.19
## Zona Sur 4726 56.81 100.00 56.81 100.00
## <NA> 0 0.00 100.00
## Total 8319 100.00 100.00 100.00 100.00
frecuencia_zona <- table(bd_clean$zona)
porcentajes <- prop.table(frecuencia_zona) * 100
pie(porcentajes, main = "Distribución por Zonas",
labels = paste(names(porcentajes),": ",format(porcentajes, digits = 2), "%"))
Aproximadamente el 57% de las viviendas vendidas se encuentran ubicadas
al sur de la ciudad de Cali.
summarytools::freq(bd_clean$tipo)
## Frequencies
## bd_clean$tipo
## Type: Character
##
## Freq % Valid % Valid Cum. % Total % Total Cum.
## ----------------- ------ --------- -------------- --------- --------------
## Apartamento 5100 61.31 61.31 61.31 61.31
## Casa 3219 38.69 100.00 38.69 100.00
## <NA> 0 0.00 100.00
## Total 8319 100.00 100.00 100.00 100.00
frecuencia_tipo <- table(bd_clean$tipo)
porcentajes2 <- prop.table(frecuencia_tipo) * 100
pie(porcentajes2, main = "Distribución por tipo de vivienda",
labels = paste(names(porcentajes2),": ",format(porcentajes2, digits = 2), "%"),
col = c("#0d3b66", "#f4d35e"))
El 61% de las viviendas vendidas son apartamentos.
summarytools::freq(bd_clean$estrato)
## Frequencies
## bd_clean$estrato
## Type: Numeric
##
## Freq % Valid % Valid Cum. % Total % Total Cum.
## ----------- ------ --------- -------------- --------- --------------
## 3 1453 17.47 17.47 17.47 17.47
## 4 2129 25.59 43.06 25.59 43.06
## 5 2750 33.06 76.11 33.06 76.11
## 6 1987 23.89 100.00 23.89 100.00
## <NA> 0 0.00 100.00
## Total 8319 100.00 100.00 100.00 100.00
frecuencia_estrato <- table(bd_clean$estrato)
porcentajes_estrato <- prop.table(frecuencia_estrato) * 100
barplot(frecuencia_estrato,
main = "Distribución por Estrato",xlab = "Estrato",ylab = "Frecuencia",
col = "#0d3b66",border = "white")
text(x = seq_along(frecuencia_estrato), y = frecuencia_estrato,
labels = paste0(frecuencia_estrato," (", round(porcentajes_estrato, 2), "%)"),
pos = 3, cex = 0.8)
El 59% de las viviendas se enccuentran en los estrato 4 y 5.
El método de Análisis de Componentes Principales (PCA) es aplicable a variables númericas, por lo que inicialmente se debe crear un nuevo conjunto de datos que almacenara estas variables. Estas variables son: “preciom”, “areaconst”, “banios”, “habitaciones”, “piso” y “parqueaderos”.
data_num <- variables # Base con variables numéricas
summarytools::descr(data_num)
## Descriptive Statistics
## data_num
## N: 8319
##
## areaconst banios habitaciones parqueaderos piso preciom
## ----------------- ----------- --------- -------------- -------------- --------- ---------
## Mean 174.93 3.11 3.61 1.76 3.54 433.90
## Std.Dev 142.96 1.43 1.46 1.04 2.26 328.67
## Min 30.00 0.00 0.00 1.00 1.00 58.00
## Q1 80.00 2.00 3.00 1.00 2.00 220.00
## Median 123.00 3.00 3.00 2.00 3.00 330.00
## Q3 229.00 4.00 4.00 2.00 4.00 540.00
## Max 1745.00 10.00 10.00 10.00 12.00 1999.00
## MAD 84.51 1.48 1.48 1.48 1.48 207.56
## IQR 149.00 2.00 1.00 1.00 2.00 320.00
## CV 0.82 0.46 0.40 0.59 0.64 0.76
## Skewness 2.69 0.93 1.63 2.50 1.59 1.85
## SE.Skewness 0.03 0.03 0.03 0.03 0.03 0.03
## Kurtosis 12.91 1.13 3.98 9.98 2.62 3.67
## N.Valid 8319.00 8319.00 8319.00 8319.00 8319.00 8319.00
## Pct.Valid 100.00 100.00 100.00 100.00 100.00 100.00
Ya que se presentan diferencias en las escalas de las variables, se realizó la estandarización para su análisis y prevenir que las variables con escalas de valores más grandes sesguen las estimaciones, asegurando que las variables contribuyan manera equitativa en el análisis:
data_numz= scale(data_num) #Se estandarizan los datos
summary(data_numz)
## preciom areaconst banios habitaciones
## Min. :-1.1437 Min. :-1.0138 Min. :-2.17847 Min. :-2.4702
## 1st Qu.:-0.6508 1st Qu.:-0.6640 1st Qu.:-0.77812 1st Qu.:-0.4148
## Median :-0.3161 Median :-0.3633 Median :-0.07794 Median :-0.4148
## Mean : 0.0000 Mean : 0.0000 Mean : 0.00000 Mean : 0.0000
## 3rd Qu.: 0.3228 3rd Qu.: 0.3782 3rd Qu.: 0.62224 3rd Qu.: 0.2704
## Max. : 4.7620 Max. :10.9822 Max. : 4.82330 Max. : 4.3813
## piso parqueaderos
## Min. :-1.1256 Min. :-0.7298
## 1st Qu.:-0.6828 1st Qu.:-0.7298
## Median :-0.2400 Median : 0.2273
## Mean : 0.0000 Mean : 0.0000
## 3rd Qu.: 0.2028 3rd Qu.: 0.2273
## Max. : 3.7451 Max. : 7.8840
Teniendo las variables estandarizadas se realiza el cálculo de los componentes principales:
data_pca = prcomp(data_numz) #Calculo de componentes principales
data_pca
## Standard deviations (1, .., p=6):
## [1] 1.8083042 1.0359130 0.8645895 0.6255943 0.5719934 0.4368757
##
## Rotation (n x k) = (6 x 6):
## PC1 PC2 PC3 PC4 PC5
## preciom 0.4563098 0.35271739 0.2352750 0.33933742 0.1786403
## areaconst 0.4801047 -0.02235070 0.0297751 0.46175001 -0.6393381
## banios 0.4772284 0.03718653 -0.2712414 0.08620164 0.6863697
## habitaciones 0.3582706 -0.41625591 -0.6572569 -0.28195653 -0.1844438
## piso -0.1538954 0.80029142 -0.5464489 -0.05391418 -0.1787679
## parqueaderos 0.4260191 0.24489324 0.3736230 -0.76275821 -0.1491765
## PC6
## preciom 0.68187629
## areaconst -0.38229201
## banios -0.46772228
## habitaciones 0.39098280
## piso -0.04877242
## parqueaderos -0.12201278
fviz_eig(data_pca, addlabels = TRUE) #Variabilidad capturada por cada componente
summary(data_pca)
## Importance of components:
## PC1 PC2 PC3 PC4 PC5 PC6
## Standard deviation 1.808 1.0359 0.8646 0.62559 0.57199 0.43688
## Proportion of Variance 0.545 0.1789 0.1246 0.06523 0.05453 0.03181
## Cumulative Proportion 0.545 0.7238 0.8484 0.91366 0.96819 1.00000
Vemos que el primer componente principal explica el 54.5% de la variabilidad contenida en la base de datos y entre los dos primeros hacen el 72.4%, lo cual sugiere que con solo dos componentes principales, se puede resumir gran parte de la variabilidad que contiene la base de datos.
fviz_pca_var(data_pca,col.var = "contrib",
gradient.cols = c("#FF7F00", "#034D94"),repel = TRUE) #Circulo de correlaciones
El círculo de correlaciones nos muestra cómo las variables originales se
proyectan en el espacio de los componentes. Se observa que las variables
“precio”-“baños”-“area construida” tienen una mayor relación con el
segundo componente principal, mientras que la variable “piso” presenta
mayor efecto sobre el primer componente.
Las variables “parqueaderos” y “habitaciones” son las que menos contribuyen a la conformación del primer componente principal.
También se observa que el precio de la viviendas y la cantidad de parqueaderos estan correlacionadas, por lo que se puede decir que las viviendas con mayor cantidad de parqueaderos pueden tener precios más altos. Ocurre lo mismo con el área contruida y número de baños, al presentar una correlación alta se puede pensar que las viviendas a mayor área construida tienen más baños.
fviz_contrib(data_pca, choice = "var", axes = 1:2) #Contribución de las variables
Se genera un gráfico que muestra la contribución de cada variable a los primeros dos componentes principales, ayudando a identificar qué variables tienen mayor influencia en la formación de los componentes. Se observa que las primeras 4 variables “preciom”, “piso” ,“areaconst”, “baños” tienen las contribuciones más altas, confirmando lo anteriormente dicho.
Con las variables numericas ya estandarizadas “preciom”, “areaconst”, “banios”, “habitaciones”, “piso” y “parqueaderos”, se utiliza el método del codo para determinar el número óptimo de clusters para el algoritmo K-means. En este caso se sugiere un k=3:
fviz_nbclust(data_numz, kmeans, method = "wss")
Aplicando el algoritmo K-means con 3 centros (clusters) y 25 iniciaciones aleatorias (nstart = 25) para encontrar el mejor agrupamiento. Se muestra la distribución de las observaciones en cada cluster:
set.seed(123)
# Aplicar K-means con el número óptimo de clusters
kmeans_mod <- kmeans(data_numz, centers = 3, nstart = 25)
data_num$cluster = as.factor(kmeans_mod$cluster)
table(data_num$cluster)
##
## 1 2 3
## 929 4896 2494
fviz_cluster(kmeans_mod, data = data_numz,
geom = "point",
ellipse.type = "convex",
ggtheme = theme_minimal(),
main = "Clusters Plot")
colores <- c("#F4A261", "#2A9D8F", "#E9C46A")
ggplot(data_num, aes(x = factor(cluster))) +
geom_bar(fill = colores) +
labs(title = "Distribución de viviendas por clúster",
x = "Clúster",
y = "Cantidad de viviendas") +
theme_minimal() +
geom_text(stat='count', aes(label=..count..), vjust=-0.5)
# Tabla resumen
resumen_clusters <- data_num %>%
group_by(cluster) %>%
summarise(across(everything(), mean, na.rm = TRUE))
print(resumen_clusters)
## # A tibble: 3 × 7
## cluster preciom areaconst banios habitaciones piso parqueaderos
## <fct> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 1 1128. 422. 5.17 4.50 3.12 3.78
## 2 2 264. 96.0 2.24 2.87 4.12 1.30
## 3 3 509. 238. 4.06 4.72 2.57 1.92
Se observa que el cluster “2” presenta la mayor concentración con un total de 4.896 viviendas, seguido del clúster 3 que cuenta con 2494 viviendas. Lo cual ayuda a entender cómo se agrupan las viviendas en función de sus características.
# Ajusta 'variable1' y 'variable2' a las columnas que deseas visualizar
ggplot(data_num, aes(x = areaconst, y = preciom, color = cluster)) +
geom_point(size = 2) +
labs(title = "Area construida vs Precio por Clúster",
x = "Area construida", y = "Precio") +
theme_minimal()
Se puede evidenciar que a medida que aumenta el área construida de la
vivienda, también se incrementa su precio.
Se extraen las columnas categoricas “zona”, “estrato” y “tipo” en un nuevo conjunto de datos, al que se le aplica el Análisis de Correspondencias Múltiples (MCA), técnica de reducción de dimensionalidad para datos categóricos:
data_cat = bd_clean[, c("zona", "estrato", "tipo")]
data_cat$estrato = as.factor(data_cat$estrato)
glimpse(data_cat)
## Rows: 8,319
## Columns: 3
## $ zona <chr> "Zona Oriente", "Zona Oriente", "Zona Oriente", "Zona Sur", "Z…
## $ estrato <fct> 3, 3, 3, 4, 5, 5, 4, 5, 5, 5, 6, 4, 5, 6, 4, 5, 5, 4, 5, 3, 3,…
## $ tipo <chr> "Casa", "Casa", "Casa", "Casa", "Apartamento", "Apartamento", …
# Realizar MCA
mca_result = MCA(data_cat)
# Mapa factorial de las variables
fviz_mca_var(mca_result,
geom = "point",
repel = TRUE,
title = "Mapa Factorial de Variables")
# Mapa factorial de los individuos
fviz_mca_ind(mca_result,
geom = "point",
repel = TRUE,
title = "Mapa Factorial de Individuos")
De acuerdo con los resultados obtenidos, se evidencia un alta
correspondencia entre las variables estrato y zona. Por otro lado, vemos
que la variable tipo no presenta una correspondencia significativa en
relación con las otras dos variables.
El análisis de componentes principales ofrece una visión clara sobre el estado de las viviendas al correlacionar diversas variables numéricas, especialmente aquellas relacionadas con los precios. Esto ayuda a identificar qué variables influyen más en los valores de las viviendas.
El análisis de conglomerados complementa este enfoque al segmentar las viviendas en grupos similares, lo que facilita a la empresa la identificación de nichos de mercado y la definición de metas específicas. Aunque hay margen para profundizar, el análisis actual ya proporciona una comprensión útil de las viviendas en oferta y sus características.
Por otro lado, el análisis de correspondencias es limitado al trabajar con solo dos variables y puede no ser adecuado con un gran número de categorías. Sin embargo, revela relaciones entre variables categóricas y facilita el análisis al responder preguntas específicas sobre las viviendas.