1 Introducción

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.

1.1 Racionalización del análisis

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.

2 Librerías y carga de datos

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

3 Preparación y limpieza de datos

3.1 Diccionario de variables

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)
Tabla 1. Diccionario de variables de la base de datos ‘vivienda’
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.

3.2 Clasificación y transformación de variables

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.

3.3 Reducción de niveles de barrio

La 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"))
Tabla 2. Frecuencia de publicaciones por barrio agrupado
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).

3.4 Tratamiento de valores faltantes y atípicos en variables numéricas

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"))
Tabla 3. Número de valores faltantes por variable numérica
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.

3.4.1 Imputación de 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:

  • Regla de 1.5×RIC (rango intercuartílico): marca como atípico todo valor fuera de \([Q_1 - 1.5\text{RIC},\ Q_3 + 1.5\text{RIC}]\). Es el estándar en boxplots, pero asume implícitamente una distribución razonablemente simétrica. Como se observó en el Gráfico 1 y en el histograma de precio (Gráfico 2), 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.
  • Percentil 1%-99%: en cambio, recorta un porcentaje fijo y conocido de antemano en cada extremo (el 1% más bajo y el 1% más alto), sin hacer ningún supuesto sobre la forma de la distribución. Esto da control directo y predecible sobre cuánta información se descarta (como máximo un 2% de los registros), independientemente de qué tan asimétrica sea la variable.

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.

4 Análisis exploratorio de datos (EDA)

# 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.

4.1 Matriz de correlación entre variables cuantitativas

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

5 Análisis de Componentes Principales (PCA)

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)

5.1 Selección del número de componentes

# 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"))
Tabla 4. Valores propios y varianza explicada por el PCA
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%).

5.2 Círculo de correlaciones (contribución de variables)

# 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.

5.3 Mapa de individuos coloreado por estrato

# 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.

5.3.1 Síntesis del PCA

  • 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.

6 Análisis de Conglomerados

6.1 Preparación: estandarización

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.

6.2 Número óptimo de clústeres

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.

6.3 K-means con el k seleccionado

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.

6.4 Perfil de los conglomerados

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"))
Tabla 5. Perfil promedio de variables numéricas por conglomerado
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.

7 Análisis de Correspondencias

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

7.1 Tipo de vivienda vs. Zona

7.1.1 Formulación de hipótesis

Antes de calcular el estadístico, se plantea formalmente la prueba de independencia que se va a evaluar:

  • \(H_0\) (hipótesis nula): 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.
  • \(H_1\) (hipótesis alternativa): 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"))
Tabla 6. Tabla de contingencia: Tipo de vivienda x Zona
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)

7.1.2 Formulación de hipótesis

De forma análoga al cruce anterior, se plantea:

  • \(H_0\): tipo de vivienda y barrio_agrup son independientes (la proporción de Casas y Apartamentos es la misma en todos los barrios).
  • \(H_1\): tipo de vivienda y barrio_agrup no son independientes (la proporción varía según el barrio).
  • Nivel de significancia: \(\alpha = 0.05\).
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"))
Tabla 7. Tabla de contingencia: Tipo de vivienda x Barrio (agrupado)
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.

8 Visualización integrada: mapa de propiedades y conglomerados

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.

9 Conclusiones y recomendaciones

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:

    • Cluster 1 (Lujo / Premium, \(N=904\)): Propiedades de alto valor (\(\sim\$1,017\text{M}\)), gran tamaño (\(355\text{ m}^2\)) y equipamiento superior (\(3.5\) parqueaderos).
    • Cluster 2 (Económico / Entrada, \(N=4,063\)): El segmento más denso del mercado, centrado en inmuebles compactos (\(88\text{ m}^2\)) y de bajo costo (\(\sim\$225\text{M}\)).
    • Cluster 3 (Familiar Multihabitacional, \(N=758\)): Inmuebles caracterizados por su alta capacidad ocupacional (\(6.8\) habitaciones promedio) y amplitud (\(308\text{ m}^2\)) a un precio intermedio.
    • Cluster 4 (Gama Media Estándar, \(N=2,438\)): El balance del mercado (\(\sim\$489\text{M}\) y \(179\text{ m}^2\)).
  • 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”).

10 Anexos

10.1 Anexo A. Diccionario de datos completo

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.

10.2 Anexo B. Índice de tablas y gráficos

Tablas:

  1. Diccionario de variables de la base de datos vivienda.
  2. Frecuencia de publicaciones por barrio agrupado.
  3. Número de valores faltantes por variable numérica.
  4. Valores propios y varianza explicada por el PCA.
  5. Perfil promedio de variables numéricas por conglomerado.
  6. Tabla de contingencia: Tipo de vivienda x Zona.
  7. Tabla de contingencia: Tipo de vivienda x Barrio (agrupado).

Gráficos:

  1. Distribución y atípicos de variables numéricas.
  2. Distribución del precio de venta.
  3. Precio por estrato socioeconómico.
  4. Precio por tipo de vivienda.
  5. Matriz de correlación entre variables numéricas.
  6. Varianza explicada por componente (scree plot).
  7. Círculo de correlaciones - Variables (PCA).
  8. Individuos en el plano PC1-PC2, coloreados por estrato.
  9. Método del codo (selección de k).
  10. Método de la silueta (selección de k).
  11. Conglomerados k-means proyectados en componentes principales.
  12. Composición de estratos dentro de cada conglomerado.
  13. Composición de tipo de vivienda dentro de cada conglomerado.
  14. Análisis de Correspondencias: Tipo de vivienda x Zona.
  15. Análisis de Correspondencias: Tipo de vivienda x Barrio (agrupado).
  16. Mapa interactivo de propiedades coloreadas por conglomerado.
  17. Distribución geográfica de los conglomerados (versión estática).

10.3 Anexo C. Criterios metodológicos aplicados

Resumen consolidado de las decisiones de preparación y modelado justificadas a lo largo del informe:

  • Tratamiento diferenciado de valores faltantes en variables numéricas activas: 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.
  • Recorte de atípicos extremos mediante percentil 1%-99% en 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.
  • Estandarización (media 0, varianza 1) de las variables numéricas antes de PCA y k-means, para evitar que la escala de una variable domine el cálculo de varianza/distancia.
  • Exclusión de longitud/latitud como variables activas en PCA y clustering, reservándolas para la etapa de visualización geográfica.
  • Agrupación de barrio en las 15 categorías más frecuentes + “Otros” para el Análisis de Correspondencias.
  • Uso de estrato y tipo como variables suplementarias (ilustrativas) en el PCA, para interpretar los componentes sin que influyan en su cálculo.

10.4 Anexo D. Información de sesión y reproducibilidad

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