Una empresa inmobiliaria requiere comprender la estructura del mercado de vivienda urbana a partir de una base de datos obtenida mediante webscraping del portal OLX, con el fin de apoyar decisiones de compra, venta y valoración de propiedades. El presente informe desarrolla un análisis multivariado sobre registros de oferta inmobiliaria.
Las tres familias de técnicas aplicadas comparten un mismo objetivo estadístico reducir la complejidad de un conjunto de datos multidimensional preservando la mayor cantidad de información posible, pero difieren en el tipo de variable sobre la que operan y en la pregunta de negocio que responden:
En las secciones siguientes cada técnica se acompaña de las pruebas estadísticas necesarias para verificar que sus supuestos se cumplen, de modo que las conclusiones no dependan únicamente de la inspección visual de los resultados.
La base contiene 8322 observaciones y 13 variables, incluyendo el
precio de oferta en millones de pesos (preciom), el área
construida en m² (areaconst), el número de parqueaderos,
baños y habitaciones, así como variables categóricas de localización
(zona, barrio) y tipo de inmueble
(tipo), además de coordenadas geográficas.
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>
na_tab <- data.frame(Variable = names(vivienda),
NAs = colSums(is.na(vivienda)),
Porcentaje = round(100 * colSums(is.na(vivienda)) / nrow(vivienda), 1))
rownames(na_tab) <- NULL
kable(na_tab, caption = "Valores faltantes por variable")
| Variable | NAs | Porcentaje |
|---|---|---|
| id | 3 | 0.0 |
| zona | 3 | 0.0 |
| piso | 2638 | 31.7 |
| estrato | 3 | 0.0 |
| preciom | 2 | 0.0 |
| areaconst | 3 | 0.0 |
| parqueaderos | 1605 | 19.3 |
| banios | 3 | 0.0 |
| habitaciones | 3 | 0.0 |
| tipo | 3 | 0.0 |
| barrio | 3 | 0.0 |
| longitud | 3 | 0.0 |
| latitud | 3 | 0.0 |
La variable piso presenta un porcentaje de datos
faltantes muy alto (cerca del 32%), asociado principalmente a los
registros de tipo Casa, donde el concepto de piso no aplica; por esta
razón se excluye del análisis multivariado. La variable
parqueaderos presenta faltantes que se interpretan como
ausencia de parqueadero en la oferta y se imputan con 0. Para las demás
variables, dado que el porcentaje de registros incompletos es marginal
(menor al 0.1%), se descartan los casos con datos faltantes sin afectar
la representatividad de la muestra.
df <- vivienda %>%
filter(!is.na(preciom), !is.na(areaconst), !is.na(banios),
!is.na(habitaciones), !is.na(estrato), !is.na(zona), !is.na(tipo))
df$parqueaderos[is.na(df$parqueaderos)] <- 0
cat("Observaciones tras la depuración:", nrow(df))
## Observaciones tras la depuración: 8319
p1 <- ggplot(df, aes(x = preciom)) +
geom_histogram(bins = 40, fill = "#034a94") +
labs(title = "Distribución del precio de oferta", x = "Precio (millones COP)", y = "Frecuencia") +
theme_minimal()
p2 <- ggplot(df, aes(x = zona, fill = tipo)) +
geom_bar(position = "dodge") +
labs(title = "Oferta por zona y tipo de vivienda", x = "", y = "Número de inmuebles") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 25, hjust = 1))
p3 <- ggplot(df, aes(x = factor(estrato), y = preciom)) +
geom_boxplot(fill = "#FF7F00", alpha = 0.7) +
labs(title = "Precio según estrato socioeconómico", x = "Estrato", y = "Precio (millones COP)") +
theme_minimal()
p4 <- ggplot(df, aes(x = areaconst, y = preciom, color = tipo)) +
geom_point(alpha = 0.3, size = 0.8) +
labs(title = "Precio vs. área construida", x = "Área construida (m²)", y = "Precio (millones COP)") +
theme_minimal()
grid.arrange(p1, p2, p3, p4, ncol = 2)
La oferta se concentra fuertemente en la Zona Sur y, en menor medida, en la Zona Norte, siendo los apartamentos el tipo de inmueble predominante. El precio muestra una relación creciente y aproximadamente lineal con el área construida, y se incrementa de forma marcada con el estrato socioeconómico, aunque con alta dispersión dentro de cada estrato, lo que sugiere la presencia de factores adicionales (localización, número de parqueaderos, baños) que también inciden sobre el valor de la propiedad.
El ACP construye nuevas variables (componentes) como combinaciones lineales de las variables originales, ordenadas de forma que la primera componente capture la máxima varianza posible, la segunda capture la máxima varianza restante bajo la restricción de ser ortogonal a la primera, y así sucesivamente. Matemáticamente equivale a descomponer en valores propios (eigenvalores) la matriz de correlación de las variables estandarizadas; cada eigenvalor representa la varianza capturada por su componente asociado, y el eigenvector correspondiente define los coeficientes (cargas) con que cada variable original contribuye a esa componente.
Se seleccionan las variables cuantitativas que describen las características físicas y el valor de la propiedad: precio, área construida, parqueaderos, baños, habitaciones y estrato. Dado que estas variables están en escalas de medición muy distintas (millones de pesos frente a conteos de habitaciones, por ejemplo), el ACP se realiza sobre la matriz de correlación (equivalente a estandarizar cada variable a media 0 y varianza 1) y no sobre la matriz de covarianza, evitando que las variables de mayor escala numérica dominen artificialmente los primeros componentes.
num_vars <- df %>% select(preciom, areaconst, parqueaderos, banios, habitaciones, estrato)
corr_mat <- cor(num_vars)
corrplot(corr_mat, method = "color", addCoef.col = "black", tl.col = "black",
type = "upper", number.cex = 0.8)
Todas las variables presentan correlaciones positivas entre sí, siendo especialmente fuerte la relación entre precio, área construida y número de baños, lo cual es consistente con el hecho de que estas tres variables reflejan, en buena medida, el tamaño y la calidad de la propiedad.
Antes de interpretar los componentes es necesario verificar que las variables están, en conjunto, suficientemente correlacionadas como para que reducir su dimensionalidad tenga sentido; de lo contrario, el ACP simplemente reproduciría las variables originales sin ninguna ganancia de síntesis. Dos pruebas estándar permiten evaluarlo:
bartlett_test <- cortest.bartlett(corr_mat, n = nrow(num_vars))
kmo_test <- KMO(corr_mat)
cat("Prueba de esfericidad de Bartlett:\n")
## Prueba de esfericidad de Bartlett:
cat(" Chi-cuadrado =", round(bartlett_test$chisq, 1), "| gl =", bartlett_test$df,
"| p-valor =", format.pval(bartlett_test$p.value, digits = 3), "\n\n")
## Chi-cuadrado = 26904.6 | gl = 15 | p-valor = <2e-16
cat("Índice KMO global:", round(kmo_test$MSA, 3), "\n")
## Índice KMO global: 0.767
kable(data.frame(Variable = names(kmo_test$MSAi), MSA = round(kmo_test$MSAi, 2)),
caption = "Adecuación muestral (MSA) por variable")
| Variable | MSA | |
|---|---|---|
| preciom | preciom | 0.76 |
| areaconst | areaconst | 0.80 |
| parqueaderos | parqueaderos | 0.91 |
| banios | banios | 0.79 |
| habitaciones | habitaciones | 0.60 |
| estrato | estrato | 0.71 |
La prueba de Bartlett resulta altamente significativa (p < 0.001)
y el índice KMO global es de 0.77, calificado como aceptable-bueno según
los criterios convencionales (Kaiser, 1974). Ambos resultados confirman
que existe suficiente estructura de correlación en los datos para
justificar la aplicación del ACP. La variable habitaciones
presenta el MSA individual más bajo (0.60), lo que indica que su
varianza está menos explicada por las correlaciones con el resto del
conjunto, aunque se mantiene por encima del umbral mínimo aceptable de
0.5 y por tanto se conserva en el análisis.
res.pca <- PCA(num_vars, scale.unit = TRUE, graph = FALSE)
kable(round(res.pca$eig, 2), caption = "Varianza explicada por componente")
| eigenvalue | percentage of variance | cumulative percentage of variance | |
|---|---|---|---|
| comp 1 | 3.42 | 56.92 | 56.92 |
| comp 2 | 1.26 | 20.99 | 77.91 |
| comp 3 | 0.46 | 7.66 | 85.57 |
| comp 4 | 0.43 | 7.24 | 92.82 |
| comp 5 | 0.24 | 4.02 | 96.83 |
El criterio para decidir cuántos componentes retener combina dos reglas complementarias: el criterio de Kaiser (retener componentes con eigenvalor mayor a 1, ya que un eigenvalor menor a 1 indica que el componente explica menos varianza que una variable original) y la inspección del gráfico de sedimentación (scree plot), buscando el punto de inflexión (“codo”) a partir del cual añadir componentes adicionales aporta poca varianza extra.
fviz_eig(res.pca, addlabels = TRUE, barfill = "#034a94", barcolor = "#034a94",
main = "Varianza explicada por componente principal")
De las seis variables originales, 2 componentes tienen eigenvalor mayor a 1 (criterio de Kaiser), y el gráfico de sedimentación muestra un codo claro entre la segunda y la tercera componente: la caída de varianza explicada se hace mucho más leve a partir de ese punto. Ambos criterios coinciden en recomendar la retención de las dos primeras componentes, que explican en conjunto cerca del 77.9% de la variabilidad total del conjunto de variables cuantitativas una síntesis notable, considerando que se pasa de 6 variables a solo 2 sin perder una quinta parte de la información.
fviz_pca_var(res.pca, col.var = "cos2", gradient.cols = c("#686868", "#0eb0c6", "#FF7F00"),
repel = TRUE, title = "Círculo de correlación de variables (color = calidad de representación)")
pca_var_tab <- data.frame(
Variable = rownames(res.pca$var$coord),
Coord_Dim1 = round(res.pca$var$coord[,1], 2),
Coord_Dim2 = round(res.pca$var$coord[,2], 2),
cos2_Dim1 = round(res.pca$var$cos2[,1], 2),
cos2_Dim2 = round(res.pca$var$cos2[,2], 2),
Contrib_Dim1 = round(res.pca$var$contrib[,1], 1),
Contrib_Dim2 = round(res.pca$var$contrib[,2], 1)
)
kable(pca_var_tab, caption = "Coordenadas, calidad de representación (cos2, %) y contribución (%) de cada variable")
| Variable | Coord_Dim1 | Coord_Dim2 | cos2_Dim1 | cos2_Dim2 | Contrib_Dim1 | Contrib_Dim2 | |
|---|---|---|---|---|---|---|---|
| preciom | preciom | 0.88 | -0.21 | 0.78 | 0.04 | 22.9 | 3.5 |
| areaconst | areaconst | 0.82 | 0.27 | 0.67 | 0.07 | 19.5 | 5.7 |
| parqueaderos | parqueaderos | 0.76 | -0.29 | 0.57 | 0.09 | 16.8 | 6.8 |
| banios | banios | 0.86 | 0.22 | 0.74 | 0.05 | 21.8 | 3.7 |
| habitaciones | habitaciones | 0.53 | 0.76 | 0.28 | 0.58 | 8.1 | 46.4 |
| estrato | estrato | 0.61 | -0.65 | 0.37 | 0.43 | 10.9 | 33.8 |
Para interpretar correctamente el círculo de correlación conviene distinguir tres elementos: la coordenada de una variable en cada dimensión indica su correlación con esa componente (cercana a +-1 significa una relación fuerte); el cos2 mide qué tan bien queda representada esa variable específicamente en el plano de las dos dimensiones mostradas (valores cercanos a 1 indican una representación fiel; valores bajos advierten que la posición de la flecha en el gráfico puede ser engañosa); y la contribución indica qué porcentaje de la varianza de cada componente es explicado por esa variable en particular, es decir, cuánto “pesa” cada variable en la construcción de la componente.
Interpretación de las componentes:
preciom, banios y
areaconst las de mayor contribución. Esta componente puede
interpretarse como un factor general de tamaño y valor de la propiedad:
a mayor puntuación en Dim 1, mayor precio, área, número de baños y
parqueaderos. Su cos² es alto para casi todas las variables (>0.57),
lo que confirma que están bien representadas en este eje.habitaciones (carga positiva alta, cos² = 0.58) con
estrato (carga negativa, cos² = 0.43), siendo ambas las
variables que más contribuyen a este eje. Configura un factor de
composición socioeconómica versus densidad habitacional, que separa
viviendas con muchas habitaciones y estrato bajo perfil típico de
vivienda familiar tradicional o multigeneracional de viviendas de
estrato alto con menos habitaciones pero mayor dotación de parqueaderos,
propio de vivienda unifamiliar o de pareja de mayor poder
adquisitivo.Es importante notar que preciom y
areaconst, pese a ser las variables más directamente
ligadas al “valor de mercado” en sentido estricto, tienen un cos²
relativamente bajo en Dim 2 (0.04 y 0.07 respectivamente): su
comportamiento queda casi enteramente capturado por la Dimensión 1, y
prácticamente no aportan información adicional para diferenciar los
perfiles socioeconómicos que sí distingue la Dimensión 2.
fviz_pca_ind(res.pca, geom = "point", col.ind = df$tipo, palette = c("#FF7F00", "#034a94"),
addEllipses = TRUE, legend.title = "Tipo", alpha.ind = 0.4,
title = "Individuos proyectados según tipo de vivienda")
Los apartamentos y las casas se distribuyen de forma relativamente superpuesta en el plano principal, aunque las casas muestran una mayor dispersión hacia valores positivos de Dim 2, coherente con el mayor número de habitaciones por unidad de área que suelen tener las casas frente a los apartamentos de tamaño comparable. La superposición general de las elipses indica que el tipo de vivienda por sí solo no es el factor que mejor separa las propiedades en este plano lo que motiva recurrir al análisis de conglomerados de la siguiente sección para encontrar la segmentación óptima usando simultáneamente las seis variables.
El análisis de conglomerados busca particionar las observaciones en grupos tales que la variabilidad dentro de cada grupo sea mínima y la variabilidad entre grupos sea máxima. Se emplea el algoritmo k-means, que itera dos pasos hasta la convergencia: (1) asigna cada observación al centroide más cercano en distancia euclidiana, y (2) recalcula cada centroide como el promedio de las observaciones asignadas a él. El algoritmo minimiza la suma de cuadrados dentro de los grupos (WSS, within-cluster sum of squares), por lo que es sensible a la escala de las variables: se reutilizan las mismas seis variables cuantitativas del ACP, estandarizadas para que ninguna domine la distancia euclidiana por su magnitud numérica.
num_scaled <- scale(num_vars)
Nótese que k-means asume implícitamente que los grupos son de forma aproximadamente esférica y de tamaño similar; se trata de un supuesto razonable como primera aproximación exploratoria, y sus resultados se contrastan más adelante con una validación estadística formal (ANOVA) que no depende de ese supuesto.
La elección de k no tiene una única respuesta “correcta”; se combinan aquí dos criterios estadísticos con el criterio de utilidad práctica para el negocio.
Método del codo: grafica la suma de cuadrados dentro de los grupos (WSS) en función de k. A medida que k crece, el WSS siempre disminuye (más grupos ajustan mejor los datos), pero interesa el punto donde la ganancia marginal por añadir un grupo adicional se vuelve pequeña.
fviz_nbclust(num_scaled, kmeans, method = "wss", k.max = 8, nstart = 10) +
labs(title = "Método del codo para selección de k") +
geom_vline(xintercept = 4, linetype = 2, color = "#FF7F00")
Coeficiente de silueta: para cada observación compara, en una escala de -1 a 1, qué tan cerca está de las demás observaciones de su propio clúster frente a las del clúster vecino más próximo; valores promedio más altos indican una partición más nítida. Por el costo computacional de calcular la matriz de distancias completa sobre 8319 observaciones, esta validación se realiza sobre una muestra aleatoria de 1.500 registros, suficiente para estimar el ancho de silueta promedio con precisión.
set.seed(123)
idx_sil <- sample(1:nrow(num_scaled), 1500)
sil_widths <- sapply(2:6, function(k) {
km_s <- kmeans(num_scaled[idx_sil,], centers = k, nstart = 10)
ss <- silhouette(km_s$cluster, dist(num_scaled[idx_sil,]))
mean(ss[,3])
})
sil_tab <- data.frame(k = 2:6, ancho_silueta_promedio = round(sil_widths, 3))
kable(sil_tab, caption = "Ancho de silueta promedio por número de clústeres (muestra n=1.500)")
| k | ancho_silueta_promedio |
|---|---|
| 2 | 0.414 |
| 3 | 0.273 |
| 4 | 0.307 |
| 5 | 0.276 |
| 6 | 0.262 |
Aquí aparece una tensión típica entre el rigor estadístico puro y la utilidad de negocio: el ancho de silueta es máximo en k = 2 (0.41), lo que indica que, desde el punto de vista de la compacidad geométrica de los grupos, la partición más “limpia” es una división binaria entre vivienda de mayor y menor valor. Sin embargo, k = 2 es demasiado grueso para orientar decisiones comerciales diferenciadas, y el método del codo muestra que k = 4 todavía captura una reducción sustancial de varianza (silueta = 0.31) antes de que las ganancias adicionales se aplanen. Se opta por k = 4, privilegiando la capacidad de segmentación accionable para la empresa sobre la solución estadísticamente más compacta pero menos informativa; esta decisión se documenta explícitamente porque en análisis aplicado la mejor partición matemática no siempre coincide con la más útil para la pregunta de negocio.
km <- kmeans(num_scaled, centers = 4, nstart = 25)
df$cluster <- factor(km$cluster)
perfil <- df %>%
group_by(cluster) %>%
summarise(n = n(),
precio_prom = round(mean(preciom),0),
area_prom = round(mean(areaconst),0),
parqueaderos_prom = round(mean(parqueaderos),1),
banios_prom = round(mean(banios),1),
habitaciones_prom = round(mean(habitaciones),1),
estrato_prom = round(mean(estrato),1))
kable(perfil, caption = "Perfil promedio de cada clúster")
| cluster | n | precio_prom | area_prom | parqueaderos_prom | banios_prom | habitaciones_prom | estrato_prom |
|---|---|---|---|---|---|---|---|
| 1 | 1006 | 1104 | 402 | 3.6 | 5.1 | 4.3 | 5.7 |
| 2 | 3537 | 212 | 90 | 0.7 | 2.0 | 2.9 | 3.9 |
| 3 | 2895 | 467 | 162 | 1.7 | 3.4 | 3.3 | 5.3 |
| 4 | 881 | 453 | 302 | 1.2 | 4.5 | 6.6 | 3.9 |
fviz_cluster(km, data = num_scaled, geom = "point", ellipse.type = "convex",
palette = c("#034a94","#0eb0c6","#FF7F00","#686868"),
ggtheme = theme_minimal(), main = "Segmentación de la oferta inmobiliaria (k=4)")
Este gráfico proyecta las seis variables originales sobre las dos
primeras componentes del ACP calculado en la sección anterior (el propio
fviz_cluster recalcula internamente un ACP de las variables
de agrupamiento con fines exclusivamente visuales). Los cuatro grupos
aparecen razonablemente separados a lo largo del eje horizontal que,
como se estableció en la Sección 3.3, resume el tamaño y valor general
de la propiedad, lo cual es consistente con que la variable
preciom sea, junto con banios, la de mayor
peso en la formación de los clústeres.
Perfil de los segmentos identificados:
Antes de dar por válida la segmentación conviene verificar formalmente que los grupos difieren entre sí en las variables clave, y no son un artefacto del algoritmo. Se aplica un análisis de varianza (ANOVA) de una vía para contrastar si el precio promedio y el área promedio difieren significativamente entre los cuatro clústeres.
anova_precio <- aov(preciom ~ cluster, data = df)
anova_area <- aov(areaconst ~ cluster, data = df)
cat("ANOVA: precio ~ clúster\n")
## ANOVA: precio ~ clúster
print(summary(anova_precio))
## Df Sum Sq Mean Sq F value Pr(>F)
## cluster 3 630705337 210235112 6527 <2e-16 ***
## Residuals 8315 267810832 32208
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
cat("\nANOVA: área construida ~ clúster\n")
##
## ANOVA: área construida ~ clúster
print(summary(anova_area))
## Df Sum Sq Mean Sq F value Pr(>F)
## cluster 3 92171565 30723855 3282 <2e-16 ***
## Residuals 8315 77837886 9361
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
En ambos casos el estadístico F es muy grande y el p-valor asociado es prácticamente cero (p < 2e-16), por lo que se rechaza con amplio margen la hipótesis nula de igualdad de medias entre clústeres. Esto confirma estadísticamente y no solo de forma descriptiva que los cuatro segmentos identificados corresponden a perfiles de oferta distintos en precio y tamaño, y no a una partición arbitraria del algoritmo.
tab_cluster_zona <- table(df$cluster, df$zona)
kable(tab_cluster_zona, caption = "Distribución de clústeres por zona de la ciudad")
| Zona Centro | Zona Norte | Zona Oeste | Zona Oriente | Zona Sur |
|---|---|---|---|---|
| 2 | 105 | 298 | 2 | 599 |
| 73 | 1049 | 149 | 197 | 2069 |
| 2 | 566 | 705 | 2 | 1620 |
| 47 | 200 | 46 | 150 | 438 |
ggplot(df, aes(x = longitud, y = latitud, color = cluster)) +
geom_point(alpha = 0.4, size = 0.8) +
scale_color_manual(values = c("#034a94","#0eb0c6","#FF7F00","#686868")) +
labs(title = "Distribución geográfica de los segmentos de vivienda",
x = "Longitud", y = "Latitud", color = "Clúster") +
theme_minimal() +
coord_fixed()
La proyección geográfica de los clústeres evidencia una segmentación espacial clara del mercado: los segmentos de mayor valor se concentran en zonas específicas de la ciudad, mientras que la vivienda económica y familiar tradicional se distribuye de manera más dispersa en el resto del territorio, confirmando que la localización constituye un factor determinante del perfil de oferta.
El análisis de correspondencia (AC) es, conceptualmente, el equivalente del ACP para variables categóricas. Dada una tabla de contingencia, el AC no trabaja sobre la matriz de correlación sino sobre una medida de distancia entre perfiles de fila y de columna llamada distancia chi-cuadrado, que pondera las diferencias entre frecuencias observadas y esperadas bajo el supuesto de independencia. El método descompone la inercia total de la tabla (una medida de asociación estrechamente relacionada con el estadístico chi-cuadrado de independencia: inercia = χ²/n) en un conjunto de dimensiones ortogonales, cada una capturando una porción decreciente de esa asociación. El resultado se representa habitualmente en un biplot donde categorías de fila y de columna que aparecen próximas entre sí están positivamente asociadas (se observan juntas con más frecuencia de la esperada bajo independencia), y categorías alejadas del origen están fuertemente asociadas a alguna dimensión particular.
Al igual que en el ACP, cada categoría tiene una calidad de representación (cos2) en el plano mostrado y una contribución a la formación de cada eje, que permiten distinguir qué categorías realmente “explican” la estructura del biplot de aquellas que simplemente aparecen cerca del origen por tener un comportamiento cercano al promedio.
Se examina la asociación entre las variables categóricas de
localización y tipología de vivienda. Dado que la variable
barrio presenta un número muy elevado de categorías (436
barrios distintos, varios de ellos con muy baja frecuencia), su
inclusión directa en un análisis de correspondencia produce una tabla
dispersa y de difícil interpretación, además de violar el supuesto
práctico de frecuencias esperadas suficientemente grandes en cada celda;
por esta razón se prioriza el cruce entre zona,
tipo y estrato, que son las variables
categóricas con mayor relevancia estratégica para la empresa, y se
reporta de forma complementaria la distribución de los barrios de mayor
participación en la oferta.
tab_zt <- table(df$zona, df$tipo)
kable(tab_zt, caption = "Tabla de contingencia: Zona x Tipo de vivienda")
| Apartamento | Casa | |
|---|---|---|
| Zona Centro | 24 | 100 |
| Zona Norte | 1198 | 722 |
| Zona Oeste | 1029 | 169 |
| Zona Oriente | 62 | 289 |
| Zona Sur | 2787 | 1939 |
chisq.test(tab_zt)
##
## Pearson's Chi-squared test
##
## data: tab_zt
## X-squared = 690.93, df = 4, p-value < 2.2e-16
La prueba de independencia chi-cuadrado resulta altamente significativa (p < 0.001), lo que confirma que la composición de tipo de vivienda difiere sustancialmente entre zonas de la ciudad.
Al tratarse de una tabla de 5 zonas x 2 tipos, el análisis de correspondencia sobre este cruce arroja una única dimensión (min(5-1, 2-1) = 1), que concentra el 100% de la asociación entre las variables. Por esta razón se presenta la coordenada de cada zona sobre esa dimensión, en lugar de un plano bidimensional:
res.ca1 <- CA(tab_zt, graph = FALSE)
ca1_scores <- data.frame(zona = names(res.ca1$row$coord), Dim1 = as.numeric(res.ca1$row$coord))
ggplot(ca1_scores, aes(x = reorder(zona, Dim1), y = Dim1)) +
geom_col(fill = "#034a94") +
coord_flip() +
labs(title = "Coordenadas de zona en la dimensión de correspondencia (Zona x Tipo)",
subtitle = "Valores negativos: mayor asociación con Casa | Valores positivos: mayor asociación con Apartamento",
x = "", y = "Dimensión 1 (100% de la asociación)") +
theme_minimal()
La categoría “Apartamento” se ubica en el lado positivo de la dimensión y “Casa” en el lado negativo. En consecuencia, la Zona Centro y la Zona Oriente, con las coordenadas más negativas, son las zonas más asociadas a casas (más del 80% de su oferta corresponde a este tipo), mientras que la Zona Oeste, con coordenada positiva, es la más asociada a apartamentos (cerca del 86% de su oferta). La Zona Norte y la Zona Sur se ubican cerca del origen, lo que refleja una composición más balanceada entre ambos tipos de vivienda, aunque con un leve predominio de apartamentos en ambos casos.
tab_ze <- table(df$zona, df$estrato)
kable(tab_ze, caption = "Tabla de contingencia: Zona x Estrato")
| 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 |
chisq_ze <- chisq.test(tab_ze)
res.ca2 <- CA(tab_ze, graph = FALSE)
kable(round(res.ca2$eig, 2), caption = "Varianza explicada - CA Zona x Estrato")
| eigenvalue | percentage of variance | cumulative percentage of variance | |
|---|---|---|---|
| dim 1 | 0.32 | 69.97 | 69.97 |
| dim 2 | 0.13 | 27.68 | 97.65 |
| dim 3 | 0.01 | 2.35 | 100.00 |
La prueba chi-cuadrado de independencia vuelve a ser altamente significativa (χ² = 3830.4, p < 0.001), confirmando una asociación fuerte entre zona y estrato — resultado esperable dada la conocida estratificación geográfica de las ciudades colombianas, pero que conviene siempre verificar formalmente antes de interpretar el biplot. A diferencia del cruce anterior, esta tabla es de 5×4, por lo que el AC produce min(5-1, 4-1) = 3 dimensiones posibles; las dos primeras explican cerca del 97.6% de la inercia total, lo que permite una interpretación prácticamente completa en un plano bidimensional.
fviz_ca_biplot(res.ca2, repel = TRUE, title = "Análisis de correspondencia: Zona x Estrato")
ca2_row_tab <- data.frame(
Zona = rownames(res.ca2$row$coord),
cos2_Dim1 = round(res.ca2$row$cos2[,1], 2),
cos2_Dim2 = round(res.ca2$row$cos2[,2], 2),
Contrib_Dim1 = round(res.ca2$row$contrib[,1], 1),
Contrib_Dim2 = round(res.ca2$row$contrib[,2], 1)
)
kable(ca2_row_tab, caption = "Calidad de representación (cos2, %) y contribución (%) de cada zona por dimensión")
| Zona | cos2_Dim1 | cos2_Dim2 | Contrib_Dim1 | Contrib_Dim2 | |
|---|---|---|---|---|---|
| Zona Centro | Zona Centro | 0.94 | 0.04 | 13.8 | 1.5 |
| Zona Norte | Zona Norte | 0.75 | 0.11 | 10.9 | 3.9 |
| Zona Oeste | Zona Oeste | 0.35 | 0.65 | 14.5 | 69.2 |
| Zona Oriente | Zona Oriente | 0.93 | 0.07 | 53.2 | 9.6 |
| Zona Sur | Zona Sur | 0.53 | 0.43 | 7.7 | 15.8 |
La tabla de calidad de representación permite matizar la lectura del biplot: Zona Oriente es la que más contribuye a la Dimensión 1 (53%) y está muy bien representada en ella (cos² = 0.93), mientras que Zona Oeste es la que domina la Dimensión 2 (69% de contribución, cos² = 0.65). En términos de las categorías de estrato, el estrato 3 es la categoría que mejor explica la Dimensión 1 (76% de contribución, cos² = 0.97) y el estrato 6 domina la Dimensión 2 (54% de contribución). En conjunto, se observa una clara gradación socioeconómica del territorio: la Zona Centro y la Zona Oriente se asocian fuertemente con el estrato 3 (posición opuesta al estrato 6 en la Dimensión 1), mientras que la Zona Oeste se diferencia principalmente por su asociación con los estratos altos (5 y 6) a lo largo de la Dimensión 2. La Zona Sur y la Zona Norte, con cos² más moderados en ambas dimensiones, muestran una composición más heterogénea que abarca un rango amplio de estratos, consistente con ser las zonas de mayor volumen y diversidad de oferta de la ciudad. Este patrón valida la pertinencia de usar la zona como variable proxy del nivel socioeconómico en contextos donde no se dispone del estrato exacto de una propiedad.
top_barrios <- df %>% count(barrio, sort = TRUE) %>% head(15)
kable(top_barrios, caption = "15 barrios con mayor número de inmuebles ofertados")
| 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 |
Valle del Lili, Ciudad Jardín y Pance concentran la mayor parte de la oferta registrada, todos ellos ubicados en la Zona Sur, lo que refuerza el hallazgo de que esta zona constituye el núcleo dinámico del mercado inmobiliario analizado.
Antes de presentar las conclusiones es importante ser explícitos sobre las limitaciones del análisis, tanto por rigor estadístico como porque condicionan el alcance de las recomendaciones:
barrio no modelada
directamente: por su alta cardinalidad (437 categorías), no se
incluyó en el análisis de correspondencia pese a ser, en la práctica
inmobiliaria, uno de los determinantes más finos del precio; el análisis
a nivel de zona es una aproximación necesariamente más agregada.Sobre la estructura de las variables (ACP): tras verificar la pertinencia estadística del método (KMO = 0.77, Bartlett p < 0.001), se encontró que el valor de una propiedad está determinado principalmente por un factor conjunto de tamaño físico y dotación (área, baños, parqueaderos, precio), que por sí solo explica el 56.9% de la variabilidad conjunta de las seis variables analizadas, y de forma secundaria (21.0% adicional) por un factor que diferencia la vivienda familiar de estrato bajo con muchas habitaciones frente a la vivienda de estrato alto con más parqueaderos. Para efectos de valoración, esto sugiere que un modelo de precios simplificado puede apoyarse en muy pocas variables sintéticas esencialmente dos sin perder una proporción relevante de la información original, lo cual tiene implicaciones prácticas directas para simplificar los modelos de tasación que utilice la empresa.
Sobre la segmentación de mercado (clúster): se identifican cuatro perfiles de oferta estadísticamente diferenciados —premium, medio-alto, familiar tradicional y económico—, confirmados mediante ANOVA (p < 0.001 en precio y área), cuya distribución no es homogénea en el territorio. La empresa puede utilizar esta segmentación para diseñar estrategias comerciales diferenciadas por perfil: por ejemplo, portafolios de inversión o servicios de asesoría premium enfocados en el segmento de mayor valor concentrado en zonas específicas y de menor volumen, frente a estrategias de rotación rápida y mayor estandarización en el segmento económico, que agrupa la mayor proporción de la oferta total del mercado.
Sobre la asociación entre variables categóricas (correspondencia): existe una relación estadísticamente significativa (χ2 con p < 0.001 en ambos cruces analizados) y espacialmente interpretable entre zona, tipo de vivienda y estrato. La Zona Oeste se asocia fuertemente con apartamentos y estratos altos; la Zona Centro y la Zona Oriente, con casas y estrato medio-bajo; y la Zona Sur, pese a concentrar el mayor volumen de oferta incluyendo los barrios de mayor participación como Valle del Lili y Ciudad Jardín, presenta una composición más heterogénea en tipo y estrato. Esta información resulta valiosa tanto para decisiones de expansión territorial como para calibrar estrategias de precios diferenciadas por zona, usando la localización como proxy razonable del perfil socioeconómico cuando el estrato exacto no está disponible.
Integración de los tres análisis: los resultados de las tres técnicas son consistentes entre sí y se refuerzan mutuamente: la Dimensión 1 del ACP (tamaño/valor) es, en esencia, la misma variable latente que separa los clústeres premium y económico; y la Dimensión 2 del ACP (habitaciones vs. estrato) es coherente con el eje de correspondencia que separa la Zona Oeste (estrato alto, menos habitaciones por vivienda) de la Zona Centro/Oriente (estrato medio-bajo, casas familiares con más habitaciones). Esta convergencia entre métodos cada uno con supuestos y fuentes de información distintos aumenta la confianza en que los patrones identificados reflejan una estructura real del mercado y no artefactos de una técnica en particular.
Recomendación general: se sugiere complementar este análisis descriptivo con un modelo predictivo de precios (por ejemplo, regresión múltiple o árboles de decisión) que incorpore las puntuaciones de los componentes principales y la pertenencia a los segmentos identificados como variables explicativas, con el fin de anticipar el valor de nuevas propiedades y apoyar de forma más robusta las decisiones de compra, venta y valoración de la empresa.