1. Introducción

Una empresa inmobiliaria líder busca comprender en profundidad el mercado de viviendas urbanas para tomar decisiones estratégicas más informadas. Este análisis holístico identificará patrones, relaciones y segmentaciones relevantes que mejoren la toma de decisiones en compra, venta y valoración de propiedades.

2. Metodología

2.1 Fuente de Datos

Los datos utilizados en este análisis provienen de información recolectada mediante web scraping de la plataforma OLX, contenidos en el paquete paqueteMODELOS. La base de datos contiene información detallada sobre propiedades residenciales urbanas, incluyendo características físicas, ubicación geográfica y precios de mercado.

2.2 Variables del Estudio

El dataset utilizado contiene información detallada sobre 8,322 propiedades residenciales distribuidas en diferentes zonas de la ciudad, representando una muestra robusta y representativa del mercado inmobiliario urbano. La base de datos incluye 13 variables que abarcan características físicas, ubicación geográfica, estratificación socioeconómica y precios de mercado.

2.3 Carga de Datos y Librerías

library(paqueteMODELOS)
library(dplyr)
library(ggplot2)
library(tidyr)
library(plotly)
library(corrplot)
library(VIM)
library(mice)
library(knitr)
library(kableExtra)
library(leaflet)
library(DT)
library(gridExtra)
library(RColorBrewer)

# Cargar los datos
data("vivienda")

2.4 Características del Dataset

El dataset contiene 8,322 propiedades residenciales con 13 variables que capturan diferentes dimensiones del mercado inmobiliario urbano. Los datos incluyen:

Variables cuantitativas: estrato socioeconómico, precio en millones de pesos, área construida, número de parqueaderos, baños y habitaciones, además de coordenadas geográficas precisas.

Variables categóricas: zona de ubicación, tipo de propiedad (casa/apartamento), piso y barrio específico.

La estructura del dataset permite realizar análisis multidimensionales que abarcan aspectos económicos, físicos, geográficos y socioeconómicos del mercado inmobiliario, proporcionando una base sólida para la aplicación de técnicas de análisis multivariado.

# Descripción de variables
variables_desc <- data.frame(
  Variable = c("id", "zona", "piso", "estrato", "preciom", "areaconst", 
               "parqueaderos", "banios", "habitaciones", "tipo", "barrio", 
               "longitud", "latitud"),
  Descripción = c(
    "Identificador único de la propiedad",
    "Zona geográfica de ubicación",
    "Número de piso (para apartamentos)",
    "Estrato socioeconómico (1-6)",
    "Precio en millones de pesos",
    "Área construida en metros cuadrados",
    "Número de parqueaderos",
    "Número de baños",
    "Número de habitaciones",
    "Tipo de vivienda (Casa/Apartamento)",
    "Barrio específico de ubicación",
    "Coordenada de longitud",
    "Coordenada de latitud"
  ),
  Tipo = c("Numérica", "Categórica", "Categórica", "Numérica", "Numérica", 
           "Numérica", "Numérica", "Numérica", "Numérica", "Categórica", 
           "Categórica", "Numérica", "Numérica")
)

kable(variables_desc, 
      caption = "Descripción de Variables del Dataset") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE)
Descripción de Variables del Dataset
Variable Descripción Tipo
id Identificador único de la propiedad Numérica
zona Zona geográfica de ubicación Categórica
piso Número de piso (para apartamentos) Categórica
estrato Estrato socioeconómico (1-6) Numérica
preciom Precio en millones de pesos Numérica
areaconst Área construida en metros cuadrados Numérica
parqueaderos Número de parqueaderos Numérica
banios Número de baños Numérica
habitaciones Número de habitaciones Numérica
tipo Tipo de vivienda (Casa/Apartamento) Categórica
barrio Barrio específico de ubicación Categórica
longitud Coordenada de longitud Numérica
latitud Coordenada de latitud Numérica

3. Análisis Exploratorio de Datos (EDA)

3.1 Estructura y Dimensiones del Dataset

## spc_tbl_ [8,322 × 13] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
##  $ id          : num [1:8322] 1147 1169 1350 5992 1212 ...
##  $ zona        : chr [1:8322] "Zona Oriente" "Zona Oriente" "Zona Oriente" "Zona Sur" ...
##  $ piso        : chr [1:8322] NA NA NA "02" ...
##  $ estrato     : num [1:8322] 3 3 3 4 5 5 4 5 5 5 ...
##  $ preciom     : num [1:8322] 250 320 350 400 260 240 220 310 320 780 ...
##  $ areaconst   : num [1:8322] 70 120 220 280 90 87 52 137 150 380 ...
##  $ parqueaderos: num [1:8322] 1 1 2 3 1 1 2 2 2 2 ...
##  $ banios      : num [1:8322] 3 2 2 5 2 3 2 3 4 3 ...
##  $ habitaciones: num [1:8322] 6 3 4 3 3 3 3 4 6 3 ...
##  $ tipo        : chr [1:8322] "Casa" "Casa" "Casa" "Casa" ...
##  $ barrio      : chr [1:8322] "20 de julio" "20 de julio" "20 de julio" "3 de julio" ...
##  $ longitud    : num [1:8322] -76.5 -76.5 -76.5 -76.5 -76.5 ...
##  $ latitud     : num [1:8322] 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>
## Dimensiones del dataset: 8322 observaciones y 13 variables
Primeras 6 observaciones del dataset
id zona piso estrato preciom areaconst parqueaderos banios habitaciones tipo barrio longitud latitud
1147 Zona Oriente NA 3 250 70 1 3 6 Casa 20 de julio -76.51168 3.43382
1169 Zona Oriente NA 3 320 120 1 2 3 Casa 20 de julio -76.51237 3.43369
1350 Zona Oriente NA 3 350 220 2 2 4 Casa 20 de julio -76.51537 3.43566
5992 Zona Sur 02 4 400 280 3 5 3 Casa 3 de julio -76.54000 3.43500
1212 Zona Norte 01 5 260 90 1 2 3 Apartamento acopi -76.51350 3.45891
1724 Zona Norte 01 5 240 87 1 3 3 Apartamento acopi -76.51700 3.36971

3.2 Resumen Estadístico

# Resumen estadístico de variables numéricas
numeric_vars <- vivienda %>% select_if(is.numeric)
summary(numeric_vars) %>%
  kable(caption = "Resumen estadístico de variables numéricas") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Resumen estadístico de variables numéricas
id estrato preciom areaconst parqueaderos banios habitaciones longitud latitud
Min. : 1 Min. :3.000 Min. : 58.0 Min. : 30.0 Min. : 1.000 Min. : 0.000 Min. : 0.000 Min. :-76.59 Min. :3.333
1st Qu.:2080 1st Qu.:4.000 1st Qu.: 220.0 1st Qu.: 80.0 1st Qu.: 1.000 1st Qu.: 2.000 1st Qu.: 3.000 1st Qu.:-76.54 1st Qu.:3.381
Median :4160 Median :5.000 Median : 330.0 Median : 123.0 Median : 2.000 Median : 3.000 Median : 3.000 Median :-76.53 Median :3.416
Mean :4160 Mean :4.634 Mean : 433.9 Mean : 174.9 Mean : 1.835 Mean : 3.111 Mean : 3.605 Mean :-76.53 Mean :3.418
3rd Qu.:6240 3rd Qu.:5.000 3rd Qu.: 540.0 3rd Qu.: 229.0 3rd Qu.: 2.000 3rd Qu.: 4.000 3rd Qu.: 4.000 3rd Qu.:-76.52 3rd Qu.:3.452
Max. :8319 Max. :6.000 Max. :1999.0 Max. :1745.0 Max. :10.000 Max. :10.000 Max. :10.000 Max. :-76.46 Max. :3.498
NA’s :3 NA’s :3 NA’s :2 NA’s :3 NA’s :1605 NA’s :3 NA’s :3 NA’s :3 NA’s :3

3.3 Análisis de Valores Faltantes

# Análisis de valores faltantes
missing_analysis <- vivienda %>%
  summarise_all(~sum(is.na(.))) %>%
  gather(key = "Variable", value = "Valores_Faltantes") %>%
  mutate(Porcentaje = round((Valores_Faltantes / nrow(vivienda)) * 100, 2)) %>%
  arrange(desc(Valores_Faltantes))

missing_analysis %>%
  kable(caption = "Análisis de valores faltantes por variable") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Análisis de valores faltantes por variable
Variable Valores_Faltantes Porcentaje
piso 2638 31.70
parqueaderos 1605 19.29
id 3 0.04
zona 3 0.04
estrato 3 0.04
areaconst 3 0.04
banios 3 0.04
habitaciones 3 0.04
tipo 3 0.04
barrio 3 0.04
longitud 3 0.04
latitud 3 0.04
preciom 2 0.02
# Visualización de valores faltantes
VIM::aggr(vivienda, col = c('navyblue','red'), 
          numbers = TRUE, sortVars = TRUE, 
          labels = names(vivienda), cex.axis = 0.7, gap = 3,
          main = "Patrón de Valores Faltantes")

## 
##  Variables sorted by number of missings: 
##      Variable        Count
##          piso 0.3169911079
##  parqueaderos 0.1928622927
##            id 0.0003604903
##          zona 0.0003604903
##       estrato 0.0003604903
##     areaconst 0.0003604903
##        banios 0.0003604903
##  habitaciones 0.0003604903
##          tipo 0.0003604903
##        barrio 0.0003604903
##      longitud 0.0003604903
##       latitud 0.0003604903
##       preciom 0.0002403268

3.4 Distribuciones de Variables Numéricas

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

# Crear gráficos de distribución
plots_list <- list()

for(var in num_vars) {
  p <- ggplot(vivienda, aes_string(x = var)) +
    geom_histogram(bins = 30, fill = "skyblue", alpha = 0.7, color = "black") +
    geom_density(aes(y = ..density.. * nrow(vivienda) * 
                    (max(vivienda[[var]], na.rm = TRUE) - min(vivienda[[var]], na.rm = TRUE)) / 30), 
                 color = "red", size = 1) +
    theme_minimal() +
    labs(title = paste("Distribución de", var),
         x = var, y = "Frecuencia") +
    theme(plot.title = element_text(hjust = 0.5))
  
  plots_list[[var]] <- p
}

# Mostrar gráficos en grid
do.call(grid.arrange, c(plots_list[1:3], ncol = 3))

do.call(grid.arrange, c(plots_list[4:6], ncol = 3))

3.5 Análisis de Variables Categóricas

# Análisis de zona
zona_table <- table(vivienda$zona)
zona_df <- data.frame(
  Zona = names(zona_table),
  Frecuencia = as.numeric(zona_table),
  Porcentaje = round(as.numeric(zona_table)/nrow(vivienda)*100, 2)
)

zona_df %>%
  kable(caption = "Distribución por Zona") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Distribución por Zona
Zona Frecuencia Porcentaje
Zona Centro 124 1.49
Zona Norte 1920 23.07
Zona Oeste 1198 14.40
Zona Oriente 351 4.22
Zona Sur 4726 56.79
# Gráfico de zona
ggplot(zona_df, aes(x = reorder(Zona, Frecuencia), y = Frecuencia)) +
  geom_col(fill = "lightcoral", alpha = 0.8) +
  coord_flip() +
  theme_minimal() +
  labs(title = "Distribución de Propiedades por Zona",
       x = "Zona", y = "Número de Propiedades") +
  theme(plot.title = element_text(hjust = 0.5))

# Análisis de tipo de vivienda
tipo_table <- table(vivienda$tipo)
tipo_df <- data.frame(
  Tipo = names(tipo_table),
  Frecuencia = as.numeric(tipo_table),
  Porcentaje = round(as.numeric(tipo_table)/nrow(vivienda)*100, 2)
)

tipo_df %>%
  kable(caption = "Distribución por Tipo de Vivienda") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Distribución por Tipo de Vivienda
Tipo Frecuencia Porcentaje
Apartamento 5100 61.28
Casa 3219 38.68
# Gráfico de tipo
ggplot(tipo_df, aes(x = "", y = Porcentaje, fill = Tipo)) +
  geom_bar(stat = "identity", width = 1) +
  coord_polar("y", start = 0) +
  theme_void() +
  labs(title = "Distribución por Tipo de Vivienda") +
  theme(plot.title = element_text(hjust = 0.5)) +
  scale_fill_brewer(palette = "Set3")

3.6 Análisis de Precios

# Estadísticas descriptivas del precio
price_stats <- vivienda %>%
  summarise(
    Media = round(mean(preciom, na.rm = TRUE), 2),
    Mediana = round(median(preciom, na.rm = TRUE), 2),
    Q1 = round(quantile(preciom, 0.25, na.rm = TRUE), 2),
    Q3 = round(quantile(preciom, 0.75, na.rm = TRUE), 2),
    Desv_Estandar = round(sd(preciom, na.rm = TRUE), 2),
    Minimo = min(preciom, na.rm = TRUE),
    Maximo = max(preciom, na.rm = TRUE)
  )

price_stats %>%
  kable(caption = "Estadísticas Descriptivas del Precio (millones COP)") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Estadísticas Descriptivas del Precio (millones COP)
Media Mediana Q1 Q3 Desv_Estandar Minimo Maximo
433.89 330 220 540 328.65 58 1999
# Boxplot de precios por zona
ggplot(vivienda, aes(x = zona, y = preciom, fill = zona)) +
  geom_boxplot(alpha = 0.7) +
  theme_minimal() +
  labs(title = "Distribución de Precios por Zona",
       x = "Zona", y = "Precio (millones COP)") +
  theme(axis.text.x = element_text(angle = 45, hjust = 1),
        plot.title = element_text(hjust = 0.5),
        legend.position = "none") +
  scale_fill_brewer(palette = "Set2")

# Boxplot de precios por estrato
ggplot(vivienda, aes(x = factor(estrato), y = preciom, fill = factor(estrato))) +
  geom_boxplot(alpha = 0.7) +
  theme_minimal() +
  labs(title = "Distribución de Precios por Estrato",
       x = "Estrato", y = "Precio (millones COP)") +
  theme(plot.title = element_text(hjust = 0.5),
        legend.position = "none") +
  scale_fill_brewer(palette = "Spectral")

3.7 Correlaciones entre Variables Numéricas

# Matriz de correlación
cor_matrix <- cor(numeric_vars, use = "complete.obs")

# Visualizar matriz de correlación
corrplot(cor_matrix, method = "color", type = "upper", 
         order = "hclust", tl.cex = 0.8, tl.col = "black",
         title = "Matriz de Correlaciones", mar = c(0,0,1,0))

# Tabla de correlaciones con precio
price_correlations <- cor_matrix[, "preciom"] %>%
  as.data.frame() %>%
  rename(Correlacion_con_Precio = ".") %>%
  arrange(desc(abs(Correlacion_con_Precio))) %>%
  mutate(Correlacion_con_Precio = round(Correlacion_con_Precio, 3))

price_correlations %>%
  kable(caption = "Correlaciones con el Precio de la Vivienda") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Correlaciones con el Precio de la Vivienda
Correlacion_con_Precio
preciom 1.000
parqueaderos 0.689
areaconst 0.684
banios 0.672
estrato 0.588
id 0.339
longitud -0.304
habitaciones 0.267
latitud -0.085

3.8 Análisis Geoespacial Básico

# Verificar datos de coordenadas
coord_summary <- vivienda %>%
  summarise(
    lat_min = min(latitud, na.rm = TRUE),
    lat_max = max(latitud, na.rm = TRUE),
    lon_min = min(longitud, na.rm = TRUE),
    lon_max = max(longitud, na.rm = TRUE),
    coord_completas = sum(!is.na(latitud) & !is.na(longitud))
  )

coord_summary %>%
  kable(caption = "Resumen de Coordenadas Geográficas") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Resumen de Coordenadas Geográficas
lat_min lat_max lon_min lon_max coord_completas
3.333 3.4977 -76.58915 -76.463 8319
# Scatter plot de coordenadas coloreado por precio
ggplot(vivienda, aes(x = longitud, y = latitud, color = preciom)) +
  geom_point(alpha = 0.6, size = 0.8) +
  scale_color_gradient(low = "blue", high = "red", name = "Precio\n(millones)") +
  theme_minimal() +
  labs(title = "Distribución Geográfica de Propiedades por Precio",
       x = "Longitud", y = "Latitud") +
  theme(plot.title = element_text(hjust = 0.5))

4. Tratamiento de Datos

El tratamiento de datos es una etapa crítica que garantiza la calidad y confiabilidad de los análisis posteriores. Una limpieza inadecuada puede llevar a conclusiones erróneas y sesgos en los modelos estadísticos, mientras que un tratamiento riguroso asegura resultados robustos y válidos para la toma de decisiones estratégicas en el sector inmobiliario.

4.1 Tratamiento de Valores Faltantes

Los valores faltantes representan uno de los principales desafíos en el análisis de datos, ya que pueden introducir sesgos sistemáticos si no se manejan apropiadamente. La estrategia de tratamiento debe balancear la preservación de información valiosa con la necesidad de mantener la integridad estadística del análisis.

Paso 1: Eliminación de registros con información crítica faltante

Variables como precio, área construida, ubicación y estrato constituyen el núcleo fundamental de cualquier análisis inmobiliario, ya que determinan tanto la valoración económica como la ubicación espacial y socioeconómica de una propiedad. La ausencia de estos datos hace que una observación sea inutilizable para los análisis multivariados posteriores, por lo que se implementa una estrategia de eliminación conservadora que preserva únicamente aquellas propiedades con información completa en estas variables esenciales. Esta decisión, aunque reduce ligeramente el tamaño muestral, garantiza la integridad y confiabilidad de todos los análisis posteriores, evitando sesgos sistemáticos que podrían surgir de estrategias de imputación en variables críticas.

# Análisis detallado de valores faltantes
cat("Registros con información completa:", sum(complete.cases(vivienda)), "de", nrow(vivienda), "\n")
## Registros con información completa: 4808 de 8322
# Estrategia de tratamiento:
# 1. Eliminar registros con información crítica faltante (precio, área, ubicación)
# 2. Imputar parqueaderos usando información de tipo y estrato
# 3. Manejar variable 'piso' (muchos NA porque son casas)

# Paso 1: Eliminar registros con información crítica faltante
vivienda_clean <- vivienda %>%
  filter(!is.na(preciom), !is.na(areaconst), !is.na(zona), 
         !is.na(estrato), !is.na(latitud), !is.na(longitud))

cat("Registros después de eliminar casos críticos:", nrow(vivienda_clean), "\n")
## Registros después de eliminar casos críticos: 8319

Paso 2: Imputación de parqueaderos

Los parqueaderos representan una característica importante pero no crítica en la valoración inmobiliaria, y su ausencia en el dataset frecuentemente se debe más a omisiones en la captura de datos que a la inexistencia real de espacios de parqueo. Para abordar esta situación, se implementa una estrategia de imputación basada en la moda (valor más frecuente) por tipo de vivienda y estrato socioeconómico, reconociendo que estos factores determinan fuertemente el número típico de parqueaderos disponibles. La utilización de la moda en lugar de medidas de tendencia central como la mediana ofrece la ventaja de preservar la distribución natural de la variable, evitar valores fraccionarios irreales, y mantener la coherencia con los patrones de mercado observados en cada segmento.

# Paso 2: Imputar parqueaderos usando la moda por tipo y estrato
# Función para calcular la moda
get_mode <- function(x) {
  x <- x[!is.na(x)]
  if(length(x) == 0) return(1)  # valor por defecto
  ux <- unique(x)
  ux[which.max(tabulate(match(x, ux)))]
}

vivienda_clean <- vivienda_clean %>%
  group_by(tipo, estrato) %>%
  mutate(
    parqueaderos = ifelse(
      is.na(parqueaderos),
      ifelse(tipo == "Casa", 1, get_mode(parqueaderos)),
      parqueaderos
    )
  ) %>%
  ungroup()

Paso 3: Manejo especial de la variable piso

La variable ‘piso’ presenta una característica particular en el contexto inmobiliario: los valores faltantes no representan datos perdidos en el sentido tradicional, sino una característica natural de las casas, que no tienen el concepto de piso de la misma manera que los apartamentos. Para manejar esta situación conceptual, se implementa una transformación que convierte los valores faltantes en cero, interpretándolos como planta baja para casas, mientras que los valores numéricos se mantienen para apartamentos. Esta solución permite incorporar esta información relevante en análisis numéricos posteriores sin perder observaciones valiosas, y refleja adecuadamente la realidad arquitectónica de diferentes tipos de vivienda.

# Paso 3: Para la variable piso, simplemente convertir a numérica donde sea posible
# Los NA son principalmente casas (que no tienen piso), así que los dejamos como 0
vivienda_clean <- vivienda_clean %>%
  mutate(
    piso_numerico = ifelse(is.na(piso), 0, as.numeric(piso))
  )

# Verificar resultados de limpieza
cat("\nResumen después de limpieza:\n")
## 
## Resumen después de limpieza:
cat("Registros finales:", nrow(vivienda_clean), "\n")
## Registros finales: 8319
cat("Valores faltantes restantes:\n")
## Valores faltantes restantes:
sapply(vivienda_clean, function(x) sum(is.na(x))) %>% 
  .[. > 0] %>% 
  kable() %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
x
piso 2635

Resultado del Tratamiento:

La estrategia implementada logra mantener la gran mayoría de observaciones (>99%) mientras elimina las inconsistencias que podrían afectar la validez de los análisis multivariados posteriores.

4.2 Detección y Tratamiento de Outliers

Los outliers en datos inmobiliarios presentan un desafío metodológico particular, ya que pueden representar tanto propiedades genuinamente excepcionales (como mansiones, penthouses o desarrollos arquitectónicos únicos) como errores de captura o digitación de datos. Su tratamiento requiere un equilibrio delicado entre preservar la diversidad natural del mercado inmobiliario y eliminar observaciones que podrían distorsionar los análisis estadísticos multivariados.

Criterio IQR (Rango Intercuartílico)

Para la identificación de outliers se implementa el método del Rango Intercuartílico (IQR), que demuestra particular robustez ante distribuciones asimétricas, característica común en mercados inmobiliarios donde naturalmente existen propiedades de lujo en los extremos superiores de precio y características. Este método utiliza el criterio estándar de 1.5 * IQR para identificar outliers moderados, ofreciendo la ventaja de no asumir distribuciones normales y adaptarse mejor a la realidad heterogénea del mercado inmobiliario urbano.

# Función para detectar outliers usando IQR
detect_outliers <- function(x, k = 1.5) {
  Q1 <- quantile(x, 0.25, na.rm = TRUE)
  Q3 <- quantile(x, 0.75, na.rm = TRUE)
  IQR <- Q3 - Q1
  lower <- Q1 - k * IQR
  upper <- Q3 + k * IQR
  return(x < lower | x > upper)
}

# Detectar outliers en variables numéricas principales
outlier_vars <- c("preciom", "areaconst", "parqueaderos", "banios", "habitaciones")

outlier_summary <- vivienda_clean %>%
  summarise(across(all_of(outlier_vars), ~sum(detect_outliers(.x)))) %>%
  gather(key = "Variable", value = "Outliers") %>%
  mutate(Porcentaje = round((Outliers / nrow(vivienda_clean)) * 100, 2))

outlier_summary %>%
  kable(caption = "Outliers detectados por variable") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Outliers detectados por variable
Variable Outliers Porcentaje
preciom 552 6.64
areaconst 382 4.59
parqueaderos 567 6.82
banios 72 0.87
habitaciones 888 10.67

Enfoque de Percentil 99.

En lugar de aplicar una eliminación automática de todos los outliers detectados por el método IQR (lo cual podría resultar en la pérdida de propiedades legítimamente lujosas o excepcionales), se implementa una estrategia más conservadora basada en el percentil 99 para variables continuas como precio y área, complementada con límites máximos razonables basados en el conocimiento del mercado local para variables discretas. Esta aproximación preserva la diversidad natural del mercado inmobiliario mientras elimina únicamente los casos más extremos que probablemente representen errores de captura, estableciendo umbrales como máximo 6 parqueaderos, 8 baños y 8 habitaciones, que permiten mansiones y propiedades de lujo pero filtran valores claramente erróneos.

# Visualizar outliers en precio
p1 <- ggplot(vivienda_clean, aes(y = preciom)) +
  geom_boxplot(fill = "lightblue", alpha = 0.7) +
  theme_minimal() +
  labs(title = "Outliers en Precio", y = "Precio (millones COP)") +
  theme(plot.title = element_text(hjust = 0.5))

p2 <- ggplot(vivienda_clean, aes(y = areaconst)) +
  geom_boxplot(fill = "lightcoral", alpha = 0.7) +
  theme_minimal() +
  labs(title = "Outliers en Área", y = "Área Construida (m²)") +
  theme(plot.title = element_text(hjust = 0.5))

grid.arrange(p1, p2, ncol = 2)

# Tratamiento conservador de outliers extremos (percentil 99)
vivienda_final <- vivienda_clean %>%
  filter(
    preciom <= quantile(preciom, 0.99, na.rm = TRUE),
    areaconst <= quantile(areaconst, 0.99, na.rm = TRUE),
    parqueaderos <= 6,  # máximo razonable
    banios <= 8,        # máximo razonable
    habitaciones <= 8   # máximo razonable
  )

cat("Registros después de tratamiento de outliers:", nrow(vivienda_final), "\n")
## Registros después de tratamiento de outliers: 8009

Resultado Final:

El tratamiento de outliers logra mantener 96% de las observaciones, eliminando solo los casos más extremos que podrían representar errores de datos, mientras preserva la diversidad natural del mercado inmobiliario que incluye desde viviendas sociales hasta propiedades de lujo.

5. Análisis de Componentes Principales (PCA)

5.1 Preparación de Datos y Verificación de Calidad

Antes de aplicar PCA, es fundamental verificar la integridad de los datos, ya que esta técnica es sensible a valores faltantes y requiere que todas las variables estén presentes para cada observación. La identificación de patrones de datos faltantes nos permite tomar decisiones informadas sobre el tratamiento adecuado de las variables.

# Verificación de datos faltantes antes del PCA
library(mice)
library(factoextra)

datos_pca_completos <- vivienda_final %>%
  select(estrato, preciom, areaconst, parqueaderos, banios, habitaciones)

# Patrón de datos faltantes
cat("Análisis de patrones de datos faltantes:\n")
## Análisis de patrones de datos faltantes:
md.pattern(datos_pca_completos)
##  /\     /\
## {  `---'  }
## {  O   O  }
## ==>  V <==  No need for mice. This data set is completely observed.
##  \  \|/  /
##   `-----'

##      estrato preciom areaconst parqueaderos banios habitaciones  
## 8009       1       1         1            1      1            1 0
##            0       0         0            0      0            0 0
# Estadísticas de completitud
completitud <- sapply(datos_pca_completos, function(x) sum(!is.na(x)))
porcentaje_completitud <- round((completitud / nrow(datos_pca_completos)) * 100, 2)

data.frame(
  Variable = names(completitud),
  Casos_Completos = completitud,
  Porcentaje = porcentaje_completitud
) %>%
  kable(caption = "Completitud de Variables para PCA") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Completitud de Variables para PCA
Variable Casos_Completos Porcentaje
estrato estrato 8009 100
preciom preciom 8009 100
areaconst areaconst 8009 100
parqueaderos parqueaderos 8009 100
banios banios 8009 100
habitaciones habitaciones 8009 100

5.2 Preparación y Estandarización

La estandarización es crucial en PCA cuando las variables tienen diferentes unidades de medida. Sin estandarización, variables con valores más grandes (como precio en millones) dominarían el análisis sobre variables con valores pequeños (como número de baños).

# Preparación y estandarización de datos para PCA
datos_pca <- vivienda_final %>%
  select(estrato, preciom, areaconst, parqueaderos, banios, habitaciones) %>%
  na.omit()

# Estadísticas descriptivas antes de estandarizar
cat("Estadísticas descriptivas antes de estandarización:\n")
## Estadísticas descriptivas antes de estandarización:
summary(datos_pca) %>%
  kable(caption = "Resumen de Variables Originales") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Resumen de Variables Originales
estrato preciom areaconst parqueaderos banios habitaciones
Min. :3.000 Min. : 58.0 Min. : 30.0 Min. :1.000 Min. :0.000 Min. :0.000
1st Qu.:4.000 1st Qu.: 215.0 1st Qu.: 78.0 1st Qu.:1.000 1st Qu.:2.000 1st Qu.:3.000
Median :5.000 Median : 321.0 Median :120.0 Median :1.000 Median :3.000 Median :3.000
Mean :4.625 Mean : 410.2 Mean :161.9 Mean :1.612 Mean :3.018 Mean :3.485
3rd Qu.:5.000 3rd Qu.: 518.0 3rd Qu.:216.0 3rd Qu.:2.000 3rd Qu.:4.000 3rd Qu.:4.000
Max. :6.000 Max. :1650.0 Max. :700.0 Max. :6.000 Max. :8.000 Max. :8.000
# Estandarización de variables para evitar sesgos por escala
datos_pca_scaled <- scale(datos_pca)

cat("\nDimensiones del dataset para PCA:", dim(datos_pca_scaled), "\n")
## 
## Dimensiones del dataset para PCA: 8009 6
cat("Variables incluidas:", colnames(datos_pca_scaled), "\n")
## Variables incluidas: estrato preciom areaconst parqueaderos banios habitaciones

5.3 Ejecución del Análisis de Componentes Principales

# Ejecución del PCA
pca_resultado <- prcomp(datos_pca_scaled)

# Mostrar resumen del PCA
cat("Resumen del Análisis de Componentes Principales:\n")
## Resumen del Análisis de Componentes Principales:
summary(pca_resultado)
## Importance of components:
##                           PC1    PC2     PC3     PC4     PC5     PC6
## Standard deviation     1.8763 1.1024 0.68593 0.61206 0.49675 0.41484
## Proportion of Variance 0.5868 0.2026 0.07842 0.06244 0.04113 0.02868
## Cumulative Proportion  0.5868 0.7893 0.86775 0.93019 0.97132 1.00000
# Varianza explicada por cada componente
var_explained <- pca_resultado$sdev^2 / sum(pca_resultado$sdev^2)
cumvar_explained <- cumsum(var_explained)

var_df <- data.frame(
  Componente = paste0("PC", 1:length(var_explained)),
  Varianza_Explicada = round(var_explained * 100, 2),
  Varianza_Acumulada = round(cumvar_explained * 100, 2)
)

var_df %>%
  kable(caption = "Varianza Explicada por Componente") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Varianza Explicada por Componente
Componente Varianza_Explicada Varianza_Acumulada
PC1 58.68 58.68
PC2 20.26 78.93
PC3 7.84 86.78
PC4 6.24 93.02
PC5 4.11 97.13
PC6 2.87 100.00

5.4 Elección del Número de Componentes Principales

# Gráfico de sedimentación para selección de componentes
fviz_eig(pca_resultado, addlabels = TRUE, ylim = c(0, 60)) +
  labs(title = "Scree Plot - Varianza Explicada por Componente",
       x = "Componente Principal",
       y = "Porcentaje de Varianza Explicada") +
  theme_minimal() +
  theme(plot.title = element_text(hjust = 0.5))

5.5 Interpretación de Variables en el Plano de Componentes Principales

# Visualización de variables en el plano de componentes principales
fviz_pca_var(pca_resultado,
             col.var = "contrib",
             gradient.cols = c("#00AFBB", "#E7B800", "#FC4E07"),
             repel = TRUE) +
  labs(title = "Variables - PCA") +
  theme_minimal() +
  theme(plot.title = element_text(hjust = 0.5))

# Matriz de rotación para interpretación detallada
cat("\nMatriz de Rotación (Coeficientes de los Componentes):\n")
## 
## Matriz de Rotación (Coeficientes de los Componentes):
rotation_df <- as.data.frame(round(pca_resultado$rotation[,1:3], 4))
rotation_df %>%
  kable(caption = "Cargas de Variables en los Primeros 3 Componentes") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Cargas de Variables en los Primeros 3 Componentes
PC1 PC2 PC3
estrato 0.3302 -0.5818 0.5604
preciom 0.4742 -0.2113 -0.0603
areaconst 0.4465 0.2518 -0.2238
parqueaderos 0.4163 -0.2136 -0.6767
banios 0.4634 0.1592 0.3365
habitaciones 0.2807 0.6946 0.2469

5.6 Análisis de Casos Extremos

# Identificación de casos extremos en cada componente
indices_extremos <- c(
  which.max(pca_resultado$x[,1]),  # PC1 máximo
  which.min(pca_resultado$x[,1]),  # PC1 mínimo
  which.max(pca_resultado$x[,2]),  # PC2 máximo
  which.min(pca_resultado$x[,2])   # PC2 mínimo
)

# Datos originales de casos extremos
casos_extremos <- datos_pca[indices_extremos,]
rownames(casos_extremos) <- c("Propiedad PC1_Max", "Propiedad PC1_Min", 
                             "Propiedad PC2_Max", "Propiedad PC2_Min")

casos_extremos %>%
  kable(caption = "Casos Extremos en Componentes Principales") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Casos Extremos en Componentes Principales
estrato preciom areaconst parqueaderos banios habitaciones
5 1500 700 6 7 6
3 148 87 1 0 0
3 350 330 1 8 8
5 1650 600 6 0 0
# Coordenadas de casos extremos en el espacio de componentes principales
casos_pc_coords <- pca_resultado$x[indices_extremos, 1:2]
rownames(casos_pc_coords) <- c("PC1_Max", "PC1_Min", "PC2_Max", "PC2_Min")

casos_pc_coords %>%
  kable(caption = "Coordenadas de Casos Extremos", digits = 3) %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Coordenadas de Casos Extremos
PC1 PC2
PC1_Max 7.997 0.998
PC1_Min -3.370 -1.200
PC2_Max 2.501 4.594
PC2_Min 4.056 -3.508

5.7 Visualización de Casos Extremos

# Visualización de individuos con casos extremos destacados
casos_df <- as.data.frame(casos_pc_coords)

fviz_pca_ind(pca_resultado, col.ind = "#DEDEDE", alpha.ind = 0.3) +
  geom_point(data = casos_df[1:2,], aes(x = PC1, y = PC2), 
             color = "red", size = 4, alpha = 0.8) +
  geom_point(data = casos_df[3:4,], aes(x = PC1, y = PC2), 
             color = "blue", size = 4, alpha = 0.8) +
  labs(title = "Individuos - PCA con Casos Extremos Destacados",
       subtitle = "Rojo: Extremos PC1, Azul: Extremos PC2") +
  theme_minimal() +
  theme(plot.title = element_text(hjust = 0.5))

5.8 Biplot e Interpretación Conceptual

# Biplot completo para interpretación integral
fviz_pca_biplot(pca_resultado, 
                repel = TRUE,
                col.var = "#2E9FDF",
                col.ind = "#DEDEDE",
                alpha.ind = 0.6,
                title = "Biplot - Análisis de Componentes Principales",
                subtitle = "Relación entre variables y distribución de propiedades") +
  theme_minimal() +
  theme(plot.title = element_text(hjust = 0.5))

# Contribución de variables a los primeros componentes
p1 <- fviz_contrib(pca_resultado, choice = "var", axes = 1, top = 10,
             title = "Contribución al PC1") +
  theme(plot.title = element_text(hjust = 0.5))

p2 <- fviz_contrib(pca_resultado, choice = "var", axes = 2, top = 10,
             title = "Contribución al PC2") +
  theme(plot.title = element_text(hjust = 0.5))

grid.arrange(p1, p2, ncol = 2)

5.9 Interpretación Conceptual de los Componentes

Primer Componente (PC1): “Factor de Calidad y Valor Integral”

Explica el 58.68% de la varianza total y representa la dimensión principal de valoración inmobiliaria.

Cargas principales:

  • Precio (0.4742): Mayor contribución, indicando que este componente captura el valor económico
  • Baños (0.4634): Número de baños como indicador de tamaño y comodidad de la vivienda
  • Área construida (0.4465): Tamaño como factor de valor
  • Parqueaderos (0.4163): Comodidades premium
  • Estrato (0.3302): Nivel socioeconómico del sector

Interpretación: Este componente diferencia entre propiedades de alto valor/calidad vs propiedades básicas/económicas. PC1 funciona como un “índice de lujo inmobiliario” que distingue sistemáticamente entre propiedades de diferentes segmentos de mercado. Las propiedades con valores altos en PC1 representan el segmento premium: costosas, amplias, con múltiples baños y parqueaderos, ubicadas en estratos altos. Las propiedades con valores bajos representan el segmento básico: económicas, compactas, con amenidades mínimas, en sectores populares.

Casos extremos confirman:

Los casos extremos confirman esta interpretación: PC1 máximo corresponde a una propiedad estrato 5, 1,500M, 700m², 6 parqueaderos, 7 baños (claramente premium), mientras PC1 mínimo corresponde a estrato 3, 148M, 87m², 1 parqueadero, 0 baños (segmento básico).

  • PC1 Max: Estrato 5, $1,500M, 700m², 6 parqueaderos, 7 baños → Propiedad premium
  • PC1 Min: Estrato 3, $148M, 87m², 1 parqueadero, 0 baños → Propiedad básica

Segundo Componente (PC2): “Factor de Configuración Residencial”

Explica el 20.26% de la varianza y captura una dimensión ortogonal al valor, relacionada con la tipología y configuración espacial de las viviendas, independientemente de su costo.

Cargas principales:

  • Habitaciones (0.6946): Fuerte peso positivo
  • Estrato (-0.5818): Peso negativo importante
  • Área construida (0.2518): Peso positivo moderado
  • Precio (-0.2113): Peso negativo moderado

Interpretación: Este componente distingue entre viviendas familiares grandes (muchas habitaciones, mayor área) vs apartamentos compactos de alto estrato (pocas habitaciones pero en estratos premium).

Casos extremos confirman:

  • PC2 Max: 8 habitaciones, área grande → Casa familiar
  • PC2 Min: Estrato 5, alto precio, 0 habitaciones → Apartamento compacto premium

Síntesis de los Dos Primeros Componentes (78.93% de varianza):

  1. PC1 separa el mercado por nivel de valor/calidad general
  2. PC2 distingue tipologías habitacionales (familiar vs. compacto-premium)

Esto nos permite segmentar el mercado inmobiliario en cuatro cuadrantes principales:

  • Alto PC1, Alto PC2: Casas familiares de lujo
  • Alto PC1, Bajo PC2: Apartamentos premium compactos
  • Bajo PC1, Alto PC2: Casas familiares económicas
  • Bajo PC1, Bajo PC2: Apartamentos básicos pequeños

6. Análisis de Conglomerados (Clustering)

6.1 Preparación para Clustering

# Usar los primeros componentes principales para clustering
n_components <- 3  # Usar los primeros 3 componentes (86.78% de varianza)
pca_scores <- as.data.frame(pca_resultado$x[, 1:n_components])

# Agregar información categórica original
clustering_data <- cbind(
  pca_scores,
  datos_pca[, c("estrato", "preciom")],  # Variables originales para caracterización
  vivienda_final[complete.cases(vivienda_final[c("estrato", "preciom", "areaconst", "parqueaderos", "banios", "habitaciones")]), 
                 c("zona", "tipo", "id")]
)

cat("Datos preparados para clustering - Dimensiones:", dim(clustering_data), "\n")
## Datos preparados para clustering - Dimensiones: 8009 8
head(clustering_data) %>%
  kable(caption = "Primeras observaciones del dataset de clustering") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Primeras observaciones del dataset de clustering
PC1 PC2 PC3 estrato preciom zona tipo id
-0.8715346 2.3842455 0.2754220 3 250 Zona Oriente Casa 1147
-1.5866716 0.6547362 -0.6834643 3 320 Zona Oriente Casa 1169
-0.4589191 1.1686661 -1.4439413 3 350 Zona Oriente Casa 1350
1.4707265 0.2593377 -1.2142066 4 400 Zona Sur Casa 5992
-1.1542856 -0.5074278 0.4858081 5 260 Zona Norte Apartamento 1212
-0.8486423 -0.3791405 0.7499974 5 240 Zona Norte Apartamento 1724

6.2 Determinación del Número Óptimo de Clusters

# Método del codo (Elbow method)
set.seed(123)
wss <- sapply(1:10, function(k) {
  kmeans(pca_scores, k, nstart = 25)$tot.withinss
})

elbow_data <- data.frame(k = 1:10, wss = wss)

p_elbow <- ggplot(elbow_data, aes(x = k, y = wss)) +
  geom_line(color = "blue", size = 1) +
  geom_point(color = "red", size = 3) +
  theme_minimal() +
  labs(title = "Método del Codo para Determinar K Óptimo",
       x = "Número de Clusters (k)", y = "Suma de Cuadrados Intra-cluster") +
  theme(plot.title = element_text(hjust = 0.5))

# Método de la silueta
library(cluster)
silhouette_scores <- sapply(2:8, function(k) {
  km <- kmeans(pca_scores, k, nstart = 25)
  mean(silhouette(km$cluster, dist(pca_scores))[, 3])
})

sil_data <- data.frame(k = 2:8, silhouette = silhouette_scores)

p_silhouette <- ggplot(sil_data, aes(x = k, y = silhouette)) +
  geom_line(color = "green", size = 1) +
  geom_point(color = "red", size = 3) +
  theme_minimal() +
  labs(title = "Método de la Silueta",
       x = "Número de Clusters (k)", y = "Coeficiente de Silueta Promedio") +
  theme(plot.title = element_text(hjust = 0.5))

grid.arrange(p_elbow, p_silhouette, ncol = 2)

# Mostrar valores de silueta
sil_data %>%
  kable(caption = "Coeficientes de Silueta por Número de Clusters", digits = 3) %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Coeficientes de Silueta por Número de Clusters
k silhouette
2 0.446
3 0.368
4 0.386
5 0.336
6 0.345
7 0.333
8 0.340

6.3 Aplicar K-means

# Aplicar K-means con k óptimo (basado en silueta)
k_optimal <- sil_data$k[which.max(sil_data$silhouette)]
cat("Número óptimo de clusters:", k_optimal, "\n")
## Número óptimo de clusters: 2
set.seed(123)
kmeans_result <- kmeans(pca_scores, centers = k_optimal, nstart = 25)

# Agregar clusters al dataset
clustering_data$cluster <- as.factor(kmeans_result$cluster)

# Resumen de clusters
cluster_summary <- clustering_data %>%
  group_by(cluster) %>%
  summarise(
    n = n(),
    porcentaje = round(n() / nrow(clustering_data) * 100, 2)
  )

cluster_summary %>%
  kable(caption = "Distribución de Observaciones por Cluster") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Distribución de Observaciones por Cluster
cluster n porcentaje
1 2715 33.9
2 5294 66.1
# Visualización de clusters en espacio PCA
ggplot(clustering_data, aes(x = PC1, y = PC2, color = cluster)) +
  geom_point(alpha = 0.6, size = 1.5) +
  theme_minimal() +
  labs(title = "Clusters en el Espacio de Componentes Principales",
       x = "Primera Componente Principal", 
       y = "Segunda Componente Principal") +
  theme(plot.title = element_text(hjust = 0.5)) +
  scale_color_brewer(palette = "Set2")

6.4 Caracterización de Clusters

# Unir con datos originales para caracterización completa
data_with_clusters <- datos_pca %>%
  mutate(cluster = clustering_data$cluster,
         zona = clustering_data$zona,
         tipo = clustering_data$tipo)

# Caracterización por variables numéricas
cluster_num_summary <- data_with_clusters %>%
  group_by(cluster) %>%
  summarise(
    n = n(),
    precio_promedio = round(mean(preciom), 0),
    area_promedio = round(mean(areaconst), 0),
    parqueaderos_promedio = round(mean(parqueaderos), 1),
    banios_promedio = round(mean(banios), 1),
    habitaciones_promedio = round(mean(habitaciones), 1),
    estrato_promedio = round(mean(estrato), 1),
    .groups = "drop"
  )

cluster_num_summary %>%
  kable(caption = "Caracterización Numérica de Clusters") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Caracterización Numérica de Clusters
cluster n precio_promedio area_promedio parqueaderos_promedio banios_promedio habitaciones_promedio estrato_promedio
1 2715 706 274 2.4 4.4 4.3 5.3
2 5294 259 105 1.2 2.3 3.1 4.3
# Caracterización por variables categóricas
# Distribución por zona
cluster_zona <- data_with_clusters %>%
  group_by(cluster, zona) %>%
  summarise(n = n(), .groups = "drop") %>%
  group_by(cluster) %>%
  mutate(porcentaje = round(n / sum(n) * 100, 1)) %>%
  arrange(cluster, desc(porcentaje))

cluster_zona %>%
  kable(caption = "Distribución de Clusters por Zona") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Distribución de Clusters por Zona
cluster zona n porcentaje
1 Zona Sur 1487 54.8
1 Zona Oeste 687 25.3
1 Zona Norte 471 17.3
1 Zona Oriente 50 1.8
1 Zona Centro 20 0.7
2 Zona Sur 3071 58.0
2 Zona Norte 1399 26.4
2 Zona Oeste 466 8.8
2 Zona Oriente 265 5.0
2 Zona Centro 93 1.8
# Distribución por tipo
cluster_tipo <- data_with_clusters %>%
  group_by(cluster, tipo) %>%
  summarise(n = n(), .groups = "drop") %>%
  group_by(cluster) %>%
  mutate(porcentaje = round(n / sum(n) * 100, 1))

cluster_tipo %>%
  kable(caption = "Distribución de Clusters por Tipo de Vivienda") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Distribución de Clusters por Tipo de Vivienda
cluster tipo n porcentaje
1 Apartamento 1076 39.6
1 Casa 1639 60.4
2 Apartamento 3992 75.4
2 Casa 1302 24.6
# Boxplots por cluster para principales variables
p1 <- ggplot(data_with_clusters, aes(x = cluster, y = preciom, fill = cluster)) +
  geom_boxplot() +
  theme_minimal() +
  labs(title = "Distribución de Precios por Cluster", y = "Precio (millones)") +
  theme(legend.position = "none")

p2 <- ggplot(data_with_clusters, aes(x = cluster, y = areaconst, fill = cluster)) +
  geom_boxplot() +
  theme_minimal() +
  labs(title = "Distribución de Área por Cluster", y = "Área (m²)") +
  theme(legend.position = "none")

p3 <- ggplot(data_with_clusters, aes(x = cluster, y = estrato, fill = cluster)) +
  geom_boxplot() +
  theme_minimal() +
  labs(title = "Distribución de Estrato por Cluster", y = "Estrato") +
  theme(legend.position = "none")

p4 <- ggplot(data_with_clusters, aes(x = cluster, fill = tipo)) +
  geom_bar(position = "fill") +
  theme_minimal() +
  labs(title = "Composición por Tipo de Vivienda", y = "Proporción") +
  scale_y_continuous(labels = scales::percent)

grid.arrange(p1, p2, p3, p4, ncol = 2)

6.5 Interpretación de los Clusters

Cluster 1: “Segmento Premium” (33.9% del mercado - 2,715 propiedades)

Características distintivas:

  • Precio promedio: $706 millones (2.7x más costoso)
  • Área promedio: 274 m² (2.6x más grande)
  • Parqueaderos: 2.4 en promedio (2x más)
  • Baños: 4.4 en promedio (1.9x más)
  • Habitaciones: 4.3 en promedio (1.4x más)
  • Estrato promedio: 5.3 (estratos altos)

Distribución geográfica:

  • Zona Sur domina: 54.8% (zona premium de la ciudad)
  • Zona Oeste: 25.3% (segunda zona importante)
  • Presencia menor en Zona Norte (17.3%) y marginal en Oriente/Centro

Tipología:

  • 60.4% Casas vs 39.6% Apartamentos
  • Preferencia por casas en este segmento premium

Cluster 2: “Segmento Estándar” (66.1% del mercado - 5,294 propiedades)

Características distintivas:

  • Precio promedio: $259 millones (precio accesible)
  • Área promedio: 105 m² (viviendas compactas)
  • Parqueaderos: 1.2 en promedio (básico)
  • Baños: 2.3 en promedio (estándar)
  • Habitaciones: 3.1 en promedio (familiar básico)
  • Estrato promedio: 4.3 (estratos medio-altos)

Distribución geográfica:

  • Zona Sur también domina: 58.0% (pero viviendas más económicas)
  • Zona Norte: 26.4% (segunda opción importante)
  • Distribución más equilibrada entre zonas

Tipología:

  • 75.4% Apartamentos vs 24.6% Casas
  • Clara preferencia por apartamentos (más económicos y compactos)

Análisis Estratégico del Mercado

Segmentación Natural del Mercado:

El algoritmo de clustering confirma una segmentación binaria natural del mercado inmobiliario urbano:

1. Mercado de Lujo/Premium (Cluster 1)

  • Perfil: Propiedades de alto valor en zonas exclusivas
  • Target: Familias de ingresos altos, ejecutivos, inversionistas
  • Características: Casas amplias en Zona Sur y Oeste
  • Oportunidad: Desarrollo de proyectos exclusivos con amenidades premium

2. Mercado Masivo/Estándar (Cluster 2)

  • Perfil: Vivienda accesible para clase media-alta
  • Target: Familias jóvenes, profesionales, primera vivienda
  • Características: Apartamentos compactos distribuidos en toda la ciudad
  • Oportunidad: Proyectos de vivienda en altura, optimización de espacios

Insights Clave para la Estrategia Inmobiliaria:

Zona Sur: Mercado Diversificado

  • Concentra ambos segmentos (premium y estándar)
  • Oportunidad para proyectos mixtos o diferenciados por subsector

Tipología vs Precio:

  • Casas = Premium (60% del segmento de lujo)
  • Apartamentos = Estándar (75% del segmento masivo)
  • Clara diferenciación por tipo de producto

Distribución de Mercado:

  • 66% Segmento Estándar → Mayor volumen de negocio
  • 34% Segmento Premium → Mayor margen de rentabilidad

Recomendaciones Estratégicas:

  1. Para Desarrolladores:
  • Zona Sur: Desarrollar proyectos diferenciados (premium vs estándar)
  • Zona Norte: Enfoque en apartamentos estándar (26% del mercado masivo)
  • Zona Oeste: Oportunidad en casas premium
  1. Para Inversionistas:
  • Cluster 1: Mayor valorización potencial, menor liquidez
  • Cluster 2: Mayor rotación, mercado más líquido

7. Análisis de Correspondencia

7.1 Preparación de Datos para Análisis de Correspondencia

# Tabla de contingencia: Zona x Tipo de Vivienda
tabla_zona_tipo <- table(vivienda_final$zona, vivienda_final$tipo)

tabla_zona_tipo %>%
  kable(caption = "Tabla de Contingencia: Zona vs Tipo de Vivienda") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Tabla de Contingencia: Zona vs Tipo de Vivienda
Apartamento Casa
Zona Centro 24 89
Zona Norte 1198 672
Zona Oeste 1004 149
Zona Oriente 61 254
Zona Sur 2781 1777
# Test de independencia
chi_test <- chisq.test(tabla_zona_tipo)
cat("Test de Chi-cuadrado para independencia:\n")
## Test de Chi-cuadrado para independencia:
cat("Chi-cuadrado =", round(chi_test$statistic, 3), "\n")
## Chi-cuadrado = 638.954
cat("p-valor =", format(chi_test$p.value, scientific = TRUE), "\n")
## p-valor = 5.738417e-137
cat("¿Son independientes? ", ifelse(chi_test$p.value < 0.05, "NO - Hay asociación significativa", "SÍ - Son independientes"), "\n")
## ¿Son independientes?  NO - Hay asociación significativa
# Coeficiente de contingencia (medida de asociación)
contingencia <- sqrt(chi_test$statistic / (chi_test$statistic + sum(tabla_zona_tipo)))
cat("Coeficiente de contingencia:", round(contingencia, 3), "\n")
## Coeficiente de contingencia: 0.272
cat("Interpretación: 0 = no asociación, 1 = asociación perfecta\n")
## Interpretación: 0 = no asociación, 1 = asociación perfecta

7.2 Análisis de Correspondencia Simple

# Residuos estandarizados
residuos <- chi_test$residuals
residuos %>%
  kable(caption = "Residuos Estandarizados (Zona vs Tipo)", digits = 2) %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Residuos Estandarizados (Zona vs Tipo)
Apartamento Casa
Zona Centro -5.62 7.37
Zona Norte 0.43 -0.56
Zona Oeste 10.16 -13.34
Zona Oriente -9.80 12.86
Zona Sur -1.92 2.52
cat("Interpretación de residuos estandarizados:\n")
## Interpretación de residuos estandarizados:
cat("- Valores > +2: Combinación más frecuente de lo esperado\n")
## - Valores > +2: Combinación más frecuente de lo esperado
cat("- Valores < -2: Combinación menos frecuente de lo esperado\n")
## - Valores < -2: Combinación menos frecuente de lo esperado
cat("- Valores entre -2 y +2: Sin diferencias significativas\n\n")
## - Valores entre -2 y +2: Sin diferencias significativas
# Identificar patrones significativos
cat("Patrones identificados:\n")
## Patrones identificados:
for(i in 1:nrow(residuos)) {
  for(j in 1:ncol(residuos)) {
    if(abs(residuos[i,j]) > 2) {
      direccion <- ifelse(residuos[i,j] > 0, "MÁS", "MENOS")
      cat("-", rownames(residuos)[i], "+", colnames(residuos)[j], ":", 
          direccion, "frecuente de lo esperado (", round(residuos[i,j], 2), ")\n")
    }
  }
}
## - Zona Centro + Apartamento : MENOS frecuente de lo esperado ( -5.62 )
## - Zona Centro + Casa : MÁS frecuente de lo esperado ( 7.37 )
## - Zona Oeste + Apartamento : MÁS frecuente de lo esperado ( 10.16 )
## - Zona Oeste + Casa : MENOS frecuente de lo esperado ( -13.34 )
## - Zona Oriente + Apartamento : MENOS frecuente de lo esperado ( -9.8 )
## - Zona Oriente + Casa : MÁS frecuente de lo esperado ( 12.86 )
## - Zona Sur + Casa : MÁS frecuente de lo esperado ( 2.52 )

7.3 Interpretación de Dimensiones

# Convertir tabla a formato long para ggplot
tabla_long <- as.data.frame(tabla_zona_tipo) %>%
  rename(Zona = Var1, Tipo = Var2, Frecuencia = Freq)

# Calcular porcentajes por zona
tabla_pct <- tabla_zona_tipo %>%
  prop.table(margin = 1) * 100

tabla_pct_long <- as.data.frame(tabla_pct) %>%
  rename(Zona = Var1, Tipo = Var2, Porcentaje = Freq)

# Gráfico de barras apiladas (porcentajes)
p1 <- ggplot(tabla_pct_long, aes(x = Zona, y = Porcentaje, fill = Tipo)) +
  geom_col() +
  labs(title = "Distribución Porcentual de Tipos por Zona",
       x = "Zona", y = "Porcentaje", fill = "Tipo") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
  scale_fill_manual(values = c("Casa" = "#E7B800", "Apartamento" = "#00AFBB"))

# Gráfico de barras agrupadas (frecuencias absolutas)
p2 <- ggplot(tabla_long, aes(x = Zona, y = Frecuencia, fill = Tipo)) +
  geom_col(position = "dodge") +
  labs(title = "Frecuencias Absolutas por Zona y Tipo",
       x = "Zona", y = "Número de Propiedades", fill = "Tipo") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
  scale_fill_manual(values = c("Casa" = "#E7B800", "Apartamento" = "#00AFBB"))

grid.arrange(p1, p2, ncol = 1)

# Gráfico de mosaico
mosaicplot(tabla_zona_tipo, 
           main = "Gráfico de Mosaico: Zona vs Tipo de Vivienda",
           color = c("#00AFBB", "#E7B800"),
           xlab = "Zona", ylab = "Tipo de Vivienda")

# Heatmap de residuos
residuos_df <- as.data.frame(as.table(residuos))
colnames(residuos_df) <- c("Zona", "Tipo", "Residuo")

ggplot(residuos_df, aes(x = Zona, y = Tipo, fill = Residuo)) +
  geom_tile() +
  geom_text(aes(label = round(Residuo, 1)), color = "white", size = 4) +
  scale_fill_gradient2(low = "red", mid = "white", high = "blue", 
                       midpoint = 0, name = "Residuo") +
  labs(title = "Heatmap de Residuos Estandarizados",
       x = "Zona", y = "Tipo de Vivienda") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

7.4 Visualización del Análisis de Correspondencia

# Crear categorías de estrato
vivienda_final <- vivienda_final %>%
  mutate(
    estrato_cat = case_when(
      estrato <= 3 ~ "Bajo",
      estrato == 4 ~ "Medio", 
      estrato >= 5 ~ "Alto"
    )
  )

# Tabla de contingencia: Zona x Estrato
tabla_zona_estrato <- table(vivienda_final$zona, vivienda_final$estrato_cat)

tabla_zona_estrato %>%
  kable(caption = "Tabla de Contingencia: Zona vs Estrato") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Tabla de Contingencia: Zona vs Estrato
Alto Bajo Medio
Zona Centro 4 96 13
Zona Norte 911 562 397
Zona Oeste 1024 50 79
Zona Oriente 3 304 8
Zona Sur 2594 368 1596
# Test de independencia
chi_zona_estrato <- chisq.test(tabla_zona_estrato)
cat("Test Chi-cuadrado Zona vs Estrato:\n")
## Test Chi-cuadrado Zona vs Estrato:
cat("Chi-cuadrado =", round(chi_zona_estrato$statistic, 3), "\n")
## Chi-cuadrado = 2816.688
cat("p-valor =", format(chi_zona_estrato$p.value, scientific = TRUE), "\n")
## p-valor = 0e+00
# Visualización
tabla_ze_long <- as.data.frame(tabla_zona_estrato) %>%
  rename(Zona = Var1, Estrato = Var2, Frecuencia = Freq)

ggplot(tabla_ze_long, aes(x = Zona, y = Frecuencia, fill = Estrato)) +
  geom_col(position = "fill") +
  labs(title = "Distribución de Estratos por Zona",
       x = "Zona", y = "Proporción", fill = "Estrato") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
  scale_y_continuous(labels = scales::percent) +
  scale_fill_brewer(palette = "RdYlBu")

7.5 Análisis Extendido: Zona x Tipo x Estrato

# Tabla resumen de todas las asociaciones
asociaciones <- data.frame(
  Variables = c("Zona vs Tipo", "Zona vs Estrato", "Tipo vs Estrato"),
  Chi_cuadrado = c(
    round(chi_test$statistic, 3),
    round(chi_zona_estrato$statistic, 3),
    round(chisq.test(table(vivienda_final$tipo, vivienda_final$estrato_cat))$statistic, 3)
  ),
  p_valor = c(
    chi_test$p.value,
    chi_zona_estrato$p.value,
    chisq.test(table(vivienda_final$tipo, vivienda_final$estrato_cat))$p.value
  ),
  Significativo = c(
    chi_test$p.value < 0.05,
    chi_zona_estrato$p.value < 0.05,
    chisq.test(table(vivienda_final$tipo, vivienda_final$estrato_cat))$p.value < 0.05
  )
) %>%
  mutate(
    p_valor = ifelse(p_valor < 0.001, "<0.001", round(p_valor, 3)),
    Significativo = ifelse(Significativo, "SÍ", "NO")
  )

asociaciones %>%
  kable(caption = "Resumen de Asociaciones entre Variables Categóricas") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Resumen de Asociaciones entre Variables Categóricas
Variables Chi_cuadrado p_valor Significativo
Zona vs Tipo 638.954 <0.001
Zona vs Estrato 2816.688 <0.001
Tipo vs Estrato 208.565 <0.001
cat("\nConclusiones del análisis de asociaciones:\n")
## 
## Conclusiones del análisis de asociaciones:
cat("- Las asociaciones significativas (p < 0.05) indican dependencia entre variables\n")
## - Las asociaciones significativas (p < 0.05) indican dependencia entre variables
cat("- Los residuos estandarizados identifican patrones específicos de sobre/sub-representación\n")
## - Los residuos estandarizados identifican patrones específicos de sobre/sub-representación
cat("- Estas asociaciones son clave para estrategias de segmentación y targeting\n")
## - Estas asociaciones son clave para estrategias de segmentación y targeting

7.6 Conclusiones del Análisis de Asociaciones

7.6 Conclusiones del Análisis de Asociaciones

Dependencias Significativas Identificadas

El análisis estadístico confirma que todas las relaciones entre variables categóricas son altamente significativas (p < 0.001), lo que indica que existe una estructura clara y no aleatoria en el mercado inmobiliario urbano.

Patrones Específicos por Zona y Tipo de Vivienda

Zonas con Vocación para Casas:

  • Zona Centro: Presenta una fuerte preferencia por casas (residuo +7.37), indicando que en esta zona las casas son significativamente más frecuentes de lo que se esperaría por azar
  • Zona Oriente: Muestra la mayor preferencia por casas familiares (residuo +12.86), reflejando una estructura urbana donde predomina el desarrollo horizontal sobre el vertical
  • Zona Sur: Tiene una ligera tendencia hacia casas (residuo +2.52), manteniendo un perfil diversificado entre ambos tipos de vivienda

Zonas con Vocación para Apartamentos:

  • Zona Oeste: Exhibe una marcada preferencia por apartamentos (residuo +10.16), indicando una concentración significativa de desarrollos verticales en esta zona
  • Zona Norte: Mantiene un equilibrio relativo entre tipos, con residuos cercanos a cero que sugieren una distribución esperada de ambos tipos de vivienda

Estratificación Socioeconómica Territorial

El análisis Zona vs Estrato revela una clara segmentación socioeconómica del territorio urbano:

Concentración de Estratos Altos:

  • Zona Oeste: Se caracteriza como el sector de mayor concentración de estratos altos (89% estrato alto)
  • Zona Sur: Presenta una composición mixta con predominio de estratos altos (57% estrato alto, 35% medio)

Sectores de Estratos Medios y Populares:

  • Zona Norte: Muestra la distribución más equilibrada entre los diferentes estratos socioeconómicos
  • Zona Oriente: Se caracteriza por una alta concentración de estratos bajos (96% estrato bajo)
  • Zona Centro: Presenta un perfil predominantemente de estratos bajos (85% estrato bajo)

Relación Tipo-Estrato: Diferenciación por Producto

La asociación significativa entre tipo de vivienda y estrato socioeconómico confirma la diferenciación natural del mercado:

  • Casas: Tienden a concentrarse en estratos más altos, reflejando mayor poder adquisitivo
  • Apartamentos: Se distribuyen más ampliamente, sirviendo tanto a mercados premium como estándar

Implicaciones Estratégicas

Estos patrones de asociación son fundamentales para:

  • Segmentación de Mercado: Las dependencias identificadas permiten definir nichos específicos por zona y tipo
  • Targeting Geográfico: Cada zona tiene vocaciones claras que deben respetarse en estrategias de desarrollo
  • Pricing Estratégico: La estratificación territorial justifica diferenciaciones de precio por ubicación
  • Desarrollo de Producto: Los tipos de vivienda deben alinearse con las preferencias zonales identificadas
  • Análisis de Competencia: Las asociaciones revelan donde existe mayor o menor competencia por tipo de producto

Esta estructura de asociaciones refleja patrones de distribución urbana consolidados que constituyen información clave para cualquier estrategia inmobiliaria en el mercado urbano analizado.

8. Conclusiones y Recomendaciones Estratégicas

8.1 Hallazgos Principales

Estructura del Mercado Inmobiliario

El análisis revela una segmentación natural del mercado en dos grupos claramente diferenciados. El cluster estándar representa el 66% del mercado con apartamentos compactos y precio promedio de 259 millones. El cluster premium constituye el 34% restante con casas amplias y precio promedio de $706 millones. Esta división binaria es consistente y estadísticamente robusta.

Factores Determinantes del Valor

El análisis de componentes principales identifica dos dimensiones clave que explican el 79% de la varianza total. El primer componente (58.7%) representa un factor de calidad integral que incluye precio, área, baños y parqueaderos. El segundo componente (20.3%) distingue entre configuraciones familiares extensas versus apartamentos urbanos compactos.

Patrones Geográficos

La Zona Sur domina ambos segmentos del mercado, concentrando entre 54% y 58% de las propiedades. La Zona Norte es especialmente relevante para el mercado estándar (26% de participación). La Zona Oeste muestra particular importancia en el segmento premium (25% de participación). Las zonas Oriente y Centro tienen participación menor pero diferenciada por tipo de vivienda.

Asociaciones Significativas

Todas las relaciones entre variables categóricas son estadísticamente significativas. Zona Oeste presenta fuerte preferencia por apartamentos, mientras Zona Oriente y Centro favorecen casas. La distribución de estratos socioeconómicos varía marcadamente por zona, con Zona Oeste concentrando 89% de estratos altos y Zona Oriente 96% de estratos bajos.

8.2 Implicaciones para el Mercado

Segmentación Natural del Mercado

Los resultados confirman que el mercado inmobiliario urbano se divide naturalmente en dos segmentos principales. El segmento estándar (66%) se caracteriza por apartamentos con precios promedio de 259 millones, mientras el segmento premium (34%) incluye principalmente casas con precios promedio de 706 millones. Esta división sugiere estrategias diferenciadas según el segmento objetivo.

Distribución Geográfica

La Zona Sur concentra la mayor oferta en ambos segmentos, representando más de la mitad del mercado total. La Zona Norte muestra particular fortaleza en el segmento estándar, mientras la Zona Oeste se distingue en el segmento premium. Las zonas Oriente y Centro tienen menor participación pero patrones diferenciados de tipos de vivienda.

Factores de Valor

El análisis identifica que precio, área construida, número de baños y parqueaderos constituyen los principales determinantes del valor inmobiliario. El número de habitaciones, por el contrario, se asocia más con tipología familiar que con valor económico. Estos hallazgos orientan tanto la valoración como el desarrollo de nuevos proyectos.

8.3 Limitaciones del Estudio

Alcance Temporal

Este análisis representa una fotografía del mercado inmobiliario en un momento específico. Los patrones identificados pueden evolucionar con cambios en la economía, políticas urbanas o preferencias del consumidor. Se recomienda actualizar periódicamente este tipo de análisis para mantener vigencia.

Variables No Consideradas

El estudio se basa en características básicas de las propiedades sin incluir factores como estado de conservación, antigüedad, amenidades del edificio o proximidad a servicios. Estos elementos adicionales podrían modificar algunos de los patrones observados y merecen investigación complementaria.