1. Introducción y planteamiento del problema

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:

  • Análisis de Componentes Principales (ACP): opera sobre variables cuantitativas correlacionadas y busca un pequeño número de combinaciones lineales ortogonales (componentes) que concentren la máxima varianza posible del conjunto original. Responde a la pregunta: ¿cuáles son los “ejes” latentes que realmente explican por qué varían el precio y las características de las propiedades?
  • Análisis de Conglomerados: parte de una medida de distancia o disimilitud entre observaciones y busca agrupar las viviendas en subconjuntos internamente homogéneos y externamente heterogéneos. Responde a la pregunta: ¿existen perfiles o “arquetipos” de vivienda que se repiten en el mercado y que la empresa podría tratar de forma diferenciada?
  • Análisis de Correspondencia (AC): es el análogo del ACP para variables categóricas organizadas en tablas de contingencia; descompone la asociación (medida mediante la distancia chi-cuadrado) entre las categorías en un pequeño número de dimensiones, representables gráficamente. Responde a la pregunta: ¿qué zonas de la ciudad están más asociadas a qué tipo de vivienda o estrato, más allá de lo que se observaría si ambas variables fueran independientes?

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.

2. Descripción de los datos

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>

2.1 Tratamiento de datos faltantes

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")
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

2.2 Exploración descriptiva

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.

3. Análisis de Componentes Principales

3.1 Fundamento y variables de entrada

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.

3.2 Verificación de la pertinencia del ACP

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:

  • Prueba de esfericidad de Bartlett: contrasta la hipótesis nula de que la matriz de correlación es igual a la matriz identidad (es decir, que las variables no están correlacionadas entre sí). Un p-valor pequeño rechaza esa hipótesis y respalda el uso del ACP.
  • Índice KMO (Kaiser-Meyer-Olkin): mide, en una escala de 0 a 1, qué tan adecuada es la matriz de correlación para el análisis factorial, comparando las correlaciones observadas con las correlaciones parciales entre variables. Valores por encima de 0.7 se consideran adecuados, y por encima de 0.8, muy buenos.
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")
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.

3.3 Extracción y selección de componentes

res.pca <- PCA(num_vars, scale.unit = TRUE, graph = FALSE)
kable(round(res.pca$eig, 2), caption = "Varianza explicada por componente")
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")
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:

  • Dimensión 1 (56.9% de la varianza): todas las variables cargan positivamente y con magnitudes similares (entre 0.53 y 0.88), siendo 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.
  • Dimensión 2 (21% de la varianza): contrapone 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.

4. Análisis de Conglomerados

4.1 Fundamento del método

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.

4.2 Selección del número de clústeres

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)")
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.

4.3 Segmentación final

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")
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:

  1. Clúster de vivienda premium: precios y áreas más altas, mayor número de parqueaderos y baños; corresponde a la oferta de mayor valor del mercado, aunque es el segmento con menor volumen de unidades.
  2. Clúster de vivienda de estrato medio-alto: valores cercanos al promedio general, con estrato por encima de la media pero tamaño moderado; es el segundo grupo en tamaño de muestra.
  3. Clúster de vivienda familiar tradicional: destaca por el número elevado de habitaciones y estrato más bajo, coherente con casas familiares de barrios consolidados donde conviven varias generaciones o núcleos familiares.
  4. Clúster de vivienda económica: agrupa la oferta de menor precio, área y estrato; es el segmento de mayor participación en el mercado analizado, lo que indica que la mayor parte de la oferta inmobiliaria de la ciudad corresponde a vivienda de gama económica o media-baja.

4.4 Validación estadística de las diferencias entre clústeres

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")
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.

5. Análisis de Correspondencia

5.1 Fundamento del método

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.

5.2 Zona vs. Tipo de vivienda

tab_zt <- table(df$zona, df$tipo)
kable(tab_zt, caption = "Tabla de contingencia: Zona x Tipo de vivienda")
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.

5.3 Zona vs. Estrato socioeconómico

tab_ze <- table(df$zona, df$estrato)
kable(tab_ze, caption = "Tabla de contingencia: Zona x Estrato")
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")
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")
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.

5.4 Barrios con mayor participación en la oferta

top_barrios <- df %>% count(barrio, sort = TRUE) %>% head(15)
kable(top_barrios, caption = "15 barrios con mayor número de inmuebles ofertados")
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.

6. Limitaciones metodológicas

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:

  • Naturaleza de la fuente de datos: al provenir de anuncios de oferta en OLX, la base refleja precios pedidos por los vendedores y no necesariamente precios de transacción efectivamente pagados, lo que puede introducir un sesgo al alza. Además, un mismo inmueble publicado varias veces o por distintos agentes podría generar duplicados no identificables con las variables disponibles.
  • K-means asume grupos esféricos: el algoritmo tiende a formar clústeres de forma y tamaño similares, lo cual puede no ser adecuado si los segmentos reales del mercado tienen formas más irregulares o densidades muy distintas. La validación con ANOVA confirma que los grupos difieren significativamente en sus medias, pero no garantiza que la forma geométrica de cada segmento sea la más natural para los datos; un análisis futuro podría contrastar estos resultados con métodos basados en densidad (p. ej. DBSCAN) o con clustering jerárquico.
  • Tensión entre óptimo estadístico y utilidad de negocio: como se documentó en la Sección 4.2, el criterio de silueta favorece una partición más simple (k=2) que la finalmente adoptada (k=4); los resultados de segmentación deben leerse como una herramienta de apoyo a la decisión y no como una verdad estadística única e incuestionable.
  • Variable 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.
  • Relaciones exploradas son de asociación, no de causalidad: tanto el ACP como el AC describen patrones de correlación y asociación en los datos observados, pero no permiten, por sí solos, establecer relaciones causales (por ejemplo, no se puede concluir que estar en la Zona Oestecausa que una vivienda sea apartamento; ambas son características que covarían en el mercado observado).

7. Conclusiones y recomendaciones

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.