#1. Introducción

Problema: una inmobiliaria necesita entender el mercado urbano para mejorar la valoración (pricing) y decisiones de compra/venta. Objetivo: usar análisis estadístico multivariado para (i) identificar factores dominantes (PCA), (ii) segmentar inmuebles (clustering), (iii) estudiar asociaciones entre variables categóricas (correspondencia) y (iv) visualizar resultados (gráficos y mapa).

knitr::opts_chunk$set(echo = TRUE, message = FALSE, warning = FALSE)
library(paqueteMODELOS)
data("vivienda")
df <- vivienda

library(tidyverse)
library(janitor)
library(skimr)
library(ggplot2)
library(forcats)
library(dplyr)
library(stringr)
library(stringi)
library(janitor)
library(mice)
library(factoextra)
library(cluster)
library(factoextra)
library(FactoMineR)
df <- df %>% clean_names()
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…

Aca podemos identificar que las variables son las siguientes:

n0 <- nrow(df)
df <- df %>% distinct()
n1 <- nrow(df)
tibble(
  filas_iniciales = n0,
  filas_despues = n1,
  duplicadas_removidas = n0 - n1
)
## # A tibble: 1 × 3
##   filas_iniciales filas_despues duplicadas_removidas
##             <int>         <int>                <int>
## 1            8322          8321                    1
na_tab <- df %>%
  summarise(across(everything(), ~sum(is.na(.)))) %>%
  pivot_longer(everything(), names_to = "variable", values_to = "n_na") %>%
  arrange(desc(n_na))
na_tab
## # A tibble: 13 × 2
##    variable      n_na
##    <chr>        <int>
##  1 piso          2637
##  2 parqueaderos  1604
##  3 id               2
##  4 zona             2
##  5 estrato          2
##  6 areaconst        2
##  7 banios           2
##  8 habitaciones     2
##  9 tipo             2
## 10 barrio           2
## 11 longitud         2
## 12 latitud          2
## 13 preciom          1
df <- df %>%
  mutate(
    zona = as.factor(zona),
    tipo = as.factor(tipo),
    barrio = as.factor(barrio),
    piso_cat = fct_explicit_na(as.factor(piso), na_level = "Sin dato"),
    estrato = as.numeric(estrato)
  )
df <- df %>%
  mutate(
    barrio_txt = as.character(barrio),
    barrio_txt = str_squish(barrio_txt),
    barrio_txt = str_to_lower(barrio_txt),
    barrio_txt = stri_trans_general(barrio_txt, "Latin-ASCII"),
    barrio_txt = if_else(barrio_txt == "ponce", "pance", barrio_txt),
    barrio_txt = if_else(barrio_txt == "laflora", "la flora", barrio_txt)
  ) %>%
  filter(!is.na(barrio_txt)) %>%
  filter(!(barrio_txt %in% c("santa", "norte"))) %>%
  mutate(barrio = as.factor(barrio_txt)) %>%
  select(-barrio_txt)

Ya normalizamos barrio para que no tenga minusculas ni tildes ni espacios y evitar duplicados

continuaremos con el histograma

ggplot(df, aes(x = preciom)) +
  geom_histogram(bins = 40) +
  labs(title = "Distribución del precio por m²", x = "preciom", y = "Frecuencia")

El precio por m² es asimétrico (cola a la derecha): hay pocos inmuebles muy caros. Esto sugiere segmentación del mercado.

ggplot(df, aes(x = areaconst, y = preciom)) +
  geom_point(alpha = 0.3) +
  geom_smooth(method = "lm") +
  labs(title = "Precio vs Área construida", x = "Área construida", y = "Precio por m²")

Se observa relación positiva general: a mayor área, tiende a cambiar el precio por m² (no siempre lineal; depende de zona/estrato).

ggplot(df, aes(x = zona, y = preciom)) +
  geom_boxplot() +
  theme(axis.text.x = element_text(angle = 25, hjust = 1)) +
  labs(title = "Precio por zona", x = "Zona", y = "preciom")

ggplot(df, aes(x = tipo, y = preciom)) +
  geom_boxplot() +
  labs(title = "Precio por tipo de vivienda", x = "Tipo", y = "preciom")

Existen diferencias de precio por zona y tipo, lo que apoya usar técnicas multivariadas y segmentación

df_imp <- df %>%
  mutate(parqueaderos = as.numeric(parqueaderos)) %>%
  select(-piso) # piso original no lo usamos; usamos piso_cat

# Configurar MICE
ini  <- mice(df_imp, maxit = 0)
meth <- ini$method
pred <- ini$predictorMatrix

# Método para parqueaderos
meth["parqueaderos"] <- "pmm"

# No usar id como predictor si existe
if ("id" %in% colnames(pred)) pred[, "id"] <- 0

set.seed(123)
imp <- mice(df_imp, method = meth, predictorMatrix = pred, m = 5, seed = 123)
## 
##  iter imp variable
##   1   1  parqueaderos
##   1   2  parqueaderos
##   1   3  parqueaderos
##   1   4  parqueaderos
##   1   5  parqueaderos
##   2   1  parqueaderos
##   2   2  parqueaderos
##   2   3  parqueaderos
##   2   4  parqueaderos
##   2   5  parqueaderos
##   3   1  parqueaderos
##   3   2  parqueaderos
##   3   3  parqueaderos
##   3   4  parqueaderos
##   3   5  parqueaderos
##   4   1  parqueaderos
##   4   2  parqueaderos
##   4   3  parqueaderos
##   4   4  parqueaderos
##   4   5  parqueaderos
##   5   1  parqueaderos
##   5   2  parqueaderos
##   5   3  parqueaderos
##   5   4  parqueaderos
##   5   5  parqueaderos
df2 <- complete(imp, 1)

Como parqueaderos tiene ~19% de faltantes y es importante para valorar, se usa imputación múltiple (MICE) con pmm para conservar la distribución y evitar sesgos.

ggplot() +
  geom_density(data = df,  aes(x = parqueaderos), na.rm = TRUE) +
  geom_density(data = df2, aes(x = parqueaderos), na.rm = TRUE) +
  labs(title = "Densidad parqueaderos: antes vs despues de imputacion",
       x = "parqueaderos", y = "densidad")

Continuaremos con el PCA

# Variables numéricas que sí describen el inmueble (EXCLUIMOS id)
vars_num <- c("estrato","preciom","areaconst","parqueaderos","banios","habitaciones","longitud","latitud")

df_pca <- df2 %>% select(all_of(vars_num)) %>% drop_na()

# Estandarización para evitar sesgos por escalas distintas
Xz <- scale(df_pca)

# PCA
pca <- prcomp(Xz, center = TRUE, scale. = FALSE)
fviz_eig(pca, addlabels = TRUE) +
  labs(title = "PCA: Varianza explicada por componente")

El primer componente principal (PC1) explica 45.7 de la variabilidad Los dos primeros componentes (PC1 + PC2) explican 64.3% acumulado Por lo tanto, el plano PC1–PC2 permite un resumen inicial del mercado, aunque puede requerirse PC3 para capturar más variación

eig <- get_eigenvalue(pca)
eig
##       eigenvalue variance.percent cumulative.variance.percent
## Dim.1  3.6548756        45.685945                    45.68594
## Dim.2  1.4890094        18.612618                    64.29856
## Dim.3  0.8992776        11.240971                    75.53953
## Dim.4  0.6991753         8.739691                    84.27922
## Dim.5  0.4759328         5.949160                    90.22838
## Dim.6  0.3623483         4.529354                    94.75774
## Dim.7  0.2362330         2.952913                    97.71065
## Dim.8  0.1831480         2.289350                   100.00000
round(pca$rotation[,1:2], 3)
##                 PC1    PC2
## estrato       0.327 -0.464
## preciom       0.464 -0.070
## areaconst     0.421  0.265
## parqueaderos  0.412  0.008
## banios        0.444  0.183
## habitaciones  0.260  0.547
## longitud     -0.231  0.464
## latitud      -0.115  0.402

El primer componente principal (45.7% de la varianza) está asociado principalmente con las variables precio por m², número de baños, área construida, parqueaderos y estrato. Esto sugiere que PC1 captura un eje de valorización y nivel socioeconómico del inmueble.

El segundo componente principal (18.6%) está asociado principalmente con número de habitaciones y variables geográficas (latitud y longitud), representando un eje de diferenciación espacial y estructural.

En conjunto, PC1 y PC2 explican el 64.3% de la variabilidad del mercado, lo que permite una representación bidimensional adecuada para análisis exploratorio.

df <- df %>%
  mutate(
    piso_cat = forcats::fct_explicit_na(as.factor(piso), na_level = "Sin dato")
  )

Se continuara con el analisis de cluster de la siguiente forma:

# 1) Tomo parqueaderos imputado desde df2
parq_imp <- df2 %>% select(id, parqueaderos)

# 2) Reemplazo parqueaderos en df usando el imputado 
df_clust_base <- df %>%
  select(id, zona, tipo, barrio, piso_cat, estrato, preciom, areaconst,
         banios, habitaciones, longitud, latitud, parqueaderos) %>%
  left_join(parq_imp, by = "id", suffix = c("_orig", "_imp")) %>%
  mutate(parqueaderos = parqueaderos_imp) %>%
  select(-parqueaderos_orig, -parqueaderos_imp)

# Variables numéricas para clustering
vars_num <- c("estrato","preciom","areaconst","parqueaderos",
              "banios","habitaciones","longitud","latitud")

df_clust <- df_clust_base %>% drop_na(all_of(vars_num))

# Estandarizar 
Xc <- scale(df_clust %>% select(all_of(vars_num)))

Se usa parqueaderos imputado para no perder observaciones Se estandarizan variables para calcular distancias sin sesgo por escala

dist_eucl <- dist(Xc, method = "euclidean")
hc <- hclust(dist_eucl, method = "average")

plot(hc, cex = 0.4, main = "Dendrograma (average)", xlab = "", sub = "")

Elegir k con Silhouette (k = 2 a 8)

sil_means <- tibble(k = 2:8, sil = NA_real_)

for (i in 2:8) {
  grp <- cutree(hc, k = i)
  sil <- silhouette(grp, dist_eucl)
  sil_means$sil[sil_means$k == i] <- mean(sil[,3])
}

sil_means
## # A tibble: 7 × 2
##       k   sil
##   <int> <dbl>
## 1     2 0.647
## 2     3 0.556
## 3     4 0.492
## 4     5 0.479
## 5     6 0.440
## 6     7 0.439
## 7     8 0.423
ggplot(sil_means, aes(k, sil)) +
  geom_line() + geom_point() +
  labs(title = "Silhouette promedio por k", x = "k", y = "Silhouette promedio")

k_final <- sil_means$k[which.max(sil_means$sil)]
k_final
## [1] 2
df_clust <- df_clust %>%
  mutate(cluster = factor(cutree(hc, k = k_final)))

plot(hc, cex = 0.4, main = paste("Dendrograma con k =", k_final))
rect.hclust(hc, k = k_final, border = 2:(k_final+1))

perfil <- df_clust %>%
  group_by(cluster) %>%
  summarise(
    n = n(),
    preciom_med = median(preciom),
    preciom_p25 = quantile(preciom, 0.25),
    preciom_p75 = quantile(preciom, 0.75),
    estrato_med = median(estrato),
    area_med = median(areaconst),
    banios_med = median(banios),
    parq_med = median(parqueaderos),
    hab_med = median(habitaciones),
    .groups = "drop"
  ) %>%
  arrange(desc(n))

perfil
## # A tibble: 2 × 10
##   cluster     n preciom_med preciom_p25 preciom_p75 estrato_med area_med
##   <fct>   <int>       <dbl>       <dbl>       <dbl>       <dbl>    <dbl>
## 1 1        8306         330        220         540            5      123
## 2 2           3         255        228.        312.           3     1440
## # ℹ 3 more variables: banios_med <dbl>, parq_med <dbl>, hab_med <dbl>

Se evaluó el número de conglomerados k=2…8 usando el índice Silhouette promedio. El valor máximo se obtuvo en k=2 (0.616), indicando la partición más coherente desde el punto de vista estadístico.

# Solución alternativa para segmentación útil
k_alt <- 3
df_clust_k3 <- df_clust %>% mutate(cluster3 = factor(cutree(hc, k = k_alt)))

perfil3 <- df_clust_k3 %>%
  group_by(cluster3) %>%
  summarise(
    n = n(),
    preciom_med = median(preciom),
    preciom_p25 = quantile(preciom, 0.25),
    preciom_p75 = quantile(preciom, 0.75),
    estrato_med = median(estrato),
    area_med = median(areaconst),
    banios_med = median(banios),
    parq_med = median(parqueaderos),
    hab_med = median(habitaciones),
    .groups = "drop"
  ) %>%
  arrange(preciom_med)

perfil3
## # A tibble: 3 × 10
##   cluster3     n preciom_med preciom_p25 preciom_p75 estrato_med area_med
##   <fct>    <int>       <dbl>       <dbl>       <dbl>       <dbl>    <dbl>
## 1 2            3         255        228.        312.           3     1440
## 2 1         8302         330        220         540            5      123
## 3 3            4         370        310         530            4      210
## # ℹ 3 more variables: banios_med <dbl>, parq_med <dbl>, hab_med <dbl>
# Obtener coordenadas PCA
df_pca_coords <- as.data.frame(pca$x[,1:2])

# Agregar cluster k=3
df_pca_coords$cluster3 <- df_clust_k3$cluster3

ggplot(df_pca_coords, aes(x = PC1, y = PC2, color = cluster3)) +
  geom_point(alpha = 0.5) +
  labs(title = "Clusters (k=3) proyectados en plano PCA",
       x = "PC1",
       y = "PC2") +
  theme_minimal()

ggplot(df_clust_k3, aes(x = areaconst, y = preciom, color = cluster3)) +
  geom_point(alpha = 0.6) +
  labs(title = "Clusters según área y precio",
       x = "Área construida",
       y = "Precio por m²") +
  theme_minimal()

# Quitar outliers extremos (ejemplo percentil 99)
df_filtrado <- df_clust %>%
  filter(preciom < quantile(preciom, 0.99))

Xf <- scale(df_filtrado %>% select(all_of(vars_num)))
hc_f <- hclust(dist(Xf), method = "average")

fviz_nbclust(Xf, FUN = hcut, method = "silhouette")

El analisis de acuerdo con las graficas planteadas es el siguiete

La solución óptima según el índice Silhouette fue k=2, lo cual separa el mercado en un grupo masivo y un pequeño grupo de inmuebles extremos (outliers premium). Sin embargo, para fines estratégicos de segmentación comercial se evaluó k=3, donde se distinguen tres niveles: mercado general, segmento alto y segmento ultra-premium. Los clusters muestran una clara diferenciación principalmente explicada por precio por m² y área construida.

Lo que se puede ver con el algoritmo de cluster es que el mercado esta dominado homogeneamente y tiene variabilidad continua no hay segmentacion natural fuerte y el algoritmo esta clasificando fronteras por lo tanto el algoritmo identifica bien outliers

tab_ze <- table(df$zona, df$estrato)
tab_ze
##               
##                   3    4    5    6
##   Zona Centro   105   14    4    1
##   Zona Norte    572  407  768  172
##   Zona Oeste     52   82  287  768
##   Zona Oriente  340    8    2    1
##   Zona Sur      382 1616 1685 1043
chi_ze <- chisq.test(tab_ze)
chi_ze
## 
##  Pearson's Chi-squared test
## 
## data:  tab_ze
## X-squared = 3839.5, df = 12, p-value < 2.2e-16
ca_ze <- CA(tab_ze, graph = FALSE)

ca_ze$eig
##       eigenvalue percentage of variance cumulative percentage of variance
## dim 1 0.32311827              69.924825                          69.92483
## dim 2 0.12817234              27.737302                          97.66213
## dim 3 0.01080317               2.337873                         100.00000
fviz_ca_biplot(ca_ze, repel = TRUE) +
  labs(title = "Análisis de Correspondencia: Zona vs Estrato")

La prueba chi-cuadrado (χ² = 3839.5, p < 0.001) confirma una asociación altamente significativa entre la zona de la ciudad y el estrato socioeconómico.

El análisis de correspondencia generó tres dimensiones, donde las dos primeras explican el 97.7% de la inercia total (Dim1 = 69.9%, Dim2 = 27.7%), permitiendo una representación bidimensional prácticamente completa.

La Dimensión 1 representa un eje socioeconómico, diferenciando zonas asociadas a estratos bajos (3) de aquellas vinculadas a estratos altos (6).

En particular:

Zona Oeste se asocia fuertemente con estrato 6, caracterizándose como zona premium.

Zona Oriente y Zona Centro se relacionan principalmente con estrato 3, reflejando un perfil socioeconómico más popular.

Zona Norte y Zona Sur presentan asociación con estratos 4 y 5, representando segmentos intermedios del mercado.

Estos resultados evidencian una clara segmentación espacial del mercado inmobiliario según nivel socioeconómico, lo cual es fundamental para estrategias de pricing, inversión y desarrollo inmobiliario.

# Top 10 barrios con más registros
top_barrios <- df %>%
  count(barrio, sort = TRUE) %>%
  slice_head(n = 10) %>%
  pull(barrio)

top_barrios
##  [1] valle del lili ciudad jardin  pance          la flora       santa teresita
##  [6] el caney       el ingenio     la hacienda    normandia      acopi         
## 385 Levels: 20 de julio 3 de julio acopi agua blanca aguablanca ... zona sur
tab_be <- df %>%
  filter(barrio %in% top_barrios) %>%
  count(barrio, estrato) %>%
  pivot_wider(names_from = estrato, values_from = n, values_fill = 0) %>%
  column_to_rownames("barrio")

tab_be
##                 3   4   5   6
## acopi          21  51  59  27
## ciudad jardin   2   4  67 467
## el caney        9 166  34   0
## el ingenio      0   9 157  37
## la flora        1  33 329   6
## la hacienda     2  29 134   1
## normandia       1   0  23 135
## pance           0   2  20 391
## santa teresita  0   4  27 232
## valle del lili  4 577 423   5
chisq.test(tab_be)
## 
##  Pearson's Chi-squared test
## 
## data:  tab_be
## X-squared = 3734, df = 27, p-value < 2.2e-16
ca_be <- CA(tab_be, graph = FALSE)

ca_be$eig
##       eigenvalue percentage of variance cumulative percentage of variance
## dim 1 0.77298332              72.226662                          72.22666
## dim 2 0.23319073              21.789070                          94.01573
## dim 3 0.06404475               5.984267                         100.00000
fviz_ca_biplot(ca_be, repel = TRUE) +
  labs(title = "Análisis de Correspondencia: Top 10 Barrios vs Estrato")

El test Chi-cuadrado (X² = 3734, p < 0.001) indica que existe una asociación estadísticamente significativa entre barrio y estrato, rechazando la hipótesis de independencia. Esto confirma que la estructura socioeconómica del mercado inmobiliario presenta una clara diferenciación espacial.

La primera dimensión explica el 72.2% de la variabilidad asociativa, capturando el eje principal de segregación socioeconómica. La segunda dimensión (21.9%) complementa esta diferenciación, permitiendo una representación bidimensional adecuada (94% acumulado).

El mapa revela que barrios como Pance, Santa Teresita y Ciudad Jardín se encuentran fuertemente asociados a estratos altos, mientras que barrios como Valle del Lili y El Caney presentan un perfil predominantemente medio. La Flora y El Ingenio muestran una asociación clara con estrato 5.

Estos resultados evidencian que el mercado inmobiliario urbano presenta patrones estructurales de concentración socioeconómica por barrio.

Desde una perspectiva aplicada:

La valoración debe considerar ubicación y estrato como variables estructurales.

El mercado no requiere segmentación excesiva; bastan grandes categorías.

Existen nichos premium claramente identificables.

La estructura socioeconómica territorial influye directamente en precios.

El análisis multivariado evidencia que el mercado inmobiliario urbano presenta una estructura organizada principalmente en torno a un eje de valorización socioeconómica y una diferenciación espacial significativa. Aunque el mercado muestra alta concentración en un núcleo dominante, existen perfiles extremos que configuran subsegmentos de alto valor. La fuerte asociación entre barrio y estrato confirma la existencia de patrones estructurales de segregación socioeconómica territorial.