Una empresa inmobiliaria líder busca comprender el mercado de
vivienda urbana a partir de una base de datos obtenida mediante
webscraping de OLX (paquete paqueteMODELOS). El
objetivo de este informe es aplicar un análisis multivariado integral
Componentes Principales, Conglomerados y Correspondencias para
identificar patrones, segmentos y relaciones entre las características
de las propiedades, y traducir esos hallazgos en recomendaciones
estratégicas de compra, venta y valoración.
Antes de entrar en el detalle técnico, es importante justificar por qué se eligió cada técnica y cómo se articulan entre sí, en lugar de aplicarlas de forma aislada:
Análisis de Componentes Principales (PCA): el conjunto de variables cuantitativas de una vivienda (área, número de baños, habitaciones, parqueaderos, precio) está naturalmente correlacionado una vivienda más grande tiende a tener más habitaciones, más baños y mayor precio. El PCA permite resumir esa redundancia en unos pocos ejes interpretables (p. ej. un eje de “tamaño/valor”), lo cual es el primer paso para entender qué dimensiones realmente diferencian a las propiedades en el mercado.
Análisis de Conglomerados: una vez identificadas las dimensiones que explican la variabilidad (vía PCA), el paso natural es preguntar si existen grupos naturales de propiedades similares entre sí. Esto traduce el análisis estadístico en un insumo directamente accionable para el negocio: segmentos de mercado con perfiles de precio, tamaño y zona propios, útiles para diseñar estrategias diferenciadas de compra, venta o inversión.
Análisis de Correspondencias (CA): las dos técnicas anteriores trabajan sobre variables cuantitativas. Sin embargo, buena parte de la información relevante del mercado inmobiliario es categórica (tipo de vivienda, zona, barrio). El CA es el análogo del PCA para variables categóricas: en lugar de varianza, descompone la asociación entre categorías, revelando qué tipos de vivienda predominan en qué zonas.
Visualización (mapas): finalmente, dado que la
ubicación geográfica (longitud, latitud) es
una característica intrínsecamente espacial, se reserva para una etapa
de visualización que permite anclar los hallazgos estadísticos
(componentes, conglomerados, asociaciones) en el mapa real de la
ciudad, facilitando su lectura por parte de la dirección de la
empresa.
En conjunto, el flujo metodológico sigue una lógica de reducción → segmentación → asociación → comunicación visual.
# devtools::install_github("centromagis/paqueteMODELOS", force = TRUE)
library(paqueteMODELOS)
library(dplyr)
library(tidyr)
library(forcats)
library(ggplot2)
library(corrplot)
library(FactoMineR)
library(factoextra)
library(cluster)
library(leaflet)
library(knitr)
library(kableExtra)
data("vivienda")
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")=<pointer: (nil)>
Antes de tomar cualquier decisión de limpieza o modelado, es indispensable tener claro qué representa cada variable, su tipo de dato original y el rol que cumplirá en los análisis siguientes. La Tabla 1 resume esta información.
diccionario <- data.frame(
Variable = c("id", "zona", "piso", "estrato", "preciom", "areaconst",
"parqueaderos", "banios", "habitaciones", "tipo", "barrio",
"longitud", "latitud"),
Descripcion = c(
"Identificador único de la publicación/propiedad.",
"Zona geográfica general de la ciudad en la que se ubica la propiedad.",
"Piso del inmueble dentro del edificio (aplica principalmente a apartamentos).",
"Estrato socioeconómico de la vivienda (escala ordinal 1 a 6).",
"Precio de venta publicado, en millones de pesos.",
"Área construida de la propiedad, en metros cuadrados.",
"Número de parqueaderos disponibles.",
"Número de baños de la propiedad.",
"Número de habitaciones de la propiedad.",
"Tipo de inmueble (p. ej. Casa, Apartamento).",
"Barrio específico en el que se ubica la propiedad.",
"Coordenada geográfica de longitud.",
"Coordenada geográfica de latitud."
),
Tipo_original = c("numérica", "texto", "texto", "numérica", "numérica",
"numérica", "numérica", "numérica", "numérica", "texto",
"texto", "numérica", "numérica"),
Rol_en_el_analisis = c(
"Identificador (no se usa como variable analítica)",
"Categórica – insumo del Análisis de Correspondencias y variable de segmentación",
"Categórica – documentada, no se usa directamente en PCA/clustering/CA",
"Ordinal – variable suplementaria en PCA y variable de perfilado de conglomerados",
"Cuantitativa – variable activa en PCA y Conglomerados",
"Cuantitativa – variable activa en PCA y Conglomerados",
"Cuantitativa – variable activa en PCA y Conglomerados (NA imputados como 0 = sin parqueadero)",
"Cuantitativa – variable activa en PCA y Conglomerados",
"Cuantitativa – variable activa en PCA y Conglomerados",
"Categórica – variable suplementaria en PCA e insumo del Análisis de Correspondencias",
"Categórica de alta cardinalidad – insumo del Análisis de Correspondencias (agrupada)",
"Geográfica – utilizada exclusivamente en la sección de visualización/mapas",
"Geográfica – utilizada exclusivamente en la sección de visualización/mapas"
)
)
kable(diccionario, caption = "Tabla 1. Diccionario de variables de la base de datos 'vivienda'",
col.names = c("Variable", "Descripción", "Tipo original", "Rol en el análisis")) %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = TRUE)
| Variable | Descripción | Tipo original | Rol en el análisis |
|---|---|---|---|
| id | Identificador único de la publicación/propiedad. | numérica | Identificador (no se usa como variable analítica) |
| zona | Zona geográfica general de la ciudad en la que se ubica la propiedad. | texto | Categórica – insumo del Análisis de Correspondencias y variable de segmentación |
| piso | Piso del inmueble dentro del edificio (aplica principalmente a apartamentos). | texto | Categórica – documentada, no se usa directamente en PCA/clustering/CA |
| estrato | Estrato socioeconómico de la vivienda (escala ordinal 1 a 6). | numérica | Ordinal – variable suplementaria en PCA y variable de perfilado de conglomerados |
| preciom | Precio de venta publicado, en millones de pesos. | numérica | Cuantitativa – variable activa en PCA y Conglomerados |
| areaconst | Área construida de la propiedad, en metros cuadrados. | numérica | Cuantitativa – variable activa en PCA y Conglomerados |
| parqueaderos | Número de parqueaderos disponibles. | numérica | Cuantitativa – variable activa en PCA y Conglomerados (NA imputados como 0 = sin parqueadero) |
| banios | Número de baños de la propiedad. | numérica | Cuantitativa – variable activa en PCA y Conglomerados |
| habitaciones | Número de habitaciones de la propiedad. | numérica | Cuantitativa – variable activa en PCA y Conglomerados |
| tipo | Tipo de inmueble (p. ej. Casa, Apartamento). | texto | Categórica – variable suplementaria en PCA e insumo del Análisis de Correspondencias |
| barrio | Barrio específico en el que se ubica la propiedad. | texto | Categórica de alta cardinalidad – insumo del Análisis de Correspondencias (agrupada) |
| longitud | Coordenada geográfica de longitud. | numérica | Geográfica – utilizada exclusivamente en la sección de visualización/mapas |
| latitud | Coordenada geográfica de latitud. | numérica | Geográfica – utilizada exclusivamente en la sección de visualización/mapas |
Este diccionario es la base de todas las decisiones de preparación
que siguen. Al dejar explícito el rol de cada variable desde el
inicio, se evita el error común de meter variables de ubicación
(longitud, latitud) o identificadores
(id) dentro de cálculos de distancia o varianza, donde no
tienen una interpretación de negocio válida.
Con base en el diccionario anterior, estrato, aunque
numérica en el tipo de dato original, se trata como variable
ordinal (categoría socioeconómica 1 a 6): la diferencia
entre estrato 3 y 4 no es una cantidad medible como sí lo es la
diferencia entre 70 m² y 140 m². tipo y zona
se convierten en factores para su uso en el Análisis de Correspondencias
y como variables suplementarias del PCA.
vivienda <- vivienda %>%
mutate(
estrato = factor(estrato, levels = 1:6, ordered = TRUE),
tipo = factor(tipo),
zona = factor(zona),
piso = as.character(piso)
)
# Revisión de NAs en piso según tipo de vivienda
table(vivienda$tipo, is.na(vivienda$piso))
##
## FALSE TRUE
## Apartamento 3719 1381
## Casa 1965 1254
Los NA en la variable piso representan una
mayor proporción de los registros para Casa (41.3%),
mientras que los Apartamentos representa un 25.2%. Esto
tiene sentido conceptual: una casa no tiene “un piso” dentro de un
edificio, así que el vacío no es un dato faltante por error de captura,
sino estructural (no aplica). Por esta razón se
documenta la ausencia en lugar de imputar un valor, y piso
no se incluye como variable activa en PCA, Conglomerados ni CA.
barrioLa variable barrio tiene una alta cardinalidad, lo que
genera dos problemas para el Análisis de Correspondencias: (1)
categorías con muy pocas observaciones producen frecuencias esperadas
bajas y estimaciones poco confiables, y (2) un mapa perceptual con 100+
etiquetas es ilegible. Se agrupan los barrios menos frecuentes en una
categoría "Otros", conservando los 15 barrios con mayor
número de publicaciones.
top_barrios <- vivienda %>%
count(barrio, sort = TRUE) %>%
slice_max(n, n = 15) %>%
pull(barrio)
vivienda <- vivienda %>%
mutate(barrio_agrup = fct_other(barrio, keep = top_barrios, other_level = "Otros"))
vivienda %>%
count(barrio_agrup, sort = TRUE) %>%
kable(caption = "Tabla 2. Frecuencia de publicaciones por barrio agrupado",
col.names = c("Barrio (agrupado)", "N° de propiedades")) %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"))
| Barrio (agrupado) | N° de propiedades |
|---|---|
| Otros | 4228 |
| 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 |
| NA | 3 |
Esta agrupación es un balance deliberado entre
interpretabilidad (un biplot legible) y pérdida
de detalle (los barrios menos frecuentes ya no se distinguen
entre sí). El umbral de 15 categorías es una decisión práctica; se puede
ajustar según cuántos barrios concentren la mayoría de las publicaciones
(ver columna N° de propiedades en la Tabla 2).
vars_num <- c("preciom", "areaconst", "parqueaderos", "banios", "habitaciones")
resumen_num <- summary(vivienda[, vars_num])
resumen_num
## preciom areaconst parqueaderos banios
## Min. : 58.0 Min. : 30.0 Min. : 1.000 Min. : 0.000
## 1st Qu.: 220.0 1st Qu.: 80.0 1st Qu.: 1.000 1st Qu.: 2.000
## Median : 330.0 Median : 123.0 Median : 2.000 Median : 3.000
## Mean : 433.9 Mean : 174.9 Mean : 1.835 Mean : 3.111
## 3rd Qu.: 540.0 3rd Qu.: 229.0 3rd Qu.: 2.000 3rd Qu.: 4.000
## Max. :1999.0 Max. :1745.0 Max. :10.000 Max. :10.000
## NAs :2 NAs :3 NAs :1605 NAs :3
## habitaciones
## Min. : 0.000
## 1st Qu.: 3.000
## Median : 3.000
## Mean : 3.605
## 3rd Qu.: 4.000
## Max. :10.000
## NAs :3
# Conteo de NAs por variable numérica
tabla_na <- data.frame(
Variable = vars_num,
NAs = sapply(vivienda[, vars_num], function(x) sum(is.na(x)))
)
kable(tabla_na, caption = "Tabla 3. Número de valores faltantes por variable numérica",
col.names = c("Variable", "N° de NA")) %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"))
| Variable | N° de NA | |
|---|---|---|
| preciom | preciom | 2 |
| areaconst | areaconst | 3 |
| parqueaderos | parqueaderos | 1605 |
| banios | banios | 3 |
| habitaciones | habitaciones | 3 |
# Gráfico 1. Boxplots de variables numéricas para identificar atípicos
vivienda %>%
select(all_of(vars_num)) %>%
pivot_longer(everything()) %>%
ggplot(aes(x = name, y = value)) +
geom_boxplot(fill = "steelblue", alpha = .6) +
facet_wrap(~name, scales = "free") +
theme_minimal() +
labs(title = "Gráfico 1. Distribución y atípicos de variables numéricas", x = NULL, y = NULL)
Interpretación del Gráfico 1: Los diagramas de caja revelan la estructura de distribución de las variables numéricas del dataset:
Asimetría extrema y outliers continuos: Las variables areaconst (área construida) y preciom (precio) presentan un comportamiento fuertemente sesgado hacia la derecha, con cajas muy comprimidas en la parte inferior y una alta densidad de valores atípicos que se extienden hasta los \(1,750\text{ m}^2\) y \(2,000\) millones respectivamente, correspondientes a inmuebles de súper lujo.
Variables discretas y acotadas: Las métricas de conteo (banio, habitaciones y parqueaderos) muestran rangos intercuartílicos estrechos y concentrados en valores típicos (2 a 4 unidades), aunque con casos puntuales atípicos en el extremo superior (hasta 10 baños/habitaciones y 10 parqueaderos).
Justificación de tratamiento: La marcada presencia de estos datos atípicos extremos en precio y área justifica la aplicación de filtros o transformaciones estadísticas para evitar que distorsionen los análisis posteriores.
parqueaderos: NA no es lo mismo que “dato faltante”Antes de decidir qué hacer con los NA restantes, vale la
pena preguntarse si todos significan lo mismo. Para
preciom, areaconst, banios o
habitaciones, un NA es efectivamente un
dato ausente: no sabemos cuánto vale, cuánto mide o
cuántos baños/habitaciones tiene esa propiedad, y no hay ninguna razón
conceptual para asumir que “no reportado” equivale a “cero”.
La variable parqueaderos es distinta. En el contexto de
publicaciones inmobiliarias de OLX, es razonable interpretar que cuando
el anunciante no diligenció el campo de parqueaderos,
lo más probable es que la propiedad no tenga
parqueadero y por eso no se reportó (a diferencia del precio o
el área, que un anunciante siempre va a especificar porque son atributos
centrales del anuncio). Es decir, aquí el NA no representa
ignorancia sobre el dato, sino la ausencia del atributo
mismo. Por esta razón, en vez de eliminar esos registros, se
imputa parqueaderos = 0.
# Antes de imputar: cuántos registros tienen NA en parqueaderos
n_na_parq_antes <- sum(is.na(vivienda$parqueaderos))
n_na_parq_antes
## [1] 1605
vivienda <- vivienda %>%
mutate(parqueaderos = ifelse(is.na(parqueaderos), 0, parqueaderos))
# Verificación: ya no deben quedar NA en parqueaderos
sum(is.na(vivienda$parqueaderos))
## [1] 0
# Proporción de propiedades sin parqueadero tras la imputación
mean(vivienda$parqueaderos == 0, na.rm = TRUE)
## [1] 0.1928623
Esta es una decisión diferenciada por variable, no
una regla general de “siempre imputar” o “siempre eliminar”. El criterio
aplicado es: ¿el valor faltante tiene un significado de negocio
plausible distinto de “dato desconocido”? Solo para
parqueaderos la respuesta es sí (NA ≈ sin parqueadero ≈ 0),
por lo que es la única variable numérica que se imputa. Para
preciom, areaconst, banios y
habitaciones no existe un valor “natural” que reemplace la
ausencia del dato sin introducir sesgo (un NA en
preciom no puede asumirse como precio 0), así que para esas
variables se mantiene la estrategia de eliminación, que se aplica en el
siguiente paso.
# Filtro de valores faltantes y atípicos extremos.
# parqueaderos ya no tiene NA (se imputó como 0 en el paso anterior), por lo
# que el filtro de NA restante solo termina afectando a preciom, areaconst,
# banios y habitaciones, donde sí se opta por eliminar el registro.
vivienda_clean <- vivienda %>%
filter(if_all(all_of(vars_num), ~ !is.na(.))) %>%
filter(
preciom <= quantile(preciom, .99, na.rm = TRUE),
areaconst <= quantile(areaconst, .99, na.rm = TRUE)
)
dim(vivienda)
## [1] 8322 14
dim(vivienda_clean)
## [1] 8163 14
# Verificación: ya no debe quedar ningún NA en las variables numéricas clave
sapply(vivienda_clean[, vars_num], function(x) sum(is.na(x)))
## preciom areaconst parqueaderos banios habitaciones
## 0 0 0 0 0
Racionalización de la eliminación de NA restantes:
se optó por eliminar los registros con NA
en preciom, areaconst, banios y
habitaciones, dado que representan una proporción pequeña
del total (ver Tabla 3) y, a diferencia de parqueaderos, no
existe un valor por defecto con significado de negocio ni una relación
clara con otras variables que justifique una imputación basada en
modelo.
Racionalización del filtro de atípicos (percentil 1%-99%
vs. IQR): se evaluaron dos estrategias posibles para tratar los
atípicos de preciom y areaconst:
preciom y
areaconst tienen una asimetría marcada hacia la derecha
(cola larga de propiedades caras/grandes), que es un rasgo
legítimo del mercado inmobiliario y no un error de
datos. Aplicar 1.5×RIC sobre una distribución así de asimétrica tiende a
marcar como “atípica” una porción demasiado grande de propiedades
genuinamente costosas o grandes, eliminando información real del
segmento alto del mercado que es precisamente uno de los segmentos que
interesa caracterizar en el Análisis de Conglomerados.Por estas razones se prefirió el percentil 1%-99%: es un criterio más conservador (elimina menos observaciones legítimas) y más robusto a la asimetría propia de variables de precio y área en el mercado inmobiliario.
Nota sobre
longitud/latitud: estas variables no
se incluyen como insumo numérico dentro del PCA ni del clustering. La
razón es doble: (1) ya existe información de localización codificada en
zona y barrio, por lo que incluir además las
coordenadas duplicaría esa señal geográfica y generaría
multicolinealidad; y (2) un componente principal que mezcle, p. ej.,
latitud con precio no tiene una lectura de
negocio clara. En su lugar, longitud/latitud
se reservan para la sección de visualización (mapas) y
para caracterizar geográficamente los conglomerados una
vez formados.
# Gráfico 2. Distribución del precio de venta
ggplot(vivienda_clean, aes(x = preciom)) +
geom_histogram(bins = 40, fill = "darkorange", alpha = .8) +
theme_minimal() +
labs(title = "Gráfico 2. Distribución del precio de venta (millones)",
x = "Precio (millones)", y = "Frecuencia")
Interpretación del Gráfico 2: El histograma refleja la típica asimetría positiva del mercado inmobiliario, en donde la oferta se concentra masivamente en el rango de 100 a 400 millones (con su pico máximo entre 150 y 200 millones), mientras que a partir de los 500 millones la frecuencia cae drásticamente, formando una cola larga que se extiende hasta más allá de los 1,500 millones a causa de una minoría de viviendas de alta gama o lujo.
# Gráfico 3. Precio por estrato socioeconómico
ggplot(vivienda_clean, aes(x = estrato, y = preciom, fill = estrato)) +
geom_boxplot(show.legend = FALSE) +
theme_minimal() +
labs(title = "Gráfico 3. Precio por estrato socioeconómico",
x = "Estrato", y = "Precio (millones)")
Interpretación del Gráfico 3: Existe una clara relación directa entre el estrato socioeconómico y el precio de la vivienda: la mediana y la dispersión de los precios aumentan progresivamente desde el estrato 3 (~150 millones) hasta el estrato 6 (~700 millones, con un rango típico de 500M a 950M). Mientras que los estratos 3, 4 y 5 mantienen concentraciones de precios más bajas pero con una gran cantidad de valores atípicos que alcanzan los 1,500 y 1,600 millones, el estrato 6 destaca por tener un rango de precios estándar significativamente más alto y mayor variabilidad sin la necesidad de depender de outliers aislados.
# Gráfico 4. Precio por tipo de vivienda
ggplot(vivienda_clean, aes(x = tipo, y = preciom, fill = tipo)) +
geom_boxplot(show.legend = FALSE) +
theme_minimal() +
labs(title = "Gráfico 4. Precio por tipo de vivienda", x = NULL, y = "Precio (millones)")
Interpretación del Gráfico 4: Las casas representan un segmento de mercado significativamente más costoso y variable, con una mediana de precio (~420 millones) e intervalo típico (~300M a 650M) superiores a los de los apartamentos (~280M de mediana y ~180M a 430M de rango típico); no obstante, aunque la oferta de apartamentos es mayoritariamente más accesible, ambos mercados exhiben una pronunciada asimetría positiva hacia la alta gama, alcanzando máximos de hasta 1,600 millones y destacando en los apartamentos una densidad notablemente superior de valores atípicos de alto costo.
mat_cor <- cor(vivienda_clean[, vars_num], use = "complete.obs")
# Gráfico 5. Matriz de correlación
corrplot(mat_cor, method = "color", type = "upper",
addCoef.col = "black", tl.col = "black", number.cex = .8,
title = "Gráfico 5. Correlación entre variables numéricas", mar = c(0,0,2,0))
Interpretación del Gráfico 5: La matriz muestra correlaciones positivas entre todas las variables, destacando que el precio (preciom) está fuertemente impulsado por el área construida (\(r = 0.70\)), el número de baños (\(r = 0.67\)) y los parqueaderos (\(r = 0.62\)), mientras que el número de habitaciones presenta una relación débil con el precio (\(r = 0.25\)) e igualmente baja con los parqueaderos (\(r = 0.19\)); asimismo, la cantidad de baños se perfila como un indicador clave de amplitud, al correlacionarse de manera moderada-alta tanto con el área (\(r = 0.68\)) como con las habitaciones (\(r = 0.58\)).
Se usa PCA() de FactoMineR, estandarizando
internamente las variables (scale.unit = TRUE) para que
ninguna domine solo por su escala (p. ej. preciom en
cientos vs parqueaderos en unidades). Se incluyen
estrato y tipo como variables
suplementarias (ilustrativas) para interpretar los
componentes sin que influyan en su cálculo.
datos_pca <- vivienda_clean %>%
select(all_of(vars_num), estrato, tipo) %>%
as.data.frame()
res_pca <- PCA(datos_pca,
quali.sup = which(names(datos_pca) %in% c("estrato", "tipo")),
graph = FALSE)
# Gráfico 6. Varianza explicada por componente (scree plot)
fviz_eig(res_pca, addlabels = TRUE, ylim = c(0, 70)) +
labs(title = "Gráfico 6. Varianza explicada por componente")
res_pca$eig %>%
kable(digits = 2, caption = "Tabla 4. Valores propios y varianza explicada por el PCA") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"))
| eigenvalue | percentage of variance | cumulative percentage of variance | |
|---|---|---|---|
| comp 1 | 3.13 | 62.68 | 62.68 |
| comp 2 | 0.94 | 18.75 | 81.42 |
| comp 3 | 0.44 | 8.80 | 90.22 |
| comp 4 | 0.30 | 6.07 | 96.29 |
| comp 5 | 0.19 | 3.71 | 100.00 |
Interpretación del Gráfico 6 y la Tabla 4: El Análisis de Componentes Principales muestra que el primer componente (comp 1) es el más dominante al concentrar por sí solo el 62.68% de la variabilidad total (con un valor propio de 3.13), mientras que los primeros dos componentes juntos logran capturar el 81.42% de la información total del conjunto de datos; según el criterio de Kaiser (valores propios > 1), únicamente el comp 1 debería retenerse, aunque conservar las dos primeras dimensiones resulta ser la opción ideal para reducir la dimensionalidad de 5 a 2 variables garantizando una excelente representación explicativa (superior al 80%).
# Gráfico 7. Círculo de correlaciones
fviz_pca_var(res_pca, col.var = "contrib",
gradient.cols = c("#00AFBB", "#E7B800", "#FC4E07"),
repel = TRUE) +
labs(title = "Gráfico 7. Círculo de correlaciones - Variables")
Interpretación del Gráfico 7: El círculo de correlaciones muestra que todas las variables están positivamente vinculadas a la primera dimensión (Dim1, 62.7%), la cual representa el tamaño general y valor de la propiedad (destacando areaconst y banios con los vectores más largos y alineados horizontalmente); por su parte, la segunda dimensión (Dim2, 18.7%) opone la variable habitaciones (en el cuadrante superior con la mayor contribución global) contra preciom y parqueaderos (en el cuadrante inferior), reflejando que un mayor número de cuartos no implica necesariamente un precio más alto o más estacionamientos.
# Gráfico 8. Individuos en el plano PC1-PC2
fviz_pca_ind(res_pca,
geom.ind = "point",
habillage = "estrato",
addEllipses = TRUE, ellipse.level = 0.68,
alpha.ind = .4) +
labs(title = "Gráfico 8. Individuos en el plano PC1-PC2, coloreados por estrato")
Interpretación del Gráfico 8: La proyección de las ofertas según su estrato evidencia una clara transición socioeconómica a lo largo de las dos dimensiones: el estrato 6 (en amarillo) se desplaza ampliamente hacia la derecha (Dim1) y hacia abajo (Dim2), asociándose con mayor precio, área y parqueaderos; mientras que los estratos 3 y 4 (púrpura y azul) se concentran en valores negativos de Dim1 y positivos de Dim2 (mayor número de habitaciones pero menor área y precio), dejando al estrato 5 (verde) en una posición intermedia, aunque con un alto grado de solapamiento entre todos los grupos que refleja la heterogeneidad de inmuebles dentro de un mismo estrato.
Reducción de dimensionalidad y varianza explicada: Las dos primeras dimensiones principales resumen el 81.42% de la variabilidad total de los datos (Dim1: 62.68% y Dim2: 18.75%), lo que valida la simplificación del espacio original de 5 variables numéricas a solo 2 ejes sin una pérdida significativa de información.
Estructura y comportamiento de las variables: Dim1 funciona como un índice general de capacidad y costo (fuertemente alineada con área construida, baños, precio y parqueaderos), mientras que Dim2 contrapone la densidad habitacional contra el valor económico, evidenciando que un mayor número de dormitorios no implica un precio más alto ni más estacionamientos.
Segmentación socioeconómica por estratos: La proyección de los inmuebles refleja un claro gradiente sobre el plano factorial; el estrato 6 se desplaza hacia propiedades de mayor dimensión y costo (valores positivos de Dim1 y negativos de Dim2), los estratos 3 y 4 se concentran en inmuebles más compactos o de menor valor relativo, y el estrato 5 se ubica como un segmento de transición con alto grado de solapamiento.
datos_cluster <- vivienda_clean %>%
select(all_of(vars_num)) %>%
scale() %>%
as.data.frame()
Al igual que en el PCA, se estandarizan las variables (media 0,
varianza 1) antes de calcular distancias, porque el algoritmo k-means se
basa en distancia euclidiana, y sin estandarizar, preciom
(en cientos) dominaría por completo el cálculo frente a
parqueaderos o banios (en unidades), sin que
eso refleje una mayor relevancia real.
set.seed(123)
muestra_idx <- sample(1:nrow(datos_cluster), min(1000, nrow(datos_cluster)))
# Gráfico 9. Método del codo
fviz_nbclust(datos_cluster[muestra_idx, ], kmeans, method = "wss", k.max = 10) +
labs(title = "Gráfico 9. Método del codo")
# Gráfico 10. Método de la silueta
fviz_nbclust(datos_cluster[muestra_idx, ], kmeans, method = "silhouette", k.max = 10) +
labs(title = "Gráfico 10. Método de la silueta")
Interpretación de los Gráficos 9 y 10: El gráfico del método del codo, muestra una reducción drástica en la suma total de cuadrados al pasar de \(k = 1\) a \(k = 4\), punto a partir del cual la curva disminuye a un ritmo mucho más suave y gradual, lo que señala a \(k = 4\) como la cantidad óptima de conglomerados para lograr un equilibrio idóneo entre la homogeneidad interna de cada grupo y la complejidad de la partición.
k_elegido <- 4 # ajustar según el resultado real del codo/silueta (Gráficos 9 y 10)
set.seed(123)
res_kmeans <- kmeans(datos_cluster, centers = k_elegido, nstart = 25)
vivienda_clean$cluster <- factor(res_kmeans$cluster)
# Gráfico 11. Conglomerados k-means proyectados en componentes principales
fviz_cluster(res_kmeans, data = datos_cluster,
geom = "point", ellipse.type = "convex",
palette = "jco", ggtheme = theme_minimal()) +
labs(title = paste("Gráfico 11. Conglomerados k-means (k =", k_elegido, ")"))
Interpretación del Gráfico 11:
Cluster 1 (Azul - Círculos): Se extiende en valores positivos de Dim1 (de 1 a 7) y valores positivos de Dim2 (hasta > 5.0). Es el grupo con mayor dispersión interna (mayor heterogeneidad) y contiene algunos valores atípicos (outliers) en la parte superior derecha.
Cluster 2 (Amarillo - Triángulos): Ubicado en valores marcadamente negativos de la Dim1 (entre -5 y -1). Presenta una dispersión moderada y se encuentra bien separado de los clusters 1 y 3, aunque limita directamente con el cluster 4.
Cluster 3 (Gris - Cuadrados): Ocupa la zona inferior de valores positivos de Dim1 y valores negativos de Dim2 (de -1 a -4.5). La Dim2 es el factor determinante para separar claramente este grupo del cluster 1.
Cluster 4 (Rojo - Cruces): Situado en la zona de transición central de Dim1 (alrededor de 0). Actúa como un punto medio o intermedio en la primera dimensión, overlapando sensiblemente en sus fronteras con el cluster 2 a la izquierda y el cluster 1 a la derecha.
perfil_cluster <- vivienda_clean %>%
group_by(cluster) %>%
summarise(
n = n(),
precio_medio = mean(preciom, na.rm = TRUE),
area_media = mean(areaconst, na.rm = TRUE),
parq_medio = mean(parqueaderos, na.rm = TRUE),
banios_medio = mean(banios, na.rm = TRUE),
habit_medio = mean(habitaciones, na.rm = TRUE)
)
kable(perfil_cluster, digits = 1,
caption = "Tabla 5. Perfil promedio de variables numéricas por conglomerado",
col.names = c("Conglomerado", "N", "Precio medio", "Área media",
"Parqueaderos (prom.)", "Baños (prom.)", "Habitaciones (prom.)")) %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"))
| Conglomerado | N | Precio medio | Área media | Parqueaderos (prom.) | Baños (prom.) | Habitaciones (prom.) |
|---|---|---|---|---|---|---|
| 1 | 904 | 1017.4 | 355.3 | 3.5 | 5.0 | 4.2 |
| 2 | 4063 | 225.2 | 87.9 | 0.8 | 2.0 | 2.9 |
| 3 | 758 | 463.9 | 307.6 | 1.1 | 4.6 | 6.8 |
| 4 | 2438 | 489.3 | 178.9 | 1.8 | 3.6 | 3.6 |
Interpretación de la Tabla 5: La tabla perfila cuatro segmentos inmobiliarios con identidades de negocio bien definidas:
Conglomerado 1 (Lujo / Premium, \(N = 904\)): Presenta el mayor valor e infraestructura, destacando por un precio medio excepcional (\(1017.4\) millones), la mayor extensión (\(355.3\text{ m}^2\)), \(3.5\) parqueaderos y \(5.0\) baños en promedio.
Conglomerado 2 (Económico / Entrada, \(N = 4,063\)): Es el grupo más numeroso y accesible, caracterizado por el menor precio medio (\(225.2\) millones), dimensiones reducidas (\(87.9\text{ m}^2\)) y un promedio de parqueaderos de apenas \(0.8\), lo que refleja la inclusión de propiedades sin estacionamiento.
Conglomerado 3 (Familiar Multihabitacional, \(N = 758\)): Muestra amplias áreas (\(307.6\text{ m}^2\)) y precios medios (\(463.9\) millones), pero destaca drásticamente por la mayor densidad de habitaciones (\(6.8\) en promedio), posicionándose como viviendas de alta capacidad ocupacional.
Conglomerado 4 (Gama Media Estándar, \(N = 2,438\)): Registra un valor equilibrado (\(489.3\) millones) para un tamaño intermedio (\(178.9\text{ m}^2\)), funcionando como el segmento promedio del mercado.
# Gráfico 12. Composición de estratos dentro de cada conglomerado
ggplot(vivienda_clean, aes(x = cluster, fill = estrato)) +
geom_bar(position = "fill") +
theme_minimal() +
labs(title = "Gráfico 12. Composición de estratos dentro de cada conglomerado",
y = "Proporción", x = "Conglomerado")
Interpretación del Gráfico 12: El gráfico muestra que los conglomerados 1 y 4 están principalmente asociados con los estratos socioeconómicos altos, especialmente el conglomerado 1, dominado por el estrato 6. En cambio, los conglomerados 2 y 3 presentan una composición más heterogénea, con mayor participación de los estratos 3, 4 y 5.
En general, el estrato socioeconómico sí parece influir en la segmentación, pero no es el único factor que diferencia los conglomerados.
# Gráfico 13. Composición de tipo de vivienda dentro de cada conglomerado
ggplot(vivienda_clean, aes(x = cluster, fill = tipo)) +
geom_bar(position = "fill") +
theme_minimal() +
labs(title = "Gráfico 13. Composición de tipo de vivienda dentro de cada conglomerado",
y = "Proporción", x = "Conglomerado")
Interpretación del Gráfico 13: El gráfico muestra una clara diferenciación por tipo de vivienda. El conglomerado 3 está compuesto casi totalmente por casas, mientras que el conglomerado 2 está dominado por apartamentos. Los conglomerados 1 y 4 presentan una composición más equilibrada entre ambos tipos.
En general, el tipo de vivienda parece ser una variable relevante para diferenciar los conglomerados, especialmente en los grupos 2 y 3.
Se cruza tipo de vivienda con zona y, por
separado, con la versión agrupada de barrio
(barrio_agrup) para no perder interpretabilidad (ver Tabla
2 y su racionalización).
Antes de calcular el estadístico, se plantea formalmente la prueba de independencia que se va a evaluar:
tipo de vivienda y zona son
independientes; es decir, la proporción de Casas y
Apartamentos es la misma en todas las zonas de la ciudad, y cualquier
diferencia observada en la Tabla 6 se debe únicamente a variación
muestral.tipo de vivienda y zona no son
independientes; la proporción de cada tipo de vivienda varía
según la zona.Regla de decisión: se fija un nivel de significancia \(\alpha = 0.05\). Si el p-valor obtenido es
menor a \(\alpha\), se rechaza \(H_0\) y se concluye que existe asociación
estadísticamente significativa entre tipo y
zona.
tabla_tipo_zona <- table(vivienda_clean$tipo, vivienda_clean$zona)
kable(as.data.frame.matrix(tabla_tipo_zona),
caption = "Tabla 6. Tabla de contingencia: Tipo de vivienda x Zona") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"))
| Zona Centro | Zona Norte | Zona Oeste | Zona Oriente | Zona Sur | |
|---|---|---|---|---|---|
| Apartamento | 24 | 1198 | 1005 | 62 | 2783 |
| Casa | 99 | 699 | 160 | 286 | 1847 |
dim(tabla_tipo_zona)
## [1] 2 5
chisq.test(tabla_tipo_zona)
##
## Pearson's Chi-squared test
##
## data: tabla_tipo_zona
## X-squared = 682.75, df = 4, p-value < 2.2e-16
res_ca_zona <- CA(tabla_tipo_zona, graph = FALSE)
# El número de dimensiones de un CA es min(filas-1, columnas-1). Cuando la
# variable con menos categorías (aquí "tipo") tiene solo 2-3 niveles, puede
# existir una sola dimensión, y fviz_ca_biplot() no puede graficar un eje 2
# que no existe. Verificamos cuántas dimensiones hay antes de graficar.
n_dim_zona <- nrow(res_ca_zona$eig)
n_dim_zona
## [1] 1
if (n_dim_zona >= 2) {
fviz_ca_biplot(res_ca_zona, repel = TRUE) +
labs(title = "Gráfico 14. Análisis de Correspondencias: Tipo de vivienda x Zona")
} else {
# Cuando solo hay 1 dimensión, FactoMineR puede devolver row$coord/col$coord
# como un vector simple (sin atributo "dim"), no como matriz. as.matrix()
# normaliza ambos casos a una matriz de 1 columna antes de indexar.
extraer_dim1 <- function(coord_obj, etiqueta) {
m <- as.matrix(coord_obj)
if (ncol(m) == 0 || is.null(colnames(m))) m <- matrix(m[, 1], ncol = 1)
data.frame(categoria = rownames(as.matrix(coord_obj)),
dim1 = m[, 1], variable = etiqueta)
}
coords <- rbind(
extraer_dim1(res_ca_zona$row$coord, "tipo"),
extraer_dim1(res_ca_zona$col$coord, "zona")
)
ggplot(coords, aes(x = dim1, y = categoria, color = variable)) +
geom_point(size = 3) +
geom_vline(xintercept = 0, linetype = "dashed") +
theme_minimal() +
labs(title = "Gráfico 14. Análisis de Correspondencias (1 dimensión): Tipo x Zona",
x = "Dimensión 1", y = NULL)
}
Resultado de la prueba de hipótesis: con los datos
de este informe, la prueba arrojó \(X^2 =
682.75\), \(df = 4\), \(p\text{-value} < 2.2\times10^{-16}\).
Como el p-valor es mucho menor que \(\alpha =
0.05\), se rechaza \(H_0\): hay evidencia estadística
contundente de que tipo de vivienda y zona
no son independientes. Esto no es solo un efecto del
gran tamaño de muestra (más de 8,000 registros, lo que hace al test muy
sensible): la magnitud del estadístico y la propia Tabla 6 muestran una
diferencia sustancial en la composición de tipo de vivienda por zona por
ejemplo, en Zona Oeste el 86% de las publicaciones son apartamentos
(1005 de 1165), mientras que en Zona Oriente el 82% son casas (286 de
348).
Interpretación del Gráfico 14: La Zona Oeste se alinean fuertemente con los apartamentos debido a su alta concentración vertical (hasta el \(86\%\) de apartamentos en el Oeste), mientras que la Zona Oriente y Zona Centro se proyectan junto a la categoría de casas por su mayor prevalencia de vivienda unifamiliar (\(82\%\) en el Oriente). Por su parte, la Zona Sur y Norte se ubican casi sobre el origen (\(0.0\)), reflejando una distribución equilibrada de ambos tipos de propiedad. ## Tipo de vivienda vs. Barrio (agrupado)
De forma análoga al cruce anterior, se plantea:
tipo de
vivienda y barrio_agrup son independientes
(la proporción de Casas y Apartamentos es la misma en todos los
barrios).tipo de
vivienda y barrio_agrup no son
independientes (la proporción varía según el barrio).tabla_tipo_barrio <- table(vivienda_clean$tipo, vivienda_clean$barrio_agrup)
kable(as.data.frame.matrix(tabla_tipo_barrio),
caption = "Tabla 7. Tabla de contingencia: Tipo de vivienda x Barrio (agrupado)") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"))
| acopi | aguacatal | ciudad jardín | el caney | el ingenio | el limonar | el refugio | la flora | la hacienda | los cristales | normandía | pance | prados del norte | santa teresita | valle del lili | Otros | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Apartamento | 88 | 98 | 219 | 124 | 128 | 59 | 77 | 267 | 108 | 136 | 145 | 205 | 95 | 241 | 839 | 2243 |
| Casa | 68 | 9 | 271 | 84 | 71 | 76 | 43 | 98 | 55 | 17 | 4 | 169 | 30 | 11 | 168 | 1917 |
dim(tabla_tipo_barrio)
## [1] 2 16
chisq.test(tabla_tipo_barrio)
##
## Pearson's Chi-squared test
##
## data: tabla_tipo_barrio
## X-squared = 720.92, df = 15, p-value < 2.2e-16
res_ca_barrio <- CA(tabla_tipo_barrio, graph = FALSE)
n_dim_barrio <- nrow(res_ca_barrio$eig)
n_dim_barrio
## [1] 1
if (n_dim_barrio >= 2) {
fviz_ca_biplot(res_ca_barrio, repel = TRUE) +
labs(title = "Gráfico 15. Análisis de Correspondencias: Tipo de vivienda x Barrio (agrupado)")
} else {
extraer_dim1 <- function(coord_obj, etiqueta) {
m <- as.matrix(coord_obj)
if (ncol(m) == 0 || is.null(colnames(m))) m <- matrix(m[, 1], ncol = 1)
data.frame(categoria = rownames(as.matrix(coord_obj)),
dim1 = m[, 1], variable = etiqueta)
}
coords <- rbind(
extraer_dim1(res_ca_barrio$row$coord, "tipo"),
extraer_dim1(res_ca_barrio$col$coord, "barrio")
)
ggplot(coords, aes(x = dim1, y = categoria, color = variable)) +
geom_point(size = 3) +
geom_vline(xintercept = 0, linetype = "dashed") +
theme_minimal() +
labs(title = "Gráfico 15. Análisis de Correspondencias (1 dimensión): Tipo x Barrio",
x = "Dimensión 1", y = NULL)
}
Resultado de la prueba de hipótesis: con los datos
de este informe, la prueba arrojó \(X^2 =
720.92\), \(df = 15\), \(p\text{-value} < 2.2\times10^{-16}\).
Los grados de libertad confirman la dimensión de la tabla:
tipo tiene 2 categorías y barrio_agrup tiene
16 (15 barrios más frecuentes + “Otros”), por lo que \(df = (2-1)\times(16-1) = 15\). Como el
p-valor es mucho menor que \(\alpha =
0.05\), se rechaza \(H_0\): existe evidencia
estadística contundente de que tipo de vivienda y
barrio_agrup no son independientes la
proporción de Apartamentos vs. Casas varía de forma significativa según
el barrio, de manera consistente con lo ya observado a nivel de zona
(Gráfico 14).
Interpretación del Gráfico 15: Se observa una clara jerarquía: los barrios Normandía, Valle del Lili, Santa Teresita, Los Cristales y Aguacatal; registran los coeficientes más altos y positivos, lo que indica que son los sectores con mayor presencia de aparatamento como oferta habitacional. En un nivel intermedio se ubican Prados del Norte y La Flora, mientras que Pance, La Hacienda, El Refugio, El Caney y El Ingenio muestran una influencia positiva pero marginal. Por el contrario, un grupo significativo de elementos presenta coeficientes negativos, entre los que se incluyen Otros, El Limonar, Ciudad Jardín, y Acopi; esto sugiere que dichas categorías tienen una relación inversa con la variable, es decir, se asocian con una mayor oferta de viviendas tipo casa.
Aquí es donde longitud/latitud entran al
análisis: no como insumo de cálculo, sino para mostrar
dónde se ubican los patrones ya identificados (ver la nota
metodológica sobre estas variables en la sección de preparación de
datos).
# Gráfico 16. Mapa interactivo de propiedades coloreadas por conglomerado
pal <- colorFactor(palette = "Set1", domain = vivienda_clean$cluster)
leaflet(data = vivienda_clean %>% sample_n(min(1500, nrow(vivienda_clean)))) %>%
addTiles() %>%
addCircleMarkers(
lng = ~longitud, lat = ~latitud,
color = ~pal(cluster), radius = 4, stroke = FALSE, fillOpacity = .7,
popup = ~paste("Tipo:", tipo, "<br>Precio:", preciom, "M<br>Estrato:", estrato,
"<br>Cluster:", cluster)
) %>%
addLegend("bottomright", pal = pal, values = ~cluster, title = "Conglomerado")
Interpretación del Gráfico 16: este mapa interactivo (Gráfico 16) permite explorar caso por caso dónde se concentra cada conglomerado dentro de la ciudad. Si los colores (conglomerados) se agrupan en zonas geográficas claramente delimitadas, esto confirma que la segmentación obtenida por k-means basada únicamente en variables numéricas, sin usar coordenadas tiene, de todas formas, una coherencia espacial fuerte, lo cual es un hallazgo valioso para el negocio.
# Gráfico 17. Distribución geográfica de los conglomerados (versión estática)
ggplot(vivienda_clean, aes(x = longitud, y = latitud, color = cluster)) +
geom_point(alpha = .3, size = .8) +
theme_minimal() +
labs(title = "Gráfico 17. Distribución geográfica de los conglomerados",
x = "Longitud", y = "Latitud")
Interpretación del Gráfico 17: versión estática (no interactiva) del Gráfico 16, útil para incluir en presentaciones impresas o exportar como imagen. Debe leerse en conjunto con el Gráfico 16 y con la Tabla 5 (perfil de conglomerados) para una interpretación de negocio completa: qué zona, qué perfil de precio/tamaño y qué composición de estrato/tipo caracteriza a cada grupo.
Conclusiones principales:
Estructura y Dimensiones del Mercado (PCA): Estructura y Dimensiones del Mercado (PCA): Las dos primeras dimensiones explican un sólido 81.42% de la variabilidad total. La Dim1 (62.68%) actúa como un eje integral de escala y costo impulsado por área construida, precio, baños y parqueaderos, mientras que la Dim2 (18.75%) separa la densidad de habitaciones del valor del inmueble, demostrando que mayor número de alcobas no implica un precio superior. La proyección por estratos revela una transición socioeconómica clara de izquierda a derecha en Dim1, con el estrato 6 ubicado en la gama alta y el estrato 5 funcionando como un segmento de transición altamente heterogéneo.
Segmentación Operativa (k-means, \(k=4\)): El análisis de conglomerados (validado por el método del codo) identifica cuatro perfiles claros de negocio:
Vocación Territorial y Tipología (Análisis de Correspondencias): Existe una dependencia estadística altamente significativa (\(X^2 = 682.75, p < 2.2 \times 10^{-16}\)) entre el tipo de vivienda y la zona geográfica. La Zona Oeste concentra oferta vertical (86% apartamentos) y la Zona Oriente predomina en casas (82%), mientras que la Zona Sur presenta un comportamiento neutral y equilibrado. La proyección en mapa confirma que los clústeres numéricos mantienen una coherencia territorial natural.
Recomendaciones de Negocio:
Modelos de Tasación Segmentados: Reemplazar las métricas globales de precio por metro cuadrado por modelos de valoración ajustados al perfil del conglomerado (Tabla 5), evitando infravalorar inmuebles con alta densidad habitacional (Cluster 3) o sobrevalorar unidades pequeñas en zonas medias.
Estrategia Comercial Focalizada por Zona: Alinear la captación de portafolio con la vocación del territorio identificada: promover proyectos de apartamentos en las Zonas Oeste y Norte, y enfocar la comercialización de casas en Oriente y Centro.
Protocolos de Limpieza y Gobernanza de Datos: Mantener el tratamiento diferenciado de datos (imputación con sentido de negocio \(0\) para parqueaderos faltantes) y la contención de valores atípicos mediante percentiles para prevenir distorsiones en futuras actualizaciones del modelo.
Limitaciones del Estudio:
Precios de Oferta vs. Cierre: Los datos provienen de scraping web (OLX), por lo que reflejan las expectativas del vendedor y no el valor final de negociación inmobiliaria.
Agrupación Geográfica: La consolidación de barrios en las 15 categorías principales limita la precisión analítica en sectores periféricos o de menor representatividad (categoría “Otros”).
Ver Tabla 1 en la sección de Preparación y limpieza de datos, que
contiene la descripción, tipo original y rol analítico de cada variable
de la base vivienda.
Tablas:
vivienda.Gráficos:
Resumen consolidado de las decisiones de preparación y modelado justificadas a lo largo del informe:
parqueaderos se imputa como 0
(un NA se interpreta como ausencia de parqueadero, un
significado de negocio plausible), mientras que preciom,
areaconst, banios y habitaciones
se tratan mediante eliminación de registros (listwise deletion), al no
existir un valor por defecto con significado de negocio para esos
casos.preciom y areaconst, en lugar de la regla de
1.5×RIC, por ser más robusto frente a la asimetría natural (cola
derecha) de estas variables en el mercado inmobiliario y por ofrecer
control directo sobre la proporción de datos descartados.longitud/latitud como
variables activas en PCA y clustering, reservándolas para la etapa de
visualización geográfica.barrio en las 15 categorías más
frecuentes + “Otros” para el Análisis de Correspondencias.estrato y tipo como variables
suplementarias (ilustrativas) en el PCA, para interpretar los
componentes sin que influyan en su cálculo.sessionInfo()
## R version 4.6.1 (2026-06-24 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
##
## Matrix products: default
## LAPACK version 3.12.1
##
## locale:
## [1] LC_COLLATE=English_United States.utf8
## [2] LC_CTYPE=English_United States.utf8
## [3] LC_MONETARY=English_United States.utf8
## [4] LC_NUMERIC=C
## [5] LC_TIME=English_United States.utf8
##
## time zone: America/Bogota
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] kableExtra_1.4.1 leaflet_2.2.3 cluster_2.1.8.3
## [4] factoextra_2.2.0 FactoMineR_2.16 corrplot_0.95
## [7] forcats_1.0.1 tidyr_1.3.2 dplyr_1.2.1
## [10] paqueteMODELOS_0.1.0 summarytools_1.1.5 knitr_1.51
## [13] gridExtra_2.3.1 GGally_2.4.0 ggplot2_4.0.3
## [16] broom_1.0.13 boot_1.3-32
##
## loaded via a namespace (and not attached):
## [1] tidyselect_1.2.1 viridisLite_0.4.3 farver_2.1.2
## [4] S7_0.2.2 fastmap_1.2.0 digest_0.6.39
## [7] estimability_2.0.0 timechange_0.4.0 lifecycle_1.0.5
## [10] multcompView_0.1-12 magrittr_2.0.5 compiler_4.6.1
## [13] rlang_1.3.0 sass_0.4.10 tools_4.6.1
## [16] yaml_2.3.12 ggsignif_0.6.4 labeling_0.4.3
## [19] htmlwidgets_1.6.4 scatterplot3d_0.3-45 plyr_1.8.9
## [22] xml2_1.6.0 showtextdb_3.0 RColorBrewer_1.1-3
## [25] abind_1.4-8 withr_3.0.3 purrr_1.2.2
## [28] grid_4.6.1 ggpubr_1.0.0 sysfonts_0.8.9
## [31] xtable_1.8-8 emmeans_2.0.4 scales_1.4.0
## [34] MASS_7.3-65 flashClust_1.1-4 cli_3.6.6
## [37] mvtnorm_1.4-2 rmarkdown_2.31 generics_0.1.4
## [40] otel_0.2.0 rstudioapi_0.19.0 reshape2_1.4.5
## [43] cachem_1.1.0 pander_0.6.6 stringr_1.6.0
## [46] matrixStats_1.5.0 base64enc_0.1-6 vctrs_0.7.3
## [49] Matrix_1.7-5 carData_3.0-6 jsonlite_2.0.0
## [52] car_3.1-5 rapportools_1.2 rstatix_1.1.0
## [55] ggrepel_0.9.8 Formula_1.2-6 irlba_2.3.7
## [58] systemfonts_1.3.2 crosstalk_1.2.2 magick_2.9.1
## [61] jquerylib_0.1.4 glue_1.8.1 ggstats_0.13.0
## [64] ggtext_0.1.2 DT_0.34.0 lubridate_1.9.5
## [67] stringi_1.8.9 gtable_0.3.6 tibble_3.3.1
## [70] pillar_1.11.1 htmltools_0.5.9 showtext_0.9-8
## [73] R6_2.6.1 textshaping_1.0.5 tcltk_4.6.1
## [76] evaluate_1.0.5 lattice_0.22-9 backports_1.5.1
## [79] leaps_3.2 gridtext_0.1.6 ggsci_5.2.0
## [82] bslib_0.12.0 Rcpp_1.1.2 svglite_2.2.2
## [85] checkmate_2.3.4 xfun_0.60 pkgconfig_2.0.3