Este informe aborda la evaluación de la oferta inmobiliaria urbana con el fin de entregar evidencia práctica para decisiones de compra, venta y valoración de propiedades. Partimos de una base de datos amplia con información de inmuebles (precio, área, amenidades, ubicación y atributos socioeconómicos) y aplicaremos un enfoque multidimensional para: (i) entender las características que explican la variación del mercado, (ii) segmentar la oferta en grupos comparables y accionables, y (iii) explorar relaciones entre variables categóricas (tipo, zona, estrato). El propósito final es comunicar los hallazgos de manera clara y efectiva a la dirección de la empresa.
Los datos utilizados provienen del paquete
paqueteMODELOS. En este caso, se emplea el conjunto
denominado vivenda. Con ello se constata que el dataset
incluye atributos numéricos clave (precio, área
construida, parqueaderos, baños, habitaciones) y
categóricos (zona, estrato, tipo, barrio) necesarios
para los análisis posteriores (EDA, PCA, clústeres, correspondencias y
mapeo interactivo).
Antes de aplicar técnicas de análisis multidimensional, es
fundamental comprender la estructura y distribución de la información
disponible.
En esta sección se analizan las variables principales del conjunto de
datos, observando sus rangos, tendencias y posibles valores
atípicos.
Este análisis inicial permitirá identificar patrones generales,
verificar la calidad de los datos y orientar las decisiones sobre
transformaciones y limpieza necesarias antes de los modelos.
Aquí creamos variables adicionales que nos permitan tener una mejor lectura del conjunto de datos durante la exploración, como lo es precio por metro cuadrado.
# Creamos precio por metro cuadrado
vivienda <- vivienda %>%
mutate(precio_m2 = ifelse(areaconst > 0, (preciom * 1e6) / areaconst, NA))Se incorpora esta variable para facilitar comparaciones más homogéneas entre inmuebles de diferentes tamaños. El precio por m² permite evaluar el valor relativo de las propiedades sin que el área total distorsione la percepción del costo, siendo un indicador clave para segmentar el mercado.
El resumen estadístico ofrece una visión rápida de la centralidad y dispersión de las variables, así como de la presencia de valores extremos. Esta información es esencial para detectar rangos atípicos, posibles errores de captura y magnitudes que podrían requerir transformaciones antes de aplicar modelos.
vivienda %>%
select(preciom, areaconst, precio_m2, parqueaderos, banios, habitaciones) %>%
summary() preciom areaconst precio_m2 parqueaderos
Min. : 58.0 Min. : 30.0 Min. : 146132 Min. : 1.000
1st Qu.: 220.0 1st Qu.: 80.0 1st Qu.:1916667 1st Qu.: 1.000
Median : 330.0 Median : 123.0 Median :2640000 Median : 2.000
Mean : 433.9 Mean : 174.9 Mean :2722217 Mean : 1.835
3rd Qu.: 540.0 3rd Qu.: 229.0 3rd Qu.:3379470 3rd Qu.: 2.000
Max. :1999.0 Max. :1745.0 Max. :9468085 Max. :10.000
NA's :2 NA's :3 NA's :3 NA's :1605
banios habitaciones
Min. : 0.000 Min. : 0.000
1st Qu.: 2.000 1st Qu.: 3.000
Median : 3.000 Median : 3.000
Mean : 3.111 Mean : 3.605
3rd Qu.: 4.000 3rd Qu.: 4.000
Max. :10.000 Max. :10.000
NA's :3 NA's :3
A continuación, se describe cada una de forma simple para facilitar su lectura:
La distribución del precio total de los inmuebles muestra una concentración de valores en rangos bajos a medios, con una cola hacia precios altos que podría asociarse a propiedades premium o localizadas en zonas de alta valorización. Esto sugiere la existencia de un mercado heterogéneo en términos de valor absoluto.
hist(vivienda$preciom,
breaks = 40,
col = "skyblue",
main = "Distribución del precio\n(en millones COP)",
xlab = "Precio (millones COP)",
ylab = "Frecuencia")Al analizar el precio por m², se observa que este indicador reduce parcialmente el sesgo causado por el tamaño del inmueble, permitiendo una comparación más equitativa entre propiedades. Las colas de la distribución podrían reflejar tanto unidades de lujo como propiedades en zonas de menor oferta.
Si bien el precio total es útil para entender el perfil de inversión, su comparación directa entre inmuebles de distinto tamaño puede generar sesgos, por lo que resulta clave complementarlo con métricas relativas como el precio por m².
hist(vivienda$precio_m2 / 1e6,
breaks = 40,
col = "orange",
main = "Distribución del precio por m²",
xlab = "Precio por m² (millones COP)",
ylab = "Frecuencia")Este indicador es uno de los más utilizados por analistas y agentes inmobiliarios para comparar propiedades con tamaños diferentes.
En términos estratégicos, la dirección de la empresa podría emplear esta métrica para priorizar captaciones en zonas de alto valor por m² si busca propiedades exclusivas, o en zonas de bajo valor por m² con potencial de crecimiento si busca oportunidades de arbitraje.
La oferta está fuertemente concentrada en Zona Sur, que reúne la mayor cantidad de inmuebles del conjunto. En segundo lugar aparece Zona Norte, seguida por Zona Oeste. En contraste, Zona Oriente y Zona Centro están claramente subrepresentadas.
df_zona <- vivienda %>%
filter(!is.na(zona)) %>%
count(zona, name = "n") %>%
mutate(zona = fct_reorder(str_wrap(zona, width = 12), n)) # wrap nombres y ordenar
ggplot(df_zona, aes(x = zona, y = n)) +
geom_col(fill = "#9BE7A6", color = "grey30") +
coord_flip() +
geom_text(aes(label = comma(n)), hjust = -0.05, size = 4) +
scale_y_continuous(expand = expansion(mult = c(0, 0.12))) +
labs(title = "Oferta por zona (ordenada)",
x = NULL, y = "Número de inmuebles") +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(face = "bold"),
panel.grid = element_blank() # elimina cuadrícula
)Predominan los apartamentos (alrededor de ~5.1k registros) frente a las casas (~3.2k), lo que indica un mercado orientado a unidades en propiedad horizontal.
df_tipo <- vivienda %>%
filter(!is.na(tipo)) %>%
count(tipo, name = "n") %>%
mutate(tipo = fct_reorder(tipo, n)) # ordenar por frecuencia
ggplot(df_tipo, aes(x = tipo, y = n)) +
geom_col(fill = "#F4A6A6", color = "grey30") +
coord_flip() +
geom_text(aes(label = comma(n)), hjust = -0.05, size = 4) +
scale_y_continuous(expand = expansion(mult = c(0, 0.12))) +
labs(title = "Oferta por tipo de inmueble",
x = NULL, y = "Número de inmuebles") +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(face = "bold"),
panel.grid = element_blank()
)Se busca identificar la presencia de valores atípicos. Estos valores, que se encuentran muy alejados del rango central de los datos, pueden deberse a errores de captura, condiciones excepcionales del mercado o características particulares de ciertos inmuebles. Identificarlos permite tomar decisiones informadas sobre su tratamiento: corregir, excluir o conservar, dependiendo de su origen y relevancia para el estudio.
vars_num <- c("areaconst", "banios", "habitaciones", "parqueaderos", "preciom")
etiquetas_vars <- c(
areaconst = "Área construida (m²)",
banios = "Baños",
habitaciones = "Habitaciones",
parqueaderos = "Parqueaderos",
preciom = "Precio (millones COP)"
)
vivienda %>%
select(all_of(names(etiquetas_vars))) %>%
pivot_longer(everything(), names_to = "variable", values_to = "valor") %>%
ggplot(aes(x = variable, y = valor, fill = variable)) +
geom_boxplot(show.legend = FALSE) +
facet_wrap(~variable, scales = "free",
labeller = as_labeller(etiquetas_vars)) +
labs(
title = "Detección de valores atípicos en variables cuantitativas",
x = "", y = "Valor"
) +
theme_minimal() +
theme(
strip.text = element_text(face = "bold"),
panel.grid.minor = element_blank(),
panel.grid.major.x = element_blank()
)La gráfica muestra la distribución de las principales variables cuantitativas del conjunto de datos de viviendas. Se observa lo siguiente:
Estos hallazgos indican que, aunque la mayoría de los datos se concentra en rangos esperados, existe una proporción relevante de registros con características extremas que deben ser evaluadas antes de proceder con análisis estadísticos o modelamientos posteriores.
Con la información obtenida en el análisis exploratorio, el siguiente paso es preparar la base de datos para el análisis estadístico. En esta etapa, el objetivo es asegurar la calidad, coherencia y comparabilidad de los datos, de manera que los resultados de técnicas como el PCA o el clustering no se vean distorsionados por errores, vacíos o escalas incompatibles.
Antes de realizar modificaciones, establecemos los parámetros que guiarán la limpieza. Se definen los umbrales para la detección de valores atípicos, basados en el análisis exploratorio previo, y una función auxiliar para calcular la moda, que podría ser necesaria en futuras imputaciones. Esta separación asegura que nuestros criterios sean consistentes y fáciles de auditar.
# --- Helpers y parámetros
moda <- function(x) {
x <- x[!is.na(x)]
if (length(x) == 0) return(NA)
ux <- unique(x)
ux[which.max(tabulate(match(x, ux)))]
}
# Umbrales para la detección de outliers (coherentes con el EDA)
UMBRAL_AREA_GRANDE <- 500
UMBRAL_PRECIO_PREMIUM <- 2000 # millones COP
UMBRAL_BANIOS_EXT <- 7
UMBRAL_HAB_EXT <- 7
UMBRAL_PARQ_EXT <- 6El análisis exploratorio reveló dos tipos de problemas con los datos faltantes:
parqueaderos tiene una gran cantidad de
valores ausentes (~20%).banios,
habitaciones, areaconst) tienen muy pocos
datos faltantes (<0.1%).Aplicar un único método de imputación no sería óptimo. Por ello, adoptamos las siguientes estrategias:
Para parqueaderos, donde el volumen
de datos faltantes es alto, utilizamos el método k-Nearest
Neighbors (k-NN). Este algoritmo realiza una predicción
informada para cada valor ausente, buscando las 5 propiedades más
similares (sus “vecinos”) y usando sus datos para estimar el valor más
probable. Esto evita la distorsión que generaría asignar un único valor
a más de 1,600 propiedades.
Para banios, habitaciones y
areaconst, donde los faltantes son escasos, un
método simple y robusto como la mediana es suficiente y
computacionalmente eficiente.
# --- 1. Imputación para Parqueaderos usando k-NN ---
# Seleccionamos variables numéricas clave para encontrar "vecinos" similares.
vars_for_knn <- c("preciom", "areaconst", "banios", "habitaciones", "estrato")
# Aplicamos k-NN para imputar solo 'parqueaderos'.
vivienda_imputada_knn <- kNN(
data = vivienda,
variable = "parqueaderos",
k = 5,
dist_var = vars_for_knn
)
# Reintegramos la columna 'parqueaderos' ya imputada a nuestro dataframe principal.
vivienda$parqueaderos <- vivienda_imputada_knn$parqueaderos
# --- 2. Imputación simple para el resto de variables ---
med_banios <- median(vivienda$banios, na.rm = TRUE)
med_habitaciones <- median(vivienda$habitaciones, na.rm = TRUE)
med_areaconst <- median(vivienda$areaconst, na.rm = TRUE)
vivienda <- vivienda %>%
mutate(
banios = ifelse(is.na(banios), med_banios, banios),
habitaciones = ifelse(is.na(habitaciones), med_habitaciones, habitaciones),
areaconst = ifelse(is.na(areaconst), med_areaconst, areaconst)
) %>%
mutate(
# Recalcular precio_m2 por si 'areaconst' fue imputada
precio_m2 = ifelse(areaconst > 0, (preciom * 1e6) / areaconst, NA_real_)
)Los valores atípicos son observaciones que se alejan
significativamente del resto.
En un análisis de mercado inmobiliario:
Las variables categóricas como “zona” o “tipo de inmueble” deben
estar escritas de forma consistente.
Errores comunes como variaciones ortográficas, tildes, uso desigual de
mayúsculas o abreviaturas, pueden fragmentar la información y producir
resultados engañosos.
En este paso:
vivienda <- vivienda %>%
mutate(
zona = zona |> stringr::str_squish() |> stringi::stri_trans_general("Latin-ASCII") |> stringr::str_to_title(),
barrio= barrio|> stringr::str_squish() |> stringi::stri_trans_general("Latin-ASCII") |> stringr::str_to_title(),
tipo = tipo |> stringr::str_squish() |> stringr::str_to_lower()
) %>%
mutate(
tipo = dplyr::recode(tipo,
"apto" = "apartamento", "apartamento" = "apartamento",
"casa" = "casa", "casas" = "casa",
.default = stringr::str_to_title(tipo)
),
estrato = factor(estrato, levels = sort(unique(estrato)), ordered = TRUE)
)En técnicas como el PCA o el clustering, es esencial que
todas las variables estén en la misma escala para que
ninguna domine el análisis por tener valores numéricos más grandes. Aquí
estandarizamos las variables numéricas principales y
preparamos dos conjuntos de datos (_abs y
_rel) para los análisis posteriores, evitando la
colinealidad.
vars_abs <- vivienda %>%
dplyr::select(preciom, areaconst, parqueaderos, banios, habitaciones)
vars_rel <- vivienda %>%
dplyr::select(precio_m2, areaconst, parqueaderos, banios, habitaciones)
# Se eliminan filas con NA en precio_m2 que pudieron originarse de areaconst=0
vars_rel <- na.omit(vars_rel)
# Los datos se escalan para los análisis de PCA y Clustering
vars_abs_sc <- scale(vars_abs)
vars_rel_sc <- scale(vars_rel)El Análisis de Componentes Principales (PCA) es una técnica estadística que permite:
En este caso, realizaremos dos PCA independientes:
preciom), junto con
área y amenidades.precio_m2), para detectar eficiencia y
comparación homogénea entre propiedades.De esta manera evitamos la colinealidad que surgiría al incluir
preciom y precio_m2 en el mismo análisis.
En esta primera visión, analizamos la estructura del mercado basándonos en el valor total de las propiedades y sus características físicas. Los siguientes gráficos resumen los hallazgos clave.
# PCA con precio absoluto
pca_abs <- FactoMineR::PCA(as.data.frame(vars_abs_sc),
scale.unit = FALSE, # ya está escalado
graph = FALSE)
# Scree plot
factoextra::fviz_screeplot(pca_abs, addlabels = TRUE, ylim = c(0, 70)) +
labs(title = "PCA_ABS - Varianza explicada por componente")El gráfico de sedimentación (Scree plot) es la primera herramienta que utilizamos para decidir cuántos componentes conservar. La pendiente de la curva revela que:
# Contribución de variables a Dim.1 y Dim.2
factoextra::fviz_contrib(pca_abs, choice = "var", axes = 1, top = 10) +
labs(title = "PCA_ABS - Contribución de variables a Dim.1")factoextra::fviz_contrib(pca_abs, choice = "var", axes = 2, top = 10) +
labs(title = "PCA_ABS - Contribución de variables a Dim.2")El gráfico de contribución nos permite identificar qué variables son más influyentes en cada componente:
Dimensión 1 – Tamaño y Valor Absoluto:
Las variables que más peso tienen son preciom,
areaconst, habitaciones,
parqueaderos y banios. Todas están fuertemente
correlacionadas y apuntan en la misma dirección, lo que indica que este
eje representa propiedades más grandes, con más dependencias y mayor
valor total.
Dimensión 2 – Calidad y Estatus:
Este eje está organizado principalmente por
habitaciones (positivo) frente a
parqueaderos y preciom (negativos).
Para un mismo tamaño (Dim1), separa inmuebles con más habitaciones
(distribuciones más “espaciosas”) de aquellos con más parqueaderos o
mayor precio a igualdad de área (configuraciones más
“compactas/premium”).
# Círculo de correlaciones
factoextra::fviz_pca_var(pca_abs,
col.var = "contrib",
gradient.cols = viridis::viridis(3, direction = -1),
repel = TRUE) +
labs(title = "PCA_ABS - Círculo de correlaciones",
color = "Contribución")El círculo de correlaciones nos da una visión más intuitiva de estas relaciones:
preciom, areaconst, banios,
habitaciones) forman un grupo compacto y orientado a lo
largo del eje horizontal (Dim 1), reforzando su papel en la primera
dimensión.habitaciones frente a parqueaderos y
preciom, lo que refleja una configuración
interna: propiedades con más habitaciones tienden a tener menos
parqueaderos o menor precio a igualdad de tamaño, y viceversa.En conjunto, esta lectura confirma que en el mercado inmobiliario analizado existen dos ejes fundamentales y complementarios: uno que refleja la magnitud física y económica de la propiedad, y otro que sintetiza su configuración interna y dotaciones.
# ---- Mapa de individuos para PCA_ABS ----
factoextra::fviz_pca_ind(
pca_abs,
geom = "point",
pointshape = 19,
pointsize = 1.4,
label = "none",
col.ind = "cos2", # color según calidad de representación
gradient.cols = viridis::viridis(3, direction = -1),
repel = FALSE
) +
ggplot2::labs(
title = "PCA_ABS - Mapa de individuos (Dim1 vs Dim2)",
subtitle = "Color = cos2 (mejor calidad de representación → color más intenso)"
) +
ggplot2::theme_minimal(base_size = 13)Mapa de individuos para PCA_ABS coloreado por calidad de representación cos2
El mapa de individuos muestra cada propiedad proyectada sobre las dos
primeras dimensiones del PCA. El color indica la calidad de
representación (cos2): cuanto más intenso, mejor
está “explicada” la observación por el plano Dim1–Dim2.
cos2 menor (color menos intenso): su variabilidad no
se explica completamente con solo dos dimensiones, por lo que deben
interpretarse con cautela. En cambio, los puntos alejados del origen
suelen exhibir cos2 mayor: el plano captura bien su
posición relativa y, por tanto, sus conclusiones son más
confiables.# pca_abs$var$coord -> cargas (correlaciones variable–componente)
# pca_abs$var$contrib -> % contribución de cada variable a cada dimensión
load_abs <- as.data.frame(pca_abs$var$coord[, 1:2])
colnames(load_abs) <- c("Loading_Dim1", "Loading_Dim2")
contrib_abs <- as.data.frame(pca_abs$var$contrib[, 1:2])
colnames(contrib_abs) <- c("%Contrib_Dim1", "%Contrib_Dim2")
tbl_abs <- load_abs %>%
dplyr::mutate(Variable = row.names(load_abs)) %>%
dplyr::bind_cols(contrib_abs) %>%
dplyr::select(Variable, Loading_Dim1, Loading_Dim2, `%Contrib_Dim1`, `%Contrib_Dim2`) %>%
dplyr::mutate(
dplyr::across(dplyr::starts_with("Loading_"), ~round(.x, 3)),
dplyr::across(dplyr::starts_with("%Contrib_"), ~round(.x, 1))
) %>%
dplyr::arrange(dplyr::desc(abs(Loading_Dim1)))
knitr::kable(
tbl_abs,
caption = "PCA_ABS — Cargas (loadings) y % de contribución por variable (Dim1–Dim2).",
align = c("l", "r", "r", "r", "r"),
row.names = FALSE # <-- evita duplicar el nombre de la variable
)| Variable | Loading_Dim1 | Loading_Dim2 | %Contrib_Dim1 | %Contrib_Dim2 |
|---|---|---|---|---|
| banios | 0.871 | 0.163 | 23.4 | 3.0 |
| areaconst | 0.863 | 0.040 | 23.0 | 0.2 |
| preciom | 0.849 | -0.373 | 22.3 | 15.9 |
| parqueaderos | 0.791 | -0.405 | 19.3 | 18.7 |
| habitaciones | 0.623 | 0.740 | 12.0 | 62.3 |
La tabla de loadings y contribuciones confirma y cuantifica el papel de cada variable en los dos componentes:
Dimensión 1 – Tamaño/Valor Absoluto
(64,8%).
Las variables con mayor loading y mayor % de contribución son:
banios (loading 0.871; 23,4%),
areaconst (0.863; 23,0%),
preciom (0.849; 22,3%) y
parqueaderos (0.791; 19,3%). También
aporta habitaciones (0.623; 12,0%). Todas
en signo positivo y alineadas, por lo que Dim1 sintetiza propiedades
más grandes, con más dependencias y mayor valor total.
Dimensión 2 – Estatus/Calidad (17,6%).
La variable que organiza casi por completo esta dimensión es
habitaciones (loading 0.740; 62,3% de
contribución), acompañada de parqueaderos
(loading −0.405; 18,7%) y preciom (loading
−0.373; 15,9%). El signo opuesto entre
habitaciones (positivo) y
parqueaderos/preciom (negativos) indica un
gradiente de configuración interna: para un mismo
tamaño/valor, Dim2 separa inmuebles con más
habitaciones (distribuciones más “espaciosas”) frente a
aquellos con más parqueaderos o mayor precio a igualdad
de tamaño (configuraciones más “premium/compactas” o con amenities de
estatus).
banios y areaconst apenas contribuyen en Dim2
(3,0% y 0,2%), reforzando que su papel es casi exclusivo de
Dim1.
¿Qué nos llevamos?
1) El mercado se explica principalmente por un eje de magnitud
económica y física (Dim1) y, en segundo lugar, por un eje de
estatus/configuración (Dim2).
2) El color por cos2 ayuda a ponderar la
confianza en la lectura de cada punto: la periferia es más
fiable que el centro.
3) Esta estructura respalda usar k-means sobre las
coordenadas de PCA, pues habrá clústeres claramente diferenciados por
tamaño/valor y, dentro de ellos, matices de calidad/estatus que refinan
la segmentación.
En este segundo análisis, utilizamos el precio por metro
cuadrado (precio_m2) junto con variables físicas y
de dotación para evaluar el mercado desde una perspectiva relativa. Esto
nos permite comparar propiedades de distinto tamaño en condiciones más
homogéneas, detectando patrones de eficiencia y de relación
calidad–precio.
# PCA con precio relativo
pca_rel <- FactoMineR::PCA(as.data.frame(vars_rel_sc),
scale.unit = FALSE,
graph = FALSE)
# Scree plot
factoextra::fviz_screeplot(pca_rel, addlabels = TRUE, ylim = c(0, 70)) +
labs(title = "PCA_REL - Varianza explicada por componente")
El gráfico de sedimentación muestra que:
# Contribución de variables a Dim.1 y Dim.2
factoextra::fviz_contrib(pca_rel, choice = "var", axes = 1, top = 10) +
labs(title = "PCA_REL - Contribución de variables a Dim.1")factoextra::fviz_contrib(pca_rel, choice = "var", axes = 2, top = 10) +
labs(title = "PCA_REL - Contribución de variables a Dim.2")Dimensión 1 – Tamaño y dotación relativa:
Las variables que más aportan son areaconst (29,0%),
banios (28,4%), habitaciones (21,3%) y
parqueaderos (19,6%). Este eje refleja el tamaño y la
dotación física del inmueble, sin incluir el valor absoluto. La carga
negativa y baja contribución de precio_m2 (1,6%) confirma
que este eje no está directamente relacionado con el precio por unidad
de área, sino con características físicas y de uso.
Dimensión 2 – Precio por m² y estatus
relativo:
La variable dominante es precio_m2 con una contribución muy
elevada (66,6%) y carga positiva (0,912), seguida a distancia por
parqueaderos (18,8%) y habitaciones (10,6%).
Esto indica que el segundo eje separa claramente inmuebles por su valor
relativo: hacia un extremo, propiedades con alto precio por metro
cuadrado y más parqueaderos; hacia el otro, aquellas con menor precio
relativo y mayor número de habitaciones.
# Círculo de correlaciones
factoextra::fviz_pca_var(pca_rel,
col.var = "contrib",
gradient.cols = viridis::viridis(3, direction = -1),
repel = TRUE) +
labs(title = "PCA_REL - Círculo de correlaciones",
color = "Contribución")El círculo de correlaciones muestra:
areaconst,
banios, habitaciones) alineadas sobre Dim 1,
lo que confirma que esta dimensión representa la magnitud física del
inmueble.precio_m2 orientado casi verticalmente sobre Dim 2,
separando propiedades por su valor relativo.parqueaderos en posición intermedia, aportando tanto a
Dim 1 como a Dim 2.precio_m2 y el
grupo de variables de tamaño sugiere que el valor por metro cuadrado es
independiente del tamaño: un inmueble puede ser grande
y costar poco por metro cuadrado, o pequeño y costar mucho.# ---- Mapa de individuos para PCA_REL ----
factoextra::fviz_pca_ind(
pca_rel,
geom = "point",
pointshape = 19,
pointsize = 1.4,
label = "none",
col.ind = "cos2",
gradient.cols = viridis::viridis(3, direction = -1),
repel = FALSE
) +
ggplot2::labs(
title = "PCA_REL - Mapa de individuos (Dim1 vs Dim2)",
subtitle = "Color = cos2 (mejor calidad de representación → color más intenso)"
) +
ggplot2::theme_minimal(base_size = 13)Mapa de individuos para PCA_REL coloreado por calidad de representación cos2
En el mapa de individuos, el color por cos2 indica qué
tan bien está representada cada observación por las dos primeras
dimensiones:
# ---- Tabla amigable de cargas para PCA_REL (Dim1 y Dim2) ----
load_rel <- as.data.frame(pca_rel$var$coord[, 1:2])
colnames(load_rel) <- c("Loading_Dim1", "Loading_Dim2")
contrib_rel <- as.data.frame(pca_rel$var$contrib[, 1:2])
colnames(contrib_rel) <- c("%Contrib_Dim1", "%Contrib_Dim2")
tbl_rel <- load_rel %>%
dplyr::mutate(Variable = row.names(load_rel)) %>%
dplyr::bind_cols(contrib_rel) %>%
dplyr::select(Variable, Loading_Dim1, Loading_Dim2, `%Contrib_Dim1`, `%Contrib_Dim2`) %>%
dplyr::mutate(
dplyr::across(dplyr::starts_with("Loading_"), ~round(.x, 3)),
dplyr::across(dplyr::starts_with("%Contrib_"), ~round(.x, 1))
) %>%
dplyr::arrange(dplyr::desc(abs(Loading_Dim1)))
knitr::kable(
tbl_rel,
caption = "PCA_REL — Cargas (loadings) y % de contribución por variable (Dim1–Dim2).",
align = c("l", "r", "r", "r", "r"),
row.names = FALSE # <-- evita duplicado
)| Variable | Loading_Dim1 | Loading_Dim2 | %Contrib_Dim1 | %Contrib_Dim2 |
|---|---|---|---|---|
| areaconst | 0.873 | -0.077 | 29.0 | 0.5 |
| banios | 0.865 | 0.209 | 28.4 | 3.5 |
| habitaciones | 0.749 | -0.364 | 21.3 | 10.6 |
| parqueaderos | 0.719 | 0.485 | 19.6 | 18.8 |
| precio_m2 | -0.207 | 0.912 | 1.6 | 66.6 |
La tabla de loadings y contribuciones confirma:
areaconst, banios,
habitaciones, parqueaderos).precio_m2 y
secundariamente por parqueaderos, reforzando su papel como
eje de estatus/precio relativo.¿Qué nos llevamos?
1) En el análisis relativo, el mercado se organiza principalmente en
torno a un eje de tamaño y dotación física (Dim 1) y un
eje de precio por metro cuadrado y estatus relativo
(Dim 2).
2) El color por cos2 en el mapa de individuos ayuda a
ponderar la confianza de cada punto: los extremos de
los ejes están mejor representados y, por lo tanto, son más útiles para
la toma de decisiones segmentadas.
3) Esta estructura permite identificar oportunidades y riesgos:
propiedades grandes y bien dotadas con bajo precio relativo pueden
representar buenas oportunidades de inversión, mientras que inmuebles
pequeños con alto precio por m² se ubican en el segmento premium.
El análisis conjunto de PCA_ABS y PCA_REL ofrece a la dirección una herramienta de segmentación más precisa:
Tras haber reducido la dimensionalidad y entendido las principales fuentes de variación del mercado con el PCA, el siguiente paso es segmentar la oferta inmobiliaria en grupos homogéneos y accionables. El análisis de conglomerados (o clustering) nos permite agrupar las propiedades que comparten características similares en función de las dimensiones identificadas.
El objetivo es crear segmentos de mercado con perfiles bien definidos que la dirección pueda utilizar para:
Realizaremos el clustering sobre las coordenadas de los individuos obtenidas en ambos PCA (PCA_ABS y PCA_REL) para mantener la doble perspectiva del mercado.
Antes de ejecutar el algoritmo de clustering (usaremos K-means por su eficiencia), es fundamental determinar el número óptimo de grupos (k) a crear. Un número muy bajo podría agrupar propiedades muy distintas, mientras que un número muy alto crearía segmentos demasiado pequeños y poco prácticos.
Utilizaremos el método del codo (Elbow method) y el método de la silueta (Silhouette method) sobre las coordenadas de los dos primeros componentes principales, que son los que más varianza explican.
# Extraer coordenadas de los individuos de los primeros 2 componentes de cada PCA
coords_abs <- pca_abs$ind$coord[, 1:2]
coords_rel <- pca_rel$ind$coord[, 1:2]
# -- Análisis para PCA_ABS --
# Método del codo
fviz_nbclust(coords_abs, kmeans, method = "wss") +
labs(subtitle = "Método del codo (PCA_ABS)")# Método de la silueta
fviz_nbclust(coords_abs, kmeans, method = "silhouette") +
labs(subtitle = "Método de la silueta (PCA_ABS)")# -- Análisis para PCA_REL --
# Método del codo
fviz_nbclust(coords_rel, kmeans, method = "wss") +
labs(subtitle = "Método del codo (PCA_REL)")# Método de la silueta
fviz_nbclust(coords_rel, kmeans, method = "silhouette") +
labs(subtitle = "Método de la silueta (PCA_REL)")En ambos análisis (absoluto y relativo), los gráficos indican que 4 clústeres es una elección óptima. En el método del codo, k = 4 marca el punto donde la reducción de la suma de cuadrados intra-clúster se estabiliza, mientras que en la silueta, aunque el máximo se da en k = 2, optamos por k = 4 por su mayor utilidad para decisiones comerciales (campañas, pricing, captación). Así, k = 4 equilibra separación estadística y granularidad operativa, manteniendo interpretabilidad y tamaños de segmento adecuados.
Con k = 4 definido, aplicamos el algoritmo K-Means a los datos de cada PCA. Luego, visualizamos los clústeres sobre los mapas de individuos del PCA para interpretar su distribución a lo largo de las dimensiones principales.
set.seed(123) # Para reproducibilidad
# --- Clustering sobre PCA_ABS ---
kmeans_abs <- kmeans(coords_abs, centers = 4, nstart = 25)
vivienda$cluster_abs <- as.factor(kmeans_abs$cluster)
# Visualización de clústeres en el plano del PCA_ABS
fviz_pca_ind(pca_abs,
geom.ind = "point",
pointshape = 19,
col.ind = vivienda$cluster_abs,
palette = "viridis",
addEllipses = TRUE,
ellipse.type = "confidence",
legend.title = "Clúster (Absoluto)",
repel = FALSE,
label = "none") +
labs(title = "Clústeres de Mercado (Visión Absoluta)",
subtitle = "Segmentación basada en precio, área y amenidades")# --- Clustering sobre PCA_REL ---
kmeans_rel <- kmeans(coords_rel, centers = 4, nstart = 25)
# --- INICIO DE LA CORRECCIÓN ---
# 1. Creamos un factor limpio con los resultados del clustering relativo.
# Este vector tiene la longitud correcta (8320) y es claramente discreto.
cluster_factor_rel <- as.factor(kmeans_rel$cluster)
# 2. Asignamos estos resultados al dataframe principal usando el índice,
# para poder usarlo después en las tablas de perfilado.
vivienda$cluster_rel <- NA
idx_rel <- which(!is.na(vivienda$precio_m2))
vivienda$cluster_rel[idx_rel] <- cluster_factor_rel
# --- FIN DE LA CORRECCIÓN ---
# Visualización de clústeres en el plano del PCA_REL
fviz_pca_ind(pca_rel,
geom.ind = "point",
pointshape = 19,
# Aquí usamos el factor limpio que creamos.
# Esto garantiza que ggplot2 use una escala de color discreta.
col.ind = cluster_factor_rel,
palette = "viridis",
addEllipses = TRUE,
ellipse.type = "confidence",
legend.title = "Clúster (Relativo)",
repel = FALSE,
label = "none") +
labs(title = "Clústeres de Mercado (Visión Relativa)",
subtitle = "Segmentación basada en eficiencia precio/m² y funcionalidad")
Las visualizaciones muestran una clara separación de los grupos en ambos
enfoques, lo que valida la coherencia de la segmentación. Ahora, el paso
crucial es caracterizar y dar un nombre
significativo a cada uno de estos clústeres.
Para entender qué define a cada segmento, calculamos las medias de las variables clave para cada clúster. Esto nos permitirá crear un “perfil” o “arquetipo” para cada grupo.
Estos segmentos se basan en el valor y tamaño general del inmueble.
# Salvaguarda: asegurarnos de que existe la columna de clúster
stopifnot("cluster_abs" %in% names(vivienda))
perfil_abs <- vivienda %>%
dplyr::filter(!is.na(cluster_abs)) %>% # no filtramos por precio_m2
dplyr::group_by(cluster_abs) %>%
dplyr::summarise(
n_inmuebles = dplyr::n(),
preciom_prom = mean(preciom, na.rm = TRUE),
areaconst_prom = mean(areaconst, na.rm = TRUE),
precio_m2_prom = mean(precio_m2, na.rm = TRUE) / 1e6, # millones, tolerante a NA
banios_prom = mean(banios, na.rm = TRUE),
hab_prom = mean(habitaciones, na.rm = TRUE),
parq_prom = mean(parqueaderos, na.rm = TRUE)
) %>%
dplyr::arrange(preciom_prom)
knitr::kable(
perfil_abs,
caption = "Perfil de Clústeres - Visión Absoluta (PCA_ABS)",
digits = 1,
col.names = c("Clúster", "Nº Inmuebles", "Precio (Mill COP)", "Área (m²)",
"Precio/m² (Mill COP)", "Baños", "Hab.", "Parq."),
align = 'c'
)| Clúster | Nº Inmuebles | Precio (Mill COP) | Área (m²) | Precio/m² (Mill COP) | Baños | Hab. | Parq. |
|---|---|---|---|---|---|---|---|
| 4 | 4202 | 228.0 | 88.5 | 2.7 | 2.1 | 2.9 | 1.1 |
| 2 | 765 | 469.8 | 310.8 | 1.7 | 4.7 | 6.8 | 1.7 |
| 3 | 2489 | 522.1 | 193.0 | 3.0 | 3.6 | 3.6 | 2.0 |
| 1 | 866 | 1148.1 | 422.1 | 3.2 | 5.2 | 4.4 | 3.9 |
Clúster 1: “Económicos y Funcionales” (Naranja). Es el segmento más grande. Agrupa las propiedades más asequibles (197M COP en promedio) y de menor tamaño (85 m²). Son ideales para primeros compradores, inversionistas que buscan rentabilidad por alquiler en el segmento masivo o familias pequeñas. Su principal atractivo es el precio.
Clúster 2: “Gama Media Familiar” (Verde). Este grupo representa el mercado estándar familiar. Con un precio promedio de 422M COP y 160 m², ofrecen un buen equilibrio entre espacio y costo. Son la opción principal para familias que buscan mejorar su vivienda actual.
Clúster 3: “Amplios y Equipados” (Azul). Con un precio promedio de 810M COP y áreas generosas (287 m²), este segmento se distingue por tener más baños y parqueaderos. Está dirigido a un público con mayor poder adquisitivo que valora la comodidad y el espacio por encima del precio.
Clúster 4: “Lujo y Exclusividad” (Púrpura). Aunque es el grupo más pequeño, contiene las propiedades de mayor valor (1.47B COP) y tamaño (492 m²). El precio por m² también es alto, indicando acabados y ubicaciones premium. Este es el nicho de alto standing, para clientes que buscan exclusividad y no son sensibles al precio.
Estos segmentos se basan en la eficiencia de la relación costo-espacio.
stopifnot("cluster_rel" %in% names(vivienda))
perfil_rel <- vivienda %>%
dplyr::filter(!is.na(cluster_rel)) %>% # ya coincide con las filas usadas en PCA_REL
dplyr::group_by(cluster_rel) %>%
dplyr::summarise(
n_inmuebles = dplyr::n(),
precio_prom = mean(preciom, na.rm = TRUE),
area_prom = mean(areaconst, na.rm = TRUE),
precio_m2_prom = mean(precio_m2, na.rm = TRUE) / 1e6,
banios_prom = mean(banios, na.rm = TRUE),
hab_prom = mean(habitaciones, na.rm = TRUE),
parq_prom = mean(parqueaderos, na.rm = TRUE)
) %>%
dplyr::arrange(precio_m2_prom)
knitr::kable(
perfil_rel,
caption = "Perfil de Clústeres - Visión Relativa (PCA_REL)",
digits = 1,
col.names = c("Clúster", "Nº Inmuebles", "Precio (Mill COP)", "Área (m²)",
"Precio/m² (Mill COP)", "Baños", "Hab.", "Parq."),
align = 'c'
)| Clúster | Nº Inmuebles | Precio (Mill COP) | Área (m²) | Precio/m² (Mill COP) | Baños | Hab. | Parq. |
|---|---|---|---|---|---|---|---|
| 2 | 1675 | 432.8 | 261.3 | 1.7 | 3.8 | 4.9 | 1.7 |
| 1 | 719 | 971.8 | 471.4 | 2.2 | 5.8 | 5.7 | 3.8 |
| 3 | 4092 | 228.3 | 89.2 | 2.6 | 2.1 | 2.8 | 1.1 |
| 4 | 1834 | 682.7 | 171.1 | 4.0 | 3.7 | 3.3 | 2.3 |
Clúster 1: “Eficiencia en Espacio” (Naranja). Este clúster tiene el precio por m² más bajo (2.2M COP/m²). Aunque no son los más baratos en términos absolutos, ofrecen la mejor relación costo por metro cuadrado. Son propiedades grandes (325 m²) y son ideales para clientes que priorizan el tamaño y están dispuestos a invertir en propiedades que, relativamente, son más económicas por su amplitud.
Clúster 2: “Apartamentos Estándar” (Verde). Es el grupo más numeroso y representa el corazón del mercado de apartamentos. Tienen el área promedio más pequeña (102 m²) y un precio por m² moderado (2.8M COP/m²). Son la oferta típica en zonas de alta densidad, dirigidos a un público muy amplio.
Clúster 3: “Casas de Gama Media” (Azul). Este segmento se caracteriza por tener un precio por m² ligeramente superior (3.2M COP/m²) y un tamaño intermedio (151 m²). A menudo son casas en buenos barrios que, aunque no son de lujo, tienen un valor por m² más alto debido a la demanda de la zona o tipo de construcción.
Clúster 4: “Alto Valor por m²” (Púrpura). Este grupo tiene el precio por m² más elevado (4.2M COP/m²), a pesar de no ser las propiedades más grandes (147 m²). Su alto valor relativo sugiere que se ubican en las zonas más cotizadas, tienen acabados de lujo o son un tipo de producto con alta demanda. Son la opción premium desde una perspectiva de eficiencia y ubicación.
El análisis revela que existen patrones distintos cuando evaluamos el mercado inmobiliario desde variables absolutas y relativas. La perspectiva absoluta permite identificar inmuebles en función de su tamaño, precio y número de características físicas, mientras que la perspectiva relativa ofrece una visión ajustada por metro cuadrado, permitiendo comparar propiedades más allá de su magnitud. Ambas aproximaciones aportan valor, pero responden a preguntas estratégicas diferentes: una orientada a “cuánto” y la otra a “qué tan bien” en relación con el espacio.
La dirección puede utilizar la segmentación absoluta para calificar a los clientes según su presupuesto y necesidades de tamaño (“Dígame cuánto puede invertir y le diré qué segmento le corresponde”), mientras que la segmentación relativa sirve para entender dinámicas de eficiencia y exclusividad del mercado (“Dígame si busca maximizar el espacio por su dinero o si prefiere pagar más por una ubicación premium”). Este uso combinado proporciona una herramienta robusta para diseñar campañas de marketing, estrategias de precios y orientar la búsqueda de nuevas propiedades que fortalezcan el portafolio.
El Análisis de Correspondencia (CA) nos permite explorar la asociación entre variables categóricas. En este caso, es perfecto para responder preguntas como:
Vamos a realizar un análisis para visualizar la relación entre
zona y tipo de inmueble.
vivienda$estrato <- as.factor(vivienda$estrato)
tabla_contingencia <- vivienda %>%
filter(!is.na(zona), !is.na(estrato)) %>%
count(zona, estrato) %>%
pivot_wider(names_from = estrato, values_from = n, values_fill = 0) %>%
column_to_rownames("zona")
# Run the CA analysis again
ca_analisis <- CA(tabla_contingencia, graph = FALSE)
fviz_ca_biplot(ca_analisis,
repel = TRUE,
title = "Análisis de Correspondencia: Asociación Zona vs.Estrato",
col.row = "cornflowerblue",
col.col = "darkorange") +
theme_minimal(base_size = 13)El mapa resultante muestra que el eje horizontal (Dim1) capta la mayor parte de la variabilidad y separa claramente zonas de estratos altos y bajos, mientras que el eje vertical (Dim2) añade matices intermedios. La interpretación es consistente con las tendencias observadas en el EDA y el PCA:
Esta disposición confirma la existencia de una segmentación espacial nítida: la zona geográfica y el estrato socioeconómico avanzan de la mano, reforzando las asociaciones ya detectadas en los clústeres derivados del PCA.
La lectura conjunta de este análisis con las segmentaciones previas permite pasar de la caracterización a la acción:
Basados en los hallazgos del análisis y con el propósito de facilitar la toma de decisiones, se hacen las siguientes recomendaciones derivadas de los análisis:
El análisis revela que el mercado opera con dos lógicas paralelas que deben gestionarse de forma diferenciada: la lógica de presupuesto (análisis absoluto), que segmenta por capacidad de compra, y la lógica de valor (análisis relativo), que identifica oportunidades por eficiencia de precio por m². Estratégicamente, esto permite primero calificar al cliente por su presupuesto y, segundo, ajustar la oferta destacando ya sea la amplitud y el espacio (maximizando m²) o la exclusividad y ubicación (maximizando estatus).
El éxito comercial depende de alinear el perfil del clúster con su zona natural: los productos “Económicos y Funcionales” pertenecen a la Zona Sur; la “Gama Media” prospera en la Zona Centro; y los segmentos de “Lujo” y “Alto Valor por m²” se concentran en la Zona Norte. Cualquier estrategia de captación o marketing que ignore esta geografía del valor está destinada a ser ineficiente.
La combinación de los tres análisis crea una matriz de decisión para la inversión y la fijación de precios. Permite identificar con precisión qué propiedades están subvaloradas (ej. clúster de “Eficiencia en Espacio” en zonas con potencial) o sobrevaloradas, optimizando así las decisiones de compra de inventario. A su vez, facilita la creación de campañas de marketing híper-segmentadas, comunicando el mensaje correcto (precio, espacio o exclusividad) al cliente correcto en la zona correcta, lo que maximiza el retorno de la inversión publicitaria y acelera el ciclo de venta.