Introducción

Este informe desarrolla un análisis multivariado del mercado de vivienda urbana utilizando la base de datos vivienda del paquete paqueteMODELOS, aplicando Análisis de Componentes Principales, Análisis de Conglomerados y Análisis de Correspondencia.

1 Conocimiento de los datos

1.1 Librerías

Se cargan los paquetes necesarios para el análisis: paqueteMODELOS trae el dataset vivienda; dplyr y ggplot2 para manipulación y visualización; corrplot para graficar la matriz de correlación; FactoMineR y factoextra para PCA, Correspondencia y su visualización; cluster para Análisis de Conglomerados.

library(paqueteMODELOS)
library(dplyr)
library(ggplot2)
library(tidyr)
library(corrplot)
library(FactoMineR)
library(factoextra)
library(cluster)
library(stringr)

1.2 Carga de datos

Se carga el dataset vivienda incluido en el paquete paqueteMODELOS.

data("vivienda")

1.3 Dimensiones y tipo de atributos

Se identifica el número de registros, número de atributos y el tipo de cada uno.

dim(vivienda)
## [1] 8322   13

El dataset tiene 8322 registros y 13 atributos.

La Tabla 1.1 caracteriza el conjunto de datos según el tipo de cada variable. Las variables cuantitativas corresponden a estrato, preciom, areaconst, parqueaderos, banios y habitaciones, mientras que zona, piso, tipo y barrio son variables categóricas. Adicionalmente, el dataset incluye las variables longitud y latitud, que aunque son de naturaleza numérica, corresponden a coordenadas geográficas y no a características intrínsecas de la vivienda; dado que el alcance de este análisis no contempla un componente espacial, estas dos variables se excluyen del conjunto de variables cuantitativas a utilizar en el Análisis de Componentes Principales. Por otro lado, la tabla también permite identificar una concentración importante de valores faltantes en las variables piso y parqueaderos, muy por encima del resto de atributos.

tabla_tipos <- data.frame(
  Variable = names(vivienda),
  Tipo = sapply(vivienda, class),
  Ejemplo = sapply(vivienda, function(x) as.character(head(x[!is.na(x)], 1))),
  NA_count = sapply(vivienda, function(x) sum(is.na(x))),
  row.names = NULL
)

knitr::kable(tabla_tipos, 
             caption = "Tipo de dato, ejemplo de valor y datos faltantes por variable",
             col.names = c("Variable", "Tipo de dato", "Ejemplo", "Valores faltantes"))
Table 1.1: Tipo de dato, ejemplo de valor y datos faltantes por variable
Variable Tipo de dato Ejemplo Valores faltantes
id numeric 1147 3
zona character Zona Oriente 3
piso character 02 2638
estrato numeric 3 3
preciom numeric 250 2
areaconst numeric 70 3
parqueaderos numeric 1 1605
banios numeric 3 3
habitaciones numeric 6 3
tipo character Casa 3
barrio character 20 de julio 3
longitud numeric -76.51168 3
latitud numeric 3.43382 3
head(vivienda)

1.4 Centralidad y dispersión

Se seleccionan las variables numéricas y se calculan medidas de centralidad (media, mediana) y dispersión (desviación estándar, rango).

variables_numericas <- vivienda %>% 
  select(where(is.numeric)) %>% 
  select(-id)

La Tabla 1.2 presenta los estadísticos de centralidad y dispersión de las variables numéricas del dataset. Se observa una asimetría marcada hacia la derecha en preciom y areaconst, donde la media supera considerablemente a la mediana (433.89 frente a 330.00 millones de pesos, y 174.93 frente a 123.00 metros cuadrados, respectivamente), lo que indica la presencia de un grupo reducido de propiedades de alto valor y tamaño que desplaza el promedio por encima del comportamiento típico del mercado. Esta asimetría se refuerza con la alta dispersión de preciom, cuya desviación estándar (328.65) es comparable en magnitud a su propia media, evidenciando un mercado heterogéneo en precios. Por su parte, la variable estrato se concentra entre los valores 3 y 6, sin registros en los estratos 1 y 2, lo que sugiere que la oferta capturada en este dataset está sesgada hacia segmentos socioeconómicos medios y altos. Finalmente, longitud y latitud presentan una dispersión prácticamente nula (desviaciones estándar de 0.02 y 0.04), lo cual es consistente con que el dataset corresponde exclusivamente a viviendas ubicadas dentro de la ciudad de Cali

resumen_stats <- variables_numericas %>%
  summarise(across(everything(), 
                    list(Media = ~mean(.x, na.rm = TRUE),
                         Mediana = ~median(.x, na.rm = TRUE),
                         DesvEst = ~sd(.x, na.rm = TRUE),
                         Min = ~min(.x, na.rm = TRUE),
                         Max = ~max(.x, na.rm = TRUE),
                         NA_count = ~sum(is.na(.x))),
                    .names = "{.col}__{.fn}")) %>%
  pivot_longer(everything(), names_to = c("Variable","Estadistico"), 
               names_sep = "__", values_to = "Valor") %>%
  pivot_wider(names_from = Estadistico, values_from = Valor)

knitr::kable(resumen_stats, digits = 2,
             caption = "Estadísticos descriptivos de las variables numéricas")
Table 1.2: Estadísticos descriptivos de las variables numéricas
Variable Media Mediana DesvEst Min Max NA_count
estrato 4.63 5.00 1.03 3.00 6.00 3
preciom 433.89 330.00 328.65 58.00 1999.00 2
areaconst 174.93 123.00 142.96 30.00 1745.00 3
parqueaderos 1.84 2.00 1.12 1.00 10.00 1605
banios 3.11 3.00 1.43 0.00 10.00 3
habitaciones 3.61 3.00 1.46 0.00 10.00 3
longitud -76.53 -76.53 0.02 -76.59 -76.46 3
latitud 3.42 3.42 0.04 3.33 3.50 3

Se identificó que las variables banios y habitaciones registran un mínimo de 0, lo cual carece de sentido en el contexto de una vivienda habitable: toda propiedad residencial, ya sea casa o apartamento, cuenta por definición con al menos un baño y una habitación. Esto sugiere que dichos valores corresponden a errores de captura durante el proceso de web scraping, probablemente campos que el anunciante dejó vacíos y que el sistema completó con 0 en lugar de un valor faltante (NA). Se identificaron 45 registros con banios == 0 y 66 con habitaciones == 0. Para descartar que este patrón estuviera asociado a un tipo de propiedad específico (por ejemplo, locales u oficinas mal clasificados como vivienda), se revisó la distribución de tipo para los registros con banios == 0, encontrando que el problema está presente tanto en apartamentos (14 casos) como en casas (31 casos).

table(vivienda$banios)
table(vivienda$habitaciones)
vivienda %>%
  filter(banios == 0) %>%
  count(tipo)
sapply(variables_numericas, sd, na.rm = TRUE)

1.5 Matriz de correlación

Se calcula la matriz de correlación entre las variables numéricas para identificar posibles atributos redundantes.

matriz_correlacion <- cor(variables_numericas, use = "pairwise.complete.obs")
round(matriz_correlacion, 2)
corrplot(matriz_correlacion, method = "color", addCoef.col = "black",
         type = "upper", tl.col = "black", number.cex = 0.7)
Matriz de correlación de las variables numéricas

Figure 1.1: Matriz de correlación de las variables numéricas

La Figura 1.1 muestra las correlaciones entre las variables numéricas del dataset. Centrando el análisis en las cinco variables seleccionadas para el Análisis de Componentes Principales (preciom, areaconst, parqueaderos, banios y habitaciones), se observa un bloque de correlaciones moderadas a fuertes (entre 0.57 y 0.69) entre preciom, areaconst, parqueaderos y banios, lo que indica que estas variables tienden a moverse conjuntamente y comparten información redundante sobre el tamaño y valor de la propiedad. En contraste, habitaciones presenta correlaciones más débiles con preciom (0.26) y parqueaderos (0.28), aunque mantiene una relación moderada con areaconst (0.52) y banios (0.59), lo que sugiere un comportamiento parcialmente distinto al resto del grupo. Ninguna correlación entre estas cinco variables supera 0.7, por lo que no se identifica redundancia extrema que amerite excluir alguna de ellas antes del análisis. Este patrón de correlaciones moderadas es favorable para el Análisis de Componentes Principales, ya que sugiere la existencia de estructura compartida susceptible de resumirse en un número reducido de componentes.

1.6 Detección de datos atípicos

boxplot(variables_numericas, las = 2, main = "Boxplot de variables numéricas")
Boxplot de las variables numéricas para detección de atípicos

Figure 1.2: Boxplot de las variables numéricas para detección de atípicos

La Figura 1.2 muestra la presencia de valores atípicos principalmente en preciom y areaconst, con observaciones que alcanzan hasta 2000 millones de pesos y 1750 metros cuadrados, respectivamente. A diferencia de los valores inválidos identificados previamente en banios y habitaciones, estos atípicos corresponden a propiedades de alto valor y gran tamaño que son plausibles dentro del mercado inmobiliario real, por lo que no se consideran errores de captura. Dado que representan la cola alta legítima del mercado, se conservan en el análisis para no distorsionar la variabilidad real capturada en los siguientes análisis multivariados.

1.7 Detección de datos faltantes

El conteo de valores faltantes por variable ya se identificó en la tabla de tipos de datos (Sección 1.3). Aquí se revisa una dimensión distinta: cuántos valores faltantes acumula cada registro (fila), lo que ayuda a decidir si conviene eliminar observaciones muy incompletas o si los NA están dispersos entre distintas propiedades.

na_por_fila <- rowSums(is.na(vivienda))

tabla_na_fila <- data.frame(
  NA_por_registro = names(table(na_por_fila)),
  Cantidad_registros = as.integer(table(na_por_fila))
)

knitr::kable(tabla_na_fila,
             caption = "Distribución del número de registros según cantidad de valores faltantes por registro",
             col.names = c("N° de valores faltantes en el registro", "Cantidad de registros"))
Table 1.3: Distribución del número de registros según cantidad de valores faltantes por registro
N° de valores faltantes en el registro Cantidad de registros
0 4808
1 2785
2 726
12 1
13 2

El máximo de valores faltantes en un mismo registro es de 13.

1.8 Desbalance de variables categóricas

Se revisa la distribución de las variables categóricas tipo y zona para detectar posible desbalance en la oferta.

knitr::kable(table(vivienda$tipo), 
             caption = "Distribución de la variable tipo de vivienda",
             col.names = c("Tipo", "Frecuencia"))
Table 1.4: Distribución de la variable tipo de vivienda
Tipo Frecuencia
Apartamento 5100
Casa 3219
knitr::kable(table(vivienda$zona),
             caption = "Distribución de la variable zona",
             col.names = c("Zona", "Frecuencia"))
Table 1.5: Distribución de la variable zona
Zona Frecuencia
Zona Centro 124
Zona Norte 1920
Zona Oeste 1198
Zona Oriente 351
Zona Sur 4726

2 Preparación de los datos para el análisis

2.1 Eliminación de filas con 12 o más valores faltantes

En la Sección 1.7 se caracterizó la distribución del número de valores faltantes por registro, encontrando registros con un número extremo de atributos vacíos. Un registro con 12 o más valores faltantes (de un total de 13 columnas) no aporta información utilizable para el análisis multivariado, ya que prácticamente todo el registro estaría compuesto por datos imputados o supuestos. Por esta razón, antes de proceder con la imputación de parqueaderos y la exclusión de valores inválidos en banios y habitaciones, se eliminan del dataset las filas que presentan 12 o más valores faltantes.

n_antes <- nrow(vivienda)

vivienda <- vivienda %>%
  filter(rowSums(is.na(.)) < 12)

n_despues <- nrow(vivienda)

cat("Registros antes de eliminar filas con 12 o más valores faltantes:", n_antes, "\n")
## Registros antes de eliminar filas con 12 o más valores faltantes: 8322
cat("Registros después de eliminar filas con 12 o más valores faltantes:", n_despues, "\n")
## Registros después de eliminar filas con 12 o más valores faltantes: 8319
cat("Registros eliminados:", n_antes - n_despues, "\n")
## Registros eliminados: 3

A partir de este punto, el dataset no contiene registros con un nivel extremo de valores faltantes, quedando listo para las siguientes etapas de preparación de los datos.

2.2 Imputación de datos faltantes en parqueaderos

La variable parqueaderos presenta 1605 valores faltantes, equivalentes a cerca del 19% del total de registros del dataset, una proporción considerablemente mayor que la de las demás variables numéricas relevantes para el análisis de Componentes Principales posterior (preciom, areaconst, banios y habitaciones, cada una con apenas 2-3 valores faltantes). Dada esta magnitud, se optó por imputar los valores faltantes en lugar de excluir las filas correspondientes, bajo el supuesto de que un NA en esta variable representa información no reportada por el anunciante y no necesariamente la ausencia de parqueadero en el inmueble. La imputación se realiza utilizando la mediana de parqueaderos calculada por grupos definidos por tipo de vivienda y estrato, con el fin de capturar de mejor manera la heterogeneidad del mercado inmobiliario según estas dos características.

na_antes <- sum(is.na(vivienda$parqueaderos))

vivienda <- vivienda %>%
  group_by(tipo, estrato) %>%
  mutate(parqueaderos = ifelse(is.na(parqueaderos),
                                median(parqueaderos, na.rm = TRUE),
                                parqueaderos)) %>%
  ungroup()

na_despues <- sum(is.na(vivienda$parqueaderos))

if (na_despues > 0) {
  vivienda$parqueaderos[is.na(vivienda$parqueaderos)] <- median(vivienda$parqueaderos, na.rm = TRUE)
}

cat("NA en parqueaderos antes de imputar:", na_antes, "\n")
## NA en parqueaderos antes de imputar: 1602
cat("NA en parqueaderos después de imputar:", na_despues, "\n")
## NA en parqueaderos después de imputar: 0

A partir de este punto, la variable parqueaderos queda completa (sin valores faltantes) y lista para ser utilizada en el Análisis de Componentes Principales.

2.3 Exclusión de registros con valores inválidos en baños y habitaciones

Como se identificó en la Sección 1.3, las variables banios y habitaciones registran un mínimo de 0, un valor sin sentido para una vivienda habitable, ya que toda propiedad residencial cuenta por definición con al menos un baño y una habitación. Se detectaron 45 registros con banios == 0 y 66 con habitaciones == 0, probablemente asociados a errores de captura durante el proceso de web scraping. A diferencia del caso de parqueaderos, aquí se optó por excluir estos registros en lugar de imputarlos, ya que 0 no es un valor faltante sino un dato inválido efectivamente registrado, y no existe un supuesto razonable que permita reconstruir el valor real de baños o habitaciones a partir de otras variables. Dado que estos registros representan una fracción muy pequeña del total del dataset, su exclusión no compromete de forma significativa el tamaño muestral ni la representatividad del análisis.

n_antes <- nrow(vivienda)

vivienda <- vivienda %>%
  filter(banios > 0, habitaciones > 0)

n_despues <- nrow(vivienda)

cat("Registros antes de excluir valores inválidos:", n_antes, "\n")
## Registros antes de excluir valores inválidos: 8319
cat("Registros después de excluir valores inválidos:", n_despues, "\n")
## Registros después de excluir valores inválidos: 8243
cat("Registros excluidos:", n_antes - n_despues, "\n")
## Registros excluidos: 76

A partir de este punto, las variables banios y habitaciones quedan libres de valores inválidos y listas para el análisis de componentes principales.

vars_pca <- c("preciom", "areaconst", "parqueaderos", "banios", "habitaciones")

# NA por variable, después de toda la preparación
sapply(vivienda[, vars_pca], function(x) sum(is.na(x)))
##      preciom    areaconst parqueaderos       banios habitaciones 
##            0            0            0            0            0
# Total de filas con al menos un NA en estas 5 variables
sum(!complete.cases(vivienda[, vars_pca]))
## [1] 0

3 Análisis de Componentes Principales

3.1 Estandarización y cálculo del PCA

Dado que las variables seleccionadas para el Análisis de Componentes Principales (preciom, areaconst, parqueaderos, banios y habitaciones) se miden en escalas y unidades muy distintas —desde millones de pesos hasta conteos de baños o habitaciones—, es necesario estandarizarlas (centrar y escalar a varianza unitaria) antes de aplicar el PCA. De lo contrario, las variables con mayor magnitud absoluta, como preciom y areaconst, dominarían artificialmente la varianza capturada por las componentes, en desmedro de variables con rangos más pequeños como banios o habitaciones. Por esta razón, el PCA se calcula sobre las variables estandarizadas (scale.unit = TRUE).

res.pca <- PCA(vivienda[, vars_pca], scale.unit = TRUE, graph = FALSE)

3.2 Varianza explicada y selección de componentes

Antes de interpretar los resultados del PCA es necesario determinar cuántas componentes conviene retener. La Tabla 3.1 y la Figura 3.1 presentan los valores propios y el porcentaje de varianza explicada por cada componente. La primera componente concentra el 65.05% de la varianza total (valor propio de 3.25), y junto con la segunda (17.76%, valor propio de 0.89) acumulan un 82.81% de la variabilidad original de las cinco variables estandarizadas. Bajo el criterio de Kaiser (retener componentes con valor propio superior a 1), solo la primera componente lo cumple de forma estricta; sin embargo, dado que la segunda componente tiene un valor propio cercano a 1 y que entre ambas se explica una proporción alta de la varianza total, se retienen las dos primeras componentes principales para las visualizaciones e interpretaciones siguientes.

tabla_eig <- round(as.data.frame(res.pca$eig), 2)

knitr::kable(tabla_eig,
             caption = "Valores propios y varianza explicada por componente",
             col.names = c("Valor propio", "% Varianza", "% Varianza acumulada"))
Table 3.1: Valores propios y varianza explicada por componente
Valor propio % Varianza % Varianza acumulada
comp 1 3.25 65.05 65.05
comp 2 0.89 17.76 82.81
comp 3 0.35 7.03 89.84
comp 4 0.32 6.44 96.28
comp 5 0.19 3.72 100.00
fviz_eig(res.pca, addlabels = TRUE, barfill = "#034D94", barcolor = "#034D94")
Varianza explicada por cada componente principal

Figure 3.1: Varianza explicada por cada componente principal

3.3 Círculo de correlaciones y contribución de variables

Para interpretar el significado de las dos primeras componentes se examina la relación entre estas y las variables originales, mostrada en la Figura 3.2, así como la contribución de cada variable, presentada en la Figura 3.3. Las cinco variables cargan de forma positiva y con magnitudes similares sobre la primera componente (banios 0.88, areaconst 0.87, preciom 0.85, parqueaderos 0.77 y habitaciones 0.63), con contribuciones relativamente equilibradas que oscilan entre 12.25% y 23.79%, por lo que la primera componente se interpreta como un eje general de tamaño y valor de la vivienda: a mayor puntuación en esta componente, mayor precio, área construida, número de parqueaderos, baños y habitaciones de la propiedad. La segunda componente, en cambio, está dominada por habitaciones (carga de 0.73 y contribución de 59.86%), que se opone a parqueaderos (carga de -0.44, contribución de 22.27%) y, en menor medida, a preciom (carga de -0.36, contribución de 14.90%), mientras que areaconst y banios apenas contribuyen a este eje. La segunda componente se interpreta entonces como un contraste entre viviendas con más habitaciones en relación con sus parqueaderos y precio, frente a viviendas con más parqueaderos y mayor precio en relación con su número de habitaciones.

fviz_pca_var(res.pca, col.var = "contrib",
             gradient.cols = c("#034D94", "#FF7F00"),
             repel = TRUE)
Círculo de correlaciones de las variables en el plano de las dos primeras componentes

Figure 3.2: Círculo de correlaciones de las variables en el plano de las dos primeras componentes

fviz_contrib(res.pca, choice = "var", axes = 1:2)
Contribución de las variables a las dos primeras componentes principales

Figure 3.3: Contribución de las variables a las dos primeras componentes principales

3.4 Visualización de individuos según tipo de vivienda

Se visualizan los individuos en el plano de las dos primeras componentes principales, coloreados según la variable tipo, con el fin de explorar si apartamentos y casas presentan patrones diferenciados en el espacio definido por las componentes.

fviz_pca_ind(res.pca, habillage = vivienda$tipo,
             geom = "point", addEllipses = TRUE,
             palette = c("#FF7F00", "#034D94"))
Individuos del PCA coloreados según tipo de vivienda

Figure 3.4: Individuos del PCA coloreados según tipo de vivienda

La Figura 3.4 muestra una separación clara entre apartamentos y casas a lo largo de la primera componente: las casas presentan, en promedio, una puntuación de 1.17 en la primera componente, frente a -0.73 en los apartamentos, lo que es consistente con la interpretación de esta componente como un eje de tamaño y valor, e indica que las casas tienden a ser propiedades de mayor tamaño y precio que los apartamentos. En la segunda componente también se observa una diferencia, aunque menos pronunciada (0.42 en casas frente a -0.26 en apartamentos), lo que sugiere que las casas tienden a tener relativamente más habitaciones en proporción a sus parqueaderos y precio que los apartamentos. A pesar de esta separación de medias, las elipses de ambos grupos se superponen de forma considerable, lo que indica que el tipo de vivienda no constituye una frontera nítida en el espacio de las dos primeras componentes principales.

3.5 Visualización de individuos según zona

Se repite la visualización de individuos coloreando ahora por la variable zona, con el objetivo de identificar si existe algún tipo de segmentación geográfica del mercado en el espacio de las componentes principales.

fviz_pca_ind(res.pca, habillage = vivienda$zona,
             geom = "point", addEllipses = TRUE)
Individuos del PCA coloreados según zona

Figure 3.5: Individuos del PCA coloreados según zona

A diferencia de lo observado para tipo, la Figura 3.5 muestra un solapamiento considerable entre las cinco zonas, sin una segmentación geográfica clara del mercado en el espacio de las dos primeras componentes. La Zona Oeste registra la puntuación promedio más alta en la primera componente (0.64), mientras que la Zona Norte presenta la más baja (-0.40), lo que sugiere una leve tendencia de la Zona Oeste hacia propiedades de mayor tamaño y valor en comparación con la Zona Norte; las zonas Centro, Oriente y Sur se ubican en valores intermedios cercanos a cero. En la segunda componente, las zonas Centro y Oriente presentan las puntuaciones promedio más altas (0.99 y 1.20, respectivamente), lo que indica una relativa mayor proporción de habitaciones frente a parqueaderos y precio en esas zonas, mientras que la Zona Oeste muestra el valor más bajo (-0.57), consistente con viviendas con relativamente más parqueaderos y mayor precio en proporción a sus habitaciones. En conjunto, estas diferencias son moderadas y las elipses de las distintas zonas se superponen ampliamente, por lo que la zona no resulta ser un factor determinante en la estructura de componentes principales, a diferencia del tipo de vivienda.

4 Análisis de Conglomerados

4.1 Determinación del número óptimo de clusters

El Análisis de Conglomerados se realiza sobre las puntuaciones (scores) de las dos primeras componentes principales obtenidas en el análisis anterior, en lugar de sobre las cinco variables originales. Esto permite trabajar sobre un espacio de menor dimensión, no correlacionado y que ya resume el 82.81% de la variabilidad original del conjunto de datos, evitando además la redundancia que introduciría el uso directo de variables correlacionadas entre sí. Se emplea el algoritmo K-means, apropiado dado el tamaño del dataset (8243 observaciones) y el objetivo de obtener segmentos discretos y fácilmente interpretables para la toma de decisiones de negocio. Como K-means requiere definir de antemano el número de clusters (k), se utiliza el método del codo, que evalúa la suma de cuadrados dentro de los clusters (WSS) para distintos valores de k, seleccionando el punto en el que aumentar el número de grupos deja de reducir significativamente dicha dispersión.

scores_pca <- res.pca$ind$coord[, 1:2]

set.seed(123)
fviz_nbclust(scores_pca, kmeans, method = "wss") +
  labs(subtitle = "Método del codo")

El valor de k se determina observando el punto en el que la curva de WSS se aplana (el “codo” de la curva), y dicho valor se utilizará en la siguiente subsección para ajustar el modelo K-means definitivo.

4.2 K-means con k = 4

A partir del método del codo aplicado en la sección anterior, se determinó que k = 4 es el número apropiado de clusters, ya que es el punto donde la curva de suma de cuadrados dentro de los clusters (WSS) pasa de una caída pronunciada a un comportamiento notablemente más plano. Se ajusta entonces el modelo K-means definitivo con cuatro centros, utilizando múltiples inicializaciones aleatorias (nstart = 25) para evitar quedar en una solución subóptima, dado que el resultado de K-means depende de la ubicación inicial de los centroides.

set.seed(123)
km_res <- kmeans(scores_pca, centers = 4, nstart = 25)

fviz_cluster(km_res, data = scores_pca,
             palette = c("#034D94", "#FF7F00", "#00AFBB", "#E7B800"),
             geom = "point", ellipse.type = "convex",
             ggtheme = theme_minimal())

4.3 Perfil de los clusters según variables originales

Para caracterizar los cuatro segmentos identificados por el modelo K-means, se calcula el perfil promedio de las variables originales (preciom, areaconst, parqueaderos, banios y habitaciones) en cada cluster, junto con el número de viviendas que agrupa cada uno.

vivienda_clusters <- vivienda %>%
  mutate(cluster = factor(km_res$cluster))

perfil_clusters <- vivienda_clusters %>%
  group_by(cluster) %>%
  summarise(
    n = n(),
    across(all_of(vars_pca), mean)
  )

knitr::kable(perfil_clusters, digits = 2,
             caption = "Perfil promedio de las viviendas según cluster",
             col.names = c("Cluster", "N° viviendas", "Precio (millones)",
                           "Área construida (m²)", "Parqueaderos",
                           "Baños", "Habitaciones"))
Table 4.1: Perfil promedio de las viviendas según cluster
Cluster N° viviendas Precio (millones) Área construida (m²) Parqueaderos Baños Habitaciones
1 742 471.89 318.39 1.57 4.75 6.87
2 2461 524.71 193.74 1.99 3.67 3.65
3 855 1145.43 416.97 3.88 5.18 4.39
4 4185 227.15 87.22 1.13 2.11 2.89

La Tabla 4.1 permite caracterizar los cuatro segmentos identificados por K-means en términos concretos del negocio inmobiliario. El Cluster 4 es el segmento más numeroso (4185 viviendas, el 50.8% del total) y corresponde a las propiedades más económicas y compactas del mercado, con un precio promedio de 227.15 millones de pesos, 87.22 m² construidos y los valores más bajos en parqueaderos, baños y habitaciones; representa la entrada del mercado de vivienda en Cali. El Cluster 1 agrupa 742 viviendas de tamaño considerable (318.39 m²) y un número notablemente alto de habitaciones (6.87 en promedio) y baños (4.75), pero con relativamente pocos parqueaderos (1.57) y el menor precio por metro cuadrado del grupo (≈1.48 millones de pesos/m²); este perfil sugiere viviendas familiares amplias, probablemente casas grandes orientadas a espacio habitable más que a comodidades como parqueaderos. El Cluster 2, el segundo más numeroso (2461 viviendas), presenta un tamaño moderado (193.74 m²) pero un precio promedio relativamente alto (524.71 millones), resultando en el precio por metro cuadrado más alto de los cuatro grupos (≈2.71 millones de pesos/m²), lo que sugiere propiedades de gama media-alta ubicadas en sectores valorizados. Finalmente, el Cluster 3 es el segmento premium del mercado: aunque el menos numeroso junto al Cluster 1 (855 viviendas), presenta los valores máximos en precio (1145.43 millones), área construida (416.97 m²), parqueaderos (3.88) y baños (5.18), consolidándose como el grupo de propiedades de mayor valor y mejor dotación de comodidades, aunque con un número de habitaciones (4.39) inferior al del Cluster 1, lo que refuerza la interpretación de la segunda componente principal como un contraste entre habitaciones y parqueaderos/precio.

5 Análisis de Correspondencia

5.1 Tratamiento de la variable barrio

Previo a la construcción del Análisis de Correspondencia, se revisaron las tres variables categóricas de interés (tipo, zona, barrio). Las variables tipo y zona no presentan valores faltantes ni problemas de calidad tras la depuración realizada en la sección 2, por lo que se incorporan directamente al análisis. La variable barrio, en cambio, presenta dos dificultades: en primer lugar, se identificaron categorías duplicadas por inconsistencias de capitalización; por ejemplo, “la flora” y “La Flora” registradas como barrios distintos, las cuales se unificaron normalizando el texto a minúsculas, reduciendo el número de categorías únicas de 433 a 404. En segundo lugar, barrio presenta una distribución altamente fragmentada: de los 404 barrios identificados, la mayoría concentra muy pocas observaciones, mientras que un número reducido de barrios (encabezados por valle del lili, ciudad jardín y pance) agrupa la mayor parte de la oferta. Dado que un número tan alto de categorías haría inmanejable e ilegible la representación gráfica del MCA, y que las categorías con muy pocas observaciones aportarían escasa robustez estadística al análisis, se optó por conservar como categorías individuales únicamente los barrios con al menos 50 viviendas (37 barrios), agrupando el resto en una categoría “Otros”. Esta categoría concentra 2732 viviendas (33.1% del total), lo cual constituye en sí mismo un hallazgo relevante: evidencia una alta dispersión geográfica de la oferta de vivienda en Cali, con la demanda repartida entre un gran número de barrios pequeños, más allá de los pocos sectores de mayor concentración. Se advierte que la categoría “Otros” agrupa barrios heterogéneos entre sí, por lo que su interpretación en los resultados del MCA debe entenderse como “el resto disperso del mercado” y no como un sector geográfico homogéneo.

# 1. Cantidad de categorías únicas en cada variable categórica de interés
sapply(vivienda[, c("tipo", "zona", "barrio")], function(x) length(unique(x)))

# 2. Verificación de NA residuales en estas mismas variables
sapply(vivienda[, c("tipo", "zona", "barrio")], function(x) sum(is.na(x)))

# 3. Distribución de frecuencias de barrio, para evaluar si hay categorías
# con muy pocas observaciones
sort(table(vivienda$barrio), decreasing = TRUE)
vivienda <- vivienda %>%
  mutate(barrio = str_to_lower(barrio) %>% str_trim())

# Volvemos a revisar la cantidad de categorías después de normalizar
length(unique(vivienda$barrio))
sort(table(vivienda$barrio), decreasing = TRUE)[1:20]
umbral_barrio <- 50

frecuencias_barrio <- table(vivienda$barrio)
barrios_frecuentes <- names(frecuencias_barrio[frecuencias_barrio >= umbral_barrio])

vivienda <- vivienda %>%
  mutate(barrio_agrupado = if_else(barrio %in% barrios_frecuentes, barrio, "Otros"))

length(barrios_frecuentes)
## [1] 37
table(vivienda$barrio_agrupado) %>% sort(decreasing = TRUE)
## 
##                 Otros        valle del lili         ciudad jardín 
##                  2732                  1009                   515 
##                 pance              la flora        santa teresita 
##                   409                   363                   262 
##              el caney            el ingenio           la hacienda 
##                   207                   202                   165 
##             normandía         los cristales                 acopi 
##                   153                   151                   148 
##            el limonar      prados del norte            el refugio 
##                   134                   127                   119 
##             aguacatal           ciudad 2000                 caney 
##                   109                    96                    87 
##             cristales urbanización la flora         brisas de los 
##                    83                    83                    82 
##              zona sur      nueva tequendama        quintas de don 
##                    74                    73                    73 
##             versalles          santa isabel   parcelaciones pance 
##                    70                    64                    61 
##              el peñon               el lido    torres de comfandi 
##                    60                    59                    57 
##                 capri          san fernando             juanamb√∫ 
##                    56                    54                    52 
##          santa monica              melendez       villa del prado 
##                    52                    51                    51 
##             el bosque           santa anita 
##                    50                    50

5.2 Cálculo del MCA

Con las variables categóricas depuradas en la sección anterior (tipo, zona y barrio_agrupado), se construye el conjunto de datos para el Análisis de Correspondencia Múltiple (MCA) y se ajusta el modelo.

vivienda_mca <- vivienda %>%
  select(tipo, zona, barrio_agrupado) %>%
  mutate(across(everything(), as.factor))

res.mca <- MCA(vivienda_mca, graph = FALSE)
summary(res.mca)
## 
## Call:
## MCA(X = vivienda_mca, graph = FALSE) 
## 
## 
## Eigenvalues
##                       Dim.1  Dim.2  Dim.3  Dim.4  Dim.5
## Variance              0.643  0.599  0.495  0.375  0.342
## % of var.             4.594  4.276  3.538  2.678  2.440
## Cumulative % of var.  4.594  8.870 12.409 15.087 17.527
## 
## Individuals (the 10 first)
##                  Dim.1    ctr   cos2    Dim.2    ctr   cos2    Dim.3    ctr
## 1             | -0.620  0.007  0.043 |  0.337  0.002  0.013 | -2.138  0.112
## 2             | -0.620  0.007  0.043 |  0.337  0.002  0.013 | -2.138  0.112
## 3             | -0.620  0.007  0.043 |  0.337  0.002  0.013 | -2.138  0.112
## 4             | -0.528  0.005  0.192 | -0.115  0.000  0.009 | -0.638  0.010
## 5             |  0.090  0.000  0.000 |  1.605  0.052  0.132 |  0.742  0.013
## 6             |  0.090  0.000  0.000 |  1.605  0.052  0.132 |  0.742  0.013
## 7             |  0.090  0.000  0.000 |  1.605  0.052  0.132 |  0.742  0.013
## 8             |  0.090  0.000  0.000 |  1.605  0.052  0.132 |  0.742  0.013
## 9             | -0.309  0.002  0.005 |  1.651  0.055  0.137 |  0.233  0.001
## 10            | -0.309  0.002  0.005 |  1.651  0.055  0.137 |  0.233  0.001
##                 cos2  
## 1              0.517 |
## 2              0.517 |
## 3              0.517 |
## 4              0.279 |
## 5              0.028 |
## 6              0.028 |
## 7              0.028 |
## 8              0.028 |
## 9              0.003 |
## 10             0.003 |
## 
## Categories (the 10 first)
##                   Dim.1     ctr    cos2  v.test     Dim.2     ctr    cos2
## Apartamento   |   0.370   4.360   0.219  42.470 |  -0.041   0.059   0.003
## Casa          |  -0.592   6.982   0.219 -42.470 |   0.066   0.094   0.003
## Zona Centro   |  -0.655   0.326   0.006  -7.254 |   0.457   0.170   0.003
## Zona Norte    |  -0.061   0.045   0.001  -3.039 |   1.677  35.853   0.835
## Zona Oeste    |   2.204  36.300   0.818  82.127 |  -0.355   1.014   0.021
## Zona Oriente  |  -0.686   1.019   0.021 -13.007 |   0.423   0.418   0.008
## Zona Sur      |  -0.465   6.399   0.287 -48.663 |  -0.626  12.460   0.521
## acopi         |  -0.091   0.008   0.000  -1.113 |   2.090   4.365   0.080
## aguacatal     |   2.639   4.772   0.093  27.731 |  -0.488   0.175   0.003
## brisas de los |   0.054   0.002   0.000   0.492 |   2.091   2.421   0.044
##                v.test     Dim.3     ctr    cos2  v.test  
## Apartamento    -4.765 |   0.414   7.094   0.274  47.540 |
## Casa            4.765 |  -0.663  11.358   0.274 -47.540 |
## Zona Centro     5.060 |  -2.706   7.230   0.109 -29.980 |
## Zona Norte     82.970 |   0.393   2.382   0.046  19.455 |
## Zona Oeste    -13.241 |  -0.535   2.771   0.048 -19.915 |
## Zona Oriente    8.034 |  -2.907  23.796   0.369 -55.154 |
## Zona Sur      -65.515 |   0.260   2.597   0.090  27.209 |
## acopi          25.652 |   0.761   0.699   0.011   9.336 |
## aguacatal      -5.129 |  -0.457   0.186   0.003  -4.801 |
## brisas de los  19.025 |   1.066   0.761   0.011   9.701 |
## 
## Categorical variables (eta2)
##                   Dim.1 Dim.2 Dim.3  
## tipo            | 0.219 0.003 0.274 |
## zona            | 0.851 0.896 0.576 |
## barrio_agrupado | 0.860 0.897 0.636 |

5.3 Mapa de categorías: relación entre zona y barrio

Se examina la relación entre las categorías de zona y barrio_agrupado en el plano de las dos primeras dimensiones del MCA, con el fin de verificar si la variable barrio_agrupado reproduce, a un nivel más detallado, la segmentación geográfica capturada por zona. Para ello se filtra el mapa de categorías general, excluyendo las categorías de tipo.

fviz_mca_var(res.mca, axes = c(1,2),
             col.var = "contrib",
             gradient.cols = c("#FF7F00", "#034D94"),
             repel = TRUE,
             max.overlaps = 30,
             select.var = list(contrib = 15))
## Warning in (function (mapping = NULL, data = NULL, stat = "identity", position
## = "identity", : Ignoring unknown parameters: `max.overlaps`

La Figura ?? evidencia una fuerte concordancia geográfica entre zona y barrio_agrupado. Cabe señalar que el filtro aplicado selecciona las 15 categorías con mayor contribución conjunta a las Dimensiones 1 y 2, sin excluir explícitamente las categorías de tipo; sin embargo, dado el bajo eta2 de esta variable en el plano (0.219 y 0.003 en las Dimensiones 1 y 2, respectivamente), Apartamento y Casa aparecen con contribuciones marginales (entre 5% y 8%) y ubicadas cerca del centro del plano, sin asociarse a ningún extremo geográfico. La Zona Oeste se ubica de forma aislada en el extremo positivo de la Dimensión 1 (2.204), siendo además la categoría de zona con mayor contribución a esta dimensión (36.30%) y la mejor representada (cos2 = 0.818); en esa misma región del plano se agrupan barrios como santa teresita, normandía, los cristales, aguacatal, cristales y el peñón, todos con coordenadas en la Dimensión 1 cercanas a 2.6-2.7, confirmando que corresponden efectivamente al sector occidental de la ciudad. La Zona Norte, por su parte, domina la Dimensión 2 (contribución de 35.85%, cos2 = 0.835) y coincide en esa dimensión con barrios como acopi, prados del norte, villa del prado, el bosque, torres de comfandi y santa mónica (coordenadas en Dimensión 2 entre 2.0 y 2.2). La Zona Sur se posiciona en el cuadrante negativo de ambas dimensiones (-0.465, -0.626) junto a barrios reconocibles del sur de Cali como valle del lili, ciudad jardín, pance y el caney. En contraste, la Zona Centro exhibe las contribuciones y valores de cos2 más bajos entre las cinco zonas (0.33% y 0.17% en las Dimensiones 1 y 2, respectivamente), lo que indica que está pobremente representada en el plano y no se asocia de forma clara con un grupo específico de barrios en estas dos primeras dimensiones. La categoría Otros, como es esperable dada su naturaleza heterogénea, se ubica cerca del origen del plano (-0.214, 0.293), sin una asociación geográfica definida.

5.4 Mapa de categorías: patrón de tipo de vivienda

Se repite el ejercicio de proyección para la variable tipo, con el objetivo de determinar si el tipo de vivienda (apartamento o casa) sigue un patrón de segmentación similar al observado para zona y barrio_agrupado en las dos primeras dimensiones del MCA.

cats_tipo <- c("Apartamento", "Casa")

fviz_mca_var(res.mca, axes = c(1,3),
             select.var = list(name = cats_tipo),
             repel = TRUE, col.var = "contrib",
             gradient.cols = c("#034D94", "#FF7F00"))
Mapa de categorías de tipo de vivienda en el plano de las dos primeras dimensiones del MCA

Figure 5.1: Mapa de categorías de tipo de vivienda en el plano de las dos primeras dimensiones del MCA

A diferencia de zona y barrio_agrupado, la Figura 5.1 muestra que las categorías Apartamento (0.370, -0.041) y Casa (-0.592, 0.066) se ubican relativamente cerca del origen del plano formado por las dos primeras dimensiones, con niveles de cos2 modestos (0.219 en ambos casos para la Dimensión 1, y apenas 0.003 para la Dimensión 2). Esto es consistente con la tabla de eta2 por variable categórica, en la que tipo explica solo el 21.9% y el 0.3% de las Dimensiones 1 y 2, respectivamente, muy por debajo del 85.1%/89.6% y 86.0%/89.7% que explican zona y barrio_agrupado en esas mismas dimensiones. Esto indica que las dos primeras dimensiones del MCA están dominadas casi en su totalidad por la estructura geográfica del mercado (zona y barrio), mientras que el tipo de vivienda opera como un patrón de segmentación distinto y en buena medida independiente de la ubicación: de hecho, tipo alcanza su mayor asociación con la Dimensión 3 (eta2 = 0.274), no representada en este mapa, lo que sugiere que la diferenciación entre apartamentos y casas se explica mejor por un eje adicional del MCA que por la segmentación geográfica capturada en el plano principal.

6 Conclusiones y recomendaciones estratégicas

6.1 Conclusiones clave

El análisis multivariado del mercado de vivienda urbana en Cali, realizado sobre 8243 registros depurados de la plataforma OLX, permite identificar tres hallazgos estructurales que caracterizan la dinámica de oferta en la ciudad:

1. El mercado se organiza principalmente por tamaño y valor de la propiedad, no por ubicación geográfica. El Análisis de Componentes Principales mostró que un único eje —que combina precio, área construida, parqueaderos y baños— concentra el 65% de la variabilidad del mercado, mientras que la zona de la ciudad explica una proporción marginal de esa misma variabilidad (las cinco zonas se solapan ampliamente en el plano de componentes). Este hallazgo se confirma de forma independiente en el Análisis de Correspondencia Múltiple, donde el tipo de vivienda resultó prácticamente no relacionado con la segmentación geográfica capturada por zona y barrio (eta2 de apenas 0.22 frente a valores superiores a 0.85 para zona y barrio). En conjunto, esto indica que el atributo más relevante para diferenciar el valor de una propiedad no es su ubicación, sino sus características físicas (tamaño, dotación de parqueaderos y baños).

2. Existe un segundo patrón relacionado con la proporción entre habitaciones y comodidades. Independientemente del tamaño general de la vivienda, se identificó un eje adicional que contrasta propiedades con mayor número de habitaciones frente a propiedades con mayor número de parqueaderos y precio relativo. Las casas tienden a inclinarse hacia el primer perfil (más habitaciones en proporción a su tamaño), mientras que las propiedades de mayor precio por metro cuadrado se inclinan hacia el segundo.

3. El mercado se segmenta naturalmente en cuatro perfiles de vivienda claramente diferenciables. El Análisis de Conglomerados identificó cuatro grupos con perfiles de negocio distintos: un segmento masivo de entrada (50.8% del mercado, viviendas compactas y económicas), un segmento familiar orientado a espacio habitable (9.0%, casas grandes con muchas habitaciones pero pocos parqueaderos), un segmento de gama media-alta con alta valorización por metro cuadrado (29.9%), y un segmento premium de propiedades de lujo (10.4%, con los valores máximos en precio, área, parqueaderos y baños).

4. La oferta está altamente fragmentada geográficamente. El 33.1% de las viviendas del dataset se distribuye en más de 360 barrios distintos, cada uno con menos de 50 propiedades registradas, evidenciando que ningún sector domina de forma individual el mercado, salvo un puñado de barrios líderes como Valle del Lili, Ciudad Jardín y Pance, y una asociación clara entre ciertos barrios y zonas específicas (por ejemplo, Zona Oeste con barrios como Santa Teresita y Normandía).

6.2 Recomendaciones estratégicas

Para la definición de precios y valoración de propiedades: Se recomienda que los modelos internos de valoración de la empresa prioricen como variables principales el área construida, el número de parqueaderos y baños, y el tipo de vivienda, por encima de la zona geográfica como criterio diferenciador de precio. Esto no significa ignorar la ubicación, sino reconocer que, dentro de esta base de datos, propiedades de características físicas similares tienden a tener precios comparables independientemente de en cuál de las cinco zonas se ubiquen.

Para la segmentación de portafolio y estrategia comercial: Se recomienda alinear la oferta y las campañas comerciales de la empresa con los cuatro segmentos identificados: (i) desarrollar y promocionar activamente el segmento de entrada, dado que concentra más de la mitad del mercado y probablemente el mayor volumen de transacciones; (ii) dirigir una oferta diferenciada de vivienda familiar (más habitaciones, menos parqueaderos) hacia compradores con necesidades de espacio antes que de comodidades vehiculares; (iii) explotar el segmento premium con una estrategia de marketing especializada, dado que representa una porción menor del volumen pero previsiblemente el mayor margen por transacción.

Para la expansión geográfica y prospección de nuevos inventarios: Dada la alta fragmentación de la oferta en barrios pequeños, se recomienda que la empresa priorice la consolidación de inventario en los barrios de mayor concentración ya identificados (Valle del Lili, Ciudad Jardín, Pance, y los barrios asociados a la Zona Oeste), donde existe mayor liquidez de mercado y comparabilidad de precios, antes que dispersar esfuerzos comerciales de forma homogénea en la totalidad de los cientos de barrios de la ciudad.

Para futuras iteraciones del análisis: Se recomienda ampliar este análisis incorporando variables adicionales no disponibles en el presente dataset (año de construcción, tiempo en el mercado, estado de la propiedad) que podrían explicar la variabilidad residual no capturada por los componentes principales actuales (cerca del 17% de la varianza no explicada por las dos primeras componentes), así como actualizar periódicamente el análisis de conglomerados para monitorear si la segmentación del mercado se mantiene estable o evoluciona con el tiempo, dado el carácter dinámico y competitivo del sector inmobiliario.