Este informe analiza la oferta inmobiliaria urbana a partir de una base de datos obtenida mediante web scraping en la plataforma OLX. El objetivo es aplicar técnicas de análisis multivariado, reducción de dimensionalidad, segmentación y análisis de correspondencias, que permitan traducir el comportamiento de los datos en información útil para la toma de decisiones comerciales, como: qué variables explican el precio de las propiedades, cómo se agrupan naturalmente las viviendas ofertadas y qué relación existe entre su tipología, ubicación y estrato socioeconómico.
| Variable | Tipo de Dato | Definición del Campo | Uso Analítico en los Modelos |
|---|---|---|---|
| preciom | Numérico Continuo | Precio comercial del inmueble (expresado en millones). | Variable clave de valor e inversión para PCA y Clustering. |
| areaconst | Numérico Continuo | Área total construida de la vivienda en metros cuadrados (\(m^2\)). | Dimensión física fundamental para PCA y Clustering. |
| parqueaderos | Numérico Discreto | Cantidad de espacios de estacionamiento privados del inmueble. | Define la capacidad y comodidades para PCA y Clustering. |
| banios | Numérico Discreto | Número de baños completos y medios disponibles. | Define el tamaño y capacidad operativa (PCA y Clustering). |
| habitaciones | Numérico Discreto | Cantidad de cuartos, alcobas o dormitorios. | Define la capacidad de habitabilidad (PCA y Clustering). |
| zona | Categórico | Sector o zona cardinal de la ciudad donde se ubica (ej. Norte, Sur). | Segmentación macro del mercado urbano. Objetivo del ACM. |
| tipo | Categórico | Clasificación arquitectónica de la propiedad (ej. Casa, Apartamento). | Segmentación de la tipología constructiva. Objetivo del ACM. |
| barrio | Categórico | Nombre específico del barrio o vecindario de ubicación. | Segmentación micro y geográfica detallada. Objetivo del ACM. |
| estrato | Categórico (Ordinal) | Nivel socioeconómico oficial asignado al predio (generalmente de 1 a 6). | Segmentación de poder adquisitivo y entorno. Objetivo del ACM. |
| piso | Categórico | Nivel o planta del edificio en el que se encuentra el inmueble. | Caracterización de accesibilidad y altura. Objetivo del ACM. |
| latitud | Coordenadas | Posición geográfica exacta en el eje Norte-Sur. | Fundamental para la georreferenciación y el mapa interactivo. |
| longitud | Coordenadas | Posición geográfica exacta en el eje Este-Oeste. | Fundamental para la georreferenciación y el mapa interactivo. |
Los métodos basados en distancias, como el PCA o el k-means, son sensibles a la escala de las variables, si una variable se mide en unidades mucho mayores que las demás (por ejemplo, el precio en millones frente al número de baños), termina dominando el cálculo de la varianza o la distancia euclidiana, aunque no sea necesariamente más relevante desde el punto de vista sustantivo [6][7]. Por esa razón, antes de correr los modelos multivariados es necesario llevar las variables cuantitativas a una escala comparable. Existen varias formas de hacerlo:
Estandarización (Z-score): centra la variable en media 0 y varianza 1. \[X'=\frac{X-\mu}{\sigma}\]
Normalización (Min-Max): reescala los valores al intervalo [0, 1]. \[X'=\frac{X-\min}{\max-\min}\]
Escalado Robusto: usa la mediana y el rango intercuartílico (IQR) en lugar de la media y la desviación estándar, lo que lo hace menos sensible a valores atípicos [8]. \[X'=\frac{X-\text{mediana}}{\text{IQR}}\]
En este trabajo se optó por la estandarización clásica (Z-score), dado que tanto el PCA como el k-means asumen, o al menos funcionan mejor, bajo esta transformación.
El PCA resume un conjunto de variables correlacionadas en un número menor de componentes no correlacionados, construidos como combinaciones lineales de las variables originales [2]:
\[PC_i = a_{i1}X_1 + a_{i2}X_2 + \dots + a_{ip}X_p\]
Dos elementos son clave para interpretar los resultados. Las cargas (loadings) indican el peso (\(a_{ij}\)) de cada variable original dentro de cada componente, y su signo y magnitud permiten leer qué variables definen cada eje. La calidad de representación (\(\cos^2\)), por su parte, mide qué tan bien queda proyectado un individuo o una variable en el plano factorial, valores cercanos a 1 indican una representación fiel, mientras que valores bajos sugieren que se necesitarían más dimensiones para capturar bien esa observación [1][2].
El clustering agrupa observaciones de modo que los elementos dentro de un mismo grupo sean más parecidos entre sí que con los de otros grupos [4]. Existen dos grandes familias de métodos:
En este informe se trabajó con K-means, ya que el tamaño de la base (más de 8.000 registros) hace poco práctico construir y visualizar un dendrograma jerárquico completo.
Mientras el PCA trabaja con variables cuantitativas, el ACM es su equivalente para variables categóricas, a partir de las tablas de contingencia entre ellas, construye un espacio factorial de baja dimensión en el que las categorías que tienden a presentarse juntas quedan próximas entre sí [1][2]. En este trabajo se usó para explorar la relación entre el tipo de vivienda, la zona y el estrato socioeconómico.
Antes de ejecutar los modelos multivariados, se realizó un preprocesamiento e imputación de los datos de la base de datos de vivienda. Este procedimiento abarcó la imputación de los valores faltantes o NA, el filtrado de variables no explicativas y, finalmente, la partición de las matrices de trabajo según su naturaleza cuantitativa o cualitativa.
Diagnóstico Inicial de Valores Nulos (NA):
tabla_nulos <- data.frame(
Variable = names(colSums(is.na(vivienda))),
Valores_Nulos = colSums(is.na(vivienda)),
Porcentaje_Faltante = paste0(round((colSums(is.na(vivienda)) / nrow(vivienda)) * 100, 2), "%")
) %>% filter(Valores_Nulos > 0) %>% arrange(desc(Valores_Nulos))
knitr::kable(tabla_nulos, caption = "Cuantificación de Datos Faltantes (NA) por Variable", row.names = FALSE)| Variable | Valores_Nulos | Porcentaje_Faltante |
|---|---|---|
| piso | 2638 | 31.7% |
| parqueaderos | 1605 | 19.29% |
| id | 3 | 0.04% |
| zona | 3 | 0.04% |
| estrato | 3 | 0.04% |
| areaconst | 3 | 0.04% |
| banios | 3 | 0.04% |
| habitaciones | 3 | 0.04% |
| tipo | 3 | 0.04% |
| barrio | 3 | 0.04% |
| longitud | 3 | 0.04% |
| latitud | 3 | 0.04% |
| preciom | 2 | 0.02% |
La tabla anterior muestra que varias variables tienen datos
faltantes, en particular piso. Como tanto el PCA como el
K-means requieren matrices completas, había dos alternativas: eliminar
las filas incompletas o imputarlas. Se descartó la primera, porque al
tratarse de un porcentaje considerable de registros, eliminarlos habría
reducido la muestra y probablemente introducido sesgo (las propiedades
sin dato de piso bien podrían corresponder a un tipo
particular de inmueble, como las casas, que no tienen ese campo).
En su lugar se aplicó Imputación Múltiple por Ecuaciones Encadenadas (MICE) con el método Predictive Mean Matching (PMM), que imputa cada valor faltante tomando un dato real observado en un vecino similar según el resto de variables, en lugar de generar un valor sintético [9]. El procedimiento siguió estos pasos:
piso a numérico.id, que no
aporta información y podría introducir ruido al modelo de
imputación.vivienda_clean.vivienda_pre <- vivienda %>%
mutate(zona = as.factor(zona), tipo = as.factor(tipo), barrio = as.factor(barrio), piso = as.numeric(piso)) %>%
select(-id)
set.seed(123)
imputacion_mice <- mice(vivienda_pre, m = 1, maxit = 1, method = "pmm", printFlag = FALSE)
vivienda_clean <- complete(imputacion_mice)Validación de la Imputación:
resumen_imputacion <- data.frame(
Métrica = c("Valores nulos restantes", "Filas eliminadas", "Valores reemplazados por PMM"),
Cantidad = c(sum(is.na(vivienda_clean)), nrow(vivienda) - nrow(vivienda_clean), sum(is.na(vivienda)))
)
knitr::kable(resumen_imputacion, caption = "Resultados Finales del Proceso de Imputación (MICE)", row.names = FALSE)| Métrica | Cantidad |
|---|---|
| Valores nulos restantes | 0 |
| Filas eliminadas | 0 |
| Valores reemplazados por PMM | 4275 |
knitr::kable(head(vivienda_clean, 5), caption = "Top 5 registros de la base de datos limpia (sin nulos)", row.names = FALSE)| zona | piso | estrato | preciom | areaconst | parqueaderos | banios | habitaciones | tipo | barrio | longitud | latitud |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Zona Oriente | 4 | 3 | 250 | 70 | 1 | 3 | 6 | Casa | 20 de julio | -76.51168 | 3.43382 |
| Zona Oriente | 1 | 3 | 320 | 120 | 1 | 2 | 3 | Casa | 20 de julio | -76.51237 | 3.43369 |
| Zona Oriente | 3 | 3 | 350 | 220 | 2 | 2 | 4 | Casa | 20 de julio | -76.51537 | 3.43566 |
| Zona Sur | 2 | 4 | 400 | 280 | 3 | 5 | 3 | Casa | 3 de julio | -76.54000 | 3.43500 |
| Zona Norte | 1 | 5 | 260 | 90 | 1 | 2 | 3 | Apartamento | acopi | -76.51350 | 3.45891 |
Tras la imputación no quedan valores nulos en el dataset de vivienda y, no se eliminó ninguna fila; el tamaño original de la muestra se conserva igual.
Puesto que el PCA opera sobre variables cuantitativas y el ACM sobre
variables categóricas, la base vivienda_clean se dividió en
dos subconjuntos: uno cuantitativo (vivienda_cuanti), con
las variables numéricas asociadas al precio y a las características
físicas de la vivienda, y uno cualitativo (vivienda_cuali),
con las variables de tipo, zona, barrio y estrato. El estrato, aunque
numérico en la base original, se recodificó como factor porque en la
práctica funciona como una categoría de nivel socioeconómico y no como
una cantidad continua.
vivienda_cuanti <- vivienda_clean %>% select(preciom, areaconst, parqueaderos, banios, habitaciones)
vivienda_cuali <- vivienda_clean %>% select(tipo, zona, barrio, estrato) %>% mutate(estrato = as.factor(estrato))
knitr::kable(head(vivienda_cuanti, 5), caption = "Top 5 registros del Dataset Cuantitativo", row.names = FALSE)| preciom | areaconst | parqueaderos | banios | habitaciones |
|---|---|---|---|---|
| 250 | 70 | 1 | 3 | 6 |
| 320 | 120 | 1 | 2 | 3 |
| 350 | 220 | 2 | 2 | 4 |
| 400 | 280 | 3 | 5 | 3 |
| 260 | 90 | 1 | 2 | 3 |
knitr::kable(head(vivienda_cuali, 5), caption = "Top 5 registros del Dataset Cualitativo", row.names = FALSE)| tipo | zona | barrio | estrato |
|---|---|---|---|
| Casa | Zona Oriente | 20 de julio | 3 |
| Casa | Zona Oriente | 20 de julio | 3 |
| Casa | Zona Oriente | 20 de julio | 3 |
| Casa | Zona Sur | 3 de julio | 4 |
| Apartamento | Zona Norte | acopi | 5 |
El PCA se aplicó únicamente sobre vivienda_cuanti, ya
que solo admite variables numéricas.
set.seed(123)
muestra_shapiro <- vivienda_cuanti %>% sample_n(min(5000, n()))
p_valores_shapiro <- apply(muestra_shapiro, 2, function(x) shapiro.test(x)$p.value)
tabla_shapiro <- data.frame(
Variable = names(p_valores_shapiro),
p_valor = formatC(p_valores_shapiro, format = "e", digits = 3)
)
knitr::kable(tabla_shapiro, caption = "Prueba de Normalidad (Shapiro-Wilk)", row.names = FALSE)| Variable | p_valor |
|---|---|
| preciom | 6.078e-60 |
| areaconst | 2.189e-65 |
| parqueaderos | 5.247e-70 |
| banios | 1.371e-48 |
| habitaciones | 1.054e-60 |
cor_matrix_spearman <- cor(vivienda_cuanti, method = "spearman")
knitr::kable(round(cor_matrix_spearman, 2), caption = "Matriz de Correlación de Spearman", row.names = FALSE)| preciom | areaconst | parqueaderos | banios | habitaciones |
|---|---|---|---|---|
| 1.00 | 0.82 | 0.73 | 0.77 | 0.43 |
| 0.82 | 1.00 | 0.64 | 0.77 | 0.65 |
| 0.73 | 0.64 | 1.00 | 0.61 | 0.34 |
| 0.77 | 0.77 | 0.61 | 1.00 | 0.62 |
| 0.43 | 0.65 | 0.34 | 0.62 | 1.00 |
La prueba de Shapiro-Wilk rechaza la normalidad en las cinco variables (p < 0.001 en todos los casos), lo cual no es atípico tratándose de variables de precio y área, que suelen estar sesgadas hacia la derecha por la presencia de propiedades de alto valor. Por esta razon, es mas conveniente trabajar con la correlación de Spearman, que no asume normalidad y capta relaciones monótonas, no necesariamente lineales. La matriz resultante muestra correlaciones positivas y fuertes entre el área construida, el precio y el número de baños, lo que sugiere que estas tres variables tienden a moverse juntas dentro del mercado.
res.pca <- PCA(vivienda_cuanti, scale.unit = TRUE, graph = FALSE)
# 1. Extraer la tabla matemática de las varianzas
tabla_varianza <- as.data.frame(res.pca$eig)
names(tabla_varianza) <- c("Eigenvalor", "% de Varianza", "% Varianza Acumulada")
# 2. Guardar el valor exacto de la segunda dimensión en una variable
var_acumulada_dim2 <- round(tabla_varianza[2, 3], 1)
# 3. Mostrar la tabla elegante en el documento
knitr::kable(round(tabla_varianza, 2), caption = "Tabla de Varianza Explicada (Eigenvalores)", row.names = TRUE)| Eigenvalor | % de Varianza | % Varianza Acumulada | |
|---|---|---|---|
| comp 1 | 3.23 | 64.54 | 64.54 |
| comp 2 | 0.87 | 17.43 | 81.96 |
| comp 3 | 0.37 | 7.36 | 89.32 |
| comp 4 | 0.34 | 6.83 | 96.15 |
| comp 5 | 0.19 | 3.85 | 100.00 |
# 4. Generar los gráficos espaciales
fviz_eig(res.pca, addlabels = TRUE, ylim = c(0, 100),
title = "Varianza Explicada por Componente")fviz_pca_var(res.pca, col.var = "cos2",
gradient.cols = c("#00AFBB", "#E7B800", "#FC4E07"),
repel = TRUE, title = "Círculo de Correlaciones - Calidad de Representación")Las dos primeras dimensiones concentran el 82% de la varianza total,
por lo que el plano factorial que forman resulta representativo del
comportamiento conjunto de las variables. En el círculo de
correlaciones, areaconst, preciom y
banios presentan valores altos de \(\cos^2\), es decir, están bien
representadas en este plano y contribuyen de forma importante a definir
el primer componente. En términos prácticos, esto respalda una lectura
razonable del mercado; el valor de una propiedad parece depender sobre
todo de su tamaño y de su capacidad, número de baños y habitaciones, más
que de atributos secundarios como el número de parqueaderos.
Para el clustering se usó vivienda_cuanti estandarizada
(vivienda_scaled), de modo que la distancia euclidiana
entre observaciones no quede dominada por la variable de mayor escala,
en este caso el precio.
Para determinar el número óptimo de grupos (\(K\)), se evaluó la métrica WSS (Within-Cluster Sum of Squares), la cual cuantifica la cohesión interna de los clústeres calculando la suma de las distancias al cuadrado entre cada observación y el centroide de su respectivo grupo. El método del codo (popularizado por Thorndike, 1953) consiste en graficar el WSS para distintos valores de \(K\) e identificar el punto de inflexión (“codo”) donde la reducción marginal de la varianza intra-clúster deja de ser significativa [11].
Para no depender de un solo criterio empírico, este método se
complementó con el cálculo del coeficiente de silueta promedio, y ambos
gráficos de validación se muestran lado a lado empleando la librería
gridExtra [10].
vivienda_scaled <- scale(vivienda_cuanti)
# Validación de K con dos criterios: WSS y silueta
p1 <- fviz_nbclust(vivienda_scaled, kmeans, method = "wss") +
labs(title = "Método del Codo (WSS)")
# Cálculo de silueta usando solo una muestra de 1000 datos para no colapsar la RAM
set.seed(123)
muestra_silueta <- vivienda_scaled[sample(nrow(vivienda_scaled), 1000), ]
p2 <- fviz_nbclust(muestra_silueta, kmeans, method = "silhouette") +
labs(title = "Coeficiente de Silueta")
gridExtra::grid.arrange(p1, p2, ncol = 2)# Ejecución de K-Means con K = 3 sobre TODA la base de datos
set.seed(123)
km.res <- kmeans(vivienda_scaled, centers = 3, nstart = 25)
fviz_cluster(km.res, data = vivienda_scaled, ellipse.type = "convex", palette = "jco", ggtheme = theme_minimal(), title = "Segmentación de Inmuebles en Clústeres (K-Means)")Tanto el método del codo como el coeficiente de silueta coinciden en señalar \(K = 3\) como una elección razonable, es el punto en el que agregar un grupo adicional deja de aportar una mejora. Con \(K = 3\), el K-means separa la oferta en tres perfiles interpretables:
Esta segmentación es útil en la práctica porque permite tratar de forma diferenciada tres tipos de propiedad e inventario, en lugar de asumir que el mercado se comporta de manera homogénea.
El ACM se corrió sobre vivienda_cuali, con el fin de
explorar cómo se asocian entre sí el tipo de vivienda, la zona y el
estrato.
res.mca <- MCA(vivienda_cuali, graph = FALSE)
fviz_mca_var(res.mca, select.var = list(contrib = 20), repel = TRUE, ggtheme = theme_minimal(), title = "Asociación Categórica: Tipo, Zona y Estrato (ACM)")El mapa del ACM muestra una asociación clara entre estrato alto, ciertas zonas de la ciudad y tipologías de vivienda específicas por ejemplo, casas de mayor formato, mientras que los estratos bajos se agrupan con las zonas y tipologías donde se concentra la mayor parte de la oferta. En otras palabras, la ubicación y el estrato no son independientes de la tipología de vivienda; el mercado inmobiliario de la ciudad está segmentado geográfica y socioeconómicamente, y esa segmentación se refleja directamente en qué tipo de propiedad se ofrece en cada zona.
Para complementar los resultados anteriores con una lectura
geográfica, se construyó un mapa interactivo con el paquete
leaflet [5]. En lugar de tomar una muestra aleatoria simple
sobre toda la base que por azar, podría dejar subrepresentadas zonas con
pocas propiedades, se muestreó dentro de cada combinación de zona y tipo
de vivienda, y luego se limitó el total a 1.000 puntos para no saturar
el mapa.
set.seed(123)
sample_mapa <- vivienda_clean %>%
filter(!is.na(latitud) & !is.na(longitud)) %>%
group_by(zona, tipo) %>%
slice_sample(prop = 0.15) %>%
ungroup() %>%
sample_n(min(1000, n())) # límite para no sobrecargar el renderizado del mapa
leaflet(data = sample_mapa) %>%
addTiles() %>%
addCircleMarkers(~longitud, ~latitud,
radius = 3,
color = ~ifelse(tipo == "Casa", "#FC4E07", "#00AFBB"),
stroke = FALSE, fillOpacity = 0.6,
popup = ~paste("<b>Tipo:</b>", tipo, "<br>",
"<b>Precio:</b> $", preciom, "M<br>",
"<b>Área:</b>", areaconst, "m2<br>",
"<b>Estrato:</b>", estrato)) %>%
addLegend("bottomright", colors = c("#FC4E07", "#00AFBB"), labels = c("Casa", "Apartamento"), title = "Tipo de Vivienda")El mapa confirma lo que suguiere el ACM, los apartamentos se concentran en corredores céntricos y zonas de mayor densidad, mientras que las casas predominan en sectores periféricos o en zonas residenciales consolidadas de la ciudad.
El análisis conjunto de PCA, clustering y ACM ofrece una lectura consistente del mercado inmobiliario estudiado.
Desde el punto de vista de la valoración, el área construida y el número de baños son las variables que más varianza explican, lo que sugiere que estos dos atributos deberían pesar más que otros en cualquier modelo de tasación que la empresa quiera construir a futuro.
En cuanto a la segmentación, el K-means identificó tres perfiles de oferta razonablemente diferenciados económico, estándar y de alto valor, respaldados tanto por el método del codo como por el coeficiente de silueta. Estos perfiles pueden usarse para orientar estrategias comerciales distintas; rotación rápida de inventario en los segmentos económico y estándar, y márgenes más altos por unidad en el segmento de alto valor.
Finalmente, el ACM y el mapa muestran que esta segmentación no ocurre al azar en el espacio urbano está fuertemente ligada a la zona y al estrato socioeconómico, lo que confirma que cualquier estrategia de adquisición o venta de inmuebles debería definirse a nivel de zona y no de manera uniforme para toda la ciudad.
A partir de estos hallazgos, se recomienda a la dirección priorizar la captación de inventario de forma diferenciada por clúster, por ejemplo, concentrarse en el Clúster 2 para maximizar rotación y en el Clúster 3 para maximizar margen y usar el mapa de zonas como insumo adicional para decidir en qué sectores de la ciudad concentrar los esfuerzos comerciales de cada segmento.
[1] S. Lê, J. Josse, and F. Husson, “FactoMineR: An R Package for Multivariate Analysis,” Journal of Statistical Software, vol. 25, no. 1, pp. 1-18, 2008.
[2] A. Kassambara, Practical Guide to Principal Component Methods in R (PCA, M(CA), FAMD, MFA, HCPC, factoextra). STHDA, 2017.
[3] J. MacQueen, “Some methods for classification and analysis of multivariate observations,” in Proceedings of the Fifth Berkeley Symposium on Mathematical Statistics and Probability, University of California Press, 1967, pp. 281-297.
[4] A. Kassambara, Practical Guide to Cluster Analysis in R: Unsupervised Machine Learning. STHDA, 2017.
[5] J. Cheng, B. Karambelkar, and Y. Xie, Leaflet: Create Interactive Web Maps with the JavaScript ‘Leaflet’ Library, R package version 2.2.1, 2023. [Online]. Available: https://rstudio.github.io/leaflet/
[6] J. Han, M. Kamber, and J. Pei, Data Mining: Concepts and Techniques, 3rd ed. Morgan Kaufmann, 2011.
[7] T. Hastie, R. Tibshirani, and J. Friedman, The Elements of Statistical Learning: Data Mining, Inference, and Prediction, 2nd ed. Springer, 2009.
[8] P. J. Rousseeuw and A. M. Leroy, Robust Regression and Outlier Detection, John Wiley & Sons, 1987.
[9] S. van Buuren and K. Groothuis-Oudshoorn, “mice: Multivariate Imputation by Chained Equations in R,” Journal of Statistical Software, vol. 45, no. 3, pp. 1-67, 2011.
[10] B. Auguie, gridExtra: Miscellaneous Functions for “Grid” Graphics, R package version 2.3, 2017. [Online]. Available: https://cran.r-project.org/package=gridExtra
[11] R. L. Thorndike, “Who belongs in the family?,” Psychometrika, vol. 18, no. 4, pp. 267-276, 1953.