#1. Introducción
Problema: una inmobiliaria necesita entender el mercado urbano para mejorar la valoración (pricing) y decisiones de compra/venta. Objetivo: usar análisis estadístico multivariado para (i) identificar factores dominantes (PCA), (ii) segmentar inmuebles (clustering), (iii) estudiar asociaciones entre variables categóricas (correspondencia) y (iv) visualizar resultados (gráficos y mapa).
knitr::opts_chunk$set(echo = TRUE, message = FALSE, warning = FALSE)
library(paqueteMODELOS)
data("vivienda")
df <- vivienda
library(tidyverse)
library(janitor)
library(skimr)
library(ggplot2)
library(forcats)
library(dplyr)
library(stringr)
library(stringi)
library(janitor)
library(mice)
library(factoextra)
library(cluster)
library(factoextra)
library(FactoMineR)
df <- df %>% clean_names()
glimpse(df)
## 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…
Aca podemos identificar que las variables son las siguientes:
n0 <- nrow(df)
df <- df %>% distinct()
n1 <- nrow(df)
tibble(
filas_iniciales = n0,
filas_despues = n1,
duplicadas_removidas = n0 - n1
)
## # A tibble: 1 × 3
## filas_iniciales filas_despues duplicadas_removidas
## <int> <int> <int>
## 1 8322 8321 1
na_tab <- df %>%
summarise(across(everything(), ~sum(is.na(.)))) %>%
pivot_longer(everything(), names_to = "variable", values_to = "n_na") %>%
arrange(desc(n_na))
na_tab
## # A tibble: 13 × 2
## variable n_na
## <chr> <int>
## 1 piso 2637
## 2 parqueaderos 1604
## 3 id 2
## 4 zona 2
## 5 estrato 2
## 6 areaconst 2
## 7 banios 2
## 8 habitaciones 2
## 9 tipo 2
## 10 barrio 2
## 11 longitud 2
## 12 latitud 2
## 13 preciom 1
piso (~31.7%) y parqueaderos (~19.3%).piso se usará
como categórica “Sin dato” para correspondencia;
parqueaderos se imputará para no perder información en
segmentación.df <- df %>%
mutate(
zona = as.factor(zona),
tipo = as.factor(tipo),
barrio = as.factor(barrio),
piso_cat = fct_explicit_na(as.factor(piso), na_level = "Sin dato"),
estrato = as.numeric(estrato)
)
df <- df %>%
mutate(
barrio_txt = as.character(barrio),
barrio_txt = str_squish(barrio_txt),
barrio_txt = str_to_lower(barrio_txt),
barrio_txt = stri_trans_general(barrio_txt, "Latin-ASCII"),
barrio_txt = if_else(barrio_txt == "ponce", "pance", barrio_txt),
barrio_txt = if_else(barrio_txt == "laflora", "la flora", barrio_txt)
) %>%
filter(!is.na(barrio_txt)) %>%
filter(!(barrio_txt %in% c("santa", "norte"))) %>%
mutate(barrio = as.factor(barrio_txt)) %>%
select(-barrio_txt)
Ya normalizamos barrio para que no tenga minusculas ni tildes ni espacios y evitar duplicados
continuaremos con el histograma
ggplot(df, aes(x = preciom)) +
geom_histogram(bins = 40) +
labs(title = "Distribución del precio por m²", x = "preciom", y = "Frecuencia")
El precio por m² es asimétrico (cola a la derecha): hay pocos inmuebles muy caros. Esto sugiere segmentación del mercado.
ggplot(df, aes(x = areaconst, y = preciom)) +
geom_point(alpha = 0.3) +
geom_smooth(method = "lm") +
labs(title = "Precio vs Área construida", x = "Área construida", y = "Precio por m²")
Se observa relación positiva general: a mayor área, tiende a cambiar el precio por m² (no siempre lineal; depende de zona/estrato).
ggplot(df, aes(x = zona, y = preciom)) +
geom_boxplot() +
theme(axis.text.x = element_text(angle = 25, hjust = 1)) +
labs(title = "Precio por zona", x = "Zona", y = "preciom")
ggplot(df, aes(x = tipo, y = preciom)) +
geom_boxplot() +
labs(title = "Precio por tipo de vivienda", x = "Tipo", y = "preciom")
Existen diferencias de precio por zona y tipo, lo que apoya usar técnicas multivariadas y segmentación
df_imp <- df %>%
mutate(parqueaderos = as.numeric(parqueaderos)) %>%
select(-piso) # piso original no lo usamos; usamos piso_cat
# Configurar MICE
ini <- mice(df_imp, maxit = 0)
meth <- ini$method
pred <- ini$predictorMatrix
# Método para parqueaderos
meth["parqueaderos"] <- "pmm"
# No usar id como predictor si existe
if ("id" %in% colnames(pred)) pred[, "id"] <- 0
set.seed(123)
imp <- mice(df_imp, method = meth, predictorMatrix = pred, m = 5, seed = 123)
##
## iter imp variable
## 1 1 parqueaderos
## 1 2 parqueaderos
## 1 3 parqueaderos
## 1 4 parqueaderos
## 1 5 parqueaderos
## 2 1 parqueaderos
## 2 2 parqueaderos
## 2 3 parqueaderos
## 2 4 parqueaderos
## 2 5 parqueaderos
## 3 1 parqueaderos
## 3 2 parqueaderos
## 3 3 parqueaderos
## 3 4 parqueaderos
## 3 5 parqueaderos
## 4 1 parqueaderos
## 4 2 parqueaderos
## 4 3 parqueaderos
## 4 4 parqueaderos
## 4 5 parqueaderos
## 5 1 parqueaderos
## 5 2 parqueaderos
## 5 3 parqueaderos
## 5 4 parqueaderos
## 5 5 parqueaderos
df2 <- complete(imp, 1)
Como parqueaderos tiene ~19% de faltantes y es
importante para valorar, se usa imputación múltiple (MICE) con
pmm para conservar la distribución y evitar sesgos.
ggplot() +
geom_density(data = df, aes(x = parqueaderos), na.rm = TRUE) +
geom_density(data = df2, aes(x = parqueaderos), na.rm = TRUE) +
labs(title = "Densidad parqueaderos: antes vs despues de imputacion",
x = "parqueaderos", y = "densidad")
Continuaremos con el PCA
# Variables numéricas que sí describen el inmueble (EXCLUIMOS id)
vars_num <- c("estrato","preciom","areaconst","parqueaderos","banios","habitaciones","longitud","latitud")
df_pca <- df2 %>% select(all_of(vars_num)) %>% drop_na()
# Estandarización para evitar sesgos por escalas distintas
Xz <- scale(df_pca)
# PCA
pca <- prcomp(Xz, center = TRUE, scale. = FALSE)
fviz_eig(pca, addlabels = TRUE) +
labs(title = "PCA: Varianza explicada por componente")
El primer componente principal (PC1) explica 45.7 de la variabilidad Los dos primeros componentes (PC1 + PC2) explican 64.3% acumulado Por lo tanto, el plano PC1–PC2 permite un resumen inicial del mercado, aunque puede requerirse PC3 para capturar más variación
eig <- get_eigenvalue(pca)
eig
## eigenvalue variance.percent cumulative.variance.percent
## Dim.1 3.6548756 45.685945 45.68594
## Dim.2 1.4890094 18.612618 64.29856
## Dim.3 0.8992776 11.240971 75.53953
## Dim.4 0.6991753 8.739691 84.27922
## Dim.5 0.4759328 5.949160 90.22838
## Dim.6 0.3623483 4.529354 94.75774
## Dim.7 0.2362330 2.952913 97.71065
## Dim.8 0.1831480 2.289350 100.00000
round(pca$rotation[,1:2], 3)
## PC1 PC2
## estrato 0.327 -0.464
## preciom 0.464 -0.070
## areaconst 0.421 0.265
## parqueaderos 0.412 0.008
## banios 0.444 0.183
## habitaciones 0.260 0.547
## longitud -0.231 0.464
## latitud -0.115 0.402
El primer componente principal (45.7% de la varianza) está asociado principalmente con las variables precio por m², número de baños, área construida, parqueaderos y estrato. Esto sugiere que PC1 captura un eje de valorización y nivel socioeconómico del inmueble.
El segundo componente principal (18.6%) está asociado principalmente con número de habitaciones y variables geográficas (latitud y longitud), representando un eje de diferenciación espacial y estructural.
En conjunto, PC1 y PC2 explican el 64.3% de la variabilidad del mercado, lo que permite una representación bidimensional adecuada para análisis exploratorio.
df <- df %>%
mutate(
piso_cat = forcats::fct_explicit_na(as.factor(piso), na_level = "Sin dato")
)
Se continuara con el analisis de cluster de la siguiente forma:
# 1) Tomo parqueaderos imputado desde df2
parq_imp <- df2 %>% select(id, parqueaderos)
# 2) Reemplazo parqueaderos en df usando el imputado
df_clust_base <- df %>%
select(id, zona, tipo, barrio, piso_cat, estrato, preciom, areaconst,
banios, habitaciones, longitud, latitud, parqueaderos) %>%
left_join(parq_imp, by = "id", suffix = c("_orig", "_imp")) %>%
mutate(parqueaderos = parqueaderos_imp) %>%
select(-parqueaderos_orig, -parqueaderos_imp)
# Variables numéricas para clustering
vars_num <- c("estrato","preciom","areaconst","parqueaderos",
"banios","habitaciones","longitud","latitud")
df_clust <- df_clust_base %>% drop_na(all_of(vars_num))
# Estandarizar
Xc <- scale(df_clust %>% select(all_of(vars_num)))
Se usa parqueaderos imputado para no perder
observaciones Se estandarizan variables para calcular distancias sin
sesgo por escala
dist_eucl <- dist(Xc, method = "euclidean")
hc <- hclust(dist_eucl, method = "average")
plot(hc, cex = 0.4, main = "Dendrograma (average)", xlab = "", sub = "")
Elegir k con Silhouette (k = 2 a 8)
sil_means <- tibble(k = 2:8, sil = NA_real_)
for (i in 2:8) {
grp <- cutree(hc, k = i)
sil <- silhouette(grp, dist_eucl)
sil_means$sil[sil_means$k == i] <- mean(sil[,3])
}
sil_means
## # A tibble: 7 × 2
## k sil
## <int> <dbl>
## 1 2 0.647
## 2 3 0.556
## 3 4 0.492
## 4 5 0.479
## 5 6 0.440
## 6 7 0.439
## 7 8 0.423
ggplot(sil_means, aes(k, sil)) +
geom_line() + geom_point() +
labs(title = "Silhouette promedio por k", x = "k", y = "Silhouette promedio")
k_final <- sil_means$k[which.max(sil_means$sil)]
k_final
## [1] 2
df_clust <- df_clust %>%
mutate(cluster = factor(cutree(hc, k = k_final)))
plot(hc, cex = 0.4, main = paste("Dendrograma con k =", k_final))
rect.hclust(hc, k = k_final, border = 2:(k_final+1))
perfil <- df_clust %>%
group_by(cluster) %>%
summarise(
n = n(),
preciom_med = median(preciom),
preciom_p25 = quantile(preciom, 0.25),
preciom_p75 = quantile(preciom, 0.75),
estrato_med = median(estrato),
area_med = median(areaconst),
banios_med = median(banios),
parq_med = median(parqueaderos),
hab_med = median(habitaciones),
.groups = "drop"
) %>%
arrange(desc(n))
perfil
## # A tibble: 2 × 10
## cluster n preciom_med preciom_p25 preciom_p75 estrato_med area_med
## <fct> <int> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 1 8306 330 220 540 5 123
## 2 2 3 255 228. 312. 3 1440
## # ℹ 3 more variables: banios_med <dbl>, parq_med <dbl>, hab_med <dbl>
Se evaluó el número de conglomerados k=2…8 usando el índice Silhouette promedio. El valor máximo se obtuvo en k=2 (0.616), indicando la partición más coherente desde el punto de vista estadístico.
# Solución alternativa para segmentación útil
k_alt <- 3
df_clust_k3 <- df_clust %>% mutate(cluster3 = factor(cutree(hc, k = k_alt)))
perfil3 <- df_clust_k3 %>%
group_by(cluster3) %>%
summarise(
n = n(),
preciom_med = median(preciom),
preciom_p25 = quantile(preciom, 0.25),
preciom_p75 = quantile(preciom, 0.75),
estrato_med = median(estrato),
area_med = median(areaconst),
banios_med = median(banios),
parq_med = median(parqueaderos),
hab_med = median(habitaciones),
.groups = "drop"
) %>%
arrange(preciom_med)
perfil3
## # A tibble: 3 × 10
## cluster3 n preciom_med preciom_p25 preciom_p75 estrato_med area_med
## <fct> <int> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 2 3 255 228. 312. 3 1440
## 2 1 8302 330 220 540 5 123
## 3 3 4 370 310 530 4 210
## # ℹ 3 more variables: banios_med <dbl>, parq_med <dbl>, hab_med <dbl>
# Obtener coordenadas PCA
df_pca_coords <- as.data.frame(pca$x[,1:2])
# Agregar cluster k=3
df_pca_coords$cluster3 <- df_clust_k3$cluster3
ggplot(df_pca_coords, aes(x = PC1, y = PC2, color = cluster3)) +
geom_point(alpha = 0.5) +
labs(title = "Clusters (k=3) proyectados en plano PCA",
x = "PC1",
y = "PC2") +
theme_minimal()
ggplot(df_clust_k3, aes(x = areaconst, y = preciom, color = cluster3)) +
geom_point(alpha = 0.6) +
labs(title = "Clusters según área y precio",
x = "Área construida",
y = "Precio por m²") +
theme_minimal()
# Quitar outliers extremos (ejemplo percentil 99)
df_filtrado <- df_clust %>%
filter(preciom < quantile(preciom, 0.99))
Xf <- scale(df_filtrado %>% select(all_of(vars_num)))
hc_f <- hclust(dist(Xf), method = "average")
fviz_nbclust(Xf, FUN = hcut, method = "silhouette")
El analisis de acuerdo con las graficas planteadas es el siguiete
La solución óptima según el índice Silhouette fue k=2, lo cual separa el mercado en un grupo masivo y un pequeño grupo de inmuebles extremos (outliers premium). Sin embargo, para fines estratégicos de segmentación comercial se evaluó k=3, donde se distinguen tres niveles: mercado general, segmento alto y segmento ultra-premium. Los clusters muestran una clara diferenciación principalmente explicada por precio por m² y área construida.
Lo que se puede ver con el algoritmo de cluster es que el mercado esta dominado homogeneamente y tiene variabilidad continua no hay segmentacion natural fuerte y el algoritmo esta clasificando fronteras por lo tanto el algoritmo identifica bien outliers
tab_ze <- table(df$zona, df$estrato)
tab_ze
##
## 3 4 5 6
## Zona Centro 105 14 4 1
## Zona Norte 572 407 768 172
## Zona Oeste 52 82 287 768
## Zona Oriente 340 8 2 1
## Zona Sur 382 1616 1685 1043
chi_ze <- chisq.test(tab_ze)
chi_ze
##
## Pearson's Chi-squared test
##
## data: tab_ze
## X-squared = 3839.5, df = 12, p-value < 2.2e-16
ca_ze <- CA(tab_ze, graph = FALSE)
ca_ze$eig
## eigenvalue percentage of variance cumulative percentage of variance
## dim 1 0.32311827 69.924825 69.92483
## dim 2 0.12817234 27.737302 97.66213
## dim 3 0.01080317 2.337873 100.00000
fviz_ca_biplot(ca_ze, repel = TRUE) +
labs(title = "Análisis de Correspondencia: Zona vs Estrato")
La prueba chi-cuadrado (χ² = 3839.5, p < 0.001) confirma una asociación altamente significativa entre la zona de la ciudad y el estrato socioeconómico.
El análisis de correspondencia generó tres dimensiones, donde las dos primeras explican el 97.7% de la inercia total (Dim1 = 69.9%, Dim2 = 27.7%), permitiendo una representación bidimensional prácticamente completa.
La Dimensión 1 representa un eje socioeconómico, diferenciando zonas asociadas a estratos bajos (3) de aquellas vinculadas a estratos altos (6).
En particular:
Zona Oeste se asocia fuertemente con estrato 6, caracterizándose como zona premium.
Zona Oriente y Zona Centro se relacionan principalmente con estrato 3, reflejando un perfil socioeconómico más popular.
Zona Norte y Zona Sur presentan asociación con estratos 4 y 5, representando segmentos intermedios del mercado.
Estos resultados evidencian una clara segmentación espacial del mercado inmobiliario según nivel socioeconómico, lo cual es fundamental para estrategias de pricing, inversión y desarrollo inmobiliario.
# Top 10 barrios con más registros
top_barrios <- df %>%
count(barrio, sort = TRUE) %>%
slice_head(n = 10) %>%
pull(barrio)
top_barrios
## [1] valle del lili ciudad jardin pance la flora santa teresita
## [6] el caney el ingenio la hacienda normandia acopi
## 385 Levels: 20 de julio 3 de julio acopi agua blanca aguablanca ... zona sur
tab_be <- df %>%
filter(barrio %in% top_barrios) %>%
count(barrio, estrato) %>%
pivot_wider(names_from = estrato, values_from = n, values_fill = 0) %>%
column_to_rownames("barrio")
tab_be
## 3 4 5 6
## acopi 21 51 59 27
## ciudad jardin 2 4 67 467
## el caney 9 166 34 0
## el ingenio 0 9 157 37
## la flora 1 33 329 6
## la hacienda 2 29 134 1
## normandia 1 0 23 135
## pance 0 2 20 391
## santa teresita 0 4 27 232
## valle del lili 4 577 423 5
chisq.test(tab_be)
##
## Pearson's Chi-squared test
##
## data: tab_be
## X-squared = 3734, df = 27, p-value < 2.2e-16
ca_be <- CA(tab_be, graph = FALSE)
ca_be$eig
## eigenvalue percentage of variance cumulative percentage of variance
## dim 1 0.77298332 72.226662 72.22666
## dim 2 0.23319073 21.789070 94.01573
## dim 3 0.06404475 5.984267 100.00000
fviz_ca_biplot(ca_be, repel = TRUE) +
labs(title = "Análisis de Correspondencia: Top 10 Barrios vs Estrato")
El test Chi-cuadrado (X² = 3734, p < 0.001) indica que existe una asociación estadísticamente significativa entre barrio y estrato, rechazando la hipótesis de independencia. Esto confirma que la estructura socioeconómica del mercado inmobiliario presenta una clara diferenciación espacial.
La primera dimensión explica el 72.2% de la variabilidad asociativa, capturando el eje principal de segregación socioeconómica. La segunda dimensión (21.9%) complementa esta diferenciación, permitiendo una representación bidimensional adecuada (94% acumulado).
El mapa revela que barrios como Pance, Santa Teresita y Ciudad Jardín se encuentran fuertemente asociados a estratos altos, mientras que barrios como Valle del Lili y El Caney presentan un perfil predominantemente medio. La Flora y El Ingenio muestran una asociación clara con estrato 5.
Estos resultados evidencian que el mercado inmobiliario urbano presenta patrones estructurales de concentración socioeconómica por barrio.
Desde una perspectiva aplicada:
La valoración debe considerar ubicación y estrato como variables estructurales.
El mercado no requiere segmentación excesiva; bastan grandes categorías.
Existen nichos premium claramente identificables.
La estructura socioeconómica territorial influye directamente en precios.
El análisis multivariado evidencia que el mercado inmobiliario urbano presenta una estructura organizada principalmente en torno a un eje de valorización socioeconómica y una diferenciación espacial significativa. Aunque el mercado muestra alta concentración en un núcleo dominante, existen perfiles extremos que configuran subsegmentos de alto valor. La fuerte asociación entre barrio y estrato confirma la existencia de patrones estructurales de segregación socioeconómica territorial.