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…
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()
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.
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.
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.
# 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)
| 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)
| 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.