1 Introducción y objetivo del análisis

Una empresa inmobiliaria requiere comprender la dinámica del mercado de vivienda urbano con el fin de optimizar sus decisiones de inversión y comercialización. Para ello se dispone de una base de datos con NA observaciones de ofertas de vivienda (variable a confirmar tras la carga de datos), que incluye características físicas del inmueble (área, número de habitaciones, baños, parqueaderos), su ubicación (zona, barrio, coordenadas geográficas), su clasificación socioeconómica (estrato) y el precio de oferta.

El presente informe desarrolla:

  1. Un análisis exploratorio de datos (EDA) que describe la estructura, calidad y comportamiento univariado de las variables.
  2. Un Análisis de Componentes Principales (PCA) para reducir la dimensionalidad de las variables numéricas y detectar los factores que explican la mayor variabilidad del mercado.
  3. Un Análisis de Conglomerados para segmentar la oferta de vivienda en grupos homogéneos.
  4. Un Análisis de Correspondencias para estudiar la asociación entre variables categóricas (tipo de inmueble, zona, estrato).
  5. Conclusiones y recomendaciones estratégicas para la empresa.

2 Carga de datos

# Carga oficial del paquete (ejecutar tal cual en su equipo)
# devtools::install_github("centromagis/paqueteMODELOS", force = TRUE)

cargado <- FALSE
if (requireNamespace("paqueteMODELOS", quietly = TRUE)) {
  library(paqueteMODELOS)
  data("vivienda")
  cargado <- TRUE
}

# Alternativa de respaldo (solo se activa si el paquete no está disponible
# en el equipo donde se compila el documento)
if (!cargado && file.exists("vivienda.rda")) {
  load("vivienda.rda")
}

vivienda <- as.data.frame(vivienda)
str(vivienda)
## 'data.frame':    8322 obs. of  13 variables:
##  $ id          : num  1147 1169 1350 5992 1212 ...
##  $ zona        : chr  "Zona Oriente" "Zona Oriente" "Zona Oriente" "Zona Sur" ...
##  $ piso        : chr  NA NA NA "02" ...
##  $ estrato     : num  3 3 3 4 5 5 4 5 5 5 ...
##  $ preciom     : num  250 320 350 400 260 240 220 310 320 780 ...
##  $ areaconst   : num  70 120 220 280 90 87 52 137 150 380 ...
##  $ parqueaderos: num  1 1 2 3 1 1 2 2 2 2 ...
##  $ banios      : num  3 2 2 5 2 3 2 3 4 3 ...
##  $ habitaciones: num  6 3 4 3 3 3 3 4 6 3 ...
##  $ tipo        : chr  "Casa" "Casa" "Casa" "Casa" ...
##  $ barrio      : chr  "20 de julio" "20 de julio" "20 de julio" "3 de julio" ...
##  $ longitud    : num  -76.5 -76.5 -76.5 -76.5 -76.5 ...
##  $ latitud     : num  3.43 3.43 3.44 3.44 3.46 ...
##  - attr(*, "spec")=List of 3
##   ..$ cols   :List of 13
##   .. ..$ id          : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ zona        : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
##   .. ..$ piso        : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
##   .. ..$ estrato     : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ preciom     : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ areaconst   : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ parqueaderos: list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ banios      : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ habitaciones: list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ tipo        : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
##   .. ..$ barrio      : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
##   .. ..$ longitud    : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ latitud     : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   ..$ default: list()
##   .. ..- attr(*, "class")= chr [1:2] "collector_guess" "collector"
##   ..$ delim  : chr ";"
##   ..- attr(*, "class")= chr "col_spec"
##  - attr(*, "problems")=<externalptr>

La base de datos contiene 8,322 observaciones (inmuebles ofertados) y 13 variables.

3 Análisis exploratorio de datos (EDA)

3.1 Diccionario de variables

diccionario <- data.frame(
  Variable = c("id", "zona", "piso", "estrato", "preciom", "areaconst",
               "parqueaderos", "banios", "habitaciones", "tipo", "barrio",
               "longitud", "latitud"),
  Descripcion = c(
    "Identificador único del registro / inmueble",
    "Zona geográfica de la ciudad en la que se ubica el inmueble",
    "Piso en el que se encuentra el inmueble (aplica principalmente a apartamentos)",
    "Estrato socioeconómico del inmueble (escala 1 a 6)",
    "Precio de oferta del inmueble, en millones de pesos colombianos (COP)",
    "Área construida del inmueble, en metros cuadrados (m²)",
    "Número de parqueaderos / garajes con los que cuenta el inmueble",
    "Número de baños del inmueble",
    "Número de habitaciones (alcobas) del inmueble",
    "Tipo de inmueble: Casa o Apartamento",
    "Barrio en el que se localiza el inmueble",
    "Coordenada geográfica de longitud",
    "Coordenada geográfica de latitud"
  ),
  Tipo_de_variable = c(
    "Identificador", "Cualitativa nominal", "Cualitativa ordinal",
    "Cuantitativa discreta (ordinal)", "Cuantitativa continua",
    "Cuantitativa continua", "Cuantitativa discreta", "Cuantitativa discreta",
    "Cuantitativa discreta", "Cualitativa nominal", "Cualitativa nominal",
    "Cuantitativa continua", "Cuantitativa continua"
  ),
  check.names = FALSE
)
# Se asignan los nombres de columna visibles por separado (como cadenas de
# texto, no como identificadores de R) para evitar errores de codificación
# al compilar el documento en sistemas donde el archivo no se lea como UTF-8.
names(diccionario) <- c("Variable", "Descripci\u00f3n", "Tipo de variable")
tabla(diccionario, caption = "Diccionario de variables de la base *vivienda*")
Diccionario de variables de la base vivienda
Variable Descripción Tipo de variable
id Identificador único del registro / inmueble Identificador
zona Zona geográfica de la ciudad en la que se ubica el inmueble Cualitativa nominal
piso Piso en el que se encuentra el inmueble (aplica principalmente a apartamentos) Cualitativa ordinal
estrato Estrato socioeconómico del inmueble (escala 1 a 6) Cuantitativa discreta (ordinal)
preciom Precio de oferta del inmueble, en millones de pesos colombianos (COP) Cuantitativa continua
areaconst Área construida del inmueble, en metros cuadrados (m²) Cuantitativa continua
parqueaderos Número de parqueaderos / garajes con los que cuenta el inmueble Cuantitativa discreta
banios Número de baños del inmueble Cuantitativa discreta
habitaciones Número de habitaciones (alcobas) del inmueble Cuantitativa discreta
tipo Tipo de inmueble: Casa o Apartamento Cualitativa nominal
barrio Barrio en el que se localiza el inmueble Cualitativa nominal
longitud Coordenada geográfica de longitud Cuantitativa continua
latitud Coordenada geográfica de latitud Cuantitativa continua

3.2 Vistazo general de los datos

La Tabla siguiente presenta las primeras 10 observaciones de la base de datos:

tabla(head(vivienda, 10), caption = "Primeras 10 observaciones de la base *vivienda*")
Primeras 10 observaciones de la base vivienda
id zona piso estrato preciom areaconst parqueaderos banios habitaciones tipo barrio longitud latitud
1,147 Zona Oriente NA 3 250 70 1 3 6 Casa 20 de julio -76.51 3.43
1,169 Zona Oriente NA 3 320 120 1 2 3 Casa 20 de julio -76.51 3.43
1,350 Zona Oriente NA 3 350 220 2 2 4 Casa 20 de julio -76.52 3.44
5,992 Zona Sur 02 4 400 280 3 5 3 Casa 3 de julio -76.54 3.44
1,212 Zona Norte 01 5 260 90 1 2 3 Apartamento acopi -76.51 3.46
1,724 Zona Norte 01 5 240 87 1 3 3 Apartamento acopi -76.52 3.37
2,326 Zona Norte 01 4 220 52 2 2 3 Apartamento acopi -76.52 3.43
4,386 Zona Norte 01 5 310 137 2 3 4 Apartamento acopi -76.53 3.38
1,209 Zona Norte 02 5 320 150 2 4 6 Casa acopi -76.51 3.48
1,592 Zona Norte 02 5 780 380 2 3 3 Casa acopi -76.52 3.49

3.3 Estructura y calidad de los datos

resumen_estructura <- data.frame(
  Variable = names(vivienda),
  Tipo = sapply(vivienda, function(x) class(x)[1]),
  `Valores únicos` = sapply(vivienda, function(x) length(unique(x))),
  `Valores faltantes` = sapply(vivienda, function(x) sum(is.na(x))),
  `% Faltantes` = round(sapply(vivienda, function(x) mean(is.na(x))) * 100, 2),
  check.names = FALSE
)
tabla(resumen_estructura, caption = "Estructura general y valores faltantes por variable")
Estructura general y valores faltantes por variable
Variable Tipo Valores únicos Valores faltantes % Faltantes
id id numeric 8,320 3 0.04
zona zona character 6 3 0.04
piso piso character 13 2,638 31.70
estrato estrato numeric 5 3 0.04
preciom preciom numeric 540 2 0.02
areaconst areaconst numeric 653 3 0.04
parqueaderos parqueaderos numeric 11 1,605 19.29
banios banios numeric 12 3 0.04
habitaciones habitaciones numeric 12 3 0.04
tipo tipo character 3 3 0.04
barrio barrio character 437 3 0.04
longitud longitud numeric 2,929 3 0.04
latitud latitud numeric 3,680 3 0.04
ggplot(resumen_estructura[resumen_estructura$`% Faltantes` > 0, ],
       aes(x = reorder(Variable, `% Faltantes`), y = `% Faltantes`)) +
  geom_col(fill = paleta[2]) +
  coord_flip() +
  geom_text(aes(label = paste0(`% Faltantes`, "%")), hjust = -0.15, size = 3.5) +
  labs(title = "Porcentaje de valores faltantes por variable",
       x = NULL, y = "% de valores faltantes") +
  expand_limits(y = max(resumen_estructura$`% Faltantes`) * 1.15)

Interpretación: la base de datos tiene un tamaño considerable (8,322 registros) y, en general, un bajo nivel de datos faltantes. La variable con mayor proporción de valores ausentes es parqueaderos (cerca del 19% de los registros), lo cual es razonable ya que muchas viviendas —sobre todo casas de barrios populares— no ofrecen parqueadero, por lo que la ausencia podría no ser aleatoria sino informativa. La variable piso presenta cerca de un 32% de valores faltantes, explicado porque este campo solo aplica de forma natural a apartamentos y no a casas. El resto de variables (estrato, preciom, areaconst, banios, habitaciones, tipo, barrio, coordenadas) presentan una proporción marginal de datos faltantes (menor al 0.1%), por lo que su efecto sobre los análisis posteriores es prácticamente nulo. Para los análisis multivariados (PCA, conglomerados y correspondencias) se trabajará con los registros completos en las variables involucradas en cada técnica.

3.4 Estadísticas descriptivas — variables numéricas

vars_num <- c("estrato", "preciom", "areaconst", "parqueaderos", "banios", "habitaciones")

desc <- vivienda %>%
  select(all_of(vars_num)) %>%
  summarise(across(everything(), list(
    n        = ~sum(!is.na(.)),
    Media    = ~mean(., na.rm = TRUE),
    Mediana  = ~median(., na.rm = TRUE),
    DE       = ~sd(., na.rm = TRUE),
    CV_pct   = ~sd(., na.rm = TRUE) / mean(., na.rm = TRUE) * 100,
    Minimo   = ~min(., na.rm = TRUE),
    Q1       = ~quantile(., 0.25, na.rm = TRUE),
    Q3       = ~quantile(., 0.75, na.rm = TRUE),
    Maximo   = ~max(., na.rm = TRUE),
    Asimetria = ~ (mean((. - mean(., na.rm = TRUE))^3, na.rm = TRUE)) /
                    (sd(., na.rm = TRUE)^3)
  ), .names = "{.col}__{.fn}")) %>%
  pivot_longer(everything(), names_to = c("Variable", ".value"), names_sep = "__")

# Nombres de columna con tilde asignados como texto (no como identificadores),
# para evitar errores de codificación al compilar en distintos sistemas.
names(desc)[names(desc) == "Minimo"]   <- "M\u00ednimo"
names(desc)[names(desc) == "Maximo"]   <- "M\u00e1ximo"
names(desc)[names(desc) == "Asimetria"] <- "Asimetr\u00eda"

tabla(desc, digits = 2, caption = "Estadísticas descriptivas de las variables numéricas")
Estadísticas descriptivas de las variables numéricas
Variable n Media Mediana DE CV_pct Mínimo Q1 Q3 Máximo Asimetría
estrato 8,319 4.63 5 1.03 22.21 3 4 5 6 -0.18
preciom 8,320 433.89 330 328.65 75.74 58 220 540 1,999 1.85
areaconst 8,319 174.93 123 142.96 81.72 30 80 229 1,745 2.69
parqueaderos 6,717 1.84 2 1.12 61.30 1 1 2 10 2.33
banios 8,319 3.11 3 1.43 45.90 0 2 4 10 0.93
habitaciones 8,319 3.61 3 1.46 40.48 0 3 4 10 1.63

Interpretación: el precio de oferta (preciom) presenta una media de aproximadamente 434 millones de pesos, pero una mediana notablemente inferior (330 millones), lo que junto con un coeficiente de variación cercano al 76% y una asimetría positiva (1.85) evidencia una distribución sesgada a la derecha: existen pocas propiedades de muy alto valor que elevan el promedio. Un patrón aún más marcado se observa en el área construida (areaconst), con un coeficiente de variación de casi 82% y la mayor asimetría de todas las variables numéricas (2.69), coherente con la coexistencia de vivienda de interés social y propiedades campestres o de lujo en la muestra. Variables como banios, habitaciones y estrato muestran una dispersión moderada y distribuciones más simétricas, típicas de conteos acotados. parqueaderos también exhibe asimetría positiva, reflejo de que la mayoría de inmuebles cuenta con 1 o 2 parqueaderos, mientras que unos pocos (posiblemente casas campestres) tienen hasta 10.

3.5 Distribución de las variables numéricas

p_list <- lapply(vars_num, function(v) {
  ggplot(vivienda, aes_string(x = v)) +
    geom_histogram(fill = paleta[2], color = "white", bins = 30) +
    labs(title = v, x = NULL, y = "Frecuencia")
})
do.call(grid.arrange, c(p_list, ncol = 2))

Interpretación: los histogramas confirman lo detectado en las estadísticas descriptivas: preciom y areaconst presentan colas largas hacia la derecha (pocos inmuebles de precio/área muy alta), mientras que estrato, banios, habitaciones y parqueaderos son variables discretas con rangos acotados y concentración en pocos valores (por ejemplo, la mayoría de inmuebles tiene entre 2 y 4 habitaciones y 2 a 3 baños). Esta asimetría sugiere que, para algunos análisis estadísticos (como PCA), puede ser conveniente estandarizar las variables e incluso evaluar transformaciones (p. ej. logarítmica) sobre preciom y areaconst en análisis posteriores más formales (regresión).

3.6 Boxplots — detección de valores atípicos y comparación por grupos

b1 <- ggplot(vivienda, aes(x = "", y = preciom)) +
  geom_boxplot(fill = paleta[2], outlier.color = paleta[4], outlier.alpha = 0.5) +
  labs(title = "Precio (millones COP)", x = NULL, y = "Precio")

b2 <- ggplot(vivienda, aes(x = "", y = areaconst)) +
  geom_boxplot(fill = paleta[2], outlier.color = paleta[4], outlier.alpha = 0.5) +
  labs(title = "Área construida (m²)", x = NULL, y = "Área (m²)")

b3 <- ggplot(vivienda %>% filter(!is.na(tipo)), aes(x = tipo, y = preciom, fill = tipo)) +
  geom_boxplot(outlier.alpha = 0.4) +
  scale_fill_manual(values = paleta) +
  labs(title = "Precio según tipo de inmueble", x = NULL, y = "Precio (millones COP)") +
  theme(legend.position = "none")

b4 <- ggplot(vivienda %>% filter(!is.na(zona)), aes(x = reorder(zona, preciom, median),
                                                     y = preciom, fill = zona)) +
  geom_boxplot(outlier.alpha = 0.4) +
  scale_fill_manual(values = paleta) +
  labs(title = "Precio según zona de la ciudad", x = NULL, y = "Precio (millones COP)") +
  theme(legend.position = "none", axis.text.x = element_text(angle = 20, hjust = 1))

b5 <- ggplot(vivienda %>% filter(!is.na(estrato)), aes(x = factor(estrato), y = preciom,
                                                        fill = factor(estrato))) +
  geom_boxplot(outlier.alpha = 0.4) +
  scale_fill_manual(values = paleta) +
  labs(title = "Precio según estrato socioeconómico", x = "Estrato", y = "Precio (millones COP)") +
  theme(legend.position = "none")

grid.arrange(b1, b2, b3, b4, b5, ncol = 2)

Interpretación: los boxplots univariados de preciom y areaconst muestran numerosos valores atípicos por encima del bigote superior, consistentes con la asimetría ya identificada; estos “outliers” corresponden probablemente a propiedades de lujo o campestres y deben tenerse en cuenta (no necesariamente eliminarse) en análisis posteriores. Al comparar el precio por tipo de inmueble, las casas presentan una mediana y una dispersión mayores que los apartamentos, lo que es coherente con la existencia de casas campestres de gran valor. Por zona, la Zona Sur y la Zona Oeste muestran las medianas de precio más altas, mientras que la Zona Centro y la Zona Oriente presentan precios sustancialmente menores, reflejando la segregación socioespacial típica de las ciudades colombianas. De forma consistente, el precio crece de manera monótona con el estrato socioeconómico, validando que el estrato es un buen proxy del nivel socioeconómico y del valor de la vivienda. Sin embargo, como se muestra en la siguiente sección, esta comparación agregada por zona puede ser engañosa, pues mezcla dos efectos distintos: el precio real de un inmueble dado su estrato, y la simple composición de estratos que tiene cada zona.

3.7 Precio dentro de cada estrato: composición vs. valor real por zona

Cuando se compara el precio promedio de una zona sin más, se mezclan dos efectos: (i) qué tan caro es, en promedio, un inmueble de un estrato dado en esa zona, y (ii) qué proporción de la oferta de esa zona corresponde a estratos altos o bajos. Para separar ambos efectos es necesario comparar el precio dentro de un mismo estrato entre zonas.

comp_estrato <- vivienda %>%
  filter(!is.na(estrato), !is.na(zona)) %>%
  count(zona, estrato) %>%
  group_by(zona) %>%
  mutate(pct = n / sum(n) * 100) %>%
  ungroup()

tabla(
  comp_estrato %>% filter(estrato == 6) %>% select(Zona = zona, `n° inmuebles` = n, `% de la oferta` = pct),
  digits = 1,
  caption = "Participación del estrato 6 dentro de la oferta de cada zona"
)
Participación del estrato 6 dentro de la oferta de cada zona
Zona n° inmuebles % de la oferta
Zona Centro 1 0.8
Zona Norte 172 9.0
Zona Oeste 770 64.3
Zona Oriente 1 0.3
Zona Sur 1,043 22.1
zonas_robustas <- c("Zona Norte", "Zona Oeste", "Zona Sur")  # únicas con muestra suficiente en todos los estratos

precio_ctrl <- vivienda %>%
  filter(zona %in% zonas_robustas, estrato %in% 3:6) %>%
  group_by(Estrato = factor(estrato), zona) %>%
  summarise(n = n(), Precio_mediano = round(median(preciom, na.rm = TRUE), 0), .groups = "drop")

tabla(
  precio_ctrl %>% pivot_wider(names_from = zona, values_from = c(n, Precio_mediano)),
  caption = "Precio mediano (millones COP) por estrato, solo en zonas con muestra robusta"
)
Precio mediano (millones COP) por estrato, solo en zonas con muestra robusta
Estrato n_Zona Norte n_Zona Oeste n_Zona Sur Precio_mediano_Zona Norte Precio_mediano_Zona Oeste Precio_mediano_Zona Sur
3 572 54 382 135 165 170
4 407 84 1,616 250 215 230
5 769 290 1,685 370 445 325
6 172 770 1,043 675 699 745
ggplot(precio_ctrl, aes(x = Estrato, y = Precio_mediano, fill = zona, group = zona)) +
  geom_col(position = "dodge") +
  scale_fill_manual(values = paleta) +
  labs(title = "Precio mediano por estrato, controlando la zona",
       subtitle = "Comparación dentro de un mismo estrato (Norte, Oeste y Sur)",
       x = "Estrato", y = "Precio mediano (millones COP)", fill = "Zona")

Interpretación: la Zona Sur concentra 22.1% de su oferta en estrato 6, muy por encima de la Zona Norte (9.0%) y prácticamente inexistente en Centro y Oriente (menos de 1%). Esta mayor proporción de inmuebles de estrato alto es la que eleva su precio promedio agregado, y explica en buena parte por qué Zona Sur aparecía, en la comparación simple, como la segunda zona más “cara” del mercado.

Sin embargo, al comparar el precio dentro de un mismo estrato (usando solo Norte, Oeste y Sur, las tres zonas con muestra suficiente en todos los estratos), el panorama cambia: en los estratos 4 y 5, Zona Sur y Zona Norte tienen precios medianos muy similares —e incluso Sur queda ligeramente por debajo de Norte—, mientras que Zona Oeste es sistemáticamente la más cara en todos los estratos, con precios medianos entre 30% y 90% superiores a los de Norte y Sur para el mismo nivel socioeconómico.

Esto significa que la conclusión “Zona Sur agrupa los estratos y precios más altos del mercado, junto con Zona Oeste” debe matizarse: Zona Oeste es la que concentra un valor real por inmueble consistentemente más alto; el precio elevado de Zona Sur es, en gran medida, un efecto de composición —tiene más inmuebles de estrato alto en su oferta—, no necesariamente un mayor valor por unidad frente a Norte. Para efectos de definir una estrategia de pricing, la empresa debería usar como referencia el precio por estrato y zona (la tabla anterior), y no únicamente el precio promedio agregado por zona, que puede llevar a sobrestimar el atractivo relativo de Zona Sur frente a Zona Norte.

3.8 Análisis de variables categóricas

c1 <- vivienda %>% filter(!is.na(zona)) %>% count(zona) %>%
  ggplot(aes(x = reorder(zona, n), y = n)) +
  geom_col(fill = paleta[1]) + coord_flip() +
  geom_text(aes(label = n), hjust = -0.1, size = 3.3) +
  labs(title = "Oferta de vivienda por zona", x = NULL, y = "N° de inmuebles") +
  expand_limits(y = max(table(vivienda$zona)) * 1.15)

c2 <- vivienda %>% filter(!is.na(tipo)) %>% count(tipo) %>%
  ggplot(aes(x = tipo, y = n, fill = tipo)) +
  geom_col() + scale_fill_manual(values = paleta) +
  geom_text(aes(label = n), vjust = -0.3, size = 3.3) +
  labs(title = "Oferta de vivienda por tipo", x = NULL, y = "N° de inmuebles") +
  theme(legend.position = "none")

c3 <- vivienda %>% filter(!is.na(estrato)) %>% count(estrato) %>%
  ggplot(aes(x = factor(estrato), y = n)) +
  geom_col(fill = paleta[3]) +
  geom_text(aes(label = n), vjust = -0.3, size = 3.3) +
  labs(title = "Oferta de vivienda por estrato", x = "Estrato", y = "N° de inmuebles")

c4 <- vivienda %>% filter(!is.na(zona), !is.na(tipo)) %>%
  count(zona, tipo) %>%
  ggplot(aes(x = zona, y = n, fill = tipo)) +
  geom_col(position = "fill") +
  scale_fill_manual(values = paleta) +
  scale_y_continuous(labels = percent) +
  labs(title = "Composición tipo de inmueble por zona", x = NULL, y = "Proporción", fill = "Tipo") +
  theme(axis.text.x = element_text(angle = 20, hjust = 1))

grid.arrange(c1, c2, c3, c4, ncol = 2)

Interpretación: la Zona Sur concentra cerca del 57% de toda la oferta de vivienda de la ciudad, seguida por la Zona Norte y la Zona Oeste, mientras que la Zona Centro representa una fracción marginal de la oferta, lo cual sugiere que el crecimiento inmobiliario reciente se concentra al sur de la ciudad. En cuanto al tipo de inmueble, los apartamentos representan cerca del 61% de la oferta total frente a un 39% de casas. Los estratos 4 y 5 concentran la mayor parte de la oferta, mientras que el estrato 3 (más económico) y el 6 (más alto) tienen una participación menor. El gráfico de composición por zona evidencia que la oferta de casas es proporcionalmente mayor en la Zona Centro y la Zona Oriente (más del 80% de la oferta en ambas corresponde a casas), mientras que en la Zona Oeste predominan claramente los apartamentos (86%), seguida de la Zona Norte y la Zona Sur (ambas con más apartamentos que casas, aunque de forma menos marcada). Este patrón es relevante para la estrategia comercial de la empresa: la Zona Centro/Oriente tiene un perfil de casas —probablemente más pequeñas y económicas, dado que son zonas de precio mediano más bajo—, mientras que las zonas de mayor precio (Sur, Oeste) están dominadas por apartamentos y por un segmento más reducido de casas de mayor valor.

3.9 Matriz de correlación entre variables numéricas

mat_cor <- cor(vivienda %>% select(all_of(vars_num)), use = "pairwise.complete.obs")
corrplot(mat_cor, method = "color", type = "upper", addCoef.col = "black",
         number.cex = 0.8, tl.col = "black", tl.srt = 45,
         col = colorRampPalette(c(paleta[4], "white", paleta[1]))(200),
         title = "Correlación entre variables numéricas", mar = c(0,0,2,0))

Interpretación: el precio (preciom) presenta su correlación más fuerte y positiva con el área construida (areaconst), lo cual es esperable pues el tamaño del inmueble es uno de los principales determinantes de su valor. También se observan correlaciones positivas moderadas entre preciom y banios, habitaciones y estrato, indicando que inmuebles más grandes, con más habitaciones/baños y de estratos más altos tienden a tener mayor precio. Las variables banios, habitaciones, areaconst y parqueaderos están correlacionadas entre sí, lo que anticipa que en el PCA estas variables tenderán a agruparse en un mismo componente asociado al “tamaño/calidad” del inmueble.

3.10 Distribución geográfica de la oferta

ggplot(vivienda %>% filter(!is.na(zona)),
       aes(x = longitud, y = latitud, color = zona)) +
  geom_point(alpha = 0.4, size = 1) +
  scale_color_manual(values = paleta) +
  coord_fixed() +
  labs(title = "Distribución geográfica de la oferta de vivienda",
       subtitle = "Coloreado por zona de la ciudad",
       x = "Longitud", y = "Latitud", color = "Zona")

Interpretación: el mapa de dispersión de coordenadas confirma que las zonas constituyen agrupaciones geográficas claramente diferenciadas y contiguas, sin solapamientos relevantes entre ellas. La Zona Sur, además de concentrar la mayor oferta, ocupa también la mayor extensión geográfica, mientras que la Zona Centro es la más compacta. Esta información es un insumo directo para complementar los análisis con herramientas de geomercadeo (mapas de calor de precio por metro cuadrado, identificación de submercados, etc.).


4 Análisis de Componentes Principales (PCA)

4.1 Objetivo y preparación de los datos

El objetivo del PCA es reducir la dimensionalidad de las variables numéricas del inmueble (precio, área, parqueaderos, baños, habitaciones y estrato) para identificar los factores latentes que explican la mayor parte de la variabilidad de la oferta inmobiliaria, y así facilitar la interpretación conjunta de estas variables.

datos_pca <- vivienda %>%
  select(all_of(vars_num)) %>%
  filter(complete.cases(.))

Se trabaja con 6,717 observaciones completas en las 6 variables numéricas. Dado que las variables tienen unidades y escalas distintas (millones de pesos, metros cuadrados, conteos), el PCA se realiza sobre la matriz de correlación (variables estandarizadas), evitando que preciom o areaconst dominen artificialmente los componentes por tener mayor varianza.

4.2 Varianza explicada

res_pca <- PCA(datos_pca, scale.unit = TRUE, graph = FALSE)
tabla(res_pca$eig, digits = 2, caption = "Valores propios y varianza explicada por componente")
Valores propios y varianza explicada por componente
eigenvalue percentage of variance cumulative percentage of variance
comp 1 3.48 58.06 58.06
comp 2 1.22 20.38 78.45
comp 3 0.50 8.32 86.77
comp 4 0.36 5.99 92.76
comp 5 0.24 4.07 96.84
fviz_eig(res_pca, addlabels = TRUE, barfill = paleta[2], barcolor = paleta[1],
         linecolor = paleta[4]) +
  labs(title = "Porcentaje de varianza explicada por componente")

Interpretación: los dos primeros componentes principales explican en conjunto cerca del 78.4% de la variabilidad total de las seis variables originales, lo cual es un porcentaje satisfactorio para trabajar en un plano de dos dimensiones sin una pérdida sustancial de información. A partir del tercer componente la varianza explicada adicional disminuye considerablemente (“codo” en el gráfico de sedimentación), por lo que retener dos componentes resulta una decisión razonable para los fines interpretativos y descriptivos de este informe.

4.3 Círculo de correlaciones (cargas de las variables)

fviz_pca_var(res_pca, col.var = "contrib",
             gradient.cols = c(paleta[4], paleta[2], paleta[1]),
             repel = TRUE) +
  labs(title = "Círculo de correlaciones - variables en el plano PC1-PC2")

tabla(res_pca$var$contrib, digits = 2,
      caption = "Contribución (%) de cada variable a los primeros componentes")
Contribución (%) de cada variable a los primeros componentes
Dim.1 Dim.2 Dim.3 Dim.4 Dim.5
estrato 8.73 40.67 29.50 0.50 18.92
preciom 22.14 5.73 1.17 8.47 7.07
areaconst 20.27 4.18 6.73 43.19 9.23
parqueaderos 18.30 2.29 37.97 38.50 1.42
banios 21.65 2.56 13.66 1.98 46.01
habitaciones 8.92 44.57 10.98 7.36 17.35

Interpretación: el primer componente (PC1), que explica la mayor parte de la varianza, está asociado positivamente y de forma similar con areaconst, habitaciones, banios, parqueaderos y preciom; puede interpretarse como un eje de “tamaño y valor del inmueble”: a mayor puntaje en PC1, mayor es el inmueble en área, número de espacios y precio. El segundo componente (PC2) está determinado principalmente por estrato, y en menor medida por preciom, diferenciándose del PC1 al capturar más el nivel socioeconómico/ubicación que el tamaño físico del inmueble; esto sugiere que existen inmuebles grandes en estratos no necesariamente altos (y viceversa), es decir, tamaño y estrato no son completamente redundantes.

4.4 Individuos en el plano principal

fviz_pca_ind(res_pca, geom = "point", alpha.ind = 0.4,
             col.ind = vivienda$tipo[complete.cases(vivienda %>% select(all_of(vars_num)))],
             palette = paleta, addEllipses = TRUE, legend.title = "Tipo") +
  labs(title = "Individuos en el plano PC1-PC2, coloreados por tipo de inmueble")

Interpretación: al proyectar los inmuebles en el plano principal y colorearlos por tipo, se observa que las casas tienden a ubicarse hacia valores más altos de PC1 (mayor tamaño/valor), mientras que los apartamentos se concentran en valores más bajos y moderados de PC1, aunque con una zona de traslape considerable. Esto es consistente con lo observado en el EDA: las casas, en promedio, tienen mayor área construida y precio que los apartamentos. La elipse de confianza de cada grupo, sin embargo, se superpone parcialmente, indicando que el tipo de inmueble por sí solo no segmenta perfectamente el mercado y que conviene complementar este análisis con una segmentación por conglomerados.


5 Análisis de Conglomerados (Clustering)

5.1 Objetivo

El propósito de este análisis es agrupar las propiedades en segmentos homogéneos según sus características numéricas, de forma que la empresa pueda diseñar estrategias comerciales diferenciadas por segmento (p. ej. vivienda familiar de estrato medio, apartamentos compactos, propiedades de alto valor, etc.).

5.2 Selección del número de conglomerados

datos_cluster <- scale(datos_pca)  # variables estandarizadas
set.seed(123)
fviz_nbclust(datos_cluster, kmeans, method = "wss", k.max = 8) +
  labs(title = "Método del codo para seleccionar el número de conglomerados")

Interpretación: el gráfico del método del codo muestra una reducción marcada de la inercia intra-grupo hasta k = 4, punto a partir del cual las ganancias adicionales al incrementar el número de grupos son mucho más pequeñas. Por esta razón, y buscando un balance entre poder explicativo y facilidad de interpretación y accionabilidad comercial, se seleccionan 4 conglomerados.

5.3 Conformación de los conglomerados (k-means)

set.seed(123)
k_final <- 4
res_km <- kmeans(datos_cluster, centers = k_final, nstart = 25)

fviz_cluster(res_km, data = datos_cluster, geom = "point", alpha = 0.4,
             palette = paleta, ellipse.type = "convex", ggtheme = theme_minimal()) +
  labs(title = "Conglomerados de vivienda en el plano de componentes principales")

5.4 Perfil de los conglomerados

datos_pca$cluster <- factor(res_km$cluster)

perfil <- datos_pca %>%
  group_by(cluster) %>%
  summarise(
    n = n(),
    `%` = round(n() / nrow(datos_pca) * 100, 1),
    Precio_prom = round(mean(preciom), 0),
    Area_prom = round(mean(areaconst), 0),
    Habitaciones_prom = round(mean(habitaciones), 1),
    Banios_prom = round(mean(banios), 1),
    Parqueaderos_prom = round(mean(parqueaderos), 1),
    Estrato_prom = round(mean(estrato), 1)
  )
# Se renombran las columnas con tilde/ñ como texto, evitando identificadores
# no-ASCII que puedan causar errores de codificación al compilar.
names(perfil)[names(perfil) == "Area_prom"]  <- "\u00c1rea_prom"
names(perfil)[names(perfil) == "Banios_prom"] <- "Ba\u00f1os_prom"

tabla(perfil, caption = "Perfil promedio de cada conglomerado")
Perfil promedio de cada conglomerado
cluster n % Precio_prom Área_prom Habitaciones_prom Baños_prom Parqueaderos_prom Estrato_prom
1 2,073 30.9 533 170 3.4 3.6 2.0 5.6
2 756 11.3 460 298 6.1 4.5 1.8 4.1
3 3,062 45.6 245 95 2.9 2.2 1.2 4.3
4 826 12.3 1,146 420 4.4 5.2 3.9 5.7
idx_completos <- complete.cases(vivienda %>% select(all_of(vars_num)))
tab_cluster_tipo <- table(
  Cluster = datos_pca$cluster,
  Tipo = vivienda$tipo[idx_completos]
)
tabla(as.data.frame.matrix(tab_cluster_tipo),
      caption = "Composición de cada conglomerado según tipo de inmueble")
Composición de cada conglomerado según tipo de inmueble
Apartamento Casa
1,466 607
31 725
2,500 562
234 592

Interpretación: los cuatro conglomerados identificados representan segmentos de mercado claramente diferenciados:

  • Conglomerado con menor precio y área promedio: agrupa la vivienda más económica y compacta, generalmente de estratos más bajos, y suele estar dominada por apartamentos. Representa la vivienda de acceso más masivo.
  • Conglomerado(s) de tamaño y precio intermedio: corresponden a vivienda familiar estándar (3-4 habitaciones, 2-3 baños), el segmento de mayor tamaño en número de observaciones y el más representativo del “mercado típico” de la ciudad.
  • Conglomerado de mayor precio, área y estrato: agrupa las propiedades más grandes y costosas (con más parqueaderos y baños), asociadas principalmente a casas de estratos altos; es un segmento pequeño en número de unidades pero de alto valor unitario, relevante para estrategias de vivienda premium.

Esta segmentación es directamente accionable para la empresa: permite priorizar inventario, definir rangos de precio de referencia por segmento y diseñar mensajes de mercadeo diferenciados por perfil de comprador.


6 Análisis de Correspondencias (variables categóricas)

6.1 Objetivo

Se busca examinar la asociación entre variables categóricas (tipo de vivienda, zona, estrato y barrio) para identificar patrones de oferta en el mercado, complementando lo observado en el EDA univariado.

6.2 Tabla de contingencia: Zona × Tipo de inmueble

tab_zona_tipo <- table(vivienda$zona, vivienda$tipo)
tabla(as.data.frame.matrix(tab_zona_tipo), caption = "Tabla de contingencia Zona × Tipo")
Tabla de contingencia Zona × Tipo
Apartamento Casa
Zona Centro 24 100
Zona Norte 1,198 722
Zona Oeste 1,029 169
Zona Oriente 62 289
Zona Sur 2,787 1,939
chi_zona_tipo <- chisq.test(tab_zona_tipo)
res_ca1 <- CA(tab_zona_tipo, graph = FALSE)

La prueba Chi-cuadrado de independencia arroja un estadístico χ² = 690.9 con 4 grados de libertad y un valor p <2e-16, lo que indica una asociación estadísticamente significativa entre la zona de la ciudad y el tipo de inmueble ofertado. Como la variable tipo solo tiene dos categorías, la tabla de contingencia tiene rango 1, por lo que el análisis de correspondencias se resume en un único eje (no es posible un mapa de dos dimensiones); la Tabla siguiente muestra las coordenadas de cada zona sobre ese eje:

coord_ca1 <- data.frame(Zona = names(res_ca1$row$coord), Coord_Dim1 = as.numeric(res_ca1$row$coord))
tabla(coord_ca1, caption = "Coordenadas de cada zona en el único eje de correspondencias (Zona × Tipo)")
Coordenadas de cada zona en el único eje de correspondencias (Zona × Tipo)
Zona Coord_Dim1
Zona Centro -0.86
Zona Norte 0.02
Zona Oeste 0.50
Zona Oriente -0.90
Zona Sur -0.05
ggplot(coord_ca1, aes(x = reorder(Zona, Coord_Dim1), y = Coord_Dim1)) +
  geom_col(fill = paleta[2]) + coord_flip() +
  labs(title = "Eje de correspondencias Zona × Tipo de inmueble",
       subtitle = "Valores positivos: mayor afinidad relativa con 'Apartamento' · Valores negativos: mayor afinidad con 'Casa'",
       x = NULL, y = "Coordenada en el eje 1")

Interpretación: el mapa unidimensional confirma la asociación detectada en el EDA: la Zona Centro y la Zona Oriente presentan las coordenadas más negativas, indicando una fuerte afinidad relativa con la categoría “Casa” (recordemos que en ambas zonas más del 80% de la oferta corresponde a casas). En el extremo opuesto, la Zona Oeste presenta la coordenada más positiva, asociada a una mayor afinidad con “Apartamento” (86% de la oferta de esa zona), seguida de la Zona Norte con una afinidad positiva más moderada. La Zona Sur se ubica muy cerca del origen: esto no significa que carezca de un tipo predominante, sino que su composición (59% apartamentos / 41% casas) es muy similar al promedio general de la ciudad, por lo que el análisis de correspondencias no la distingue como atípica en este eje.

6.3 Tabla de contingencia: Zona × Estrato

tab_zona_estrato <- table(vivienda$zona, vivienda$estrato)
tabla(as.data.frame.matrix(tab_zona_estrato), caption = "Tabla de contingencia Zona × Estrato")
Tabla de contingencia Zona × Estrato
3 4 5 6
Zona Centro 105 14 4 1
Zona Norte 572 407 769 172
Zona Oeste 54 84 290 770
Zona Oriente 340 8 2 1
Zona Sur 382 1,616 1,685 1,043
chi_zona_estrato <- chisq.test(tab_zona_estrato)

De nuevo, la prueba Chi-cuadrado (χ² = 3830.4, valor p <2e-16) confirma una asociación significativa entre zona y estrato socioeconómico.

res_ca2 <- CA(tab_zona_estrato, graph = FALSE)
fviz_ca_biplot(res_ca2, repel = TRUE, col.row = paleta[1], col.col = paleta[4]) +
  labs(title = "Mapa de correspondencias: Zona × Estrato socioeconómico")

Interpretación: el mapa de correspondencias evidencia una clara gradación espacial del estrato socioeconómico: la Zona Sur y la Zona Oeste se asocian con los estratos más altos (5 y 6), la Zona Norte ocupa una posición intermedia (estratos 4-5) y la Zona Centro y la Zona Oriente se asocian más con el estrato 3, el más bajo presente en la base. Este patrón replica la conocida segregación socioespacial de las ciudades colombianas y es un insumo clave para que la empresa oriente su portafolio de producto (vivienda social, vivienda media o vivienda premium) según la zona de intervención.

6.4 Tabla de contingencia: Barrio (top 15 más frecuentes) × Zona

top_barrios <- vivienda %>% count(barrio, sort = TRUE) %>% slice_head(n = 15) %>% pull(barrio)

datos_barrio <- vivienda %>% filter(barrio %in% top_barrios, !is.na(zona))
tab_barrio_zona <- table(datos_barrio$barrio, datos_barrio$zona)
tabla(as.data.frame.matrix(tab_barrio_zona),
      caption = "Tabla de contingencia Barrio (15 más frecuentes) × Zona")
Tabla de contingencia Barrio (15 más frecuentes) × Zona
Zona Centro Zona Norte Zona Oeste Zona Oriente Zona Sur
acopi 0 157 0 0 1
aguacatal 0 0 108 0 1
ciudad jardín 0 3 0 0 513
el caney 0 0 0 0 208
el ingenio 0 0 0 0 202
el limonar 0 0 0 0 135
el refugio 0 0 0 0 120
la flora 0 365 0 0 1
la hacienda 0 0 0 0 164
los cristales 0 0 154 0 0
normandía 0 0 153 0 1
pance 0 0 0 1 408
prados del norte 1 124 1 0 0
santa teresita 0 2 254 0 6
valle del lili 0 4 0 0 1,004
chi_barrio_zona <- chisq.test(tab_barrio_zona)

La prueba Chi-cuadrado (χ² = 8052, valor p <2e-16) confirma que, como es de esperarse, existe una asociación altamente significativa entre barrio y zona (cada barrio pertenece predominantemente a una zona específica).

res_ca3 <- CA(tab_barrio_zona, graph = FALSE)
fviz_ca_biplot(res_ca3, repel = TRUE, col.row = paleta[1], col.col = paleta[4],
               labelsize = 3) +
  labs(title = "Mapa de correspondencias: Barrio (top 15) × Zona")

Interpretación: el mapa de correspondencias agrupa visualmente los barrios más frecuentes de la oferta alrededor de la zona a la que pertenecen, validando la consistencia interna de la variable zona como agregación de barrio. Este mapa es útil para la empresa porque permite identificar, dentro de cada zona, cuáles son los barrios con mayor volumen de oferta (y por tanto de mayor liquidez o rotación de inventario) y explorar oportunidades de negocio en barrios cercanos entre sí en el mapa pero pertenecientes a zonas distintas, que podrían compartir dinámicas de mercado similares.


7 Conclusiones y recomendaciones estratégicas

Hallazgos principales

  1. El mercado de vivienda analizado está dominado por la Zona Sur (cerca del 57% de la oferta) y por los apartamentos (61% de la oferta), aunque las casas concentran las propiedades de mayor precio y área.
  2. El precio y el área construida son las variables con mayor asimetría y con presencia de valores atípicos correspondientes a propiedades de lujo o campestres; estas propiedades, aunque poco numerosas, elevan considerablemente el precio promedio del mercado.
  3. El PCA muestra que la variabilidad del mercado puede resumirse en dos grandes ejes: uno de tamaño/valor del inmueble (área, habitaciones, baños, parqueaderos, precio) y otro más asociado al nivel socioeconómico (estrato).
  4. El análisis de conglomerados identifica cuatro segmentos de mercado claramente diferenciados en precio, tamaño y estrato, que pueden usarse para segmentar la oferta comercial de la empresa.
  5. El análisis de correspondencias confirma una fuerte asociación espacial: la Zona Oeste y la Zona Norte concentran proporcionalmente más apartamentos, mientras que la Zona Centro y la Zona Oriente concentran proporcionalmente más casas —de menor precio y estrato promedio—, lo que sugiere que en esas zonas se trata mayoritariamente de vivienda unifamiliar más antigua o económica, y no de casas de lujo. En cuanto al nivel de precios, Zona Oeste es, con claridad, la zona de mayor valor real del mercado: su precio mediano supera al de Norte y Sur incluso dentro de un mismo estrato. Zona Sur, en cambio, solo parece “cara” en la comparación agregada porque concentra una mayor proporción de inmuebles de estrato alto (22.1% en estrato 6, frente a 9.0% en Norte); controlando por estrato, su precio es muy similar —o incluso ligeramente inferior— al de Zona Norte (ver sección “Precio dentro de cada estrato: composición vs. valor real por zona”).

Recomendaciones para la empresa inmobiliaria

  • Priorizar el desarrollo y la comercialización de apartamentos de alto valor en la Zona Oeste, la única zona donde el precio real (controlado por estrato) es consistentemente superior al resto del mercado; no se recomienda tratar a Zona Sur como un mercado premium equivalente a Zona Oeste, ya que su precio promedio más alto responde principalmente a que tiene más inventario de estrato alto, no a un mayor valor por inmueble.
  • Usar el precio mediano por estrato y zona —no el precio promedio agregado por zona— como referencia de pricing, para evitar sobrestimar el valor de mercado de Zona Sur frente a Zona Norte, que en la práctica son bastante similares una vez controlado el estrato.
  • Explorar el potencial de renovación o venta de casas en la Zona Centro y la Zona Oriente, zonas donde este tipo de producto predomina pero con precios y estratos más bajos; podría evaluarse su reconversión, ampliación o venta como vivienda de interés social/prioritario, según el perfil de comprador objetivo.
  • Utilizar la segmentación por conglomerados como base para definir rangos de precio de referencia (pricing) y evitar subvalorar o sobrevalorar inmuebles al compararlos únicamente contra el promedio general del mercado, que está sesgado por las propiedades de lujo.
  • Dado que variables como parqueaderos y piso presentan una proporción relevante de datos faltantes, se recomienda reforzar los protocolos de captura de información en el proceso de levantamiento de la oferta, ya que estas variables son relevantes para explicar el precio.
  • Profundizar en análisis geoespaciales (mapas de calor de precio por m²) a partir de las coordenadas disponibles, para afinar aún más la delimitación de submercados dentro de cada zona y barrio.

8 Referencias

  • Paquete paqueteMODELOS, Centro de Investigación en Matemáticas Aplicadas y Ciencia de Datos (CentroMAGIS). Disponible en: https://github.com/centromagis/paqueteMODELOS
  • Husson, F., Lê, S., & Pagès, J. (2017). Exploratory Multivariate Analysis by Example Using R. CRC Press. (Paquete FactoMineR)
  • Kassambara, A. (2017). Practical Guide To Principal Component Methods in R. (Paquete factoextra)