1 1. Contexto del caso

Una empresa inmobiliaria requiere comprender el mercado de viviendas urbanas para mejorar decisiones de compra, venta y valoración. A partir de una base real (web scraping de OLX) se construye un análisis multivariado con:

2 2. Carga y entendimiento de la base

data("vivenda")   # Nota: el objeto se llama 'vivienda' en el paquete
df <- vivienda %>% as_tibble() %>% 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…
skim(df)
Data summary
Name df
Number of rows 8322
Number of columns 13
_______________________
Column type frequency:
character 4
numeric 9
________________________
Group variables None

Variable type: character

skim_variable n_missing complete_rate min max empty n_unique whitespace
zona 3 1.00 8 12 0 5 0
piso 2638 0.68 2 2 0 12 0
tipo 3 1.00 4 11 0 2 0
barrio 3 1.00 4 29 0 436 0

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
id 3 1.00 4160.00 2401.63 1.00 2080.50 4160.00 6239.50 8319.00 ▇▇▇▇▇
estrato 3 1.00 4.63 1.03 3.00 4.00 5.00 5.00 6.00 ▅▆▁▇▆
preciom 2 1.00 433.89 328.65 58.00 220.00 330.00 540.00 1999.00 ▇▂▁▁▁
areaconst 3 1.00 174.93 142.96 30.00 80.00 123.00 229.00 1745.00 ▇▁▁▁▁
parqueaderos 1605 0.81 1.84 1.12 1.00 1.00 2.00 2.00 10.00 ▇▁▁▁▁
banios 3 1.00 3.11 1.43 0.00 2.00 3.00 4.00 10.00 ▇▇▃▁▁
habitaciones 3 1.00 3.61 1.46 0.00 3.00 3.00 4.00 10.00 ▂▇▂▁▁
longitud 3 1.00 -76.53 0.02 -76.59 -76.54 -76.53 -76.52 -76.46 ▁▅▇▂▁
latitud 3 1.00 3.42 0.04 3.33 3.38 3.42 3.45 3.50 ▃▇▅▇▅

2.1 2.1 Las variables se definen de esta forma:

  • zona: macro-zona de la ciudad.
  • piso: piso/altura.
  • estrato: estrato socioeconómico.
  • preciom: precio de oferta (según el paquete).
  • areaconst: área construida.
  • parqueaderos, banios, habitaciones: atributos físicos.
  • tipo: casa/apartamento, entre otros.
  • barrio: ubicación específica.
  • longitud, latitud: coordenadas aproximadas.

3 3. Calidad de datos y preparación

# Valores faltantes
miss <- naniar::miss_var_summary(df)
miss
# Exploración rápida del 'piso'
df %>% count(piso, sort = TRUE) %>% head(15)

Se identificaron cierta cantidad de valores atipicos por la cual se deben trabajar posteriormente. En los que mas se presentan son en la variable Piso y Parqueaderos.

# =========================
# 1) Gráfica de valores faltantes por variable
# =========================
library(dplyr)
library(ggplot2)
library(naniar)
library(scales)

miss <- naniar::miss_var_summary(df)

ggplot(miss, aes(x = reorder(variable, -pct_miss), y = pct_miss)) +
  geom_col() +
  coord_flip() +
  scale_y_continuous(labels = percent_format(scale = 1)) +
  labs(
    title = "Porcentaje de valores faltantes por variable",
    x = "Variable",
    y = "% de faltantes"
  )

# 2) Gráfica: Top 15 valores más frecuentes de 'piso'
top_piso <- df %>%
  count(piso, sort = TRUE) %>%
  head(15)

ggplot(top_piso, aes(x = reorder(as.character(piso), n), y = n)) +
  geom_col() +
  coord_flip() +
  labs(
    title = "Top 15 categorías más frecuentes de 'piso'",
    x = "Piso",
    y = "Frecuencia"
  )

Se realizó las respectivas graficas de valores faltantes comprobando asi la cantidad de valores faltantes. Para el caso de los pisos se presentó la frecuencia identificando que en su mayor cantidad son valores N/A.

3.1 3.1 Limpieza y transformaciones

Estrategia: 1) Tipar variables (categóricas vs numéricas).
2) Tratar faltantes de piso como categoría “Sin dato”.
3) Crear variables auxiliares para análisis categórico (estrato en grupos) y para perfiles de clusters.

df2 <- df %>%
  mutate(
    zona = as.factor(zona),
    tipo = as.factor(tipo),
    barrio = as.factor(barrio),
    piso = as.factor(if_else(is.na(piso) | piso == "", "Sin dato", as.character(piso))),
    estrato = as.numeric(estrato),
    estrato_cat = cut(estrato,
                      breaks = c(-Inf,2,3,4,5,Inf),
                      labels = c("1-2","3","4","5","6+"),
                      right = TRUE)
  )

# Variables numéricas principales (para ACP y clustering)
num_vars <- c("preciom","areaconst","parqueaderos","banios","habitaciones","estrato")

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

skim(df_num)
Data summary
Name df_num
Number of rows 6717
Number of columns 6
_______________________
Column type frequency:
numeric 6
________________________
Group variables None

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
preciom 0 1 468.88 335.04 58 248 355 580 1999 ▇▃▁▁▁
areaconst 0 1 181.14 144.10 30 86 130 233 1745 ▇▁▁▁▁
parqueaderos 0 1 1.84 1.12 1 1 2 2 10 ▇▁▁▁▁
banios 0 1 3.26 1.38 0 2 3 4 10 ▆▇▃▁▁
habitaciones 0 1 3.61 1.36 0 3 3 4 10 ▁▇▂▁▁
estrato 0 1 4.83 0.95 3 4 5 6 6 ▂▅▁▇▆

Se tiparon variables categóricas como factores, se trató el faltante de piso como una categoría informativa (‘Sin dato’), se normalizó el tipo de estrato a numérico y se creó una agrupación estrato_cat para análisis categórico. Finalmente, se seleccionaron variables numéricas clave para ACP y clustering, eliminando registros incompletos para evitar errores por valores faltantes # 4. Exploración descriptiva (EDA)

3.2 4.1 Distribuciones y relaciones básicas

p1 <- df2 %>% 
  ggplot(aes(preciom)) + 
  geom_histogram(bins = 60) +
  scale_x_continuous(labels = comma) +
  labs(title = "Distribución del precio (preciom)", x = "Precio", y = "Frecuencia")

p2 <- df2 %>% 
  ggplot(aes(areaconst, preciom)) +
  geom_point(alpha = 0.15) +
  scale_y_continuous(labels = comma) +
  labs(title = "Precio vs área construida", x = "Área construida", y = "Precio")

(p1 | p2)

El histograma de preciom evidencia una distribución asimétrica a la derecha, con concentración en rangos bajos/medios y una cola de precios altos (posibles outliers). El diagrama de dispersión preciom vs areaconst muestra una asociación positiva: el precio tiende a aumentar con el área construida, aunque con alta dispersión para valores similares de área, lo que sugiere influencia de factores adicionales como zona, estrato y tipo de vivienda

3.3 4.2 Comparaciones por zona y tipo

g1 <- df2 %>%
  ggplot(aes(zona, preciom)) +
  geom_boxplot(outlier.alpha = 0.15) +
  scale_y_continuous(labels = comma) +
  coord_flip() +
  labs(title = "Precio por zona", x = NULL, y = "Precio")

g2 <- df2 %>%
  ggplot(aes(tipo, preciom)) +
  geom_boxplot(outlier.alpha = 0.15) +
  scale_y_continuous(labels = comma) +
  labs(title = "Precio por tipo de vivienda", x = NULL, y = "Precio")

(g1 | g2)

El precio presenta heterogeneidad espacial: las medianas y la dispersión cambian según la zona. Se observan outliers altos en la mayoría de zonas, indicando presencia de probablemente ofertas premium y asimetría en la distribución del precio.

El tipo de vivienda se asocia al precio: las casas tienden a presentar precios más altos (mediana superior) y mayor variabilidad que los apartamentos, con presencia de outliers en ambos casos.

4 5. Análisis de Componentes Principales (ACP)

4.1 5.1 Ajuste del modelo

Se estandarizan variables (media 0, varianza 1) porque están en escalas distintas.

pca <- PCA(df_num, scale.unit = TRUE, graph = FALSE)

# Varianza explicada
fviz_eig(pca, addlabels = TRUE) + 
  labs(title = "Varianza explicada por componente") + geom_col(fill = "skyblue", color = "black") +
  theme(plot.title = element_text(hjust = 0.5, face = "bold"))

Se aplicó un ACP sobre las variables numéricas estandarizadas (scale.unit = TRUE) para eliminar el efecto de las distintas escalas. Los resultados muestran que la primera componente explica el 58.1% de la varianza y la segunda el 20.4%, acumulando 78.5% en las dos primeras dimensiones. A partir de la tercera componente el aporte marginal es inferior al 10%, por lo que el análisis puede enfocarse principalmente en Dim1 y Dim2 para interpretación y segmentación.

4.2 5.2 Interpretación: cargas y contribuciones

# Variables en el plano factorial
fviz_pca_var(pca, col.var = "contrib", repel = TRUE) +
  labs(title = "Círculo de correlaciones (ACP)")

# Contribución de variables a Dim 1 y Dim 2
fviz_contrib(pca, choice = "var", axes = 1, top = 10) + labs(title = "Top contribuciones a Dim 1")

fviz_contrib(pca, choice = "var", axes = 2, top = 10) + labs(title = "Top contribuciones a Dim 2")

El ACP se interpretó mediante el círculo de correlaciones y las gráficas de contribución. En el círculo, flechas en direcciones similares indican correlación positiva; además, la contribución muestra qué variables “construyen” cada dimensión. Los resultados evidencian que Dim1 (58.1%) está dominada por precio, baños, área construida y parqueaderos, por lo que representa un eje de valor y dotación/tamaño: viviendas con valores altos en Dim1 tienden a ser más costosas y con mayor dotación física. Por su parte, Dim2 (20.4%) está explicada casi totalmente por habitaciones y estrato, capturando un patrón adicional de perfil habitacional vs nivel socioeconómico. En conjunto, las dos primeras dimensiones acumulan 78.5% de la variabilidad, por lo que resumen adecuadamente la estructura del mercado.

4.3 5.3 Proyección de individuos y coloreo por zona/tipo

# 1) Dataset del PCA
df_pca_base <- df2 %>%
  select(all_of(num_vars), zona, tipo, estrato_cat) %>%
  drop_na(all_of(num_vars))

# 2) Coordenadas del PCA + variables categóricas alineadas
df_pca_ind <- as_tibble(pca$ind$coord) %>%
  bind_cols(df_pca_base %>% select(zona, tipo, estrato_cat))

# 3) Gráficos
a <- ggplot(df_pca_ind, aes(Dim.1, Dim.2, color = zona)) +
  geom_point(alpha = 0.25) +
  labs(title = "Individuos en el plano (Dim1-Dim2) coloreado por zona",
       x = "Dim 1", y = "Dim 2")

b <- ggplot(df_pca_ind, aes(Dim.1, Dim.2, color = tipo)) +
  geom_point(alpha = 0.25) +
  labs(title = "Individuos en el plano (Dim1-Dim2) coloreado por tipo",
       x = "Dim 1", y = "Dim 2")

a

b

##Proyección de individuos en el plano factorial (Dim1–Dim2): tendencias por zona y por tipo

La proyección de las viviendas en el plano Dim1–Dim2 permite observar cómo se distribuyen las ofertas según los factores principales identificados por el ACP. En este caso, Dim1 se interpreta como un eje asociado al nivel general de valor y dotación/tamaño (principalmente influenciado por precio, área, baños y parqueaderos), mientras que Dim2 captura un patrón adicional de perfil dominado por la relación entre habitaciones y estrato. Con base en ello, las dos gráficas (coloreadas por zona y por tipo) permiten evaluar si existen agrupamientos o tendencias estructurales del mercado.

4.3.1 Tendencias en la gráfica por zona.

Primero, algunas zonas muestran mayor presencia en valores altos de Dim1 (hacia la derecha), lo cual indica una concentración relativa de viviendas con mayor precio y mayor dotación física. Segundo, otras zonas se concentran más cerca del centro o hacia valores medios/bajos de Dim1, sugiriendo una oferta más abundante en segmentos de menor valor relativo. En términos de Dim2, no se observa una separación nítida por zonas, lo que implica que los perfiles de composición (habitaciones/estrato) se presentan de forma transversal en distintas ubicaciones. En conjunto, la lectura más importante es que existen diferencias espaciales, pero el mercado muestra heterogeneidad interna dentro de cada zona.

4.3.2 Tendencias en la gráfica por tipo de vivienda.

Cuando el plano se colorea por tipo (Apartamento vs Casa), la diferenciación visual es más clara que por zona. Se aprecia que los apartamentos tienden a concentrarse en una región más específica del plano, mientras que las casas muestran una mayor dispersión, ocupando un rango más amplio tanto en Dim1 como en Dim2. Esta mayor dispersión sugiere que las casas abarcan desde ofertas de valor medio hasta segmentos de alto valor y dotación, reflejando diversidad en tamaños, configuraciones y ubicaciones. Además, el contraste en Dim2 indica que el tipo de vivienda se asocia con perfiles distintos de composición (relación habitaciones–estrato), lo cual es consistente con la existencia de tipologías de mercado: por ejemplo, ciertos apartamentos pueden concentrarse en configuraciones particulares, mientras que casas pueden incluir configuraciones familiares más variadas.

4.4 Resumen del análisis conjunto.

Al comparar las dos gráficas, se ve que el tipo de vivienda (casa o apartamento) separa mejor los datos que la zona. En la gráfica por zona, los colores aparecen muy mezclados, lo que indica que dentro de una misma zona pueden existir viviendas muy diferentes: unas más caras o más baratas, más grandes o más pequeñas, con mayor o menor dotación (baños, parqueaderos, etc.). Por eso, la ubicación influye, pero no alcanza por sí sola para definir claramente el perfil de una vivienda. En cambio, en la gráfica por tipo se nota una diferencia más clara entre casas y apartamentos, lo que sugiere que el diseño y la configuración del inmueble explican patrones más consistentes en las variables numéricas analizadas. En términos prácticos, esto justifica aplicar técnicas de segmentación (clustering) usando las dimensiones del ACP, ya que el mercado parece tener varios subgrupos que no se identifican únicamente por zona o por tipo, sino por una combinación de características físicas y socioeconómicas.

5 6. Análisis de Conglomerados (segmentación)

5.1 6.1 Selección de K (número de clusters)

Se sugiere usar coordenadas del ACP (reduce ruido y multicolinealidad) y escoger K con criterios como silhouette o gap statistic.

asegurando que se conserve al menos ~75% de la información con Dim1 + Dim2 ya se alcanza ~78.5%, entonces lo más probable es que m = 2.

# Usamos las primeras componentes que acumulen ~70-80% de varianza
eig <- pca$eig
cumvar <- eig[,3]
m <- which(cumvar >= 75)[1]
m
## comp 2 
##      2
X <- pca$ind$coord[, 1:m, drop = FALSE]

# Método silhouette (kmeans)
fviz_nbclust(X, kmeans, method = "silhouette") + 
  labs(title = "Selección de K por silhouette (k-means)")

Para la segmentación se utilizaron las coordenadas del ACP, lo que reduce ruido y dependencia entre variables. Se seleccionó el número de componentes m como el mínimo necesario para explicar al menos el 75% de la variabilidad (en este caso, las dos primeras dimensiones superan dicho umbral). Posteriormente, se determinó el número de clusters K mediante el criterio de silhouette aplicado a k-means. El mayor promedio de silhouette se obtuvo en K=2, indicando que dos grupos proporcionan la separación más consistente entre viviendas en el espacio reducido del ACP.

El clustering se usa para segmentar el mercado agrupando viviendas con características similares. Cada cluster representa un segmento con un perfil típico (precio, área, dotación y estrato). Esta segmentación facilita comparar inmuebles entre pares similares y apoyar decisiones comerciales y de valoración

5.2 6.2 Clustering final y perfilamiento

set.seed(2026)

# Elegimos K como el máximo en silhouette (puede ajustar manualmente)
sil <- fviz_nbclust(X, kmeans, method = "silhouette")
# Para automatizar sin depender del gráfico, usamos un rango razonable:
ks <- 2:8
sil_vals <- map_dbl(ks, ~{
  km <- kmeans(X, centers = .x, nstart = 25)
  ss <- silhouette(km$cluster, dist(X))
  mean(ss[,3])
})
k_best <- ks[which.max(sil_vals)]
k_best
## [1] 2
km <- kmeans(X, centers = k_best, nstart = 50)

df_cluster <- df2 %>%
  drop_na(all_of(num_vars)) %>%
  mutate(cluster = factor(km$cluster))

# Visualización del clustering sobre el ACP
fviz_cluster(list(data = X, cluster = km$cluster),
             ellipse.type = "norm",
             show.clust.cent = TRUE) +
  labs(title = "Clusters en el espacio reducido (ACP)")

Se aplicó k-means sobre el espacio reducido del ACP (primeras componentes que superan el 75% de varianza explicada). El número óptimo de conglomerados se determinó mediante el promedio del índice silhouette evaluado para K=2…8, seleccionando el K con mejor desempeño. Con el K final, se ejecutó k-means con múltiples reinicios (nstart=50) para garantizar estabilidad, y se asignó a cada vivienda su etiqueta de cluster. La visualización en el plano Dim1–Dim2 muestra una separación principalmente sobre Dim1: un cluster concentra ofertas de menor valor/dotación, mientras el otro agrupa viviendas con mayor valor/dotación, incluyendo un subconjunto con valores extremos (segmento premium).

El cluster 1 (rojo) queda más hacia la izquierda (Dim1 menor). El cluster 2 (azul) queda más hacia la derecha (Dim1 mayor). Como Dim1 está asociado a precio/área/baños/parqueaderos entonces

Cluster 1: viviendas con menor nivel de valor y dotación (más económicas/menos dotadas). Cluster 2: viviendas con mayor nivel de valor y dotación (más costosas/más dotadas).

5.2.1 6.3 Resumen comparativo por cluster

library(dplyr)
library(scales)
library(knitr)

# 1) Perfil numérico por cluster (medianas + tamaño del cluster)
profile_num <- df_cluster %>%
  group_by(cluster) %>%
  summarise(
    n = n(),
    precio_med = median(preciom, na.rm = TRUE),
    area_med   = median(areaconst, na.rm = TRUE),
    estrato_med= median(estrato, na.rm = TRUE),
    hab_med    = median(habitaciones, na.rm = TRUE),
    ban_med    = median(banios, na.rm = TRUE),
    park_med   = median(parqueaderos, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  arrange(desc(precio_med))

kable(
  profile_num %>%
    mutate(
      precio_med = comma(precio_med),
      area_med   = comma(area_med)
    ),
  caption = "Tabla 1. Perfil numérico por cluster (medianas)"
)
Tabla 1. Perfil numérico por cluster (medianas)
cluster n precio_med area_med estrato_med hab_med ban_med park_med
2 1999 750 300 6 4 5 3
1 4718 290 100 5 3 2 1
# 2) Distribución de TIPO por cluster (conteo y porcentaje)
profile_cat_tipo <- df_cluster %>%
  count(cluster, tipo) %>%
  group_by(cluster) %>%
  mutate(pct = n / sum(n)) %>%
  ungroup() %>%
  arrange(cluster, desc(pct))

kable(
  profile_cat_tipo %>%
    mutate(pct = percent(pct, accuracy = 0.1)),
  caption = "Tabla 2. Distribución del tipo de vivienda por cluster"
)
Tabla 2. Distribución del tipo de vivienda por cluster
cluster tipo n pct
1 Apartamento 3522 74.7%
1 Casa 1196 25.3%
2 Casa 1290 64.5%
2 Apartamento 709 35.5%
# 3) Top 5 ZONAS por cluster (conteo y porcentaje)
profile_cat_zona <- df_cluster %>%
  count(cluster, zona) %>%
  group_by(cluster) %>%
  mutate(pct = n / sum(n)) %>%
  ungroup()

top_zonas <- profile_cat_zona %>%
  group_by(cluster) %>%
  slice_max(order_by = pct, n = 5, with_ties = FALSE) %>%
  ungroup() %>%
  arrange(cluster, desc(pct))

kable(
  top_zonas %>%
    mutate(pct = percent(pct, accuracy = 0.1)),
  caption = "Tabla 3. Top 5 zonas por cluster (porcentaje dentro del cluster)"
)
Tabla 3. Top 5 zonas por cluster (porcentaje dentro del cluster)
cluster zona n pct
1 Zona Sur 2930 62.1%
1 Zona Norte 1033 21.9%
1 Zona Oeste 574 12.2%
1 Zona Oriente 127 2.7%
1 Zona Centro 54 1.1%
2 Zona Sur 1175 58.8%
2 Zona Oeste 524 26.2%
2 Zona Norte 254 12.7%
2 Zona Oriente 36 1.8%
2 Zona Centro 10 0.5%
# 6.3 Gráficas comparativas por cluster

library(dplyr)
library(ggplot2)
library(tidyr)
library(scales)

# --- 1) Barras: Medianas numéricas por cluster (precio, área, etc.) ---
profile_num <- df_cluster %>%
  group_by(cluster) %>%
  summarise(
    n = n(),
    precio_med = median(preciom, na.rm = TRUE),
    area_med   = median(areaconst, na.rm = TRUE),
    estrato_med= median(estrato, na.rm = TRUE),
    hab_med    = median(habitaciones, na.rm = TRUE),
    ban_med    = median(banios, na.rm = TRUE),
    park_med   = median(parqueaderos, na.rm = TRUE),
    .groups = "drop"
  )

profile_long <- profile_num %>%
  pivot_longer(
    cols = c(precio_med, area_med, estrato_med, hab_med, ban_med, park_med),
    names_to = "variable",
    values_to = "mediana"
  ) %>%
  mutate(
    variable = recode(variable,
      precio_med  = "Precio (mediana)",
      area_med    = "Área const. (mediana)",
      estrato_med = "Estrato (mediana)",
      hab_med     = "Habitaciones (mediana)",
      ban_med     = "Baños (mediana)",
      park_med    = "Parqueaderos (mediana)"
    )
  )

ggplot(profile_long, aes(x = variable, y = mediana, fill = cluster)) +
  geom_col(position = position_dodge(width = 0.8)) +
  coord_flip() +
  scale_y_continuous(labels = comma) +
  labs(
    title = "Comparación de medianas por cluster",
    x = NULL,
    y = "Mediana",
    fill = "Cluster"
  )

# --- 2) Boxplots: Distribución de precio y área por cluster (más informativo que solo medianas) ---
p_precio <- ggplot(df_cluster, aes(x = cluster, y = preciom)) +
  geom_boxplot(outlier.alpha = 0.15) +
  scale_y_continuous(labels = comma) +
  labs(title = "Distribución del precio por cluster", x = "Cluster", y = "Precio")

p_area <- ggplot(df_cluster, aes(x = cluster, y = areaconst)) +
  geom_boxplot(outlier.alpha = 0.15) +
  scale_y_continuous(labels = comma) +
  labs(title = "Distribución del área construida por cluster", x = "Cluster", y = "Área construida")

p_precio

p_area

# --- 3) Composición: Top zonas por cluster (porcentaje) ---
profile_cat_zona <- df_cluster %>%
  count(cluster, zona) %>%
  group_by(cluster) %>%
  mutate(pct = n / sum(n)) %>%
  ungroup()

top_zonas <- profile_cat_zona %>%
  group_by(cluster) %>%
  slice_max(order_by = pct, n = 5, with_ties = FALSE) %>%
  ungroup()

ggplot(top_zonas, aes(x = reorder(zona, pct), y = pct, fill = cluster)) +
  geom_col(position = position_dodge(width = 0.8)) +
  coord_flip() +
  scale_y_continuous(labels = percent_format(accuracy = 1)) +
  labs(
    title = "Top 5 zonas por cluster (porcentaje dentro del cluster)",
    x = "Zona",
    y = "% dentro del cluster",
    fill = "Cluster"
  )

# --- 4) Composición: Tipo de vivienda por cluster (porcentaje) ---
profile_cat_tipo <- df_cluster %>%
  count(cluster, tipo) %>%
  group_by(cluster) %>%
  mutate(pct = n / sum(n)) %>%
  ungroup()

ggplot(profile_cat_tipo, aes(x = tipo, y = pct, fill = cluster)) +
  geom_col(position = position_dodge(width = 0.8)) +
  scale_y_continuous(labels = percent_format(accuracy = 1)) +
  labs(
    title = "Distribución del tipo de vivienda por cluster",
    x = "Tipo de vivienda",
    y = "% dentro del cluster",
    fill = "Cluster"
  )

La segmentación mediante k-means (K=2) permitió identificar dos perfiles de mercado claramente diferenciados. El Cluster 1 (n=4.718) representa el segmento estándar/compacto, con una mediana de precio de 290 y área construida de 100, estrato mediano 5 y dotación moderada (3 habitaciones, 2 baños, 1 parqueadero). Este cluster está dominado por apartamentos (74.7%) y se concentra principalmente en Zona Sur (62.1%), seguida por Zona Norte (21.9%) y Zona Oeste (12.2%). Por su parte, el Cluster 2 (n=1.999) corresponde al segmento alto/familiar–premium, con precio mediano de 750 y área mediana de 300, estrato mediano 6 y mayor dotación (4 habitaciones, 5 baños, 3 parqueaderos). Predominan las casas (64.5%), aunque existe participación de apartamentos premium (35.5%). A nivel espacial, también se concentra en Zona Sur (58.8%), pero presenta una mayor presencia relativa en Zona Oeste (26.2%), lo que sugiere una asociación de esta zona con oferta de mayor valor. # 7. Análisis de Correspondencia / MCA (variables categóricas)

5.3 7.1 Asociación zona vs tipo (CA)

library(dplyr)
library(ggplot2)
library(scales)
library(FactoMineR)
library(factoextra)



df_mca <- df2 %>% select(zona, tipo) %>% drop_na()
mca_zt <- MCA(df_mca, graph = FALSE)

fviz_mca_biplot(
  mca_zt,
  repel = TRUE,
  invisible = "ind"   # oculta individuos (los puntos de viviendas)
) +
  labs(title = "MCA: Relación entre zona y tipo (solo categorías)")

df_mca <- df2 %>% select(zona, tipo) %>% drop_na()

tab_pct <- df_mca %>%
  count(zona, tipo) %>%
  group_by(zona) %>%
  mutate(pct = n/sum(n))

ggplot(tab_pct, aes(x = zona, y = pct, fill = tipo)) +
  geom_col() +
  scale_y_continuous(labels = percent_format()) +
  labs(title = "Proporción de tipo de vivienda dentro de cada zona",
       x = "Zona", y = "% dentro de la zona", fill = "Tipo") +
  coord_flip()

library(dplyr)

# Tabla de contingencia
tab_zt <- df2 %>%
  select(zona, tipo) %>%
  drop_na() %>%
  with(table(zona, tipo))

tab_zt
##               tipo
## zona           Apartamento Casa
##   Zona Centro           24  100
##   Zona Norte          1198  722
##   Zona Oeste          1029  169
##   Zona Oriente          62  289
##   Zona Sur            2787 1939
# Chi-cuadrado
chi <- chisq.test(tab_zt)
chi
## 
##  Pearson's Chi-squared test
## 
## data:  tab_zt
## X-squared = 690.93, df = 4, p-value < 2.2e-16
# Observados / Esperados / Residuos estandarizados
chi$expected
##               tipo
## zona           Apartamento       Casa
##   Zona Centro     76.01875   47.98125
##   Zona Norte    1177.06455  742.93545
##   Zona Oeste     734.43924  463.56076
##   Zona Oriente   215.18211  135.81789
##   Zona Sur      2897.29535 1828.70465
chi$stdres
##               tipo
## zona           Apartamento       Casa
##   Zona Centro    -9.663528   9.663528
##   Zona Norte      1.118502  -1.118502
##   Zona Oeste     18.885875 -18.885875
##   Zona Oriente  -17.153034  17.153034
##   Zona Sur       -5.012367   5.012367
# Porcentaje dentro de cada zona (filas)
round(100 * prop.table(tab_zt, margin = 1), 1)
##               tipo
## zona           Apartamento Casa
##   Zona Centro         19.4 80.6
##   Zona Norte          62.4 37.6
##   Zona Oeste          85.9 14.1
##   Zona Oriente        17.7 82.3
##   Zona Sur            59.0 41.0
# ---- Tabla larga (robusta) ----
tab_long <- as.data.frame(tab_zt) %>%
  setNames(c("zona","tipo","n")) %>%          # renombra sin importar cómo venían
  group_by(zona) %>%
  mutate(pct_dentro_zona = n / sum(n)) %>%
  ungroup()

tab_long

5.3.1 Relación entre zona y tipo de vivienda

Para evaluar si la zona está asociada con el tipo de vivienda (Casa/Apartamento), se construyó la tabla de contingencia zona×tipo y se aplicó la prueba Chi-cuadrado de independencia. El resultado fue altamente significativo (\(X^2 = 690.93\), \(gl = 4\), \(p < 2.2\times 10^{-16}\)), por lo que se rechaza la hipótesis de independencia. En consecuencia, existe evidencia estadística sólida de que la proporción de casas y apartamentos cambia según la zona.

La tabla de proporciones dentro de cada zona y la gráfica de barras apiladas al 100% permiten interpretar esa asociación. Se observa un predominio marcado de casas en Zona Centro (80.6%) y Zona Oriente (82.3%). En contraste, Zona Oeste concentra principalmente apartamentos (85.9%). Por su parte, Zona Norte (62.4% apartamentos) y Zona Sur (59.0% apartamentos) también muestran mayor proporción de apartamentos, aunque con una participación relevante de casas (37.6% y 41.0%, respectivamente). Todo esto indica que la composición de la oferta inmobiliaria (casas vs apartamentos) no es homogénea entre zonas.

Finalmente, el MCA (solo categorías) complementa la lectura anterior al representar gráficamente la relación entre categorías: categorías más cercanas en el plano tienden a presentarse conjuntamente con mayor frecuencia. En el biplot se aprecia una mayor cercanía de “Apartamento” con Zona Oeste, coherente con su alta proporción de apartamentos, mientras que “Casa” se ubica más próximo a zonas donde predominan casas (Centro y Oriente). En conjunto, las tablas y gráficas confirman una estructura espacial en la tipología de vivienda, donde la zona se relaciona con el tipo de inmueble ofertado.

5.4 7.2 MCA con varias categóricas (zona, tipo, estrato_cat, piso)

df_cat <- df2 %>%
  select(zona, tipo, estrato_cat, piso) %>%
  drop_na()

mca <- MCA(df_cat, graph = FALSE)

fviz_screeplot(mca, addlabels = TRUE) +
  labs(title = "MCA: varianza explicada")

fviz_mca_biplot(
  mca,
  repel = TRUE,
  ggtheme = theme_minimal(),
  invisible = "ind"   # oculta individuos
) +
  labs(title = "MCA: categorías en el plano factorial")

5.4.1 MCA con variables categóricas (zona, tipo, estrato_cat, piso): interpretación

Se aplicó un Análisis de Correspondencias Múltiples (MCA) usando las variables categóricas zona, tipo, estrato_cat y piso, con el fin de explorar patrones de asociación entre categorías. En el MCA es normal que la varianza se distribuya en varias dimensiones; en este caso, Dim1 explica 9.3% y Dim2 7.0%, por lo que el plano Dim1–Dim2 debe entenderse como un mapa exploratorio (no como un resumen completo de toda la información).

Para facilitar la lectura se graficaron solo las categorías (sin individuos). La interpretación del mapa sigue estas reglas: (i) categorías cercanas tienden a aparecer juntas con mayor frecuencia, (ii) categorías en lados opuestos del origen tienden a estar asociadas de forma contrastante, y (iii) categorías más alejadas del origen (0,0) son más “discriminantes” (diferencian mejor los perfiles), mientras que las cercanas al origen representan patrones más comunes o menos diferenciadores.

En el plano factorial se observa que Dim1 establece un contraste principal: hacia valores positivos se ubican categorías como Apartamento y niveles altos de estrato (por ejemplo 6+), junto con varias categorías de piso de mayor numeración; hacia valores negativos se ubica Casa y categorías de piso más bajas o puntuales. Esto sugiere que Dim1 está capturando un eje de tipología/segmentación asociado a combinaciones de apartamentos + estratos altos + pisos altos, frente a casas + pisos más bajos. Por su parte, Dim2 parece diferenciar principalmente por zona, ya que categorías como Zona Centro y Zona Oriente se ubican en la parte superior del plano, mientras que otras zonas se ubican más cerca del centro o hacia la parte inferior; esto indica que la ubicación aporta un patrón adicional de diferenciación sobre la tipología y el estrato.

Finalmente, la categoría “Sin dato” aparece relativamente cerca del centro del plano, lo que sugiere que no define un perfil muy específico en estas dos dimensiones (es menos discriminante) y, por tanto, su asociación con otras categorías en Dim1–Dim2 es más débil. En conjunto, el MCA confirma que existe una estructura de asociación entre zona, tipo de vivienda, estrato agrupado y piso, destacándose una relación consistente entre categorías vinculadas a apartamentos y estratos altos frente a perfiles asociados a casas y categorías de piso más bajas.

6 8. Visualización geográfica (mapas simples con coordenadas)

Sin requerir shapefiles, se puede usar un “mapa de puntos” con latitud/longitud.

df_map <- df2 %>%
  drop_na(latitud, longitud, preciom)

ggplot(df_map, aes(longitud, latitud, color = preciom)) +
  geom_point(alpha = 0.25, size = 0.8) +
  scale_color_viridis_c(labels = comma) +
  labs(title = "Mapa de oferta: puntos coloreados por precio",
       x = "Longitud", y = "Latitud", color = "Precio")

6.0.1 Mapa de oferta: distribución espacial del precio

El mapa presenta la ubicación de las viviendas (longitud y latitud) y colorea cada punto según su precio, lo que permite identificar patrones espaciales de valorización. En términos generales se observa una alta concentración de oferta en el área central del plano (mayor densidad de puntos), lo cual sugiere que la mayor parte de los registros se localiza en ese rango geográfico.

En cuanto al precio, los colores muestran que la oferta no es homogénea: predominan tonos asociados a precios medios y bajos en gran parte del territorio, mientras que los precios altos (colores más claros/amarillentos) aparecen de forma más puntual y dispersa, formando “bolsones” o focos específicos. Esto indica la existencia de microzonas con mayor valorización, en lugar de un gradiente uniforme (por ejemplo, no se observa que todo el norte/sur sea sistemáticamente más caro; más bien se aprecian concentraciones localizadas).

También se evidencia que dentro de zonas con alta densidad de puntos coexisten diferentes niveles de precio (mezcla de colores), lo que sugiere heterogeneidad: en un mismo sector geográfico pueden encontrarse viviendas más económicas y otras más costosas, posiblemente por diferencias en tipo de vivienda, área, estrato, equipamientos o características del entorno. En conjunto, el mapa respalda la idea de que el precio depende tanto de la localización como de atributos del inmueble, y sugiere que la segmentación posterior (por ejemplo, clustering) puede capturar grupos de oferta con perfiles diferentes incluso dentro de áreas cercanas.

6.0.2 Promedios espaciales por zona

df_zone <- df_map %>%
  group_by(zona) %>%
  summarise(
    n = n(),
    precio_med = median(preciom, na.rm = TRUE),
    lon = median(longitud, na.rm = TRUE),
    lat = median(latitud, na.rm = TRUE),
    .groups = "drop"
  )

ggplot(df_map, aes(longitud, latitud)) +
  geom_point(alpha = 0.10, size = 0.6) +
  geom_point(data = df_zone, aes(lon, lat, size = n, color = precio_med), alpha = 0.9) +
  scale_color_viridis_c(labels = comma) +
  labs(title = "Centroides por zona (tamaño = #ofertas, color = precio mediano)",
       x = "Longitud", y = "Latitud", color = "Precio mediano", size = "N")

6.0.3 Centroides por zona: volumen de oferta y precio mediano

La figura resume el comportamiento espacial de la oferta mediante centroides por zona. Cada círculo representa la ubicación promedio (centroide) de las viviendas de una zona en el plano longitud–latitud. El tamaño del círculo indica el número de ofertas (N) registradas en esa zona, mientras que el color representa el precio mediano de la zona (medida robusta frente a valores extremos).

En términos de volumen, se observan zonas con círculos claramente más grandes, lo que indica que concentran la mayor parte de la oferta. Esto sugiere que la actividad del mercado (cantidad de anuncios) no está distribuida uniformemente, sino que se concentra en ciertas zonas.

En cuanto a precio, el gradiente de color evidencia diferencias entre zonas: algunas presentan colores asociados a un precio mediano más alto, mientras que otras se ubican en rangos medios o bajos. Esto permite identificar de manera rápida qué zonas, además de concentrar (o no) más ofertas, tienden a estar asociadas con un nivel de precio típico mayor.

Finalmente, comparar simultáneamente tamaño y color permite distinguir escenarios relevantes para el análisis: (i) zonas con alta oferta y precio mediano alto (mercados grandes y valorizados), (ii) zonas con alta oferta pero precio mediano medio/bajo (mercados grandes más “accesibles”), y (iii) zonas con baja oferta pero precio mediano alto (mercados más pequeños, potencialmente más exclusivos). En conjunto, este resumen facilita priorizar zonas para análisis posteriores y complementa el mapa de puntos, ya que reduce el detalle individual a un panorama comparativo por zona.

6.1 9. Conclusiones y recomendaciones

6.1.1 9.1 Hallazgos clave

  1. Factores principales (ACP):

    • Dim 1 (≈ 58.1%) se asocia principalmente con un eje de valor–tamaño–dotación, dominado por precio (preciom), área construida (areaconst), baños y parqueaderos. En términos prácticos, moverse hacia valores altos en Dim1 representa viviendas más costosas, más grandes y mejor dotadas.
    • Dim 2 (≈ 20.4%) captura un patrón complementario relacionado con el perfil habitacional y socioeconómico, donde destacan habitaciones y estrato. Esta dimensión ayuda a diferenciar configuraciones de vivienda (por ejemplo, propiedades con más habitaciones y/o asociadas a estratos distintos), aportando una separación adicional que no depende únicamente del tamaño o el precio.
  2. Segmentos (Clustering):
    Con base en el criterio silhouette se identificaron 2 clusters claramente diferenciados:

    • Cluster 1 (n = 4,718 | “Estándar/compacto”):
      Perfil típico: precio mediano 290, área mediana 100, estrato mediano 5, con dotación moderada (3 habitaciones, 2 baños, 1 parqueadero). Predominan los apartamentos (74.7%). Zonas con mayor participación: Zona Sur (62.1%), seguida por Zona Norte (21.9%) y Zona Oeste (12.2%).
    • Cluster 2 (n = 1,999 | “Alto/familiar–premium”):
      Perfil típico: precio mediano 750, área mediana 300, estrato mediano 6, con alta dotación (4 habitaciones, 5 baños, 3 parqueaderos). Predominan las casas (64.5%), aunque aparece una proporción relevante de apartamentos premium (35.5%). Zonas con mayor participación: Zona Sur (58.8%), seguida por Zona Oeste (26.2%) y Zona Norte (12.7%).

    En síntesis, la segmentación separa un mercado compacto y mayoritariamente de apartamentos frente a un mercado de mayor valor, mayor área y dotación, dominado por casas.

  3. Asociación categórica (Chi-cuadrado + MCA):

    • La prueba Chi-cuadrado fue altamente significativa (\(X^2 = 690.93\), \(gl = 4\), \(p < 2.2\times 10^{-16}\)), lo que indica una asociación estadística fuerte: la distribución de Casa/Apartamento cambia según la zona.
    • La tabla de proporciones lo confirma:
      Zona Centro (80.6% casas) y Zona Oriente (82.3% casas) se caracterizan por predominio de casas, mientras que Zona Oeste (85.9% apartamentos) se caracteriza por predominio de apartamentos. Zona Norte (62.4% aptos) y Zona Sur (59.0% aptos) presentan predominio de apartamentos pero con mezcla relevante de casas.
    • El MCA (categorías) respalda estos patrones: categorías cercanas en el plano tienden a presentarse juntas; se observa coherencia entre zonas con alta proporción de casas (Centro/Oriente) y la categoría Casa, y entre Zona Oeste y Apartamento, además de asociaciones con estratos/pisos según el mapa factorial.
  4. Patrones geográficos (mapas):

    • El mapa de puntos coloreados por precio evidencia heterogeneidad espacial: existen áreas con alta densidad de oferta donde coexisten precios distintos, y aparecen bolsones puntuales de precios altos (no un patrón uniforme por dirección).
    • El resumen por centroides permite comparar zonas en términos de volumen de oferta (tamaño del punto) y precio típico (mediana, color), identificando escenarios como zonas con alta oferta y precio medio/bajo (mercados grandes y competitivos) frente a zonas con menor oferta y mayor precio típico (mercados más exclusivos).