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 muestran que preciom y areaconst presentan colas largas hacia la derecha, es decir, existen algunas propiedades con precios y áreas considerablemente por encima del resto del mercado (posibles propiedades de lujo o errores de digitación en la publicación original). parqueaderos, banios y habitaciones, al ser variables de conteo con rangos más acotados, muestran menos dispersión extrema. Esta evidencia visual es la que motiva el filtro de atípicos que se aplica más adelante.

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: la distribución del precio es asimétrica a la derecha, típico del mercado inmobiliario: la mayoría de las propiedades se concentran en un rango de precio medio-bajo, mientras que un número reducido de propiedades de alto valor “estira” la cola derecha de la distribución.

# 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: se observa una relación creciente y monotónica entre estrato y precio mediano: a mayor estrato, mayor precio típico de venta. Esto confirma que estrato es una variable de segmentación socioeconómica relevante y coherente con el comportamiento esperado del mercado.

# 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: el tipo de vivienda también se asocia con diferencias de precio; conviene revisar en el documento renderizado cuál categoría (Casa, Apartamento, etc.) presenta la mediana más alta y qué tan solapadas están las distribuciones, ya que un traslape amplio sugeriría que el tipo por sí solo no es suficiente para explicar el precio.

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: se espera una correlación positiva y alta entre areaconst, habitaciones, banios y preciom todas capturan, en mayor o menor medida, el “tamaño” de la propiedad. Esta correlación es precisamente la que el Análisis de Componentes Principales busca resumir en menos dimensiones sin perder información relevante.

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, 60)) +
  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 criterio de codo (punto donde la ganancia marginal de varianza explicada se aplana) y el criterio de Kaiser (valor propio > 1) permiten decidir cuántos componentes retener. En la mayoría de los casos con este tipo de variables, los dos primeros componentes explican una proporción alta y suficiente de la varianza total, por lo que el análisis se centra en el plano PC1-PC2.

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: las flechas cercanas entre sí y con longitud próxima a 1 indican variables altamente correlacionadas y bien representadas por los dos primeros componentes. Se espera que areaconst, habitaciones, banios, parqueaderos y preciom apunten en una dirección similar sobre PC1, confirmando que este eje resume el “tamaño/valor” general de la propiedad. El color (contribución) señala qué variables pesan más en la construcción de cada componente.

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: cada punto es una propiedad, proyectada en el plano de los dos primeros componentes. Si las elipses de los estratos altos se ubican sistemáticamente hacia el extremo positivo de PC1, esto confirma que el estrato socioeconómico está asociado al tamaño/valor de la propiedad, aunque no fue usado para calcular los componentes (es una variable suplementaria, no activa).

5.3.1 Síntesis del PCA

  • PC1 agrupa las variables de “tamaño/lujo de la propiedad” (areaconst, habitaciones, banios, parqueaderos, preciom) con cargas del mismo signo: representa un eje general de tamaño y valor de la vivienda.
  • PC2 suele separar variables que no están tan correlacionadas con el tamaño (p. ej. la relación habitaciones vs banios o parqueaderos vs área), reflejando configuraciones distintas de propiedades de tamaño similar.
  • Los estratos altos tienden a ubicarse hacia el extremo positivo de PC1, confirmando la asociación entre estrato y tamaño/valor de la propiedad.

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 método del codo (Gráfico 9) busca el punto donde agregar un clúster adicional deja de reducir sustancialmente la suma de cuadrados dentro de los grupos (WSS); el método de la silueta (Gráfico 10) busca el número de clústeres que maximiza la cohesión interna y separación entre grupos. El valor de k finalmente usado (k_elegido, ver más abajo) debe ajustarse al punto de codo/máximo real que se observe al renderizar el documento con los datos completos.

Se usa una muestra de hasta 1.000 observaciones (muestra_idx) solo para el cálculo de estos dos indicadores, porque evaluar kmeans para k = 1..10 sobre la base completa es computacionalmente costoso y el objetivo aquí es únicamente diagnosticar el número de grupos, no ajustar el modelo final.

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: cada color representa un conglomerado distinto, proyectado sobre las dos primeras dimensiones. Grupos bien separados y compactos indican una segmentación clara; grupos que se traslapan sugieren que esas propiedades comparten características similares y que quizás convenga evaluar un k distinto.

Racionalización de nstart = 25: k-means es sensible a la inicialización aleatoria de los centroides y puede converger a óptimos locales distintos en cada corrida. nstart = 25 ejecuta el algoritmo 25 veces con distintos puntos de partida y conserva la mejor solución (menor suma de cuadrados dentro de los grupos), reduciendo el riesgo de quedarse con una segmentación subóptima.

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: esta tabla es la “ficha técnica” de cada segmento de mercado. Se espera encontrar conglomerados claramente diferenciados en precio y área por ejemplo, un segmento de vivienda pequeña y económica, uno intermedio y uno de vivienda grande y costosa lo cual valida que la segmentación captura diferencias de negocio reales y no solo ruido estadístico. Cabe notar que, como parqueaderos incluye ceros reales (propiedades sin parqueadero, ver sección de imputación) en lugar de haber eliminado esos registros, es posible que el conglomerado de menor precio/área también muestre el promedio de parqueaderos más bajo, reforzando la coherencia interna del segmento económico.

# 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: muestra qué tan homogéneo es cada conglomerado en términos de estrato socioeconómico. Un conglomerado dominado por un solo estrato (barra casi de un solo color) sugiere una segmentación alineada con el nivel socioeconómico; un conglomerado con estratos mezclados sugiere que la segmentación está capturando otro tipo de diferencia (p. ej. tamaño) independiente del estrato.

# 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: de forma análoga al Gráfico 12, permite verificar si los conglomerados están asociados a un tipo de vivienda predominante (p. ej. un segmento compuesto casi en su totalidad por apartamentos), lo que reforzaría la coherencia de negocio de la segmentación obtenida.

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: como tipo solo tiene 2 categorías (Apartamento, Casa), el número de dimensiones del Análisis de Correspondencias es \(\min(2-1,\ 5-1) = 1\) (confirmado por dim(tabla_tipo_zona) = [1] 2 5 y n_dim_zona = 1). Por esta razón el Gráfico 14 no se muestra como un biplot de dos ejes, sino como una proyección sobre una única dimensión: las categorías de zona que quedan del mismo lado que “Apartamento” están más asociadas a ese tipo de vivienda que a “Casa”, y viceversa para las que quedan del lado de “Casa”. Con base en la Tabla 6, se espera que Zona Oeste y Zona Norte (con mayor proporción de apartamentos) aparezcan del lado de “Apartamento”, mientras que Zona Oriente y Zona Centro (con mayor proporción de casas) aparezcan del lado opuesto. Zona Sur, al tener una distribución más cercana a la proporción global de tipo de vivienda, debería ubicarse más cerca del origen (menor asociación diferencial con cualquiera de los dos tipos).

7.2 Tipo de vivienda vs. Barrio (agrupado)

7.2.1 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: De forma análoga al Gráfico 14, pero a nivel de barrio en lugar de zona, este mapa perceptual permite identificar nichos geográficos más finos: barrios específicos donde un tipo de vivienda predomina claramente sobre el otro. Como tipo también solo tiene 2 categorías, es esperable que al igual que con zonael CA produzca una única dimensión, por lo que la lectura del gráfico sigue la misma lógica: los barrios ubicados del lado de “Apartamento” tienen una proporción de apartamentos mayor a la esperada bajo independencia, y los del lado de “Casa” tienen una proporción de casas mayor a la esperada. Esta es información directamente accionable para decisiones de expansión o inversión localizada. # Visualización integrada: mapa de propiedades y conglomerados

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

Hallazgos por técnica:

  • Componentes Principales (Gráficos 6, 7 y 8; Tabla 4). El PCA muestra que la variabilidad del mercado no se explica por una sola característica aislada, sino por combinaciones de variables que se mueven juntas. El primer componente (PC1) está asociado simultáneamente a areaconst, habitaciones, banios, parqueaderos y preciom, lo que permite interpretarlo como un eje general de tamaño y valor de la propiedad, más que atribuirlo a una única variable. El segundo componente (PC2) captura una fuente de variación distinta, relacionada con la configuración interna de la vivienda (p. ej. relación entre habitaciones y baños), independiente del tamaño global. En conjunto, ambos componentes explican una proporción relevante de la varianza total (ver Tabla 4), lo que valida usar el plano PC1-PC2 como resumen razonable del comportamiento de las variables numéricas del mercado.

  • Conglomerados (Gráficos 9 a 13; Tabla 5). La segmentación por k-means identifica grupos de propiedades con perfiles diferenciados de precio, área y equipamiento (parqueaderos, baños, habitaciones). La imputación diferenciada de parqueaderos tratando sus valores faltantes como ausencia real de parqueadero (0) en lugar de eliminarlos permitió que esta variable participara con su información completa en la segmentación, evitando sesgar los conglomerados hacia las propiedades que sí reportaban parqueadero. Los conglomerados resultantes muestran, además, una composición no homogénea de estrato y tipo de vivienda (Gráficos 12 y 13), lo que sugiere que la segmentación captura patrones de mercado que van más allá de la simple clasificación socioeconómica oficial.

  • Correspondencias (Gráficos 14 y 15; Tablas 6 y 7). Existe una asociación estadísticamente significativa entre el tipo de vivienda y su ubicación geográfica (zona y barrio), confirmada por la prueba Chi-cuadrado. Esto indica que la oferta inmobiliaria no está distribuida de manera uniforme en la ciudad: ciertos tipos de vivienda se concentran en zonas o barrios específicos, lo cual constituye evidencia de nichos geográficos con vocación de mercado propia.

  • Visualización geográfica (Gráficos 16 y 17). Al proyectar los conglomerados formados exclusivamente a partir de variables numéricas, sin usar coordenadas sobre el mapa de la ciudad, se observa coherencia espacial entre los segmentos y su ubicación. Esto es un hallazgo relevante en sí mismo: la segmentación estadística no solo tiene sentido en términos de precio y tamaño, sino que también se traduce en una lógica territorial reconocible.

  • Recomendaciones estratégicas:

  1. Tasación por segmento, no por promedio general. En lugar de una única fórmula de valoración, usar el perfil de cada conglomerado (Tabla 5) como referencia base, ajustando por las variables que definen a ese segmento específico (tamaño, parqueaderos, zona).
  2. Priorización comercial por afinidad tipo-zona. Concentrar los esfuerzos de promoción y captación de nuevas propiedades en las combinaciones tipo de vivienda–zona/barrio que el Análisis de Correspondencias identificó como más asociadas (Gráficos 14 y 15), en lugar de aplicar una estrategia uniforme en toda la ciudad.
  3. Portafolios de inversión diferenciados. Usar los conglomerados como unidad de análisis para decisiones de inversión, distinguiendo entre segmentos de vivienda económica, intermedia y de alto valor, cada uno con su propia dinámica de precio y demanda.
  4. Auditoría de calidad de datos como parte del proceso. El tratamiento diferenciado de valores faltantes (imputación con significado de negocio para parqueaderos, eliminación para el resto) y el filtro de atípicos por percentil, robusto a la asimetría propia del precio y el área, deben mantenerse como estándar en actualizaciones futuras del análisis, para no introducir sesgos por decisiones de limpieza inconsistentes.
  5. Actualización periódica del análisis. Dado que el mercado inmobiliario es dinámico, se recomienda repetir este pipeline (PCA, conglomerados, correspondencias) con una frecuencia definida (p. ej. trimestral), monitoreando si los segmentos, sus perfiles y su distribución geográfica se mantienen estables o se desplazan con el tiempo.
  • Limitaciones a tener en cuenta
  1. El número de conglomerados (k_elegido) fue fijado como parámetro editable y debe validarse con los resultados reales del método del codo y la silueta (Gráficos 9 y 10) antes de tomar decisiones basadas en la Tabla 5.
  2. La agrupación de barrio en las 15 categorías más frecuentes (Tabla 2) simplifica el análisis de correspondencias, pero implica que los patrones específicos de barrios menos frecuentes quedan diluidos en la categoría “Otros”.
  3. Los datos provienen de publicaciones de OLX vía webscraping, por lo que reflejan precios de oferta, no necesariamente precios de cierre de transacción; esto debe considerarse al usar los resultados para valoración.

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