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.
Este informe presenta un análisis holístico de la base datos con variables de propiedades residenciales urbanas de la ciudad de Cali, para el análñisis se realizara lo siguiente:
#install.packages(c("tidyverse","janitor","FactoMineR","factoextra",
# "cluster","NbClust","ggrepel","kableExtra"))
#install.packages("plotly")
#install.packages("quantmod")
library(tidyverse)
library(readr)
library(janitor)
library(scales)
library(FactoMineR)
library(factoextra)
library(cluster)
library(NbClust)
library(ggrepel)
library(knitr)
library(kableExtra)
library(plotly)
library(quantmod)
library(corrplot)
library(paqueteMODELOS)
data("vivienda") # Esto carga el objeto 'vivienda' en el entorno global
viv <- vivienda # Ahora sí viv es un data frame
write.csv(vivienda, "vivienda.csv", row.names = FALSE)
getwd()
## [1] "/Users/marioleandro/Documents/Maestría Ciencia de datos/Estadistica 2/Unidad1/Unidad2_e2_files/vfinal"
piso a numérico seguropreciom y areaconst
> 0precio_m2 = preciom / areaconstto_num_safe <- function(x){
suppressWarnings(as.numeric(gsub(",", ".", trimws(as.character(x)))))
}
viv <- viv %>%
mutate(
piso_num = to_num_safe(piso),
precio_m2 = preciom / areaconst
) %>%
filter(preciom > 0, areaconst > 0) %>%
distinct(id, .keep_all = TRUE)
num_cols <- c("estrato","preciom","areaconst","parqueaderos","banios","habitaciones","piso_num","precio_m2")
cat_cols <- c("zona","tipo","barrio")
# Imputación por mediana
viv <- viv %>%
mutate(across(all_of(num_cols), ~ ifelse(is.na(.x), median(.x, na.rm = TRUE), .x)))
A continuación se presentan las tablas resumen donde se pueden caracterizar las variables.
summary_num <- viv %>%
select(all_of(num_cols)) %>%
summary()
freq_zona <- viv %>% count(zona, sort = TRUE) %>% slice_head(n = 10)
freq_tipo <- viv %>% count(tipo, sort = TRUE)
freq_barrio <- viv %>% count(barrio, sort = TRUE) %>% slice_head(n = 20)
kable(freq_zona, caption = "Frecuencia por Zona (Top 10)") %>% kable_styling(full_width = FALSE)
| zona | n |
|---|---|
| Zona Sur | 4726 |
| Zona Norte | 1920 |
| Zona Oeste | 1198 |
| Zona Oriente | 351 |
| Zona Centro | 124 |
kable(freq_tipo, caption = "Frecuencia por Tipo de Vivienda") %>% kable_styling(full_width = FALSE)
| tipo | n |
|---|---|
| Apartamento | 5100 |
| Casa | 3219 |
kable(freq_barrio, caption = "Frecuencia por Barrio (Top 20)") %>% kable_styling(full_width = FALSE)
| barrio | n |
|---|---|
| valle del lili | 1008 |
| ciudad jardín | 516 |
| pance | 409 |
| la flora | 366 |
| santa teresita | 262 |
| el caney | 208 |
| el ingenio | 202 |
| la hacienda | 164 |
| acopi | 158 |
| los cristales | 154 |
| normandía | 154 |
| el limonar | 135 |
| prados del norte | 126 |
| el refugio | 120 |
| aguacatal | 109 |
| ciudad 2000 | 95 |
| caney | 88 |
| cristales | 83 |
| urbanización la flora | 83 |
| brisas de los | 81 |
Al revisar el grafico de velas, se observan que existen algunos posible outlier, sin embargo y por el contexto de los precios, debe validarse si los precios son reales o se deben a algun error de escritura, al igual que las áreas asociadas para las viviendas.
viv2 <- viv%>%
select_if(is.numeric)%>%
select(-id,-longitud,-latitud)
num_cols <- viv2 %>% select(where(is.numeric)) %>% names()
viv_long <- viv2 %>%
select(all_of(num_cols)) %>%
pivot_longer(cols = everything(), names_to = "variable", values_to = "valor")
# Gráfico tipo vela/boxplot
ggplot(viv_long, aes(x = variable, y = valor)) +
geom_boxplot(outlier.color = "red", fill = "lightblue") +
coord_flip() + # Girar para que sea horizontal
labs(title = "Boxplots para variables numéricas - Base vivienda",
x = "Variable", y = "Valor") +
theme_minimal()
Para poder aplicar PCA, se debe tener en cuenta lo siguiente. - No se deben tener datos faltantes, de acuerdo con el análisis que se realice, se definirá si debemos imputar los datos por alguna medida estadística o mantenerlos como nulos. ## Limpieza de datos - Se debe garantizar que los datos sean numéricos, por esta razón se deben seleccionar las variables numéricas unicamente. - Se debe realizar un escalado de variables, esto llevará a que todas las variables tengan como media cero y desviación estandar 1, esto ayudará a comprender merjor los datos que estan en diferentes unidades de medida.
Estandarizamos las variables numéricas y aplicamos PCA, tambien se muestran las varianzas de cada dimensión
Se procede entonces con la limpieza de datos, inicialmente se validan los datos faltantes de las variables numéricas
library(mice)
md.pattern(viv)
## id zona estrato preciom areaconst parqueaderos banios habitaciones tipo
## 5684 1 1 1 1 1 1 1 1 1
## 2635 1 1 1 1 1 1 1 1 1
## 0 0 0 0 0 0 0 0 0
## barrio longitud latitud piso_num precio_m2 piso
## 5684 1 1 1 1 1 1 0
## 2635 1 1 1 1 1 0 1
## 0 0 0 0 0 2635 2635
Se elimina la unica variable vacia que se tiene en piso, esto se realiza porque no es representativo el valor frente al resto de los datos.
viv <- viv[!is.na(viv$piso), ]
comprobamos que la base de datos no contenga valores nulos.
library(mice)
md.pattern(viv)
## /\ /\
## { `---' }
## { O O }
## ==> V <== No need for mice. This data set is completely observed.
## \ \|/ /
## `-----'
## id zona piso estrato preciom areaconst parqueaderos banios habitaciones
## 5684 1 1 1 1 1 1 1 1 1
## 0 0 0 0 0 0 0 0 0
## tipo barrio longitud latitud piso_num precio_m2
## 5684 1 1 1 1 1 1 0
## 0 0 0 0 0 0 0
Con los datos sin valores nulos, se procede a realizar el escalamiento para posteriormente encontrar las dimensiones representativas y las varianzas asociadas a cada un, para el análisis de pca no tendremos en cuenta los ID ni la longitud ni latitud
X <- scale(viv %>% select(all_of(num_cols)))
pca <- PCA(X, graph = FALSE)
# Varianza explicada
eig_df <- get_eigenvalue(pca)
kable(eig_df, digits = 3, caption = "Autovalores y varianza explicada") %>%
kable_styling(full_width = FALSE)
| eigenvalue | variance.percent | cumulative.variance.percent | |
|---|---|---|---|
| Dim.1 | 3.505 | 43.807 | 43.807 |
| Dim.2 | 2.025 | 25.314 | 69.122 |
| Dim.3 | 0.817 | 10.209 | 79.330 |
| Dim.4 | 0.615 | 7.691 | 87.022 |
| Dim.5 | 0.441 | 5.507 | 92.529 |
| Dim.6 | 0.303 | 3.784 | 96.313 |
| Dim.7 | 0.230 | 2.876 | 99.189 |
| Dim.8 | 0.065 | 0.811 | 100.000 |
Se realiza un grafico donde se representen las varianzas de cada una de las dimensioes y a partir de estas se puede definir la cantidad de dimensiones necesarias, para este caso se seleccionan 2 dimensiones que son las más representativas respecto al porcentaje de las varianzas.
fviz_eig(pca, addlabels = TRUE, barcolor = "grey30", barfill = "grey70")
fviz_pca_ind(pca, geom = "point", pointsize = 0.5, alpha.ind = 0.5)
fviz_pca_var(
pca,
col.var = "contrib",
gradient.cols = c("#FF7F00", "#034D94"),
repel = TRUE
) + ggtitle("Variables - PCA")
A continuación se presenta la tabla de cargas de variables según las dimensiones, donde se puede observar que para la dimensión 1 el precio y la cantidad de baños tienen el mayor aporte, y para la dimensión 2 lo tiene el número de habitaciones y el estrato.
var <- get_pca_var(pca)
loadings <- as.data.frame(var$coord) %>% rownames_to_column("variable")
contrib <- as.data.frame(var$contrib) %>% rownames_to_column("variable")
top_pc1 <- contrib %>% arrange(desc(Dim.1)) %>% slice_head(n = 5) %>% select(variable, Dim.1)
top_pc2 <- contrib %>% arrange(desc(Dim.2)) %>% slice_head(n = 5) %>% select(variable, Dim.2)
kable(loadings, digits = 3, caption = "Cargas (coeficientes) de variables en PC1-PC2") %>%
kable_styling(full_width = FALSE)
| variable | Dim.1 | Dim.2 | Dim.3 | Dim.4 | Dim.5 |
|---|---|---|---|---|---|
| estrato | 0.572 | 0.639 | -0.150 | 0.191 | -0.297 |
| preciom | 0.892 | 0.282 | -0.060 | -0.039 | -0.017 |
| areaconst | 0.845 | -0.318 | 0.067 | -0.130 | -0.318 |
| parqueaderos | 0.741 | 0.032 | -0.158 | -0.551 | 0.293 |
| banios | 0.872 | -0.076 | 0.162 | 0.225 | 0.050 |
| habitaciones | 0.571 | -0.568 | 0.326 | 0.333 | 0.266 |
| piso_num | -0.147 | 0.558 | 0.785 | -0.220 | -0.050 |
| precio_m2 | 0.112 | 0.892 | -0.114 | 0.218 | 0.299 |
kable(top_pc1, digits = 2, caption = "Top 5 contribuciones a PC1") %>% kable_styling(full_width = FALSE)
| variable | Dim.1 |
|---|---|
| preciom | 22.71 |
| banios | 21.69 |
| areaconst | 20.36 |
| parqueaderos | 15.66 |
| estrato | 9.32 |
kable(top_pc2, digits = 2, caption = "Top 5 contribuciones a PC2") %>% kable_styling(full_width = FALSE)
| variable | Dim.2 |
|---|---|
| precio_m2 | 39.29 |
| estrato | 20.15 |
| habitaciones | 15.91 |
| piso_num | 15.39 |
| areaconst | 4.98 |
Usamos los componentes o las variables estandarizadas. Aquí empleamos X escalado completo para segmentar. Elegimos k vía silhouette y método del codo, donde como resultado tenemos que con dos clusters podemos tener una clasificación adecuada.
# Elbow & silhouette con factoextra
p1 <- fviz_nbclust(X, kmeans, method = "wss") + ggtitle("Método del codo")
p2 <- fviz_nbclust(X, kmeans, method = "silhouette") + ggtitle("Silhouette")
p1
p2
# k óptimo: máximo silhouette
sil_scores <- sapply(2:8, function(k){
pamk <- kmeans(X, centers = k, nstart = 20, iter.max = 100)
ss <- silhouette(pamk$cluster, dist(X))
mean(ss[, 3])
})
best_k <- ifelse(all(is.na(sil_scores)), 4, which.max(sil_scores) + 1)
km <- kmeans(X, centers = best_k, nstart = 50, iter.max = 200)
viv$cluster <- factor(km$cluster)
best_k
## [1] 2
A continuación se prsenta un resumen de cada cluster, del cual es posible concluir que:
• Cluster 1:
• Viviendas de lujo o alta gama: casas grandes, alto estrato, alto precio y mucha dotación.
• Claramente un segmento premium en el mercado.
• Cliente objetivo: familias con alto poder adquisitivo, buscando espacio y exclusividad.
• Cluster 2:
• Viviendas más accesibles: apartamentos, menor tamaño, menos dotación, precio medio.
• Estrato medio-alto pero con un rango de inversión menor.
• Cliente objetivo: parejas jóvenes o familias pequeñas buscando comodidad sin pagar un precio alto.
profile_num <- viv %>%
group_by(cluster) %>%
summarise(across(all_of(num_cols), ~ round(mean(.x, na.rm = TRUE), 2)))
mode2 <- function(x){
ux <- na.omit(x)
if(length(ux) == 0) return(NA_character_)
ux[which.max(tabulate(match(x, ux)))]
}
profile_cat <- viv %>%
group_by(cluster) %>%
summarise(across(all_of(cat_cols), mode2))
profile <- profile_num %>% left_join(profile_cat, by = "cluster")
kable(profile, caption = paste0("Perfil de clusters (k = ", best_k, ")")) %>%
kable_styling(full_width = FALSE)
| cluster | estrato | preciom | areaconst | parqueaderos | banios | habitaciones | piso_num | precio_m2 | zona | tipo | barrio |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 5.38 | 788.82 | 309.49 | 2.73 | 4.70 | 4.63 | 3.28 | 2.93 | Zona Sur | Casa | ciudad jardín |
| 2 | 4.36 | 271.36 | 108.79 | 1.47 | 2.41 | 3.11 | 3.98 | 2.68 | Zona Sur | Apartamento | valle del lili |
Basado en las dimensiones de PCA, los cluster se pueden observar a continuación
fviz_pca_ind(pca, geom = "point", pointsize = 0.6,
habillage = viv$cluster, addEllipses = TRUE, ellipse.level = 0.95)
top20_barrio <- viv %>% count(barrio, sort = TRUE) %>% slice_head(n = 20) %>% pull(barrio)
sub <- viv %>% filter(barrio %in% top20_barrio)
tab_bz <- table(sub$barrio, sub$zona)
ca_bz <- CA(tab_bz, graph = FALSE)
fviz_ca_biplot(ca_bz, repel = TRUE, col.row = "#1f77b4", col.col = "#ff7f0e") +
ggtitle("CA: Barrio (Top20) vs Zona")
Al realizar un análisis del grafico de correlaciones, se observa que no hay correlaciones relevantes entre las variables, esto siempre y cuando no se tenga en cuenta el ID, pues spoiblemente se deba a un orden que se haya dado previamente de acuerdo con la ubicación geografica.
4.1 Matriz de correlaciones
# Seleccionar solo numéricas
num_cols <- viv %>% select(where(is.numeric))
num_mat <- as.matrix(num_cols)
# Matriz de correlación
cor_mat <- cor(num_mat, use = "pairwise.complete.obs", method = "pearson")
# Visualizar
corrplot(cor_mat, method = "color", type = "upper",
tl.col = "black", tl.srt = 45,
addCoef.col = "black", number.cex = 0.7)
Mostramos la dispersión por longitud/latitud (sin base cartográfica), teniendo en cuenta la ubicación de los cluster. Al analizar el mapa, se observa que aunque en algunas zonas predominan algunos cluster, en el centro de la ciudad tiende a tener mezclas grades de ellos, lo que indica que existen viviendas cercanas de estratos muy diferentes.
sample_n <- min(nrow(viv), 3000)
viv_map <- viv %>% slice_sample(n = sample_n)
ggplot(viv_map, aes(x = longitud, y = latitud, color = cluster)) +
geom_point(alpha = 0.6, size = 0.7) +
coord_equal() +
labs(title = "Ubicaciones por cluster", x = "Longitud", y = "Latitud") +
guides(color = guide_legend(override.aes = list(size = 3, alpha = 1)))