En el presente trabajo se realiza un análisis estadístico
multivariado sobre datos de viviendas urbanas, con el fin de apoyar la
toma de decisiones de una empresa inmobiliaria. Los datos provienen del
paquete paqueteMODELOS y fueron recopilados originalmente
mediante web scraping de la plataforma OLX.
El objetivo es aplicar tres técnicas principales: Análisis de Componentes Principales (ACP), Análisis de Conglomerados (Clustering) y Análisis de Correspondencia, para identificar patrones y segmentaciones en el mercado que resulten útiles a nivel estratégico.
# devtools::install_github("centromagis/paqueteMODELOS", force = TRUE)
library(paqueteMODELOS)
library(tidyverse)
library(factoextra)
library(FactoMineR)
library(corrplot)
library(cluster)
library(NbClust)
library(knitr)
library(kableExtra)
library(gridExtra)
library(scales)
library(RColorBrewer)
data("vivienda")Antes de aplicar las técnicas multivariadas, es necesario entender la estructura y distribución de las variables disponibles.
## 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")=
## .. cols(
## .. id = col_double(),
## .. zona = col_character(),
## .. piso = col_character(),
## .. estrato = col_double(),
## .. preciom = col_double(),
## .. areaconst = col_double(),
## .. parqueaderos = col_double(),
## .. banios = col_double(),
## .. habitaciones = col_double(),
## .. tipo = col_character(),
## .. barrio = col_character(),
## .. longitud = col_double(),
## .. latitud = col_double()
## .. )
## - attr(*, "problems")=<externalptr>
## El dataset tiene 8322 registros y 13 variables.
Se cuenta con 8,322 propiedades y 13 variables. Las variables numéricas principales son: precio (en millones), área construida, número de parqueaderos, baños, habitaciones y estrato socioeconómico. Las categóricas incluyen zona, tipo de vivienda (Casa/Apartamento) y barrio.
vivienda %>%
select(preciom, areaconst, parqueaderos, banios, habitaciones, estrato) %>%
summary() %>%
kable(caption = "Resumen estadístico de las variables numéricas") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)| preciom | areaconst | parqueaderos | banios | habitaciones | estrato | |
|---|---|---|---|---|---|---|
| Min. : 58.0 | Min. : 30.0 | Min. : 1.000 | Min. : 0.000 | Min. : 0.000 | Min. :3.000 | |
| 1st Qu.: 220.0 | 1st Qu.: 80.0 | 1st Qu.: 1.000 | 1st Qu.: 2.000 | 1st Qu.: 3.000 | 1st Qu.:4.000 | |
| Median : 330.0 | Median : 123.0 | Median : 2.000 | Median : 3.000 | Median : 3.000 | Median :5.000 | |
| Mean : 433.9 | Mean : 174.9 | Mean : 1.835 | Mean : 3.111 | Mean : 3.605 | Mean :4.634 | |
| 3rd Qu.: 540.0 | 3rd Qu.: 229.0 | 3rd Qu.: 2.000 | 3rd Qu.: 4.000 | 3rd Qu.: 4.000 | 3rd Qu.:5.000 | |
| Max. :1999.0 | Max. :1745.0 | Max. :10.000 | Max. :10.000 | Max. :10.000 | Max. :6.000 | |
| NA’s :2 | NA’s :3 | NA’s :1605 | NA’s :3 | NA’s :3 | NA’s :3 |
Del resumen se observa que el precio promedio es de aproximadamente 434 millones, con una mediana de 330 millones, lo que indica una distribución asimétrica hacia la derecha (hay propiedades muy costosas que jalan el promedio). El área construida también muestra este comportamiento, con una media de 175 m² pero mediana de 123 m². Los estratos van del 3 al 6, con mediana en 5.
na_count <- colSums(is.na(vivienda))
na_df <- data.frame(Variable = names(na_count), Faltantes = na_count,
Porcentaje = round(na_count / nrow(vivienda) * 100, 2))
na_df %>%
filter(Faltantes > 0) %>%
kable(caption = "Variables con valores faltantes", row.names = FALSE) %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| Variable | Faltantes | Porcentaje |
|---|---|---|
| id | 3 | 0.04 |
| zona | 3 | 0.04 |
| piso | 2638 | 31.70 |
| estrato | 3 | 0.04 |
| preciom | 2 | 0.02 |
| areaconst | 3 | 0.04 |
| parqueaderos | 1605 | 19.29 |
| banios | 3 | 0.04 |
| habitaciones | 3 | 0.04 |
| tipo | 3 | 0.04 |
| barrio | 3 | 0.04 |
| longitud | 3 | 0.04 |
| latitud | 3 | 0.04 |
Hay dos variables con cantidad importante de datos faltantes:
piso (31.7%) y parqueaderos (19.3%). El resto
de las variables tienen menos del 0.04% de faltantes, lo cual no es
preocupante. Para los análisis se eliminarán las filas con NA en las
variables utilizadas.
ggplot(vivienda, aes(x = preciom)) +
geom_histogram(aes(y = after_stat(density)), bins = 50, fill = "#2c7fb8", color = "white", alpha = 0.8) +
geom_density(color = "#d95f0e", linewidth = 1) +
labs(title = "Distribución del Precio de Viviendas",
subtitle = "Precio en millones de pesos",
x = "Precio (millones)", y = "Densidad") +
theme_minimal()Distribución del precio
La distribución del precio es claramente asimétrica positiva: la mayoría de propiedades se concentran entre 100 y 500 millones, pero existe una cola larga hacia la derecha con propiedades que superan los 1,000 millones.
ggplot(vivienda, aes(x = areaconst)) +
geom_histogram(aes(y = after_stat(density)), bins = 50, fill = "#7570b3", color = "white", alpha = 0.8) +
geom_density(color = "#d95f0e", linewidth = 1) +
labs(title = "Distribución del Área Construida",
x = "Área construida (m²)", y = "Densidad") +
theme_minimal()Distribución del área construida
El área construida sigue un patrón similar al precio. La mayoría de inmuebles tienen entre 50 y 250 m², pero hay algunos que superan los 1,000 m².
p1 <- ggplot(vivienda, aes(x = fct_infreq(zona), fill = zona)) +
geom_bar(alpha = 0.85) +
labs(title = "Distribución por Zona", x = "Zona", y = "Frecuencia") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 30, hjust = 1), legend.position = "none")
p2 <- ggplot(vivienda, aes(x = factor(estrato), fill = factor(estrato))) +
geom_bar(alpha = 0.85) +
labs(title = "Distribución por Estrato", x = "Estrato", y = "Frecuencia") +
scale_fill_brewer(palette = "Set2") +
theme_minimal() +
theme(legend.position = "none")
p3 <- ggplot(vivienda, aes(x = tipo, fill = tipo)) +
geom_bar(alpha = 0.85) +
labs(title = "Distribución por Tipo", x = "Tipo de vivienda", y = "Frecuencia") +
theme_minimal() +
theme(legend.position = "none")
grid.arrange(p1, p2, p3, ncol = 3)La Zona Sur concentra la mayor cantidad de ofertas, seguida de la Zona Norte. En cuanto al estrato, el 5 es el más frecuente, seguido del 4. Los apartamentos son más numerosos que las casas en la oferta total.
ggplot(vivienda, aes(x = factor(estrato), y = preciom, fill = factor(estrato))) +
geom_boxplot(alpha = 0.7, outlier.alpha = 0.3) +
facet_wrap(~ zona, scales = "free_y") +
labs(title = "Precio de Viviendas por Estrato y Zona",
x = "Estrato", y = "Precio (millones)", fill = "Estrato") +
scale_fill_brewer(palette = "YlOrRd") +
theme_minimal()Precio por estrato y zona
Se puede ver que, como era de esperarse, a mayor estrato mayor precio. Sin embargo, la dispersión también aumenta en los estratos altos, lo que indica mayor heterogeneidad en esos segmentos. La Zona Sur y Zona Norte son las que presentan mayor variabilidad de precios.
vars_num <- vivienda %>%
select(preciom, areaconst, parqueaderos, banios, habitaciones, estrato) %>%
drop_na()
cor_matrix <- cor(vars_num)
corrplot(cor_matrix, method = "color", type = "upper",
addCoef.col = "black", tl.col = "black", tl.srt = 45,
col = colorRampPalette(c("#2166ac", "white", "#b2182b"))(100),
title = "Matriz de Correlaciones", mar = c(0, 0, 2, 0))Matriz de correlaciones
De la matriz de correlaciones destaco lo siguiente:
Estas correlaciones altas entre varias variables justifican la aplicación del ACP, ya que hay redundancia de información que se puede sintetizar.
El ACP es una técnica que permite reducir la dimensionalidad de un conjunto de datos transformando las variables originales (posiblemente correlacionadas) en nuevas variables no correlacionadas llamadas componentes principales. Cada componente captura una porción de la varianza total, de manera decreciente.
Los elementos principales del ACP son:
Se seleccionan las 6 variables numéricas y se eliminan las observaciones con datos faltantes. Las variables se estandarizan automáticamente (scale.unit = TRUE) porque están en escalas muy diferentes.
datos_acp <- vivienda %>%
select(preciom, areaconst, parqueaderos, banios, habitaciones, estrato) %>%
drop_na()
cat("Se utilizan", nrow(datos_acp), "observaciones completas para el ACP.\n")## Se utilizan 6717 observaciones completas para el ACP.
eig_val <- get_eigenvalue(res_acp)
eig_val %>%
kable(caption = "Valores propios y varianza explicada", digits = 3) %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| eigenvalue | variance.percent | cumulative.variance.percent | |
|---|---|---|---|
| Dim.1 | 3.484 | 58.063 | 58.063 |
| Dim.2 | 1.223 | 20.385 | 78.447 |
| Dim.3 | 0.499 | 8.325 | 86.772 |
| Dim.4 | 0.360 | 5.992 | 92.765 |
| Dim.5 | 0.244 | 4.073 | 96.838 |
| Dim.6 | 0.190 | 3.162 | 100.000 |
fviz_eig(res_acp, addlabels = TRUE, ylim = c(0, 60),
barfill = "#2c7fb8", barcolor = "#2c7fb8") +
labs(title = "Gráfico de Sedimentación",
x = "Componente Principal", y = "% de varianza explicada") +
theme_minimal()Scree Plot
eig_df <- as.data.frame(eig_val)
cat("Varianza explicada por los 2 primeros componentes:",
round(sum(eig_df[1:2, "variance.percent"]), 2), "%\n")## Varianza explicada por los 2 primeros componentes: 78.45 %
cat("Varianza explicada por los 3 primeros componentes:",
round(sum(eig_df[1:3, "variance.percent"]), 2), "%\n")## Varianza explicada por los 3 primeros componentes: 86.77 %
Interpretación: El primer componente principal explica el 58.1% de la varianza total y el segundo el 20.4%, lo que da un acumulado del 78.4% con solo dos componentes. Aplicando el criterio de Kaiser (eigenvalue > 1), se retienen los dos primeros componentes, ya que solo estos tienen valor propio mayor a 1 (3.48 y 1.22 respectivamente). El scree plot también muestra un “codo” claro después del segundo componente. Esto significa que con dos dimensiones se puede representar la mayor parte de la información contenida en las 6 variables originales.
fviz_pca_var(res_acp,
col.var = "contrib",
gradient.cols = c("#00AFBB", "#E7B800", "#FC4E07"),
repel = TRUE) +
labs(title = "Círculo de Correlaciones",
subtitle = "Variables en el plano de los dos primeros componentes") +
theme_minimal()Círculo de correlaciones
fviz_contrib(res_acp, choice = "var", axes = 1, fill = "#2c7fb8") +
labs(title = "Contribución de las variables al Componente 1") +
theme_minimal()Contribuciones al Componente 1
fviz_contrib(res_acp, choice = "var", axes = 2, fill = "#d95f0e") +
labs(title = "Contribución de las variables al Componente 2") +
theme_minimal()Contribuciones al Componente 2
Interpretación del círculo de correlaciones: En el círculo se observa que las variables preciom, areaconst, banios y parqueaderos apuntan hacia la derecha y están cercanas entre sí, lo cual confirma que están altamente correlacionadas y contribuyen al primer componente de forma similar. Este primer componente se puede interpretar como una dimensión de “tamaño y valor” de la vivienda: a mayor score en este componente, la propiedad es más grande, más costosa y tiene más baños y parqueaderos.
El segundo componente está definido principalmente por la oposición entre habitaciones (hacia arriba) y estrato (hacia abajo). Esto genera un contraste interesante: las viviendas con muchas habitaciones pero estrato bajo se ubican arriba, mientras que las de estrato alto pero menos habitaciones van abajo. Podríamos interpretar este eje como un contraste entre “tamaño familiar vs. exclusividad”.
loadings_df <- as.data.frame(res_acp$var$coord)
loadings_df$Variable <- rownames(loadings_df)
loadings_df <- loadings_df %>% select(Variable, everything())
loadings_df %>%
kable(caption = "Cargas factoriales (coordenadas de las variables en cada componente)", digits = 3) %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| Variable | Dim.1 | Dim.2 | Dim.3 | Dim.4 | Dim.5 | |
|---|---|---|---|---|---|---|
| preciom | preciom | 0.878 | -0.265 | -0.076 | -0.174 | -0.131 |
| areaconst | areaconst | 0.840 | 0.226 | -0.183 | -0.394 | 0.150 |
| parqueaderos | parqueaderos | 0.798 | -0.167 | -0.435 | 0.372 | 0.059 |
| banios | banios | 0.868 | 0.177 | 0.261 | 0.084 | -0.335 |
| habitaciones | habitaciones | 0.557 | 0.738 | 0.234 | 0.163 | 0.206 |
| estrato | estrato | 0.551 | -0.705 | 0.384 | 0.042 | 0.215 |
Las cargas confirman lo observado en el gráfico. En el Componente 1, todas las variables tienen cargas positivas altas, especialmente preciom (0.878), banios (0.868) y areaconst (0.840). En el Componente 2, habitaciones tiene la carga más alta positiva (0.738) y estrato la más alta negativa (-0.705), lo que refuerza la interpretación de este eje como el contraste mencionado anteriormente.
fviz_pca_biplot(res_acp,
col.ind = "cos2",
gradient.cols = c("#00AFBB", "#E7B800", "#FC4E07"),
col.var = "black",
repel = TRUE,
geom.ind = "point",
pointsize = 1.5,
alpha.ind = 0.5) +
labs(title = "Biplot del ACP",
subtitle = "Individuos coloreados por calidad de representación (cos²)") +
theme_minimal()Biplot del ACP
En el biplot se visualizan simultáneamente las propiedades (puntos) y las variables (flechas). Los puntos coloreados en naranja/rojo tienen mejor representación en este plano bidimensional (cos² alto). Se nota que la mayoría de las propiedades se concentran en la zona central-izquierda, que corresponde a viviendas de tamaño y precio moderado. Los puntos más hacia la derecha son las propiedades premium (más costosas y grandes).
El análisis de conglomerados o clustering busca agrupar las propiedades de manera que las viviendas dentro de un mismo grupo sean similares entre sí y diferentes a las de otros grupos. Para esto se necesita definir:
p_elbow <- fviz_nbclust(datos_cluster_muestra, kmeans, method = "wss", k.max = 10) +
labs(title = "Método del Codo") +
theme_minimal()
p_silhouette <- fviz_nbclust(datos_cluster_muestra, kmeans, method = "silhouette", k.max = 10) +
labs(title = "Método de la Silueta") +
theme_minimal()
grid.arrange(p_elbow, p_silhouette, ncol = 2)Métodos para seleccionar k
set.seed(123)
fviz_nbclust(datos_cluster_muestra, kmeans, method = "gap_stat", k.max = 10, nboot = 50) +
labs(title = "Método del Estadístico Gap") +
theme_minimal()Estadístico Gap
Interpretación: El método del codo muestra una disminución marcada de la inercia intra-cluster entre k=1 y k=3, y después la reducción es más gradual. El método de la silueta indica que k=2 da el mejor coeficiente promedio, pero k=3 también es aceptable. El estadístico gap sugiere k=4. Considerando los tres métodos y la necesidad de tener segmentos interpretables para la empresa, se opta por k=3 clusters, ya que ofrece un buen balance entre simpleza y detalle de la segmentación.
set.seed(123)
k_optimo <- 3
km_result <- kmeans(datos_cluster, centers = k_optimo, nstart = 25, iter.max = 100)
datos_con_cluster <- datos_acp %>%
mutate(cluster = factor(km_result$cluster))
cat("Tamaño de cada cluster:\n")## Tamaño de cada cluster:
##
## 1 2 3
## 887 3498 2332
fviz_cluster(km_result, data = datos_cluster,
palette = c("#2c7fb8", "#d95f0e", "#1b9e77"),
geom = "point",
pointsize = 1.5,
ellipse.type = "convex",
ggtheme = theme_minimal()) +
labs(title = "Segmentación de Propiedades (K-Means, k=3)",
subtitle = "Proyectados en los dos primeros componentes principales")Clusters en el espacio del ACP
La visualización en el plano del ACP muestra que los tres clusters se separan bien a lo largo del Componente 1 (eje horizontal), que recordemos es el de “tamaño y valor”. El cluster naranja (2) queda a la izquierda (propiedades más pequeñas y económicas), el verde (3) en el centro, y el azul (1) a la derecha (propiedades premium).
perfil_cluster <- datos_con_cluster %>%
group_by(cluster) %>%
summarise(
n = n(),
precio_medio = round(mean(preciom), 1),
precio_mediana = round(median(preciom), 1),
area_media = round(mean(areaconst), 1),
parqueaderos_medio = round(mean(parqueaderos), 1),
banios_medio = round(mean(banios), 1),
habitaciones_medio = round(mean(habitaciones), 1),
estrato_medio = round(mean(estrato), 1)
)
perfil_cluster %>%
kable(caption = "Perfil promedio de cada cluster",
col.names = c("Cluster", "N", "Precio (M)", "Precio Med.", "Área (m²)",
"Parqueaderos", "Baños", "Habitaciones", "Estrato")) %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)| Cluster | N | Precio (M) | Precio Med. | Área (m²) | Parqueaderos | Baños | Habitaciones | Estrato |
|---|---|---|---|---|---|---|---|---|
| 1 | 887 | 1117.0 | 1100 | 419.6 | 3.8 | 5.2 | 4.6 | 5.7 |
| 2 | 3498 | 261.3 | 250 | 97.7 | 1.2 | 2.3 | 2.9 | 4.4 |
| 3 | 2332 | 533.8 | 500 | 215.6 | 2.0 | 4.0 | 4.2 | 5.2 |
Interpretación de los perfiles:
Cluster 1 (n=887): Es el segmento premium. Precio promedio de 1,117 millones, área de 420 m², casi 4 parqueaderos, 5 baños y estrato promedio de 5.7. Son propiedades grandes y exclusivas, representando solo el 13% de la oferta.
Cluster 2 (n=3,498): Es el segmento económico y el más numeroso (52% de la oferta). Precio promedio de 261 millones, área de 98 m², 1 parqueadero, 2 baños y estrato 4.4. Son viviendas de tamaño estándar orientadas a un mercado más amplio.
Cluster 3 (n=2,332): Segmento intermedio (35% de la oferta). Precio de 534 millones, área de 216 m², 2 parqueaderos, 4 baños y estrato 5.2. Son propiedades de gama media-alta.
datos_long <- datos_con_cluster %>%
pivot_longer(cols = -cluster, names_to = "variable", values_to = "valor")
ggplot(datos_long, aes(x = cluster, y = valor, fill = cluster)) +
geom_boxplot(alpha = 0.7) +
facet_wrap(~ variable, scales = "free_y", ncol = 3) +
scale_fill_manual(values = c("#2c7fb8", "#d95f0e", "#1b9e77")) +
labs(title = "Distribución de variables por cluster",
x = "Cluster", y = "Valor") +
theme_minimal()Variables por cluster
Los boxplots confirman la caracterización: el Cluster 1 tiene medianas mucho más altas en todas las variables excepto habitaciones, donde la diferencia es menor. También se nota que el Cluster 1 tiene mucha dispersión en precio y área, lo que indica que dentro de este segmento premium hay bastante heterogeneidad.
Para complementar, se aplica también un clustering jerárquico con el método de Ward sobre una muestra de 500 observaciones (por limitaciones computacionales del dendrograma).
set.seed(123)
muestra_dendro <- min(500, nrow(datos_cluster))
idx_dendro <- sample(1:nrow(datos_cluster), muestra_dendro)
dist_matrix <- dist(datos_cluster[idx_dendro, ], method = "euclidean")
hc_result <- hclust(dist_matrix, method = "ward.D2")
fviz_dend(hc_result, k = k_optimo,
cex = 0.4,
palette = c("#2c7fb8", "#d95f0e", "#1b9e77"),
rect = TRUE, rect_fill = TRUE, rect_border = "gray",
main = "Dendrograma - Método de Ward",
xlab = "Propiedades", ylab = "Distancia")Dendrograma
El dendrograma muestra que el corte en 3 grupos es razonable, ya que las ramas principales se separan a una distancia considerable. Esto es consistente con lo obtenido por K-Means.
sil <- silhouette(km_result$cluster, dist(datos_cluster))
cat("Coeficiente de silueta promedio:", round(mean(sil[, 3]), 3), "\n")## Coeficiente de silueta promedio: 0.31
fviz_silhouette(sil, palette = c("#2c7fb8", "#d95f0e", "#1b9e77")) +
labs(title = "Gráfico de Silueta",
subtitle = paste("Coeficiente promedio:", round(mean(sil[, 3]), 3))) +
theme_minimal()## cluster size ave.sil.width
## 1 1 887 0.14
## 2 2 3498 0.46
## 3 3 2332 0.15
Silueta
El coeficiente de silueta promedio es de 0.31, que se considera una estructura moderada. El Cluster 2 (económico) tiene la mejor silueta (0.46), indicando que es el grupo más compacto y bien separado. Los clusters 1 y 3 tienen siluetas más bajas (0.14 y 0.15), lo que sugiere que hay cierta superposición entre las propiedades premium e intermedias. Esto tiene sentido porque la frontera entre “gama media-alta” y “premium” no siempre es nítida en el mercado inmobiliario.
El Análisis de Correspondencia (AC) sirve para estudiar las relaciones entre variables categóricas. Su lógica es similar al ACP pero aplicada a tablas de contingencia. Los conceptos clave son:
Se elige este cruce primero porque ambas variables tienen varias categorías (5 zonas × 4 estratos), lo que permite obtener un espacio factorial con al menos 2 dimensiones.
tabla_zona_estrato <- table(vivienda$zona, vivienda$estrato)
tabla_zona_estrato %>%
kable(caption = "Tabla de contingencia: Zona vs Estrato") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| 3 | 4 | 5 | 6 | |
|---|---|---|---|---|
| Zona Centro | 105 | 14 | 4 | 1 |
| Zona Norte | 572 | 407 | 769 | 172 |
| Zona Oeste | 54 | 84 | 290 | 770 |
| Zona Oriente | 340 | 8 | 2 | 1 |
| Zona Sur | 382 | 1616 | 1685 | 1043 |
## Test Chi-cuadrado:
## Estadístico: 3830.435
## g.l.: 12
## p-valor: < 2.2e-16
El test chi-cuadrado da un estadístico de 3830.4 con p-valor prácticamente cero, lo que confirma que sí existe una asociación significativa entre la zona y el estrato. Esto justifica la aplicación del AC.
res_ca <- CA(tabla_zona_estrato, graph = FALSE)
eig_ca <- get_eigenvalue(res_ca)
as.data.frame(eig_ca) %>%
kable(caption = "Inercia explicada por cada dimensión", digits = 4) %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| eigenvalue | variance.percent | cumulative.variance.percent | |
|---|---|---|---|
| Dim.1 | 0.3222 | 69.9655 | 69.9655 |
| Dim.2 | 0.1275 | 27.6800 | 97.6455 |
| Dim.3 | 0.0108 | 2.3545 | 100.0000 |
La primera dimensión captura el 70% de la inercia y la segunda el 27.7%, acumulando el 97.6%. Esto significa que el mapa bidimensional es una representación excelente de la asociación entre zonas y estratos.
fviz_ca_biplot(res_ca,
repel = TRUE,
col.row = "#2c7fb8",
col.col = "#d95f0e",
shape.row = 17,
shape.col = 15) +
labs(title = "Análisis de Correspondencia Simple",
subtitle = "Zona vs Estrato socioeconómico") +
theme_minimal()Mapa de correspondencia: Zona vs Estrato
Interpretación del mapa: La proximidad entre puntos indica asociación. Se observa claramente que:
p_cr <- fviz_contrib(res_ca, choice = "row", axes = 1:2, fill = "#2c7fb8") +
labs(title = "Contribución de las Zonas") + theme_minimal()
p_cc <- fviz_contrib(res_ca, choice = "col", axes = 1:2, fill = "#d95f0e") +
labs(title = "Contribución de los Estratos") + theme_minimal()
grid.arrange(p_cr, p_cc, ncol = 2)Las zonas que más contribuyen a definir los ejes son Zona Oriente y Zona Oeste (las más extremas), y el estrato con mayor contribución es el 3, seguido del 6.
tabla_tipo_zona <- table(vivienda$tipo, vivienda$zona)
tabla_tipo_zona %>%
kable(caption = "Tabla de contingencia: Tipo vs Zona") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| Zona Centro | Zona Norte | Zona Oeste | Zona Oriente | Zona Sur | |
|---|---|---|---|---|---|
| Apartamento | 24 | 1198 | 1029 | 62 | 2787 |
| Casa | 100 | 722 | 169 | 289 | 1939 |
chi_test2 <- chisq.test(tabla_tipo_zona)
cat("Test Chi-cuadrado: estadístico =", round(chi_test2$statistic, 3),
", p-valor =", format.pval(chi_test2$p.value, digits = 4), "\n")## Test Chi-cuadrado: estadístico = 690.93 , p-valor = < 2.2e-16
La asociación también es significativa (chi² = 690.9, p < 0.001).
res_ca2 <- CA(tabla_tipo_zona, graph = FALSE, ncp = 5)
# Como tipo solo tiene 2 categorías, el AC genera solo 1 dimensión
ndim_ca2 <- ifelse(is.null(ncol(res_ca2$row$coord)), 1, ncol(res_ca2$row$coord))
if (ndim_ca2 >= 2) {
print(
fviz_ca_biplot(res_ca2, repel = TRUE,
col.row = "#1b9e77", col.col = "#7570b3",
shape.row = 17, shape.col = 15) +
labs(title = "AC Simple: Tipo de vivienda vs Zona") +
theme_minimal()
)
} else {
row_coords <- res_ca2$row$coord
col_coords <- res_ca2$col$coord
if (is.matrix(row_coords)) row_coords <- row_coords[, 1]
if (is.matrix(col_coords)) col_coords <- col_coords[, 1]
coords_all <- rbind(
data.frame(nombre = names(row_coords), dim1 = as.numeric(row_coords), tipo_cat = "Tipo de vivienda"),
data.frame(nombre = names(col_coords), dim1 = as.numeric(col_coords), tipo_cat = "Zona")
)
print(
ggplot(coords_all, aes(x = dim1, y = 0, color = tipo_cat, label = nombre)) +
geom_point(size = 4) +
geom_text(vjust = -1.2, size = 3.5, show.legend = FALSE) +
scale_color_manual(values = c("#1b9e77", "#7570b3")) +
labs(title = "AC Simple: Tipo de vivienda vs Zona (1 dimensión)",
x = "Dimensión 1", y = "", color = "Categoría") +
theme_minimal() +
theme(axis.text.y = element_blank(), axis.ticks.y = element_blank())
)
}AC Tipo vs Zona (1 dimensión)
Dado que la variable tipo solo tiene 2 categorías (Casa
y Apartamento), el AC genera una sola dimensión (mín(filas, columnas) -
1 = 2 - 1 = 1). En el gráfico unidimensional se ve que:
Para estudiar la relación simultánea entre tipo, zona y estrato, se aplica el ACM, que es la extensión del AC a más de dos variables categóricas.
datos_acm <- vivienda %>%
select(tipo, zona, estrato) %>%
mutate(estrato = factor(estrato, labels = paste0("Estrato_", sort(unique(estrato))))) %>%
drop_na()
res_acm <- MCA(datos_acm, graph = FALSE)eig_acm <- get_eigenvalue(res_acm)
head(eig_acm, 5) %>%
kable(caption = "Inercia del ACM", digits = 4) %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| eigenvalue | variance.percent | cumulative.variance.percent | |
|---|---|---|---|
| Dim.1 | 0.5621 | 21.0783 | 21.0783 |
| Dim.2 | 0.4531 | 16.9921 | 38.0704 |
| Dim.3 | 0.3796 | 14.2367 | 52.3071 |
| Dim.4 | 0.3334 | 12.5042 | 64.8113 |
| Dim.5 | 0.3233 | 12.1243 | 76.9355 |
fviz_mca_var(res_acm,
col.var = "contrib",
gradient.cols = c("#00AFBB", "#E7B800", "#FC4E07"),
repel = TRUE,
ggtheme = theme_minimal()) +
labs(title = "Análisis de Correspondencia Múltiple",
subtitle = "Tipo, Zona y Estrato en el espacio factorial")Mapa del ACM - Categorías
fviz_mca_biplot(res_acm,
repel = TRUE,
geom.ind = "point",
col.ind = "gray70",
col.var = "contrib",
gradient.cols = c("#00AFBB", "#E7B800", "#FC4E07"),
alpha.ind = 0.3,
ggtheme = theme_minimal()) +
labs(title = "Biplot del ACM",
subtitle = "Individuos y categorías")Biplot del ACM
Interpretación del ACM: Las dos primeras dimensiones explican el 38% de la inercia total, que en ACM es un valor aceptable (suelen ser menores que en ACP). Del mapa de categorías se puede leer:
heatmap_data <- vivienda %>%
group_by(zona, estrato) %>%
summarise(precio_medio = mean(preciom, na.rm = TRUE), .groups = "drop")
ggplot(heatmap_data, aes(x = factor(estrato), y = zona, fill = precio_medio)) +
geom_tile(color = "white", linewidth = 0.5) +
geom_text(aes(label = round(precio_medio, 0)), color = "black", size = 3.5) +
scale_fill_gradient2(low = "#2166ac", mid = "#f7f7f7", high = "#b2182b",
midpoint = median(heatmap_data$precio_medio, na.rm = TRUE)) +
labs(title = "Precio Promedio por Zona y Estrato",
x = "Estrato", y = "Zona", fill = "Precio\n(millones)") +
theme_minimal()Precio promedio por zona y estrato
El mapa de calor muestra que el estrato 6 es consistentemente el más caro en todas las zonas, pero con diferencias: en Zona Oriente el estrato 6 tiene un promedio de 1,350 millones (aunque con muy pocos datos), mientras que en Zona Sur es de 817 millones. Los precios más accesibles están en estrato 3 de la Zona Norte (171 millones).
ggplot(vivienda, aes(x = areaconst, y = preciom, color = tipo)) +
geom_point(alpha = 0.4, size = 1.5) +
geom_smooth(method = "lm", se = FALSE, linewidth = 1) +
facet_wrap(~ zona) +
scale_color_manual(values = c("#2c7fb8", "#d95f0e")) +
labs(title = "Relación Precio vs Área Construida por Zona y Tipo",
x = "Área construida (m²)", y = "Precio (millones)", color = "Tipo") +
theme_minimal() +
coord_cartesian(xlim = c(0, quantile(vivienda$areaconst, 0.98, na.rm = TRUE)),
ylim = c(0, quantile(vivienda$preciom, 0.98, na.rm = TRUE)))Precio vs Área
En todas las zonas, los apartamentos (azul) tienen una pendiente precio/m² mayor que las casas (naranja), lo que indica que el m² de apartamento tiende a ser más caro. Esto es especialmente marcado en Zona Oeste y Zona Norte.
tabla_prop <- vivienda %>%
count(zona, tipo) %>%
group_by(zona) %>%
mutate(prop = n / sum(n))
ggplot(tabla_prop, aes(x = zona, y = prop, fill = tipo)) +
geom_bar(stat = "identity", alpha = 0.85) +
scale_fill_manual(values = c("#2c7fb8", "#d95f0e")) +
scale_y_continuous(labels = percent_format()) +
labs(title = "Proporción de la oferta por tipo y zona",
x = "Zona", y = "Proporción", fill = "Tipo") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 30, hjust = 1))Oferta proporcional
La Zona Oeste tiene la mayor proporción de apartamentos (más del 85%), mientras que la Zona Oriente y Zona Centro tienen mayor proporción de casas. La Zona Sur tiene una mezcla más equilibrada.
vivienda_completa <- vivienda %>%
select(preciom, areaconst, parqueaderos, banios, habitaciones, estrato, zona, tipo) %>%
drop_na(preciom, areaconst, parqueaderos, banios, habitaciones, estrato)
vivienda_completa$cluster <- factor(km_result$cluster)
p_zona <- ggplot(vivienda_completa, aes(x = cluster, fill = zona)) +
geom_bar(position = "fill", alpha = 0.85) +
scale_y_continuous(labels = percent_format()) +
labs(title = "Composición por Zona", x = "Cluster", y = "Proporción") +
theme_minimal()
p_tipo <- ggplot(vivienda_completa, aes(x = cluster, fill = tipo)) +
geom_bar(position = "fill", alpha = 0.85) +
scale_y_continuous(labels = percent_format()) +
scale_fill_manual(values = c("#2c7fb8", "#d95f0e")) +
labs(title = "Composición por Tipo", x = "Cluster", y = "Proporción") +
theme_minimal()
grid.arrange(p_zona, p_tipo, ncol = 2)Clusters por zona y tipo
El Cluster 1 (premium) tiene mayor presencia de la Zona Sur y Zona Norte, y está compuesto casi exclusivamente por casas. El Cluster 2 (económico) también es mayoritariamente de Zona Sur pero con mucha mayor proporción de apartamentos. Esto es coherente: los apartamentos económicos dominan el mercado masivo.
A partir de los tres análisis realizados, se pueden extraer las siguientes conclusiones para la empresa inmobiliaria:
Sobre el ACP: Las propiedades se caracterizan principalmente por dos dimensiones. La primera (58% de la varianza) resume el tamaño y valor general de la vivienda, mientras que la segunda (20%) captura el contraste entre viviendas familiares (muchas habitaciones, estrato más bajo) y viviendas exclusivas (estrato alto, menos habitaciones). Con estas dos dimensiones se captura casi el 80% de la información, lo que simplifica considerablemente el análisis.
Sobre los clusters: El mercado se segmenta naturalmente en tres grupos: un segmento económico mayoritario (52% de la oferta, ~260 millones promedio), un segmento intermedio (35%, ~530 millones) y un segmento premium minoritario (13%, ~1,100 millones). La empresa puede usar estos segmentos para enfocar sus estrategias de marketing y valoración de manera diferenciada.
Sobre el análisis de correspondencia: Existe una asociación fuerte entre zona, estrato y tipo de vivienda. La Zona Oeste es el mercado de estrato 6 por excelencia (apartamentos exclusivos), la Zona Oriente concentra la oferta de estrato 3 (predominantemente casas), y la Zona Sur es el mercado más grande y diverso. Estas asociaciones deben considerarse al evaluar oportunidades de inversión.
Para el segmento económico (Cluster 2), que representa la mayor parte del mercado, la empresa debería enfocarse en volumen de operaciones, aprovechando que es un segmento grande y relativamente homogéneo.
El segmento premium (Cluster 1) ofrece mayores márgenes pero requiere estrategias personalizadas. Se recomienda especial atención a la Zona Oeste y Zona Norte en estratos 5 y 6.
La Zona Sur, por ser la más diversa y con mayor volumen de oferta, ofrece oportunidades en todos los segmentos y debería ser un foco estratégico.
Se recomienda actualizar periódicamente este análisis ya que el mercado inmobiliario es dinámico y los patrones pueden cambiar.
##
## Call:
## PCA(X = datos_acp, scale.unit = TRUE, graph = FALSE)
##
##
## Eigenvalues
## Dim.1 Dim.2 Dim.3 Dim.4 Dim.5 Dim.6
## Variance 3.484 1.223 0.499 0.360 0.244 0.190
## % of var. 58.063 20.385 8.325 5.992 4.073 3.162
## Cumulative % of var. 58.063 78.447 86.772 92.765 96.838 100.000
##
## Individuals (the 10 first)
## Dist Dim.1 ctr cos2 Dim.2 ctr cos2 Dim.3
## 1 | 2.897 | -1.105 0.005 0.146 | 2.480 0.075 0.733 | 0.193
## 2 | 2.382 | -1.844 0.015 0.599 | 0.917 0.010 0.148 | -0.916
## 3 | 2.201 | -0.890 0.003 0.164 | 1.393 0.024 0.400 | -1.411
## 4 | 2.037 | 0.851 0.003 0.175 | 0.494 0.003 0.059 | -0.950
## 5 | 1.549 | -1.400 0.008 0.817 | -0.426 0.002 0.076 | 0.302
## 6 | 1.308 | -1.100 0.005 0.707 | -0.300 0.001 0.053 | 0.581
## 7 | 1.780 | -1.506 0.010 0.715 | 0.086 0.000 0.002 | -0.737
## 8 | 0.698 | -0.247 0.000 0.125 | 0.076 0.000 0.012 | 0.164
## 9 | 1.912 | 0.583 0.001 0.093 | 1.182 0.017 0.382 | 0.890
## 10 | 1.748 | 0.954 0.004 0.298 | -0.405 0.002 0.054 | -0.669
## ctr cos2
## 1 0.001 0.004 |
## 2 0.025 0.148 |
## 3 0.059 0.411 |
## 4 0.027 0.218 |
## 5 0.003 0.038 |
## 6 0.010 0.197 |
## 7 0.016 0.171 |
## 8 0.001 0.055 |
## 9 0.024 0.217 |
## 10 0.013 0.146 |
##
## Variables
## Dim.1 ctr cos2 Dim.2 ctr cos2 Dim.3 ctr
## preciom | 0.878 22.135 0.771 | -0.265 5.735 0.070 | -0.076 1.169
## areaconst | 0.840 20.275 0.706 | 0.226 4.178 0.051 | -0.183 6.733
## parqueaderos | 0.798 18.301 0.638 | -0.167 2.289 0.028 | -0.435 37.969
## banios | 0.868 21.646 0.754 | 0.177 2.557 0.031 | 0.261 13.657
## habitaciones | 0.557 8.917 0.311 | 0.738 44.570 0.545 | 0.234 10.977
## estrato | 0.551 8.726 0.304 | -0.705 40.671 0.497 | 0.384 29.496
## cos2
## preciom 0.006 |
## areaconst 0.034 |
## parqueaderos 0.190 |
## banios 0.068 |
## habitaciones 0.055 |
## estrato 0.147 |
km_result$centers %>%
round(3) %>%
kable(caption = "Centros estandarizados de cada cluster") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| preciom | areaconst | parqueaderos | banios | habitaciones | estrato |
|---|---|---|---|---|---|
| 1.935 | 1.655 | 1.763 | 1.431 | 0.720 | 0.893 |
| -0.620 | -0.579 | -0.534 | -0.713 | -0.489 | -0.453 |
| 0.194 | 0.239 | 0.131 | 0.525 | 0.460 | 0.340 |
cat("Varianza explicada por el clustering:",
round(km_result$betweenss / km_result$totss * 100, 2), "%\n")## Varianza explicada por el clustering: 49.65 %
##
## Call:
## CA(X = tabla_zona_estrato, graph = FALSE)
##
## The chi square of independence between the two variables is equal to 3830.435 (p-value = 0 ).
##
## Eigenvalues
## Dim.1 Dim.2 Dim.3
## Variance 0.322 0.127 0.011
## % of var. 69.966 27.680 2.354
## Cumulative % of var. 69.966 97.646 100.000
##
## Rows
## Iner*1000 Dim.1 ctr cos2 Dim.2 ctr cos2
## Zona Centro | 47.079 | 1.725 13.761 0.942 | 0.364 1.547 0.042 |
## Zona Norte | 46.762 | 0.390 10.887 0.750 | -0.147 3.920 0.107 |
## Zona Oeste | 135.034 | -0.569 14.476 0.345 | 0.783 69.204 0.653 |
## Zona Oriente | 184.564 | 2.015 53.171 0.928 | 0.537 9.563 0.066 |
## Zona Sur | 47.004 | -0.209 7.704 0.528 | -0.188 15.767 0.428 |
## Dim.3 ctr cos2
## Zona Centro 0.228 7.148 0.016 |
## Zona Norte -0.170 61.735 0.143 |
## Zona Oeste -0.037 1.820 0.001 |
## Zona Oriente 0.160 10.006 0.006 |
## Zona Sur 0.061 19.291 0.044 |
##
## Columns
## Iner*1000 Dim.1 ctr cos2 Dim.2 ctr cos2
## 3 | 253.402 | 1.187 76.333 0.970 | 0.207 5.851 0.029 |
## 4 | 47.744 | -0.154 1.895 0.128 | -0.380 28.966 0.773 |
## 5 | 25.471 | -0.132 1.796 0.227 | -0.204 10.824 0.542 |
## 6 | 133.827 | -0.519 19.976 0.481 | 0.539 54.359 0.518 |
## Dim.3 ctr cos2
## 3 0.015 0.350 0.000 |
## 4 0.136 43.547 0.099 |
## 5 -0.133 54.323 0.231 |
## 6 0.028 1.780 0.001 |
##
## Call:
## MCA(X = datos_acm, graph = FALSE)
##
##
## Eigenvalues
## Dim.1 Dim.2 Dim.3 Dim.4 Dim.5 Dim.6 Dim.7
## Variance 0.562 0.453 0.380 0.333 0.323 0.272 0.201
## % of var. 21.078 16.992 14.237 12.504 12.124 10.192 7.551
## Cumulative % of var. 21.078 38.070 52.307 64.811 76.936 87.127 94.678
## Dim.8
## Variance 0.142
## % of var. 5.322
## Cumulative % of var. 100.000
##
## Individuals (the 10 first)
## Dim.1 ctr cos2 Dim.2 ctr cos2 Dim.3 ctr
## 1 | 2.421 0.125 0.606 | 0.963 0.025 0.096 | 0.504 0.008
## 2 | 2.421 0.125 0.606 | 0.963 0.025 0.096 | 0.504 0.008
## 3 | 2.421 0.125 0.606 | 0.963 0.025 0.096 | 0.504 0.008
## 4 | 0.103 0.000 0.006 | -0.722 0.014 0.298 | 0.899 0.026
## 5 | -0.072 0.000 0.003 | -0.330 0.003 0.054 | -1.308 0.054
## 6 | -0.072 0.000 0.003 | -0.330 0.003 0.054 | -1.308 0.054
## 7 | -0.069 0.000 0.002 | -0.539 0.008 0.127 | -0.518 0.008
## 8 | -0.072 0.000 0.003 | -0.330 0.003 0.054 | -1.308 0.054
## 9 | 0.394 0.003 0.067 | -0.401 0.004 0.069 | -0.914 0.026
## 10 | 0.394 0.003 0.067 | -0.401 0.004 0.069 | -0.914 0.026
## cos2
## 1 0.026 |
## 2 0.026 |
## 3 0.026 |
## 4 0.462 |
## 5 0.857 |
## 6 0.857 |
## 7 0.117 |
## 8 0.857 |
## 9 0.361 |
## 10 0.361 |
##
## Categories (the 10 first)
## Dim.1 ctr cos2 v.test Dim.2 ctr cos2
## Apartamento | -0.406 5.980 0.261 -46.558 | 0.056 0.139 0.005
## Casa | 0.643 9.474 0.261 46.558 | -0.088 0.221 0.005
## Zona Centro | 2.712 6.500 0.111 30.423 | 0.978 1.049 0.014
## Zona Norte | 0.449 2.763 0.061 22.448 | -0.251 1.066 0.019
## Zona Oeste | -1.067 9.727 0.192 -39.923 | 1.761 32.849 0.522
## Zona Oriente | 3.083 23.782 0.419 59.014 | 1.421 6.267 0.089
## Zona Sur | -0.212 1.516 0.059 -22.190 | -0.476 9.461 0.298
## Estrato_3 | 1.720 30.646 0.626 72.168 | 0.612 4.816 0.079
## Estrato_4 | -0.199 0.600 0.014 -10.636 | -0.894 15.033 0.275
## Estrato_5 | -0.206 0.830 0.021 -13.188 | -0.471 5.394 0.110
## v.test Dim.3 ctr cos2 v.test
## Apartamento 6.380 | -0.282 4.271 0.126 -32.337 |
## Casa -6.380 | 0.446 6.767 0.126 32.337 |
## Zona Centro 10.972 | 1.171 1.794 0.021 13.135 |
## Zona Norte -12.516 | -1.378 38.467 0.570 -68.830 |
## Zona Oeste 65.872 | -0.154 0.298 0.004 -5.746 |
## Zona Oriente 27.200 | 0.723 1.935 0.023 13.833 |
## Zona Sur -49.767 | 0.514 13.192 0.348 53.793 |
## Estrato_3 25.688 | -0.238 0.867 0.012 -9.976 |
## Estrato_4 -47.795 | 0.702 11.075 0.170 37.552 |
## Estrato_5 -30.184 | -0.758 16.683 0.284 -48.589 |
##
## Categorical variables (eta2)
## Dim.1 Dim.2 Dim.3
## tipo | 0.261 0.005 0.126 |
## zona | 0.747 0.689 0.634 |
## estrato | 0.679 0.665 0.379 |