Introducción

El mercado inmobiliario urbano desempeña un papel fundamental en el desarrollo económico y social de las ciudades. En este informe se analiza el comportamiento del mercado de viviendas en la ciudad de Cali, a partir de un conjunto de datos que contiene información detallada sobre características físicas, económicas y geográficas de las propiedades. Este estudio tiene como objetivo proporcionar una base sólida para la toma de decisiones estratégicas en proyectos de inversión, planificación urbana y oferta habitacional, que estén alineados con el comportamiento del mercado inmobiliario caleño.

# Instalar y cargar el paquete
if (!require("paqueteMODELOS")) {
  remotes::install_github("centromagis/paqueteMODELOS")
}

# Cargar datos
data("vivienda")

Analisis descriptivo inicial

# Resumen estadístico
# Tabla para variables numéricas
vivienda %>%
  select(where(is.numeric)) %>%
  sapply(summary) %>%
  t() %>%
  as.data.frame() %>%
  rename(
    "Minimo" = "Min.",
    "1er Cuartil" = "1st Qu.",
    "Mediana" = "Median",
    "Media" = "Mean",
    "3er Cuartil" = "3rd Qu.",
    "Maximo" = "Max.",
    "NAs" = "NA's"
  ) %>%
  kable(
    caption = "RESUMEN DE VARIABLES NUMERICAS",
    align = c("l", rep("c", 6)),
    digits = 2
  ) %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed"),
    full_width = FALSE,
    position = "center",
    font_size = 12
  ) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#3498db") %>%
  column_spec(1, bold = TRUE)
RESUMEN DE VARIABLES NUMERICAS
Minimo 1er Cuartil Mediana Media 3er Cuartil Maximo NAs
id 1.00 2080.50 4160.00 4160.00 6239.50 8319.00 3
estrato 3.00 4.00 5.00 4.63 5.00 6.00 3
preciom 58.00 220.00 330.00 433.89 540.00 1999.00 2
areaconst 30.00 80.00 123.00 174.93 229.00 1745.00 3
parqueaderos 1.00 1.00 2.00 1.84 2.00 10.00 1605
banios 0.00 2.00 3.00 3.11 4.00 10.00 3
habitaciones 0.00 3.00 3.00 3.61 4.00 10.00 3
longitud -76.59 -76.54 -76.53 -76.53 -76.52 -76.46 3
latitud 3.33 3.38 3.42 3.42 3.45 3.50 3
# Tabla para variables categóricas
# Convertir las variables categóricas a factor
vars_categoricas <- c("zona", "tipo", "barrio", "estrato")
vivienda <- vivienda %>%
  mutate(across(all_of(vars_categoricas), as.factor))

vars_categoricas <- c("zona", "tipo", "barrio", "estrato")

# Crear una función para resumir cada variable
resumen_variable <- function(x) {
  tab <- table(x)
  moda <- names(tab)[which.max(tab)]
  porcentaje_moda <- round(max(tab) / length(x) * 100, 1)
  n_na <- sum(is.na(x))
  n_cat <- n_distinct(x, na.rm = TRUE)
  tibble(Categorías = n_cat, Moda = moda, `Porcentaje Moda` = porcentaje_moda, NAs = n_na)
}

# Aplicar la función a cada variable y unir resultados
tabla_resumen <- lapply(vivienda[vars_categoricas], resumen_variable) %>%
  bind_rows(.id = "Variable")

tabla_resumen %>%
  kable(
    caption = "RESUMEN DE VARIABLES CATEGORICAS",
    align = c("l", "c", "l", "c", "c")
  ) %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed"),
    full_width = FALSE,
    position = "center",
    font_size = 12
  ) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#3498db") %>%
  column_spec(1, bold = TRUE)
RESUMEN DE VARIABLES CATEGORICAS
Variable Categorías Moda Porcentaje Moda NAs
zona 5 Zona Sur 56.8 3
tipo 2 Apartamento 61.3 3
barrio 436 valle del lili 12.1 3
estrato 4 5 33.0 3

El dataset contiene información detallada sobre propiedades inmobiliarias en Cali. Según el resumen estadístico presentado, las viviendas presentan una amplia variabilidad en precios (desde $58M hasta $1,999M) y áreas construidas (desde 30-1,745 m²), con una mediana de 123 m² y $330M. El estrato predominante es el 4, mientras que características como baños (mediana: 3) y habitaciones (mediana: 3) muestran cierta homogeneidad. La zona sur concentra la mayor cantidad de las propiedades, siendo los apartamentos el tipo predominante. Destaca la presencia significativa de valores faltantes en la variables “parqueaderos”, por lo cual se procede a realizar la limpieza del conjunto de datos antes de seguir con el análisis. (ver anexos)

Análisis de componentes principales

# Seleccionar solo las variables numéricas
df_num <- vivienda_mice %>% 
  select(where(is.numeric)) %>% 
  select(-latitud, -longitud)  # Excluir latitud y longitud

# Ejecutar el PCA solo con las variables numéricas
res_pca <- PCA(df_num, graph = FALSE, scale.unit = TRUE)

# Visualizar la varianza explicada
fviz_screeplot(res_pca, addlabels = TRUE) +
  labs(title = "Varianza explicada por los componentes principales")

El gráfico de varianza explicada muestra que los dos primeros componentes principales (PC1 y PC2) capturan conjuntamente el 82% de la variabilidad total de los datos, lo que indica una alta concentración de información en estas dimensiones. Esto nos permite reducir eficientemente la dimensionalidad del conjunto de datos sin perder información significativa.

# Visualizar variables
fviz_pca_var(res_pca,
             col.var = "contrib", # color por contribución
             gradient.cols = c("blue", "yellow", "red"),
             repel = TRUE)

La Dim1, que explica la mayor parte de la variabilidad (64,9%), está fuertemente influenciada por variables como el precio del inmueble, área construida, número de baños y parqueaderos, lo cual sugiere que esta dimensión representa una combinación de valor económico y tamaño físico del inmueble. Por otro lado, la Dim2 está dominada principalmente por la variable habitaciones, lo que indica que este componente representa una dimensión adicional asociada a la cantidad de espacios habitables en las viviendas. También observamos fuertes correlaciones entre varibles como el precio y el número de parqueaderos, indicando que una mayor disponibilidad de espacios de parqueo es un factor relevante en el precio de la vivienda.

# Visualizar individuos proyectados en los primeros 2 componentes
fviz_pca_ind(res_pca,
             geom.ind = "point",
             col.ind = "cos2", # color por calidad de representación
             gradient.cols = c("blue", "yellow", "red"),
             repel = TRUE)

Finalmente, esta gráfica de individuos nos muestra una distribución alargada sobre el eje Dim1, confirmando la mayor variablidad explicada por esta dimesión. Además, vemos una mayor densidad de puntos en el centro, lo que podria representar el grupo más comun en este mercado, mientras que las observaciones en los extremos representan viviendas más atípicas, como propiedades muy lujosas o muy pequeñas.

Análisis de Conglomerados

# Clusterización jerárquica sobre los componentes del PCA
Cluster_propiedades <- HCPC(res_pca, nb.clust = -1, consol = TRUE)

# Mapa factorial con clusters
fviz_cluster(Cluster_propiedades,
             geom = "point", 
             ellipse.type = "convex",
             show.clust.cent = FALSE,
             repel = TRUE) +
  labs(title = "Mapa factorial por clusters") +
  theme_minimal() +
  theme(plot.title = element_text(hjust = 0.5, face = "bold"))

El análisis realizado revela tres grupos diferenciados:

Cluster 1 (rojo), ubicado a la izquierda del eje Dim1, agrupa viviendas con menores valores en precio, área construida, baños y parqueaderos, correspondiendo a propiedades más económicas y compactas.

Cluster 2 (verde), en posición intermedia en Dim1 pero con dispersión hacia valores positivos de Dim2, representa viviendas con características moderadas en precio y tamaño, pero con mayor variabilidad en el número de habitaciones.

Cluster 3 (azul), situado a la derecha de Dim1, reúne propiedades con valores altos en todas las variables, sugiriendo viviendas más lujosas y espaciosas.

# Añadir clúster al dataset original
vivienda_mice$Cluster <- factor(Cluster_propiedades$data.clust$clust)

# Graficar zona vs clúster
library(ggplot2)
ggplot(vivienda_mice, aes(x = Cluster, fill = zona)) +
  geom_bar(position = "fill") +
  labs(title = "Distribucion de Zonas por Cluster",
       y = "Proporcion", x = "Cluster") +
  scale_y_continuous(labels = scales::percent) +
  theme_minimal()

Esta gráfica revela un patrón espacial diferenciado: la Zona Norte concentra principalmente propiedades del Cluster 1 (caracterizadas por ser compactas y económicas), mientras que la Zona Oeste destaca por su mayor proporción de viviendas del Cluster 3 (amplias y lujosas). Por su parte, las Zonas Sur y Centro presentan una distribución más equilibrada entre los distintos clusters, sugiriendo una oferta inmobiliaria más diversificada en estos sectores.

# Añadir clúster al dataset original
vivienda_mice$Cluster <- factor(Cluster_propiedades$data.clust$clust)

# Graficar estrato vs clúster
library(ggplot2)
ggplot(vivienda_mice, aes(x = Cluster, fill = estrato)) +
  geom_bar(position = "fill") +
  labs(title = "Distribucion de estratos por Cluster",
       y = "Proporcion", x = "Cluster") +
  scale_y_continuous(labels = scales::percent) +
  theme_minimal()

Este análisis evidencia una clara correlación entre los clusters y los estratos socioeconómicos: en el Cluster 1 predominan los estratos 3 y 4, lo que refuerza su caracterización como viviendas de menor valor. En contraste, el Cluster 3 (viviendas premium) concentra una mayor cantidad de propiedades de estrato 6, consolidando su perfil como el segmento de mayor exclusividad.

# Graficar distribución geográfica de los clusteres
ggplot(vivienda_mice, aes(x = longitud, y = latitud, color = Cluster)) +
  geom_point(alpha = 0.6) +
  labs(title = "Distribucion geografica de los clusteres",
       x = "Longitud", y = "Latitud") +
  theme_minimal() +
  scale_color_brewer(palette = "Set1")

Con este gráfico, evidenciamos primero una alta densidad de puntos rojos, distribuidas casi de forma homogenea en toda el area analizada, indicando que las viviendas más económicas son las más comunes. Las viviendas con caracteristicas de tamaño y precio más moderadas (cluster 2) támbien se distribuyen uniformemente a lo largo de toda la zona, aunque con menor ocurrencia que el cluster 1. Finalmente, encontramos una concentración de puntos verdes (cluster 3) hacia la zona inferior de la gráfica, delimitando esta como un sector específico asociado a barrios residenciales exlusivos y de estrato alto.

Análisis de Correspondencia

# Seleccionar y preparar variables categóricas
df_cat <- vivienda_mice %>%
  select(tipo, zona, estrato) %>% 
  mutate(across(everything(), as.factor)) %>%
  filter(complete.cases(.))  # Eliminar NA's para el ACM

# ACM con las variables categóricas
res.mca <- MCA(df_cat, graph = FALSE)

# Visualización
fviz_mca_var(res.mca,
             col.var = "contrib",
             gradient.cols = c("#0073C2", "#EFC000", "#CD534C"),
             repel = TRUE,
             ggtheme = theme_minimal()) +
  labs(title = "Relacion entre Tipo, Zona y Estrato") +
  theme(plot.title = element_text(hjust = 0.5, face = "bold"),
        legend.position = "right")

Este analisis de correspondencia múltiple muestra asociaciones relevantes como: los apartamentos se asocian principalmente con la Zona Sur y Oeste y el estrato 4 y 5, mientras que las Casas estan más cercanas a la Zona Norte. Vemos nuevamente que el estrato socioeconómico más alto se asocia fuertemente con la zona Oeste, y el estrato 3 con la Zona Oriente y Centro.

Conclusiones

Este estudio evidenció una segmentación precisa de las viviendas en Cali, tanto desde el punto de vista del tamaño (área construida, número de habitaciones, parqueaderos) como de variables económicas (precio y estrato socioeconómico). Esto permite identificar nichos de mercado claramente definidos y diseñar estrategias especializadas de construcción y oferta, según las necesidades y características de cada zona. Por ejemplo, se identificaron sectores como la Zona Oeste, asociada a propiedades de alto valor, y la Zona Norte, vinculada a viviendas económicas, lo que facilita orientar posibles inversiones estratégicas e identificar áreas con potencial de valorización.

Por otro lado, se encontró una relación entre el tipo de propiedad, la zona y el estrato, mostrando una mayor demanda de apartamentos en la Zona Sur y una mejor oportunidad para proyectos de casas en la Zona Norte.

Finalmente, se identificó que variables como el número de parqueaderos, el área construida y el número de baños están estrechamente relacionadas con el precio del inmueble, lo que las convierte en características clave a tener en cuenta al momento de diseñar proyectos de vivienda.

Anexos

# Librerias necesarias para el análisis
library(ggplot2)
library(VIM)
library(dplyr)
library(tidyr)
library(knitr)
library(paqueteMODELOS)
library(caret)
library(patchwork)
library(RANN)

# Instalar y cargar el paquete
if (!require("paqueteMODELOS")) {
  remotes::install_github("centromagis/paqueteMODELOS")
}

# Cargar datos
data("vivienda")

# Visualizar estructura del dataset
cat("Estructura en tabla")
data.frame(
  Variable = names(vivienda),
  Tipo = sapply(vivienda, class),
  Ejemplo = sapply(vivienda, function(x) paste(head(x, 2), collapse = ", "))
) %>% 
  kable(caption = "Estructura del Dataset") %>% 
  print()

# Convertir las variables categóricas a factor
vars_categoricas <- c("zona", "tipo", "barrio", "estrato")
vivienda <- vivienda %>%
  mutate(across(all_of(vars_categoricas), as.factor))

# Identificar filas duplicadas
duplicados <- duplicated(vivienda)

# Mostrar cuántos duplicados hay
cat("Número de filas duplicadas encontradas:", sum(duplicados), "\n")

# Eliminar filas duplicadas del dataframe original
vivienda <- vivienda[!duplicados, ]

# Contar NAs por columna
vivienda %>% summarise(across(everything(), ~sum(is.na(.))))

# Eliminar las NAs de variables con pocos valores faltantes
columnas_limpiar <- c("id", "zona", "estrato", "preciom", "areaconst", "banios")

vivienda_limpia1 <- vivienda %>%
  filter(if_all(all_of(columnas_limpiar), ~ !is.na(.)))

# Verificación (porcentaje de NAs restantes)
colMeans(is.na(vivienda_limpia1)) %>% 
  sort(decreasing = TRUE) %>% 
  round(4) * 100

# La variable piso se excluye del análisis debido a que no presenta una
# definición clara y consistente de esta en el dataset. En apartamentos,
# el valor parece indicar el piso en el que está ubicada la vivienda,
# mientras que en casas podría referirse al número total de pisos  
# (con valores atipicos > 4) o a otra característica no especificada.
# Esta ambigüedad, junto a su numerosa cantidad de faltantes (31.67%)
# impide su uso confiable en los análisis posteriores. Además, se elimina la 
# columna id, pues no contiene informacion relevante para el análisis

# Eliminar la variable piso y id
vivienda_limpia2 <- vivienda_limpia1[, !(names(vivienda_limpia1) %in% c("piso", "id"))]

# Para tratar la variable parqueaderos, ensayamos dos técnicas de imputación: KNN y Mice
# para luego verificar la distribucion de los datos antes y despues.

# Seleccionar variables relevantes para la imputación
vars_imputacion <- c("parqueaderos", "areaconst", "preciom", "habitaciones", "banios")

# Dataset original
vivienda_original <- vivienda_limpia2

# Imputación con KNN
set.seed(123)
vivienda_knn <- kNN(vivienda_original,
                    variable = vars_imputacion,
                    k = 5,
                    imp_var = FALSE)
vivienda_knn$parqueaderos <- round(vivienda_knn$parqueaderos, 0)

# Imputación con MICE
set.seed(123)
mice_data <- mice(vivienda_original[, vars_imputacion], 
                  m = 1,              # número de imputaciones
                  method = "pmm",     # predictive mean matching
                  maxit = 5,          # iteraciones
                  seed = 123)

vivienda_mice <- vivienda_original
vivienda_mice[, vars_imputacion] <- complete(mice_data)
vivienda_mice$parqueaderos <- round(vivienda_mice$parqueaderos, 0)

# Combinar datos para gráfico comparativo
df_plot <- bind_rows(
  vivienda_original %>% mutate(metodo = "Original (con NA)"),
  vivienda_knn %>% mutate(metodo = "Imputado (KNN)"),
  vivienda_mice %>% mutate(metodo = "Imputado (MICE)")
)

# Boxplot comparativo
boxplot_comp <- ggplot(df_plot, aes(x = metodo, y = parqueaderos, color = metodo)) +
  geom_boxplot(na.rm = TRUE) +
  labs(
    title = "Comparación de Distribución: Parqueaderos",
    x = "",
    y = "Número de Parqueaderos"
  ) +
  theme_minimal()

print(boxplot_comp)

# Resúmenes estadísticos
cat("Resumen Original")
print(summary(vivienda_original$parqueaderos, na.rm = TRUE))

cat("Resumen KNN")
print(summary(vivienda_knn$parqueaderos))

cat("Resumen MICE")
print(summary(vivienda_mice$parqueaderos))


# A pesar de que ambos métodos de imputación generan un cambio en la mediana (de 2 a 1)
# la técnica MICE mantiene mejor la distribución original de los datos, teniendo
# un valor de media más cercano al de la distribución original. Por ello, se selecciona este
# como metodo definitivo para el análisis.

# Función para detectar outliers y generar tabla
detectar_outliers_tabla <- function(df) {
  num_vars <- df[, sapply(df, is.numeric)]
  
  resumen <- data.frame(Variable = character(), N_outliers = integer())
  
  for (var in names(num_vars)) {
    datos <- num_vars[[var]]
    
    Q1 <- quantile(datos, 0.25, na.rm = TRUE)
    Q3 <- quantile(datos, 0.75, na.rm = TRUE)
    IQR_val <- Q3 - Q1
    
    lim_inf <- Q1 - 1.5 * IQR_val
    lim_sup <- Q3 + 1.5 * IQR_val
    
    outliers <- sum(datos < lim_inf | datos > lim_sup, na.rm = TRUE)
    
    resumen <- rbind(resumen, data.frame(Variable = var, N_outliers = outliers))
  }
  
  return(resumen)
}

# Obtener tabla de outliers
tabla_outliers <- detectar_outliers_tabla(vivienda_mice)
print(tabla_outliers)

# Crear boxplots por separado para cada variable numérica
num_vars <- vivienda_mice %>% select(where(is.numeric))

for (var in names(num_vars)) {
  p <- ggplot(data = num_vars, aes(y = .data[[var]])) +
    geom_boxplot(outlier.colour = "red", outlier.shape = 16) +
    theme_minimal() +
    labs(
      title = paste("Boxplot de", var, "con outliers"),
      y = var,
      x = ""
    )
  print(p)
}

# Se evalua si los datos atípicos de habitaciones, baños y parqueaderos se relacionan con 
# viviendas de precio y area construida elevadas, para estudiar la validez de estos valores.

# Función para obtener índices de outliers según IQR
get_outlier_idx <- function(x) {
  Q1 <- quantile(x, 0.25, na.rm = TRUE)
  Q3 <- quantile(x, 0.75, na.rm = TRUE)
  IQR_val <- Q3 - Q1
  lower <- Q1 - 1.5 * IQR_val
  upper <- Q3 + 1.5 * IQR_val
  which(x < lower | x > upper)
}

# Variables a evaluar
vars_check <- c("habitaciones", "banios", "parqueaderos")

# Usar el dataset imputado con MICE
df <- vivienda_mice

# Detectar y comparar
for (var in vars_check) {
  
  idx_outliers <- get_outlier_idx(df[[var]])
  
  cat("Variable:", var)
  cat("Número de outliers:", length(idx_outliers))
  
  # Revisar si coinciden con precios altos o área alta
  summary_precio <- summary(df$preciom[idx_outliers])
  summary_area   <- summary(df$areaconst[idx_outliers])
  
  cat("Resumen PrecioM en outliers:")
  print(summary_precio)
  
  cat("Resumen AreaConst en outliers:")
  print(summary_area)
  
  # Porcentaje de outliers que están en el 30% superior de precio o área
  p_precio <- mean(df$preciom[idx_outliers] > quantile(df$preciom, 0.7, na.rm = TRUE))
  p_area   <- mean(df$areaconst[idx_outliers] > quantile(df$areaconst, 0.7, na.rm = TRUE))
  
  cat(sprintf("Porcentaje de outliers con PrecioM alto (>70%%): %.1f%%\n", 100 * p_precio))
  cat(sprintf("Porcentaje de outliers con AreaConst alta (>70%%): %.1f%%\n", 100 * p_area))
}

# Dado que la mayoría de los outliers en estas variables están fuertemente relacionados 
# con propiedades de alta gama, con mayores precios y áreas, se recomienda conservar 
# estos valores en el análisis posterior, pues al eliminarlos se podria perder
# informacion relevante. Por ello, para los análisis posteriores se utilizará el
# dataframe con los faltantes imputados por la técnica elegida: vivienda_mice