# =========================
# Paquetes
# =========================
library(readxl)
library(dplyr)
library(ggplot2)
library(FactoMineR)
library(factoextra)
library(missMDA)
library(cluster)
library(scales)
library(forcats)
library(tidyr)
library(knitr)
library(rmdwc)
library(paqueteMODELOS)
Una empresa inmobiliaria líder en una gran ciudad está buscando comprender en profundidad el mercado de viviendas urbanas para tomar decisiones estratégicas más informadas. La empresa posee una base de datos extensa que contiene información detallada sobre diversas propiedades residenciales disponibles en el mercado. Se requiere realizar un análisis holístico de estos datos para identificar patrones, relaciones y segmentaciones relevantes que permitan mejorar la toma de decisiones en cuanto a la compra, venta y valoración de propiedades.
Este documento presenta un análisis de datos del mercado inmobiliario con el objetivo de apoyar decisiones estratégicas en una empresa inmobiliaria. Se desarrollan cuatro líneas principales:
Proveer un análisis descriptivo y multivariado que permita entender la estructura del mercado inmobiliario y segmentar la oferta para orientar decisiones estratégicas.
A continuacion vamos a cargar nuestra base de datos anteriormente descargada como un archivo .xlsx, dado que tuvimos problemas al intentar extraerla del paqueteMertodos sugerido por la Universidad. Posterior a la carga de la bse de datos, se realiza un data frame para observar una parte de nuestros datos.
# =========================
# Cargamos nuestro data set
# =========================
vivienda <- read_excel("vivienda.xlsx")
# Validación mínima de columnas
req <- c("id","zona","piso","estrato","preciom","areaconst","parqueaderos",
"banios","habitaciones","tipo","barrio","longitud","latitud")
missing_cols <- setdiff(req, names(vivienda))
if(length(missing_cols) > 0){
stop(paste("Faltan columnas en el Excel:", paste(missing_cols, collapse=", ")))
}
dim(vivienda)
## [1] 8322 13
head(vivienda, 10)
## # A tibble: 10 × 13
## id zona piso estrato preciom areaconst parqueaderos banios habitaciones
## <dbl> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 1147 Zona … <NA> 3 250 70 1 3 6
## 2 1169 Zona … <NA> 3 320 120 1 2 3
## 3 1350 Zona … <NA> 3 350 220 2 2 4
## 4 5992 Zona … 02 4 400 280 3 5 3
## 5 1212 Zona … 01 5 260 90 1 2 3
## 6 1724 Zona … 01 5 240 87 1 3 3
## 7 2326 Zona … 01 4 220 52 2 2 3
## 8 4386 Zona … 01 5 310 137 2 3 4
## 9 1209 Zona … 02 5 320 150 2 4 6
## 10 1592 Zona … 02 5 780 380 2 3 3
## # ℹ 4 more variables: tipo <chr>, barrio <chr>, longitud <dbl>, latitud <dbl>
Se eliminan filas casi vacías y se conservan filas con
preciom disponible.
v <- vivienda %>%
filter(rowSums(is.na(.)) <= 5) %>% # elimina filas casi vacías
filter(!is.na(preciom)) # precio requerido para perfiles
dim(v)
## [1] 8319 13
En el preprocesamiento se realizó una estandarización de tipos con el fin de evitar errores en cálculos y modelos posteriores (por ejemplo, medianas, distancias, ACP/MCA y clustering). Para ello:
Regla de Transformacion
Para una variable categorica C: \[ C'=\mathrm{factor}\left( \begin{cases} \text{``Desconocido''}, & \text{si } C \text{ es NA o } C=\text{``''} \\ C, & \text{en otro caso} \end{cases} \right) \] Para una variable numerica X: \[ X'= \begin{cases} \text{NA}, & \text{si } X \in \{+\infty,-\infty\}\ \text{o no es convertible a numérico} \\ \mathrm{as.numeric}(X), & \text{en otro caso} \end{cases} \]
# Categóricas a factor
v <- v %>%
mutate(
zona = as.factor(ifelse(is.na(zona) | zona=="", "Desconocido", as.character(zona))),
tipo = as.factor(ifelse(is.na(tipo) | tipo=="", "Desconocido", as.character(tipo))),
barrio = as.factor(ifelse(is.na(barrio)| barrio=="","Desconocido", as.character(barrio)))
)
# Numéricas a numeric
num_cols <- c("id","piso","estrato","preciom","areaconst","parqueaderos",
"banios","habitaciones","longitud","latitud")
for(col in num_cols){
v[[col]] <- suppressWarnings(as.numeric(v[[col]]))
v[[col]][is.infinite(v[[col]])] <- NA
}
str(v)
## tibble [8,319 × 13] (S3: tbl_df/tbl/data.frame)
## $ id : num [1:8319] 1147 1169 1350 5992 1212 ...
## $ zona : Factor w/ 5 levels "Zona Centro",..: 4 4 4 5 2 2 2 2 2 2 ...
## $ piso : num [1:8319] NA NA NA 2 1 1 1 1 2 2 ...
## $ estrato : num [1:8319] 3 3 3 4 5 5 4 5 5 5 ...
## $ preciom : num [1:8319] 250 320 350 400 260 240 220 310 320 780 ...
## $ areaconst : num [1:8319] 70 120 220 280 90 87 52 137 150 380 ...
## $ parqueaderos: num [1:8319] 1 1 2 3 1 1 2 2 2 2 ...
## $ banios : num [1:8319] 3 2 2 5 2 3 2 3 4 3 ...
## $ habitaciones: num [1:8319] 6 3 4 3 3 3 3 4 6 3 ...
## $ tipo : Factor w/ 2 levels "Apartamento",..: 2 2 2 2 1 1 1 1 2 2 ...
## $ barrio : Factor w/ 436 levels "20 de julio",..: 1 1 1 2 3 3 3 3 3 3 ...
## $ longitud : num [1:8319] -76.5 -76.5 -76.5 -76.5 -76.5 ...
## $ latitud : num [1:8319] 3.43 3.43 3.44 3.44 3.46 ...
Calculamos el porcentaje de valores faltantes de la siguiente manera: \[ NA(X) = \dfrac{\sum_{i=1}^{n}1(x_i\; es \; NA)}{n}*100 \] Donde;
Posterior al calculo realizamos una tabla con los resultados, la cual se presenta acontinuacion.
na_prop <- sapply(v, function(x) mean(is.na(x)))
na_tbl <- data.frame(variable = names(na_prop), prop_na = as.numeric(na_prop)) %>%
arrange(desc(prop_na))
kable(na_tbl, digits = 4, caption = "Proporción de valores faltantes por variable")
| variable | prop_na |
|---|---|
| piso | 0.3167 |
| parqueaderos | 0.1926 |
| id | 0.0000 |
| zona | 0.0000 |
| estrato | 0.0000 |
| preciom | 0.0000 |
| areaconst | 0.0000 |
| banios | 0.0000 |
| habitaciones | 0.0000 |
| tipo | 0.0000 |
| barrio | 0.0000 |
| longitud | 0.0000 |
| latitud | 0.0000 |
na_tbl2 <- na_tbl %>% filter(prop_na > 0)
ggplot(na_tbl2, aes(x = reorder(variable, prop_na), y = prop_na)) +
geom_col() +
coord_flip() +
scale_y_continuous(labels = percent_format()) +
labs(title="Valores faltantes por variable",
x="Variable", y="% de faltantes")
Con el fin de conservar la mayor cantidad posible de observaciones y garantizar la correcta ejecución de los métodos multivariados, se implementó una estrategia de imputación diferenciada según el tipo de análisis y la naturaleza de las variables.
\[ x_i'= \begin{cases} \mathrm{Mediana}(X_{\text{obs}}), & \text{si } x_i \text{ es NA} \\ x_i, & \text{en otro caso} \end{cases} \] donde \(X_{obs}\) es el conjunto de valores observafdos no faltantes de la misma variable \(X\).
missMDA::imputeFAMD(). \[
\hat{X} \approx U{\scriptstyle\sum} V^T
\]Adicionalmente, se crearon indicadores de faltantes para variables clave como piso y parqueaderos (por ejemplo, piso_miss y parq_miss). Estos indicadores toman valor 1 cuando el dato está ausente y 0 cuando está presente.
# Indicadores de faltantes
v <- v %>%
mutate(
piso_miss = as.integer(is.na(piso)),
parq_miss = as.integer(is.na(parqueaderos))
)
med_safe <- function(x){
x <- suppressWarnings(as.numeric(x))
if(all(is.na(x))) return(NA_real_)
median(x, na.rm = TRUE)
}
# Imputación simple para análisis generales
v$piso[is.na(v$piso)] <- med_safe(v$piso)
v$parqueaderos[is.na(v$parqueaderos)] <- med_safe(v$parqueaderos)
Se seleccionaron las variables numéricas para el ACP y se imputaron los valores faltantes con la mediana de cada variable, garantizando una matriz completa y robusta frente a outliers
X_num <- v %>%
select(estrato, latitud, longitud, areaconst, banios, habitaciones, parqueaderos, piso) %>%
mutate(across(everything(), ~ifelse(is.na(.), med_safe(.), .)))
summary(X_num)
## estrato latitud longitud areaconst
## Min. :3.000 Min. :3.333 Min. :-76.59 Min. : 30.0
## 1st Qu.:4.000 1st Qu.:3.381 1st Qu.:-76.54 1st Qu.: 80.0
## Median :5.000 Median :3.416 Median :-76.53 Median : 123.0
## Mean :4.634 Mean :3.418 Mean :-76.53 Mean : 174.9
## 3rd Qu.:5.000 3rd Qu.:3.452 3rd Qu.:-76.52 3rd Qu.: 229.0
## Max. :6.000 Max. :3.498 Max. :-76.46 Max. :1745.0
## banios habitaciones parqueaderos piso
## Min. : 0.000 Min. : 0.000 Min. : 1.000 Min. : 1.000
## 1st Qu.: 2.000 1st Qu.: 3.000 1st Qu.: 1.000 1st Qu.: 2.000
## Median : 3.000 Median : 3.000 Median : 2.000 Median : 3.000
## Mean : 3.111 Mean : 3.605 Mean : 1.867 Mean : 3.527
## 3rd Qu.: 4.000 3rd Qu.: 4.000 3rd Qu.: 2.000 3rd Qu.: 4.000
## Max. :10.000 Max. :10.000 Max. :10.000 Max. :12.000
Con la matriz numérica preprocesada, se ajustó un Análisis de Componentes Principales (ACP). El análisis se realizó con centrado y escalado de las variables, de modo que todas las variables contribuyan de forma comparable independientemente de su unidad o magnitud. Posteriormente, se calculó la proporción de varianza explicada por cada componente principal y su acumulado, con el fin de determinar cuántos componentes capturan la mayor parte de la variabilidad del conjunto de datos.
El procedimiento realizado para obtener la tabla fue le siguiente: \[ VarExp(PC_k)=\dfrac{\lambda_k}{\sum_{j=1}^{p}\lambda_j} \] donde:
Y el acumulado corresponde a: \[ VarAcum(m)=\sum_{k=1}^{n}VarExp(PC_k) \]
pca <- prcomp(X_num, center = TRUE, scale. = TRUE)
eig <- (pca$sdev^2) / sum(pca$sdev^2)
eig_df <- data.frame(
PC = paste0("PC", seq_along(eig)),
var_exp = eig,
var_acum = cumsum(eig)
)
kable(head(eig_df, 10), digits=4, caption="Varianza explicada (primeros 10 componentes)")
| PC | var_exp | var_acum |
|---|---|---|
| PC1 | 0.3543 | 0.3543 |
| PC2 | 0.1930 | 0.5473 |
| PC3 | 0.1258 | 0.6731 |
| PC4 | 0.0979 | 0.7710 |
| PC5 | 0.0905 | 0.8615 |
| PC6 | 0.0640 | 0.9255 |
| PC7 | 0.0454 | 0.9709 |
| PC8 | 0.0291 | 1.0000 |
En la Tabla se presenta la proporción de varianza explicada por los primeros componentes principales. Se observa que PC1 explica el 35.43% de la variabilidad total y PC2 explica el 19.30%, por lo que los dos primeros componentes acumulan 54.73%. Esto sugiere que una representación en dos dimensiones captura una parte sustancial de la información del conjunto de variables, aunque aún queda variabilidad relevante en componentes posteriores
El gráfico de sedimentación muestra la proporción de varianza explicada por cada componente principal. Se evidencia una caída pronunciada en la varianza explicada desde el primer componente (35.4%) al segundo (19.3%) y al tercero (12.6%). A partir del cuarto componente, los aportes individuales son menores (alrededor de 10% o menos), lo que sugiere un punto de ‘codo’ en los primeros componentes.
fviz_eig(pca, addlabels = TRUE) +
labs(title = "ACP: Varianza explicada por componente")
En el círculo de correlaciones del ACP se observa que las variables de tamaño y dotación (areaconst, habitaciones, baños y parqueaderos) apuntan en una dirección similar, indicando asociación positiva entre ellas. Estrato presenta una orientación distinta, sugiriendo que la segunda dimensión captura un eje relacionado con el segmento socioeconómico. Por su parte, latitud y longitud se agrupan en otra dirección, evidenciando que la ubicación también aporta variabilidad relevante en los componentes principales.
fviz_pca_var(pca, repel = TRUE) +
labs(title = "ACP: Variables en el espacio de componentes")
En el plano PC1–PC2, las propiedades se visualizan en dos dimensiones que resumen la mayor parte de la variabilidad. La coloración por cuartiles de precio permite observar un gradiente: los inmuebles del cuartil más alto tienden a concentrarse en zonas específicas del espacio factorial, lo que sugiere que las combinaciones de características capturadas por los primeros componentes están asociadas con el nivel de precio.
scores <- as.data.frame(pca$x[,1:2])
q <- quantile(v$preciom, probs = seq(0,1,0.25), na.rm = TRUE)
scores$precio_q <- cut(v$preciom, breaks = q, include.lowest = TRUE)
ggplot(scores, aes(PC1, PC2, color = precio_q)) +
geom_point(alpha = 0.6, size = 1.2) +
labs(title="ACP: Propiedades en PC1-PC2 (cuartiles de precio)",
color="Precio (cuartiles)")
Con el fin de evaluar qué dimensiones latentes del ACP se asocian con el precio, se calculó la correlación de Pearson entre los puntajes de los componentes principales (\(PC1–PC5\)) y \(log(1+precio)\).Los resultados muestran que PC1 presenta una correlación alta y positiva con el log-precio (\(r=0.82\)),indicando que la variación del precio está fuertemente alineada con el primer eje de variabilidad del conjunto de variables (principalmente características físicas y de dotación del inmueble). En contraste, PC2 y PC3 muestran asociaciones débiles (\(r=0.2\) y \(r=0.16\)), mientras que PC4 tiene una correlación leve y negativa (\(r=-0.11\)) y y PC5 es prácticamente nulo (\(r=0.007\)).
cor_pc_precio <- cor(pca$x[,1:5], log1p(v$preciom))
kable(round(cor_pc_precio, 4), caption="Correlación PCs vs log1p(preciom) (PC1–PC5)")
| PC1 | 0.8258 |
| PC2 | 0.2055 |
| PC3 | 0.1670 |
| PC4 | -0.1116 |
| PC5 | 0.0078 |
Se calculo \(r_k = corr(PC_k, log(1+precio))\) usando la correlacion se Pearson, \[ r_k = corr(PC_k,y)=\dfrac{\sum_{i=1}^{n}(z_{ik}-\bar{z}_k)(y_i-\bar{y})} {\sqrt{\sum_{i=1}^{n}(z_{ik}-\bar{z}_k)^2}{\sqrt{\sum_{i=1}^{n}(y_{i}-\bar{y})^2}}} \] donde:
Se construyó la matriz de variables mixtas para la segmentación (categóricas y numéricas), incluyendo indicadores de faltantes para capturar posible información asociada a la ausencia de datos. Posteriormente, se aseguraron los tipos de dato y, únicamente si existían NA, se imputaron mediante imputeFAMD() para obtener una matriz completa antes del análisis factorial y el clustering
# 7.1 Datos mixtos para clustering
X_mix <- v %>%
select(zona, tipo, estrato, latitud, longitud, areaconst, banios, habitaciones,
parqueaderos, piso, piso_miss, parq_miss)
# Asegurar tipos correctos (importante)
X_mix$zona <- as.factor(X_mix$zona)
X_mix$tipo <- as.factor(X_mix$tipo)
num_cols <- c("estrato","latitud","longitud","areaconst","banios","habitaciones",
"parqueaderos","piso","piso_miss","parq_miss")
for(col in num_cols){
X_mix[[col]] <- suppressWarnings(as.numeric(X_mix[[col]]))
X_mix[[col]][is.infinite(X_mix[[col]])] <- NA
}
# Imputar SOLO si hay faltantes
if (sum(is.na(X_mix)) > 0) {
imp <- missMDA::imputeFAMD(X_mix, ncp = 5)
X_complete <- imp$completeObs
cat("Se imputaron NA. NA totales luego:", sum(is.na(X_complete)), "\n")
} else {
X_complete <- X_mix
cat("No hay NA en X_mix. Se omite imputación.\n")
}
## No hay NA en X_mix. Se omite imputación.
Al verificar la matriz \(M_{mix}\) se encontro que no presentaba valores faltantes, \[ \sum 1(x_{ij} es NA)=0. \] Por lo tanto la imputacion se omitio y se trabajo directamente con la matrz original, evitando transformaciones innecesarias.
Dado que el conjunto incluye variables mixtas (categóricas y numéricas), se aplicó FAMD para proyectar las observaciones a un espacio factorial de dimensión reducida. Se conservaron 10 dimensiones\(ncp=10\) y se obtuvieron las coordenadas de cada propiedad en dicho espacio\(Z\). El resultado \(8319*10\) ndica que 8319 propiedades quedaron representadas por 10 componentes factoriales, los cuales se utilizan como entrada para el algoritmo de clustering.
res_famd <- FAMD(X_complete, ncp = 10, graph = FALSE)
Z <- res_famd$ind$coord
dim(Z)
## [1] 8319 10
Para definir el número de conglomerados \(K\), se evaluó el índice Silhouette promedio para \(K=2,...,10\).Con el fin de reducir el costo computacional, el cálculo se realizó sobre una muestra aleatoria de 2000 observaciones. El \(K\) óptimo se seleccionó como el que maximiza el Silhouette promedio, lo cual indica mejor separación y cohesión de los grupos.
Para ello se utiliza la Ecuacion de Sihouette; \[ s(i)=\dfrac{b(i)-a(i)}{\max\{a(i),b{i}\}} \] donde:
Asi, podemos aplicar, \[ \bar{s}(K) = \dfrac{1}{n}\sum_{i=1}^{n}s(i) \]
avg_sil_kmeans_sample <- function(data, k, sample_size = 2000, nstart=30, iter.max=300){
set.seed(123)
n <- nrow(data)
idx <- sample(seq_len(n), size = min(sample_size, n))
data_s <- data[idx, , drop = FALSE]
km <- kmeans(data, centers = k, nstart = nstart, iter.max = iter.max)
cl_s <- km$cluster[idx]
sil <- silhouette(cl_s, dist(data_s))
mean(sil[, 3])
}
k_grid <- 2:10
sil_vals <- sapply(k_grid, function(k) avg_sil_kmeans_sample(Z, k))
sil_df <- data.frame(k = k_grid, silhouette = sil_vals)
kable(sil_df, digits=4, caption="Silhouette promedio (muestra) por K")
| k | silhouette |
|---|---|
| 2 | 0.2049 |
| 3 | 0.1954 |
| 4 | 0.2217 |
| 5 | 0.2540 |
| 6 | 0.2725 |
| 7 | 0.2706 |
| 8 | 0.2544 |
| 9 | 0.2384 |
| 10 | 0.2354 |
ggplot(sil_df, aes(k, silhouette)) +
geom_line() + geom_point() +
labs(title="Selección de K (Silhouette promedio en muestra)", x="K", y="Silhouette")
k_best <- sil_df$k[which.max(sil_df$silhouette)]
k_best
## [1] 6
De acuerdo con el criterio de Silhouette promedio, el valor que maximiza \(\bar{s}(K)\) fue \(K=6\); por tanto, se adopto esta particion como la segmentacion final.
Con el valor optimo \(K\) eleccionado, se ajustó el algoritmo K-means sobre las coordenadas \(Z\) obtenidas por FAMD. Se usaron múltiples inicializaciones (nstart = 50) para reducir la sensibilidad a centroides iniciales y mejorar la estabilidad de la solución. Finalmente, se asignó a cada propiedad su etiqueta de clúster y se examinó la distribución de tamaños por grupo.
km <- kmeans(Z, centers = k_best, nstart = 50, iter.max = 500)
v$cluster <- factor(km$cluster)
table(v$cluster)
##
## 1 2 3 4 5 6
## 124 351 1126 1747 1799 3172
La partición final generó 6 conglomerados con tamaños desiguales, lo cual es esperable en mercados heterogéneos: algunos clústeres representan segmentos dominantes de oferta, mientras otros capturan nichos específicos (menor frecuencia). Esta distribución se utiliza posteriormente para perfilar e interpretar cada segmento
Z_2d <- Z[,1:2, drop = FALSE]
fviz_cluster(
list(data = Z_2d, cluster = km$cluster),
geom = "point", ellipse = TRUE,
main = "Conglomerados en espacio FAMD (Dim 1 vs Dim 2)"
)
En la proyección Dim 1 vs Dim 2 del espacio FAMD se
observa la separación relativa entre conglomerados. Las elipses resumen
la dispersión intra-clúster y permiten identificar qué grupos presentan
mayor solapamiento (segmentos similares) y cuáles están más claramente
diferenciados (segmentos más especializados).
Para interpretar los segmentos obtenidos, se construyó un perfil numérico por clúster calculando estadísticos robustos: tamaño del grupo (n), mediana del precio y de las variables físicas (área construida, baños, habitaciones, parqueaderos, piso y estrato), y los cuartiles \(P25\) y \(P75\) del precio.La mediana y los cuartiles permiten describir cada segmento reduciendo la influencia de valores extremos, facilitando la comparación entre clústeres y la identificación de grupos de mayor y menor valor en el mercado.
v <- v %>% mutate(across(c(preciom, areaconst, banios, habitaciones, parqueaderos, piso, estrato), as.numeric))
perfil_clusters <- v %>%
group_by(cluster) %>%
summarise(
n = n(),
precio_med = median(preciom, na.rm=TRUE),
precio_p25 = quantile(preciom, 0.25, na.rm=TRUE),
precio_p75 = quantile(preciom, 0.75, na.rm=TRUE),
areaconst_med = median(areaconst, na.rm=TRUE),
banios_med = median(banios, na.rm=TRUE),
hab_med = median(habitaciones, na.rm=TRUE),
parq_med = median(parqueaderos, na.rm=TRUE),
piso_med = median(piso, na.rm=TRUE),
estrato_med = median(estrato, na.rm=TRUE),
.groups="drop"
) %>% arrange(desc(n))
kable(perfil_clusters, digits=2, caption="Perfil numérico por clúster")
| cluster | n | precio_med | precio_p25 | precio_p75 | areaconst_med | banios_med | hab_med | parq_med | piso_med | estrato_med |
|---|---|---|---|---|---|---|---|---|---|---|
| 6 | 3172 | 255 | 185.00 | 340.00 | 90 | 2.0 | 3 | 1 | 3 | 5 |
| 5 | 1799 | 630 | 450.00 | 900.00 | 300 | 5.0 | 5 | 2 | 3 | 5 |
| 4 | 1747 | 257 | 155.00 | 375.00 | 96 | 2.0 | 3 | 2 | 3 | 4 |
| 3 | 1126 | 570 | 393.00 | 890.00 | 158 | 3.0 | 3 | 2 | 3 | 6 |
| 2 | 351 | 210 | 145.00 | 290.00 | 160 | 2.0 | 4 | 2 | 3 | 3 |
| 1 | 124 | 297 | 188.75 | 361.25 | 160 | 2.5 | 4 | 2 | 3 | 3 |
Clústeres con mayor precio_med y mayor areaconst_med representan segmentos ‘premium’, mientras que clústeres con menores medianas corresponden a oferta de entrada o económica
ggplot(v, aes(cluster, preciom)) +
geom_boxplot(outlier.alpha = 0.2) +
labs(title="Distribución de precio (preciom) por clúster", x="Clúster", y="Precio")
El diagrama de cajas evidencia diferencias claras de precio entre
segmentos. Los clústeres 3 y 4 concentran las medianas más
altas y mayor dispersión, sugiriendo oferta de mayor valor y
heterogeneidad interna. En contraste, los clústeres 1, 2, 5 y 6
presentan medianas inferiores y rangos intercuartílicos más estrechos,
asociados a segmentos más económicos. Los puntos fuera de los bigotes
corresponden a valores atípicos, típicos de propiedades ‘premium’ o
casos particulares del mercado.
Para caracterizar cada segmento, se estimó la composición interna de la oferta por zona y por estrato. Para cada clúster se calculó el porcentaje \(pct=n/\sum n\),de modo que los resultados se interpreten como ‘distribución dentro del clúster’ y no como frecuencias absolutas.
oferta_cluster_zona <- v %>%
count(cluster, zona) %>%
group_by(cluster) %>%
mutate(pct = n/sum(n)) %>%
ungroup()
oferta_cluster_estrato <- v %>%
count(cluster, estrato) %>%
group_by(cluster) %>%
mutate(pct = n/sum(n)) %>%
ungroup()
ggplot(oferta_cluster_zona, aes(x = cluster, y = pct, fill = zona)) +
geom_col() +
scale_y_continuous(labels = percent_format()) +
labs(title="Composición de oferta por clúster y zona",
x="Clúster", y="% dentro del clúster", fill="Zona")
ggplot(oferta_cluster_estrato, aes(x = cluster, y = pct, fill = factor(estrato))) +
geom_col() +
scale_y_continuous(labels = percent_format()) +
labs(title="Composición de oferta por clúster y estrato",
x="Clúster", y="% dentro del clúster", fill="Estrato")
Coimposicion por zona
Se observa una alta concentración geográfica por clúster: el clúster 1 se ubica principalmente en Zona Sur, el clúster 2 en Zona Centro, el clúster 3 en Zona Oeste, el clúster 6 en Zona Oriente. Los clústeres 4 y 5 muestran una mezcla, aunque con predominio de Zona Sur (clúster 4) y Zona Norte (clúster 5). Esto sugiere que los segmentos capturan patrones espaciales claros de oferta.
Composicion por estrato
Los clústeres también reflejan diferenciación socioeconómica: el clúster 2 está dominado por estrato 3 y el clúster 6 también se concentra en estrato 3; el clúster 3 presenta predominio de estrato 6; el clúster 4 combina principalmente estratos 5–6; y el clúster 5 concentra mayormente estratos 4–5. En conjunto, la segmentación evidencia que el mercado se estructura simultáneamente por localización y estrato.
ggplot(v, aes(longitud, latitud, color = cluster)) +
geom_point(alpha = 0.6, size = 1.2) +
labs(title="Distribución espacial de la oferta por clúster",
x="Longitud", y="Latitud")
Los clústeres presentan agrupamiento espacial claro, con solapamientos
puntuales que reflejan zonas de mezcla de segmentos.
Para explorar asociaciones entre variables categóricas (tipo, zona y barrio), se aplicó Análisis de Correspondencia Múltiple (MCA). Con el fin de mejorar la interpretabilidad y evitar alta dispersión por categorías poco frecuentes, se agruparon los barrios conservando únicamente los 30 más frecuentes y recodificando el resto en la categoría ‘Otros’.
cat_df <- v %>% select(tipo, zona, barrio) %>%
mutate(across(everything(), as.factor))
# Mantener top 30 barrios para interpretabilidad
top_barrios <- names(sort(table(cat_df$barrio), decreasing = TRUE))[1:30]
cat_df <- cat_df %>%
mutate(barrio = fct_other(barrio, keep = top_barrios, other_level = "Otros"))
Para visualizar la estructura conjunta entre tipo, zona y barrio, se utiliza MCA (Análisis de Correspondencia Múltiple), que descompone la tabla de contingencia de categorías en dimensiones latentes maximizando la inercia (variabilidad) explicada. Sea \(N[n_{ij}]\) la tabla de frecuencias y \(n=\sum_{i,j}n_{ij}\). Definimos la matriz de proporciones \(P=N/n\), los margenes \(r=P1\) y \(c=1^TP\) El MCA se basa en la descomposición SVD de la matriz estandarizada: \[ S=D_r^{-1/2}*(P-rc^T)*D_c^{-1/2} \] donde \(D_r=diag(r)\) y \(D_c=diag(c)\).
Las coordenadas factoriales (posiciones en el plano) se obtienen a partir de los autovalores/autovectores de esta descomposición, y permiten representar simultáneamente categorías y observaciones.
chi_tipo_zona <- chisq.test(table(cat_df$tipo, cat_df$zona))
chi_tipo_barrio <- chisq.test(table(cat_df$tipo, cat_df$barrio))
chi_zona_barrio <- chisq.test(table(cat_df$zona, cat_df$barrio))
chi_tipo_zona
##
## Pearson's Chi-squared test
##
## data: table(cat_df$tipo, cat_df$zona)
## X-squared = 690.93, df = 4, p-value < 2.2e-16
chi_tipo_barrio
##
## Pearson's Chi-squared test
##
## data: table(cat_df$tipo, cat_df$barrio)
## X-squared = 1069.8, df = 30, p-value < 2.2e-16
chi_zona_barrio
##
## Pearson's Chi-squared test
##
## data: table(cat_df$zona, cat_df$barrio)
## X-squared = 11160, df = 120, p-value < 2.2e-16
El biplot del MCA muestra asociaciones entre categorías: categorías cercanas tienden a co-ocurrir en la oferta, mientras que categorías ubicadas en direcciones opuestas reflejan perfiles diferenciados. En particular, se observan combinaciones tipo–zona–barrio que se agrupan, indicando que la oferta inmobiliaria no se distribuye aleatoriamente: existe una estructura espacial y tipológica (zonas y barrios se asocian con ciertos tipos de vivienda), lo cual es consistente con los resultados del test.
Una vez estimado el MCA, se analiza la inercia (análogo a varianza explicada) para decidir cuántas dimensiones son suficientes para interpretar la estructura entre categorías (tipo–zona–barrio).
Loos autovalores \(\lambda_k\) obtenidos de la descomposición anterior representan la inercia de cada dimensión. La proporción de inercia explicada por la dimensión \(k\) se calcula como: \[ Inercia\; explicada(k)=\dfrac{\lambda_k}{\sum_h\lambda_h} \] y la inercia acumulada hasta \(K\) dimensiones como: \[ Incercia\; acumulada(K)=\sum_{k=1}^{K}\dfrac{\lambda_k}{\sum_h\lambda_h}. \] La siguiente gráfica resume estas proporciones para las primeras dimensiones.
mca <- MCA(cat_df, graph = FALSE)
fviz_eig(mca, addlabels = TRUE) +
labs(title="MCA: Varianza explicada por dimensiones")
La gráfica muestra que las primeras dimensiones explican proporciones
relativamente pequeñas de inercia, lo cual es habitual en MCA cuando
existen muchas categorías (especialmente al incluir barrio). Aun así,
las primeras 2–3 dimensiones concentran la mayor parte de la estructura
interpretable y se utilizan para el biplot y la identificación de
asociaciones entre categorías. En consecuencia, la interpretación se
enfocará en Dim1–Dim2 (y opcionalmente Dim3) para describir los
principales patrones tipo–zona–barrio.
fviz_mca_var(
mca,
repel = TRUE,
select.var = list(contrib = 25) # muestra solo las 25 categorías más influyentes
) + labs(title = "MCA: Asociación entre categorías (Top 25 por contribución)")
El mapa factorial evidencia patrones espaciales claros: Zona Oeste se
asocia con barrios como cristales, normandía, aguacatal y el peñón
(agrupados en el cuadrante derecho), mientras que Zona Norte se
relaciona con acopi, la flora, prados del norte y torres de comfandi
(parte superior). Zona Sur aparece vinculada con ciudad jardín y valle
del lili (cuadrante inferior izquierdo). En contraste, las categorías
Casa y Apartamento se ubican cerca del origen, sugiriendo que su
asociación con una zona específica es más débil frente a la señal
espacial que aportan los barrios y zonas.
fviz_contrib(mca, choice = "var", axes = 1, top = 15) +
labs(title="MCA: Top contribuciones Dimensión 1")
fviz_contrib(mca, choice = "var", axes = 2, top = 15) +
labs(title="MCA: Top contribuciones Dimensión 2")
Los gráficos muestran que la estructura de Dimensión 1
y Dimensión 2 está dominada por un conjunto reducido de
categorías. Por tanto, la interpretación del plano MCA debe centrarse en
esas categorías por encima de la línea roja, ya que son las que separan
los perfiles de oferta. En términos prácticos, Dim1 y
Dim2 están capturando los contrastes principales del
mercado: categorías con alta contribución representan combinaciones
tipo–zona–barrio más distintivas, mientras que las categorías con baja
contribución tienen un papel secundario y tienden a ubicarse cerca del
origen en el biplot.
log1p(preciom) permite
identificar qué dimensiones del ACP están más asociadas al precio.