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.
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.
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.
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)| 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 |
## 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
| 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 |
# 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"))| 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 |
# 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"))| 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
# 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))# 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"))| 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"))| 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")# 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"))| 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")# 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"))| 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 |
# 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"))| 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))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.
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.
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
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()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:
## Registros finales: 8319
## Valores faltantes restantes:
sapply(vivienda_clean, function(x) sum(is.na(x))) %>%
.[. > 0] %>%
kable() %>%
kable_styling(bootstrap_options = c("striped", "hover"))| x | |
|---|---|
| piso | 2635 |
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.
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.
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"))| Variable | Outliers | Porcentaje |
|---|---|---|
| preciom | 552 | 6.64 |
| areaconst | 382 | 4.59 |
| parqueaderos | 567 | 6.82 |
| banios | 72 | 0.87 |
| habitaciones | 888 | 10.67 |
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
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.
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:
## /\ /\
## { `---' }
## { 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"))| 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 |
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"))| 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
## Variables incluidas: estrato preciom areaconst parqueaderos banios habitaciones
# 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:
## 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"))| 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 |
# 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))# 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"))| 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 |
# 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"))| 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"))| 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 |
# 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))# 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)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:
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).
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:
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:
Síntesis de los Dos Primeros Componentes (78.93% de varianza):
Esto nos permite segmentar el mercado inmobiliario en cuatro cuadrantes principales:
# 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"))| 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 |
# 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"))| k | silhouette |
|---|---|
| 2 | 0.446 |
| 3 | 0.368 |
| 4 | 0.386 |
| 5 | 0.336 |
| 6 | 0.345 |
| 7 | 0.333 |
| 8 | 0.340 |
# 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"))| 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")# 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"))| 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"))| 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"))| 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)Cluster 1: “Segmento Premium” (33.9% del mercado - 2,715 propiedades)
Características distintivas:
Distribución geográfica:
Tipología:
Cluster 2: “Segmento Estándar” (66.1% del mercado - 5,294 propiedades)
Características distintivas:
Distribución geográfica:
Tipología:
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)
2. Mercado Masivo/Estándar (Cluster 2)
Zona Sur: Mercado Diversificado
Tipología vs Precio:
Distribución de Mercado:
# 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"))| 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:
## Chi-cuadrado = 638.954
## 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
## Interpretación: 0 = no asociación, 1 = asociación perfecta
# Residuos estandarizados
residuos <- chi_test$residuals
residuos %>%
kable(caption = "Residuos Estandarizados (Zona vs Tipo)", digits = 2) %>%
kable_styling(bootstrap_options = c("striped", "hover"))| 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 |
## Interpretación de residuos estandarizados:
## - Valores > +2: Combinación más frecuente de lo esperado
## - Valores < -2: Combinación menos frecuente de lo esperado
## - Valores entre -2 y +2: Sin diferencias significativas
## 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 )
# 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))# 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"))| 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:
## Chi-cuadrado = 2816.688
## 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")# 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"))| Variables | Chi_cuadrado | p_valor | Significativo |
|---|---|---|---|
| Zona vs Tipo | 638.954 | <0.001 | SÍ |
| Zona vs Estrato | 2816.688 | <0.001 | SÍ |
| Tipo vs Estrato | 208.565 | <0.001 | SÍ |
##
## Conclusiones del análisis de asociaciones:
## - Las asociaciones significativas (p < 0.05) indican dependencia entre variables
## - Los residuos estandarizados identifican patrones específicos de sobre/sub-representación
## - Estas asociaciones son clave para estrategias de segmentación y targeting
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:
Zonas con Vocación para Apartamentos:
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:
Sectores de Estratos Medios y Populares:
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:
Implicaciones Estratégicas
Estos patrones de asociación son fundamentales para:
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.
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.
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.
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.