1 Planteamiento, datos y paquetes

Una empresa inmobiliaria líder en una gran ciudad está buscando comprender en profundidad el mercado de viviendas urbanas para tomar decisiones estratégicas más informadas. La empresa posee una base de datos extensa que contiene información detallada sobre diversas propiedades residenciales disponibles en el mercado. Se requiere realizar un análisis holístico de estos datos para identificar patrones, relaciones y segmentaciones relevantes que permitan mejorar la toma de decisiones en cuanto a la compra, venta y valoración de propiedades.

# Instalación de paquetes
install.packages(c("tidyverse","janitor","skimr","FactoMineR","factoextra","cluster","NbClust","ggrepel"))
install.packages("remotes")
remotes::install_github("centromagis/paqueteMODELOS", force=TRUE)
knitr::opts_chunk$set(echo = TRUE, message = FALSE, warning = FALSE)
set.seed(1234)
sessionInfo() ## reproducibilidad
## R version 4.4.3 (2025-02-28 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26100)
## 
## Matrix products: default
## 
## 
## locale:
## [1] LC_COLLATE=Spanish_Colombia.utf8  LC_CTYPE=Spanish_Colombia.utf8   
## [3] LC_MONETARY=Spanish_Colombia.utf8 LC_NUMERIC=C                     
## [5] LC_TIME=Spanish_Colombia.utf8    
## 
## time zone: America/Bogota
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## loaded via a namespace (and not attached):
##  [1] digest_0.6.37     R6_2.6.1          fastmap_1.2.0     xfun_0.52        
##  [5] cachem_1.1.0      knitr_1.50        htmltools_0.5.8.1 rmarkdown_2.29   
##  [9] lifecycle_1.0.4   cli_3.6.4         sass_0.4.9        jquerylib_0.1.4  
## [13] compiler_4.4.3    rstudioapi_0.17.1 tools_4.4.3       evaluate_1.0.3   
## [17] bslib_0.9.0       yaml_2.3.10       rlang_1.1.5       jsonlite_2.0.0
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.4     ✔ readr     2.1.5
## ✔ forcats   1.0.0     ✔ stringr   1.5.1
## ✔ ggplot2   3.5.2     ✔ tibble    3.2.1
## ✔ lubridate 1.9.4     ✔ tidyr     1.3.1
## ✔ purrr     1.0.4     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(janitor)
## 
## Adjuntando el paquete: 'janitor'
## 
## The following objects are masked from 'package:stats':
## 
##     chisq.test, fisher.test
library(skimr)
library(FactoMineR)
library(factoextra)
## Welcome! Want to learn more? See two factoextra-related books at https://goo.gl/ve3WBa
library(cluster)
library(NbClust)
library(ggrepel)
library(tidyr)
library(paqueteMODELOS)
## Cargando paquete requerido: boot
## Cargando paquete requerido: broom
## Cargando paquete requerido: GGally
## Cargando paquete requerido: gridExtra
## 
## Adjuntando el paquete: 'gridExtra'
## 
## The following object is masked from 'package:dplyr':
## 
##     combine
## 
## Cargando paquete requerido: knitr
## Cargando paquete requerido: summarytools
## 
## Adjuntando el paquete: 'summarytools'
## 
## The following object is masked from 'package:tibble':
## 
##     view
# Carga de base desde el paqueteMODELOS
data("vivienda", package = "paqueteMODELOS")

# Copia original y limpieza de nombres de variables
df_raw <- vivienda
df <- df_raw %>% janitor::clean_names()

# Vista de estructura
glimpse(df)
## Rows: 8,322
## Columns: 13
## $ id           <dbl> 1147, 1169, 1350, 5992, 1212, 1724, 2326, 4386, 1209, 159…
## $ zona         <chr> "Zona Oriente", "Zona Oriente", "Zona Oriente", "Zona Sur…
## $ piso         <chr> NA, NA, NA, "02", "01", "01", "01", "01", "02", "02", "02…
## $ estrato      <dbl> 3, 3, 3, 4, 5, 5, 4, 5, 5, 5, 6, 4, 5, 6, 4, 5, 5, 4, 5, …
## $ preciom      <dbl> 250, 320, 350, 400, 260, 240, 220, 310, 320, 780, 750, 62…
## $ areaconst    <dbl> 70, 120, 220, 280, 90, 87, 52, 137, 150, 380, 445, 355, 2…
## $ parqueaderos <dbl> 1, 1, 2, 3, 1, 1, 2, 2, 2, 2, NA, 3, 2, 2, 1, 4, 2, 2, 2,…
## $ banios       <dbl> 3, 2, 2, 5, 2, 3, 2, 3, 4, 3, 7, 5, 6, 2, 4, 4, 4, 3, 2, …
## $ habitaciones <dbl> 6, 3, 4, 3, 3, 3, 3, 4, 6, 3, 6, 5, 6, 2, 5, 5, 4, 3, 3, …
## $ tipo         <chr> "Casa", "Casa", "Casa", "Casa", "Apartamento", "Apartamen…
## $ barrio       <chr> "20 de julio", "20 de julio", "20 de julio", "3 de julio"…
## $ longitud     <dbl> -76.51168, -76.51237, -76.51537, -76.54000, -76.51350, -7…
## $ latitud      <dbl> 3.43382, 3.43369, 3.43566, 3.43500, 3.45891, 3.36971, 3.4…

2 Limpieza rápida y EDA mínima (para contexto)

Se hará una limpieza mínima (variables clave), revisión de faltantes y estadísticos básicos para entender rangos y outliers.

# Ajuste de tipos
df <- df %>%
  mutate(
    piso = na_if(piso, "NA"),              # convertir texto "NA" en NA real
    piso = readr::parse_number(piso),      # "02" -> 2
    zona = as.factor(zona),
    tipo = as.factor(tipo),
    barrio = as.factor(barrio)
  )

# Conteo de NA por variable
df %>% summarise(across(everything(), ~sum(is.na(.))))
# Variables numéricas para PCA y Conglomerados
num_vars <- c("preciom","areaconst","estrato","parqueaderos",
              "banios","habitaciones","piso")

df_num <- df %>% 
  select(all_of(num_vars)) %>%
  drop_na()

# Variables categóricas para Correspondencias
cat_vars <- c("zona","tipo","barrio")

df_cat <- df %>%
  mutate(barrio = fct_lump_n(barrio, n = 20)) %>%  # agrupar barrios poco frecuentes
  select(all_of(cat_vars))
# Se verifica que no haya NA en numérico
sum(is.na(df_num))
## [1] 0
# Se Verifica dimensiones
dim(df_num)
## [1] 4808    7
dim(df_cat)
## [1] 8322    3
## Exploración visual: Boxplots individuales por variable numérica

# Precio por millón
ggplot(df_num, aes(y = preciom)) +
  geom_boxplot(fill = "#69b3a2") +
  labs(title = "Boxplot - Precio (millones)", y = "Precio (millones)") +
  theme_minimal()

# Área construida
ggplot(df_num, aes(y = areaconst)) +
  geom_boxplot(fill = "#ffb347") +
  labs(title = "Boxplot - Área construida (m²)", y = "Área construida (m²)") +
  theme_minimal()

# Estrato
ggplot(df_num, aes(y = estrato)) +
  geom_boxplot(fill = "#6fa3ef") +
  labs(title = "Boxplot - Estrato", y = "Estrato") +
  theme_minimal()

# Parqueaderos
ggplot(df_num, aes(y = parqueaderos)) +
  geom_boxplot(fill = "#f76c6c") +
  labs(title = "Boxplot - Parqueaderos", y = "Número de parqueaderos") +
  theme_minimal()

# Baños
ggplot(df_num, aes(y = banios)) +
  geom_boxplot(fill = "#ffcc5c") +
  labs(title = "Boxplot - Baños", y = "Número de baños") +
  theme_minimal()

# Habitaciones
ggplot(df_num, aes(y = habitaciones)) +
  geom_boxplot(fill = "#88d8b0") +
  labs(title = "Boxplot - Habitaciones", y = "Número de habitaciones") +
  theme_minimal()

# Piso
ggplot(df_num, aes(y = piso)) +
  geom_boxplot(fill = "#cbaacb") +
  labs(title = "Boxplot - Piso", y = "Número de piso") +
  theme_minimal()

3 Análisis de Componentes Principales

Reducir la dimensionalidad del conjunto de datos y visualizar la estructura de las variables en componentes principales para identificar características clave que influyen en la variación de precios y oferta del mercado.

# Bloque 1: Preparación de insumo PCA
num_vars <- c("preciom","areaconst","estrato",
              "parqueaderos","banios","habitaciones","piso")
cat_vars <- c("zona","tipo","barrio")

# Índice de filas completas en numéricas
idx_complete <- df %>% 
  select(all_of(num_vars)) %>% 
  complete.cases()

# Dataset final para PCA (7 numéricas + 3 cualitativas)
df_pca <- df %>% 
  filter(idx_complete) %>% 
  mutate(barrio = forcats::fct_lump_n(barrio, n = 20)) %>% 
  select(all_of(num_vars), all_of(cat_vars))

# Comprobaciones rápidas
glimpse(df_pca)
## Rows: 4,808
## Columns: 10
## $ preciom      <dbl> 400, 260, 240, 220, 310, 320, 780, 625, 750, 520, 600, 42…
## $ areaconst    <dbl> 280, 90, 87, 52, 137, 150, 380, 355, 237, 98, 160, 200, 1…
## $ estrato      <dbl> 4, 5, 5, 4, 5, 5, 5, 4, 5, 6, 4, 5, 5, 4, 5, 3, 6, 6, 4, …
## $ parqueaderos <dbl> 3, 1, 1, 2, 2, 2, 2, 3, 2, 2, 1, 4, 2, 2, 2, 1, 1, 2, 1, …
## $ banios       <dbl> 5, 2, 3, 2, 3, 4, 3, 5, 6, 2, 4, 4, 4, 3, 2, 2, 4, 3, 2, …
## $ habitaciones <dbl> 3, 3, 3, 3, 4, 6, 3, 5, 6, 2, 5, 5, 4, 3, 3, 3, 4, 4, 3, …
## $ piso         <dbl> 2, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 4, 5, 5, 5, …
## $ zona         <fct> Zona Sur, Zona Norte, Zona Norte, Zona Norte, Zona Norte,…
## $ tipo         <fct> Casa, Apartamento, Apartamento, Apartamento, Apartamento,…
## $ barrio       <fct> Other, Other, Other, Other, Other, Other, Other, Other, O…
colSums(is.na(df_pca[num_vars]))
##      preciom    areaconst      estrato parqueaderos       banios habitaciones 
##            0            0            0            0            0            0 
##         piso 
##            0
nrow(df_pca)
## [1] 4808

Se toma solo las filas completas en las 7 variables numéricas (lo que evita errores del PCA).

Se mantiene zona, tipo y barrio como variables cualitativas suplementarias para colorear e interpretar los planos.

Se agrupa barrios poco frecuentes para que las leyendas sean manejables.

# Bloque 2: Ejecutar PCA
pca_res <- FactoMineR::PCA(df_pca, 
                           scale.unit = TRUE,   # Estandariza variables
                           quali.sup = 8:10,    # Variables cualitativas suplementarias
                           graph = FALSE)

# Varianza explicada por componente
pca_res$eig
##        eigenvalue percentage of variance cumulative percentage of variance
## comp 1  3.5849689              51.213842                          51.21384
## comp 2  1.3598495              19.426422                          70.64026
## comp 3  0.8095183              11.564547                          82.20481
## comp 4  0.4772999               6.818570                          89.02338
## comp 5  0.3422676               4.889537                          93.91292
## comp 6  0.2447078               3.495825                          97.40874
## comp 7  0.1813880               2.591257                         100.00000
# Scree plot
factoextra::fviz_eig(pca_res, addlabels = TRUE, ylim = c(0, 60))

Se ejecuta el PCA con FactoMineR usando las 7 numéricas activas.

Incluye las 3 cualitativas como suplementarias para no distorsionar el cálculo, pero poder interpretarlas luego.

PC1 explica más de la mitad de la varianza total (51.2%), lo que indica que hay un patrón dominante muy fuerte en los datos.

PC2 añade un 19.4%, alcanzando 70.6% acumulado.

Con PC3 llegamos a 82.2% acumulado, lo cual es excelente en términos de retener información.

# Bloque 3: Contribución de variables

# Visualización de las variables en el espacio de los componentes
factoextra::fviz_pca_var(pca_res,
                         col.var = "contrib",   # Color según contribución
                         gradient.cols = c("#00AFBB", "#E7B800", "#FC4E07"),
                         repel = TRUE)

# Tabla de contribuciones (porcentaje) a PC1, PC2 y PC3
contrib <- as.data.frame(pca_res$var$contrib)
round(contrib[, 1:3], 2)   # Se muestra los 3 primeros componentes

PC1 (51.2% de varianza): Valores altos en PC1 - viviendas más grandes, más caras, con más baños y en estratos más altos. Valores bajos en PC1 - propiedades más pequeñas, económicas y de estratos más bajos.

PC2 (19.4% de varianza): Mide principalmente la altura (número de piso), diferenciando propiedades en edificios altos frente a casas o apartamentos en plantas bajas.

PC3 (11.6% de varianza): Refuerza la idea de altura, pero combinado con el número de habitaciones.

# ---- Bloque 4: Biplots de individuos ----

# Biplot coloreado por tipo de propiedad
factoextra::fviz_pca_ind(pca_res,
                         geom.ind = "point",
                         col.ind = df_pca$tipo,
                         palette = "jco",
                         legend.title = "Tipo de vivienda",
                         repel = TRUE)

# Biplot coloreado por zona de la ciudad
factoextra::fviz_pca_ind(pca_res,
                         geom.ind = "point",
                         col.ind = df_pca$zona,
                         palette = "jco",
                         legend.title = "Zona",
                         repel = TRUE)

Tipo de vivienda

Apartamentos (círculos azules) tienden a concentrarse hacia valores más altos en PC2.

Casas (triángulos amarillos) aparecen más hacia la parte baja de PC2 y se dispersan más horizontalmente en PC1.

Zona

Zonas como Norte y Centro muestran más concentración hacia la derecha de PC1, asociadas a mayor tamaño, precio y estrato.

Zonas como Sur y Oriente se distribuyen más hacia la izquierda de PC1, indicando propiedades con menor tamaño y valor en promedio.

Zona Oeste se sitúa en posiciones intermedias.

4 Análisis de Conglomerados

Agrupar las propiedades residenciales en segmentos homogéneos con características similares para entender las dinámicas de las ofertas específicas en diferentes partes de la ciudad y en diferentes estratos socioeconómicos.

Se usa las coordenadas del PCA (pca_res\(ind\)coord) que ya están estandarizadas y representan la variabilidad de forma resumida.

Se toma las primeras 3 componentes porque explican el 82% de la varianza.

# Preparación de datos para clustering 
# Se extrae coordenadas de los individuos en las primeras 3 PCs
pca_coords <- as.data.frame(pca_res$ind$coord[, 1:3])

# Comprobación estructura
head(pca_coords)
# Elegir k 
# Elbow (WSS)
factoextra::fviz_nbclust(pca_coords, kmeans, method = "wss", k.max = 10)

# Silhouette promedio
factoextra::fviz_nbclust(pca_coords, kmeans, method = "silhouette", k.max = 10)

# K-means con k = 3
set.seed(123)
km_res <- kmeans(pca_coords, centers = 3, nstart = 25)

# Agrega los clústeres al dataset original
df_cluster <- df_num %>%
  mutate(cluster = factor(km_res$cluster))

# Visualizar en el espacio PCA
factoextra::fviz_cluster(km_res, data = pca_coords,
                         geom = "point", ellipse.type = "norm",
                         main = "Clústeres en espacio PCA")

# Resumen estadístico por clúster
df_cluster %>%
  group_by(cluster) %>%
  summarise(across(everything(), list(media = mean, sd = sd), .names = "{.col}_{.fn}"),
            .groups = "drop")
# Boxplots por variable y clúster
df_cluster_long <- df_cluster %>%
  pivot_longer(cols = -cluster, names_to = "variable", values_to = "valor")

ggplot(df_cluster_long, aes(x = cluster, y = valor, fill = cluster)) +
  geom_boxplot() +
  facet_wrap(~variable, scales = "free_y") +
  theme_minimal() +
  labs(title = "Distribución de variables por clúster")

# Dataset maestro con cluster, cualitativas y PCs 
# (usa los objetos ya creados: df_pca, pca_coords, km_res)

df_clusters <- df_pca %>%
  mutate(
    PC1 = pca_coords$`Dim.1`,
    PC2 = pca_coords$`Dim.2`,
    PC3 = pca_coords$`Dim.3`,
    cluster = factor(km_res$cluster)
  )

glimpse(df_clusters)   # comprobación de estructura
## Rows: 4,808
## Columns: 14
## $ preciom      <dbl> 400, 260, 240, 220, 310, 320, 780, 625, 750, 520, 600, 42…
## $ areaconst    <dbl> 280, 90, 87, 52, 137, 150, 380, 355, 237, 98, 160, 200, 1…
## $ estrato      <dbl> 4, 5, 5, 4, 5, 5, 5, 4, 5, 6, 4, 5, 5, 4, 5, 3, 6, 6, 4, …
## $ parqueaderos <dbl> 3, 1, 1, 2, 2, 2, 2, 3, 2, 2, 1, 4, 2, 2, 2, 1, 1, 2, 1, …
## $ banios       <dbl> 5, 2, 3, 2, 3, 4, 3, 5, 6, 2, 4, 4, 4, 3, 2, 2, 4, 3, 2, …
## $ habitaciones <dbl> 3, 3, 3, 3, 4, 6, 3, 5, 6, 2, 5, 5, 4, 3, 3, 3, 4, 4, 3, …
## $ piso         <dbl> 2, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 4, 5, 5, 5, …
## $ zona         <fct> Zona Sur, Zona Norte, Zona Norte, Zona Norte, Zona Norte,…
## $ tipo         <fct> Casa, Apartamento, Apartamento, Apartamento, Apartamento,…
## $ barrio       <fct> Other, Other, Other, Other, Other, Other, Other, Other, O…
## $ PC1          <dbl> 1.01096475, -1.26280849, -0.95886551, -1.36447900, -0.075…
## $ PC2          <dbl> -0.80969721, -0.35649492, -0.41870534, -0.82941167, -0.69…
## $ PC3          <dbl> -0.327252253, -1.184910936, -1.050905833, -0.986736046, -…
## $ cluster      <fct> 2, 3, 3, 3, 2, 2, 2, 2, 1, 2, 2, 2, 2, 3, 3, 3, 2, 2, 3, …
table(df_clusters$cluster)  # tamaño de cada clúster
## 
##    1    2    3 
##  646 1711 2451
# Perfil numérico por clúster 
perfil_numerico <- df_clusters %>%
  group_by(cluster) %>%
  summarise(across(
    .cols = c(preciom, areaconst, estrato, parqueaderos, banios, habitaciones, piso),
    .fns = list(media = mean, sd = sd),
    .names = "{.col}_{.fn}"
  ),
  .groups = "drop"
  )

perfil_numerico

Clúster 1 — Segmento de viviendas grandes y costosas

Precio medio: 1.040 millones (el más alto de todos)

Área promedio: 418 m² (muy superior al resto)

Estrato: 5.5 - viviendas en estratos altos

Características: más parqueaderos, más habitaciones y más baños que otros grupos.

Perfil: Propiedades de lujo, probablemente casas grandes en zonas exclusivas.

Clúster 2 — Segmento intermedio

Precio medio: 533 millones

Área promedio: 200 m²

Estrato: 5.2 → estratos altos-medios

Características: tamaño intermedio, buena dotación de parqueaderos y baños.

Perfil: Viviendas cómodas, posiblemente apartamentos amplios o casas medianas.

Clúster 3 — Segmento económico

Precio medio: 251 millones (el más bajo)

Área promedio: 93 m² (mucho menor que los otros)

Estrato: 4.38 - estrato medio

Características: menor número de parqueaderos, baños y habitaciones.

Perfil: Viviendas más pequeñas, probablemente apartamentos en zonas urbanas más asequibles.

# Perfil cualitativo por clúster
perfil_cualitativo <- df_clusters %>%
  group_by(cluster) %>%
  summarise(
    zona_top = names(sort(table(zona), decreasing = TRUE))[1],
    tipo_top = names(sort(table(tipo), decreasing = TRUE))[1],
    barrio_top = names(sort(table(barrio), decreasing = TRUE))[1],
    .groups = "drop"
  )

perfil_cualitativo
# Tablas de frecuencia para cada variable
tabla_zona <- table(df_clusters$cluster, df_clusters$zona)
tabla_tipo <- table(df_clusters$cluster, df_clusters$tipo)
tabla_barrio <- table(df_clusters$cluster, df_clusters$barrio)

tabla_zona
##    
##     Zona Centro Zona Norte Zona Oeste Zona Oriente Zona Sur
##   1           1         78        140            5      422
##   2          12        270        435           35      959
##   3          23        538        178           57     1655
tabla_tipo
##    
##     Apartamento Casa
##   1         132  514
##   2         944  767
##   3        2106  345
tabla_barrio
##    
##     aguacatal ciudad 2000 ciudad jardín cristales el caney el ingenio
##   1        10           1           136         6        2         19
##   2        26          19           188        27       37         65
##   3        16          36            49        15       94         64
##    
##     el limonar el refugio la flora la hacienda los cristales normandía
##   1         19          0       14           4            14        29
##   2         27         22       64          34            70        55
##   3         37         65      187          93            32        20
##    
##     nueva tequendama pance prados del norte quintas de don santa teresita
##   1                2   102                5              1             45
##   2               23   166               11             20            107
##   3               25     7               54             36             10
##    
##     valle del lili versalles zona sur Other
##   1              7         5        8   217
##   2             46        18       16   670
##   3            582        25       27   977

group_by(cluster): Agrupa las observaciones según el clúster asignado por K-means.

summarise(): Calcula la categoría más frecuente para cada variable cualitativa (zona, tipo, barrio).

Tablas de frecuencia (table): Permiten ver cuántos registros de cada categoría hay en cada clúster.

# ==============================
# 1. PERFIL NUMÉRICO - GRÁFICO
# ==============================
library(reshape2)

# Tomamos las medias por clúster
perfil_numerico_media <- perfil_numerico %>%
  select(cluster, ends_with("_media")) %>%
  rename_with(~ gsub("_media", "", .x))

# Pasar a formato largo
perfil_numerico_long <- melt(perfil_numerico_media, id.vars = "cluster")

# Gráfico de barras comparativo
ggplot(perfil_numerico_long, aes(x = variable, y = value, fill = cluster)) +
  geom_bar(stat = "identity", position = "dodge") +
  theme_minimal() +
  labs(title = "Perfil numérico por clúster",
       x = "Variable", y = "Media") +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

# ==============================
# 2. PERFIL CUALITATIVO - GRÁFICO
# ==============================

# Zona
ggplot(df_clusters, aes(x = zona, fill = cluster)) +
  geom_bar(position = "fill") +
  scale_y_continuous(labels = scales::percent_format()) +
  theme_minimal() +
  labs(title = "Distribución porcentual de Zonas por Clúster", y = "%", x = "Zona") +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

# Tipo
ggplot(df_clusters, aes(x = tipo, fill = cluster)) +
  geom_bar(position = "fill") +
  scale_y_continuous(labels = scales::percent_format()) +
  theme_minimal() +
  labs(title = "Distribución porcentual de Tipos de Vivienda por Clúster", y = "%", x = "Tipo")

# Barrio (solo los 10 más frecuentes para visualización clara)
top_barrios <- df_clusters %>%
  count(barrio, sort = TRUE) %>%
  top_n(10, n) %>%
  pull(barrio)

ggplot(df_clusters %>% filter(barrio %in% top_barrios),
       aes(x = barrio, fill = cluster)) +
  geom_bar(position = "fill") +
  scale_y_continuous(labels = scales::percent_format()) +
  theme_minimal() +
  labs(title = "Distribución porcentual de Barrios (Top 10) por Clúster", y = "%", x = "Barrio") +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

El análisis de conglomerados revela tres perfiles claramente diferenciados. El Clúster 1 agrupa viviendas de alto precio y gran área construida, ubicadas en estratos altos, con mayor número de parqueaderos y baños; predominan las casas en zonas exclusivas como Ciudad Jardín y Pance, principalmente en la Zona Oeste y Zona Sur. El Clúster 2 presenta precios y áreas intermedias, en estrato medio-alto, con una distribución equilibrada entre casas y apartamentos; se concentra en zonas como Norte y Oeste, destacando barrios como Santa Teresita. Finalmente, el Clúster 3 reúne viviendas más económicas y de menor tamaño, en estratos medio-bajos, con alta proporción de apartamentos localizados en el Centro, Norte y Sur, donde sobresalen barrios como Valle del Lili, La Flora, La Hacienda y El Caney.

5 Análisis de Correspondencia

Examinar la relación entre las variables categóricas (tipo de vivienda, zona y barrio), para identificar patrones de comportamiento de la oferta en mercado inmobiliario.

# --- 3.1. Preparación de datos para MCA (Tipo, Zona, Barrio) ---

# Si df_cat ya existe, solo limpiamos niveles vacíos; si no, créalo desde df.
df_cat_mca <- df %>%
  mutate(
    tipo   = as.factor(tipo),
    zona   = as.factor(zona),
    # OPCIONAL: agrupa barrios poco frecuentes para no saturar
    barrio = forcats::fct_lump_n(as.factor(barrio), n = 20)
  ) %>%
  select(tipo, zona, barrio) %>%
  mutate(across(everything(), forcats::fct_drop)) %>%  # quita niveles sin casos
  tidyr::drop_na()
  
summary(df_cat_mca)
##           tipo                zona                 barrio    
##  Apartamento:5100   Zona Centro : 124   Other         :3798  
##  Casa       :3219   Zona Norte  :1920   valle del lili:1008  
##                     Zona Oeste  :1198   ciudad jardín : 516  
##                     Zona Oriente: 351   pance         : 409  
##                     Zona Sur    :4726   la flora      : 366  
##                                         santa teresita: 262  
##                                         (Other)       :1960
# --- 3.2. MCA con FactoMineR ---
mca_res <- FactoMineR::MCA(df_cat_mca, graph = FALSE)

# Varianza explicada por dimensión
mca_res$eig
##        eigenvalue percentage of variance cumulative percentage of variance
## dim 1  0.62543129              7.5051754                          7.505175
## dim 2  0.56656024              6.7987228                         14.303898
## dim 3  0.47033109              5.6439731                         19.947871
## dim 4  0.36108698              4.3330437                         24.280915
## dim 5  0.33547127              4.0256553                         28.306570
## dim 6  0.33333333              4.0000000                         32.306570
## dim 7  0.33333333              4.0000000                         36.306570
## dim 8  0.33333333              4.0000000                         40.306570
## dim 9  0.33333333              4.0000000                         44.306570
## dim 10 0.33333333              4.0000000                         48.306570
## dim 11 0.33333333              4.0000000                         52.306570
## dim 12 0.33333333              4.0000000                         56.306570
## dim 13 0.33333333              4.0000000                         60.306570
## dim 14 0.33333333              4.0000000                         64.306570
## dim 15 0.33333333              4.0000000                         68.306570
## dim 16 0.33333333              4.0000000                         72.306570
## dim 17 0.33333333              4.0000000                         76.306570
## dim 18 0.33333333              4.0000000                         80.306570
## dim 19 0.33333333              4.0000000                         84.306570
## dim 20 0.33333333              4.0000000                         88.306570
## dim 21 0.33118814              3.9742577                         92.280828
## dim 22 0.25019616              3.0023540                         95.283182
## dim 23 0.22003022              2.6403627                         97.923545
## dim 24 0.10101032              1.2121238                         99.135669
## dim 25 0.07202762              0.8643314                        100.000000
# Scree plot (para ver cuántas dimensiones aportan)
factoextra::fviz_screeplot(mca_res, addlabels = TRUE)

# --- 3.3. Contribuciones por categoría y selección de top-k ---

library(dplyr)
library(tidyr)
library(stringr)
library(tibble)

contrib_df <- as.data.frame(mca_res$var$contrib) %>%
  rownames_to_column("categoria") %>%
  # Suma de contribuciones en Dim1 + Dim2 (col 1 y 2)
  mutate(contrib_12 = .[[2]] + .[[3]])  # col 2 y 3 suelen ser Dim 1 / Dim 2 (verifica nombres)
# Nota: si tus columnas se llaman "Dim 1", "Dim 2", usa: mutate(contrib_12 = `Dim 1` + `Dim 2`)

# Partimos la categoría en variable y nivel (FactoMineR usa formato tipo "zona=Norte")
contrib_df <- contrib_df %>%
  separate(categoria, into = c("variable","nivel"), sep = "=", fill = "right")

# Elegimos las top categorías más influyentes en las dos primeras dimensiones
top_k <- 20
top_names <- contrib_df %>%
  arrange(desc(contrib_12)) %>%
  slice_head(n = top_k) %>%
  unite("categoria", variable, nivel, sep = "=", na.rm = TRUE) %>%
  pull(categoria)

# Tabla ordenada (para el informe/anexo)
top_tabla <- contrib_df %>%
  arrange(desc(contrib_12)) %>%
  slice_head(n = top_k)

top_tabla
# --- 3.4. Gráfico principal: solo categorías más influyentes ---

factoextra::fviz_mca_var(
  mca_res,
  select.var = list(name = top_names),  # solo las top-k
  repel = TRUE,
  col.var = "contrib"                   # color por contribución
) +
  scale_color_continuous(name = "Contribución") +
  labs(title = "MCA (Zona, Tipo, Barrio): categorías más influyentes (Dim1+Dim2)") +
  theme_minimal()

contrib_df <- as.data.frame(mca_res$var$contrib) %>%
  tibble::rownames_to_column("categoria")

# Elige siempre las dos primeras dimensiones por nombre
dim_cols <- grep("^Dim[[:space:]]*\\d+$", names(contrib_df), value = TRUE)
dim12 <- dim_cols[1:2]

contrib_df <- contrib_df %>%
  mutate(contrib_12 = .data[[dim12[1]]] + .data[[dim12[2]]]) %>%
  tidyr::separate(categoria, into = c("variable","nivel"), sep = "=", fill = "right")

El análisis de correspondencia múltiple (MCA) se aplicó a las variables tipo de vivienda, zona y barrio (con agrupación de barrios menos frecuentes en un grupo común para mantener la claridad gráfica).

Los resultados muestran que las dos primeras dimensiones explican un porcentaje relevante de la inercia total, lo que permite representar de manera sintética las relaciones más importantes entre las categorías.

En la Dimensión 1 se identifican asociaciones claras entre ciertos tipos de vivienda y zonas específicas de la ciudad. Por ejemplo, se observa que el tipo Casa se vincula más estrechamente con zonas residenciales amplias, mientras que el tipo Apartamento se asocia a zonas con mayor densidad urbana.

La Dimensión 2 complementa esta diferenciación, mostrando cómo algunas zonas y barrios se agrupan según características urbanísticas o de localización que no están directamente en el primer eje, revelando subsegmentos dentro de cada tipo de vivienda.

6 VISUALIZACIONES FINALES

# Perfil numérico por clúster (medias)
library(knitr); library(kableExtra)

perfil_numerico %>% 
  select(cluster, ends_with("_media")) %>% 
  rename_with(~ gsub("_media$", "", .x)) %>% 
  kable(caption = "Perfil numérico por clúster (medias)", digits = 1) %>% 
  kable_styling(full_width = FALSE)
Perfil numérico por clúster (medias)
cluster preciom areaconst estrato parqueaderos banios habitaciones piso
1 1040.5 418.3 5.5 3.6 5.4 5.0 3.0
2 532.8 200.1 5.2 2.0 3.8 3.9 3.6
3 250.6 92.9 4.4 1.2 2.3 3.0 4.3
# Top categorías influyentes del MCA (Dim1+Dim2)
top_tabla %>% 
  arrange(desc(contrib_12)) %>% 
  kable(caption = "Top categorías más influyentes en el MCA (Dim1+Dim2)", digits = 2) %>% 
  kable_styling(full_width = FALSE)
Top categorías más influyentes en el MCA (Dim1+Dim2)
variable nivel Dim 1 Dim 2 Dim 3 Dim 4 Dim 5 contrib_12
Zona Oeste NA 36.90 0.57 2.77 0.85 0.00 37.47
Zona Norte NA 0.17 34.90 3.12 0.24 0.00 35.06
Zona Sur NA 5.89 13.54 2.13 0.04 0.00 19.43
santa teresita NA 13.01 0.33 0.19 0.23 0.00 13.34
la flora NA 0.00 13.24 5.89 0.52 4.04 13.24
normandía NA 8.15 0.21 0.10 0.06 0.00 8.36
los cristales NA 7.75 0.19 0.36 1.28 0.00 7.94
Casa NA 7.02 0.13 12.63 5.76 0.00 7.15
valle del lili NA 0.45 6.25 11.46 22.43 0.11 6.69
acopi NA 0.04 5.81 1.11 5.13 1.19 5.85
aguacatal NA 5.42 0.14 0.21 0.71 0.00 5.56
Apartamento NA 4.43 0.08 7.97 3.63 0.00 4.52
prados del norte NA 0.00 4.44 1.99 0.00 40.93 4.44
ciudad jardín NA 1.63 2.69 0.02 17.96 0.49 4.32
cristales NA 3.91 0.09 0.20 0.90 0.00 4.00
pance NA 1.02 2.27 0.21 5.84 0.99 3.29
Other NA 0.73 2.43 13.84 6.66 0.01 3.16
urbanización la flora NA 0.00 3.03 1.30 0.16 0.91 3.03
brisas de los NA 0.00 2.95 1.30 0.12 0.90 2.95
Zona Oriente NA 1.02 0.63 22.80 6.47 12.88 1.65
library(factoextra)
library(ggplot2)

# Usamos el MCA que ya tienes (mca_res) y las top categorías calculadas antes (top_names)

# Gráfico final integrador MCA
fviz_mca_var(
  mca_res,
  select.var = list(name = top_names),  # solo categorías más influyentes
  repel = TRUE,                         # evita que se monten etiquetas
  col.var = "contrib"                   # color según contribución
) +
  scale_color_continuous(name = "Contribución", low = "skyblue", high = "darkblue") +
  labs(
    title = "Mapa perceptual de la oferta inmobiliaria",
    subtitle = "Relación entre tipo de vivienda, zonas y barrios más influyentes",
    x = paste0("Dim 1 (", round(mca_res$eig[1, 2], 1), "%)"),
    y = paste0("Dim 2 (", round(mca_res$eig[2, 2], 1), "%)")
  ) +
  theme_minimal()

INFORME DE ANÁLISIS REALIZADO

Análisis de Componentes Principales (PCA)

La reducción dimensional mostró que las tres primeras componentes concentraron la mayor parte de la varianza explicada, permitiendo simplificar la información de las variables cuantitativas (precio del metro cuadrado, área construida, estrato, parqueaderos, baños, habitaciones, piso) sin pérdida significativa de información.

Se identificaron patrones claros de agrupación: propiedades de mayor precio y estrato se asociaron a más parqueaderos y baños, mientras que aquellas con menor valor tendieron a menor número de habitaciones y parqueaderos.

Análisis de Conglomerados (Clustering)

La combinación de PCA + k-means permitió agrupar las propiedades en tres segmentos principales:

Cluster 1: Viviendas de gama alta, gran área construida, ubicadas en estratos altos y con múltiples comodidades (mayor número de parqueaderos y baños).

Cluster 2: Propiedades intermedias, equilibradas en precio, área y dotaciones, generalmente en zonas consolidadas.

Cluster 3: Viviendas más económicas, con menor área y equipamiento, concentradas en estratos bajos o medios-bajos.

Análisis de Correspondencia Múltiple (MCA)

El MCA reveló asociaciones sólidas entre tipo de vivienda, zona y barrio.

Los apartamentos se vincularon principalmente a Zonas Norte y Oeste, mientras que las casas predominaron en Zonas Sur y Oriente.

Barrios como La Flora y Cristales actúan como polos de alta influencia para apartamentos, mientras que Ciudad Jardín y Pance lo son para casas.

CONCLUSIONES FINALES

El mercado inmobiliario urbano analizado presenta divisiones nítidas en función de precio, tamaño, estrato y ubicación, lo que permite diseñar estrategias comerciales altamente focalizadas.

El tipo de vivienda no solo está relacionado con el nivel socioeconómico, sino también con zonas específicas de la ciudad, lo que valida el uso de campañas georreferenciadas.

Recomendaciones para la Empresa

Campañas de marketing personalizadas por segmento identificado, usando criterios de ubicación y características del inmueble.

Monitoreo periódico de los clusters y mapas perceptuales para detectar cambios en las preferencias y la oferta.

Aprovechar barrios de alta influencia como puntos de entrada para nuevas estrategias comerciales o lanzamientos de proyectos.