El propósito de esta actividad es analizar de manera integral la oferta de vivienda urbana para entender mejor cómo se está comportando el mercado inmobiliario y así apoyar la toma de decisiones estratégicas de la empresa. A partir de una base de datos amplia de propiedades en oferta, se busca descubrir patrones, relaciones y segmentaciones clave que ayuden a orientar los procesos de valoración, compra y venta, así como a comprender las dinámicas del mercado según la zona y el estrato.
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.
La base de datos reúne información de viviendas residenciales que actualmente están en oferta dentro del mercado urbano. Incluye tanto variables numéricas como categóricas relacionadas con el precio, las características físicas del inmueble, su ubicación y la clasificación socioeconómica, lo que permite tener una visión más completa del comportamiento de las propiedades.
Los datos provienen de un proceso de levantamiento de información del mercado de vivienda en oferta y han sido consolidados en una estructura depurada y organizada, lista para su análisis estadístico multivariado.
# Instalar paquete si no está (solo la primera vez)
# install.packages("devtools")
# devtools::install_github("centro-magis/paqueteMODELOS")
library(paqueteMODELOS)
# Cargar dataset (ajusta el nombre si es diferente)
data("vivienda")
# Crear dataframe de trabajo
df_vivienda <- vivienda
df_vivienda <- df_vivienda |>
tibble::as_tibble()
# Vista rápida
mi_tabla(
head(df_vivienda, 5),
id="tabla_resumen",
titulo="Tabla de ofertas inmobiliarias (registros iniciales)")
| 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.512 | 3.434 |
| 1169 | Zona Oriente | NA | 3 | 320 | 120 | 1 | 2 | 3 | Casa | 20 de julio | -76.512 | 3.434 |
| 1350 | Zona Oriente | NA | 3 | 350 | 220 | 2 | 2 | 4 | Casa | 20 de julio | -76.515 | 3.436 |
| 5992 | Zona Sur | 02 | 4 | 400 | 280 | 3 | 5 | 3 | Casa | 3 de julio | -76.540 | 3.435 |
| 1212 | Zona Norte | 01 | 5 | 260 | 90 | 1 | 2 | 3 | Apartamento | acopi | -76.513 | 3.459 |
A nivel general, se consideran variables como:
Ubicación: zona, barrio, latitud, longitud.
Características del inmueble: área construida, número de habitaciones, baños, parqueaderos, piso.
Contexto socioeconómico: estrato.
Variable de valor: precio (valor comercial u ofertado).
Tipo de inmueble: casa o apartamento.
Antes de aplicar modelos, se revisará:
# Total de valores faltantes en el dataset
total_na <- sum(is.na(df_vivienda))
Diagnóstico del total de datos faltantes: Se identifica 4275 datos faltantes en la tabla de ofertas inmobiliarias.
Diagnóstico de los datos faltantes por variable: A continuación se presentan los datos faltantes para cada variable.
# Conteo de NA por variable
na_count <- colSums(is.na(df_vivienda))
# Construir dataframe resumen
df_na_resumen <- data.frame(
variable = names(na_count),
cantidad_faltantes = as.integer(na_count),
porcentaje_faltantes = round(100 * na_count / nrow(df_vivienda), 2)
) |>
dplyr::arrange(desc(porcentaje_faltantes))
df_na_resumen <- df_na_resumen |>
tibble::as_tibble()
mi_tabla(
df_na_resumen,
id="tabla_resumen",
titulo="Registros faltante por variable en tabla de ofertas inmobiliarias")
| variable | cantidad_faltantes | porcentaje_faltantes |
|---|---|---|
| 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 |
Se aplica una estrategia de limpieza e imputación de valores faltantes ajustada al contexto de las ofertas inmobiliarias. Primero, se normalizan las variables de texto convirtiendo las cadenas vacías en valores faltantes reales y se crean indicadores (piso_missing y parqueaderos_missing) para conservar la información sobre la ausencia de datos. Luego se eliminan los registros con valores faltantes en variables críticas como precio, área y coordenadas, por ser fundamentales para el análisis. Posteriormente, la variable piso se imputa con la categoría “Sin información” y parqueaderos se corrige utilizando la mediana por grupo (tipo y estrato), respetando así la estructura del mercado; si aún quedan faltantes, se aplica una segunda imputación global. Finalmente, las demás variables numéricas se completan con la mediana y las categóricas con una etiqueta explícita, dejando el dataset limpio, consistente y listo para aplicar técnicas multivariadas como PCA, clustering y análisis de correspondencia.
df_vivienda_clean <- df_vivienda %>%
# 1) Normalizar strings vacíos ("" -> NA) en caracteres
mutate(across(where(is.character), ~ na_if(str_squish(.x), ""))) %>%
# 2) Crear banderas de missing para variables clave (útil en modelos)
mutate(
piso_missing = is.na(piso),
parqueaderos_missing = is.na(parqueaderos)
) %>%
# 3) Eliminar registros con NA en variables "críticas" (son muy pocos)
# Ajusta la lista si quieres incluir/quitar campos críticos
drop_na(preciom, areaconst, latitud, longitud) %>%
# 4) Imputación de 'piso' (categórica/ordinal con NA alto)
mutate(piso = replace_na(piso, "Sin informacion")) %>%
# 5) Imputación de 'parqueaderos' por grupo (más realista)
# Primero por tipo+estrato (si existen); si no, cae a mediana global.
group_by(tipo, estrato) %>%
mutate(
parqueaderos = if_else(
is.na(parqueaderos),
as.numeric(median(parqueaderos, na.rm = TRUE)),
as.numeric(parqueaderos)
)
) %>%
ungroup() %>%
mutate(
parqueaderos = if_else(
is.na(parqueaderos),
as.numeric(median(parqueaderos, na.rm = TRUE)),
parqueaderos
)
) %>%
# 6) Imputación de otras numéricas con mediana global (robusta a outliers)
mutate(across(
where(is.numeric),
~ if_else(is.na(.x), median(.x, na.rm = TRUE), .x)
)) %>%
# 7) Imputación de otras categóricas
mutate(across(
where(is.character),
~ replace_na(.x, "Sin informacion")
))
Como verificación final, se identifican 0 datos faltantes en la tabla de ofertas inmobiliarias.
# mi_tabla(
# head(df_vivienda_clean, 5),
# id="tabla_resumen",
# titulo="Tabla de ofertas inmobiliarias corregida (registros iniciales)")
Aplicando método IQR y MAD: El método IQR no detecta valores atípicos, mientras que el método robusto basado en MAD identifica un leve porcentaje de observaciones extremas en precio y área. Esto sugiere la presencia de colas largas en la distribución, fenómeno común en mercados inmobiliarios debido a la existencia de propiedades de alto valor y gran tamaño.
# Variables a evaluar
vars_out <- c("preciom", "areaconst")
# --- Función: límites IQR ---
limites_iqr <- function(x) {
q1 <- quantile(x, 0.25, na.rm = TRUE)
q3 <- quantile(x, 0.75, na.rm = TRUE)
iqr <- q3 - q1
c(li = q1 - 1.5 * iqr, ls = q3 + 1.5 * iqr)
}
# --- Función: z-score robusto (MAD) ---
z_robusto <- function(x) {
med <- median(x, na.rm = TRUE)
madv <- mad(x, constant = 1.4826, na.rm = TRUE) # escala ~ sd
(x - med) / madv
}
# Calcular límites por variable
lim_p <- limites_iqr(df_vivienda_clean$preciom)
lim_a <- limites_iqr(df_vivienda_clean$areaconst)
df_outliers <- df_vivienda_clean %>%
mutate(
out_preciom_iqr = preciom < lim_p["li"] | preciom > lim_p["ls"],
out_areaconst_iqr = areaconst < lim_a["li"] | areaconst > lim_a["ls"],
z_preciom_rob = z_robusto(preciom),
z_areaconst_rob = z_robusto(areaconst),
out_preciom_mad = abs(z_preciom_rob) > 3.5,
out_areaconst_mad = abs(z_areaconst_rob) > 3.5
) %>%
mutate(
outlier_any = out_preciom_iqr | out_areaconst_iqr | out_preciom_mad | out_areaconst_mad
)
# Resumen de cantidad de outliers
res_out <- tibble::tibble(
variable = c("preciom", "areaconst"),
n_out_iqr = c(sum(df_outliers$out_preciom_iqr, na.rm = TRUE),
sum(df_outliers$out_areaconst_iqr, na.rm = TRUE)),
n_out_mad = c(sum(df_outliers$out_preciom_mad, na.rm = TRUE),
sum(df_outliers$out_areaconst_mad, na.rm = TRUE)),
pct_out_iqr = round(100 * n_out_iqr / nrow(df_outliers), 2),
pct_out_mad = round(100 * n_out_mad / nrow(df_outliers), 2)
)
mi_tabla(
res_out,
id="tabla_resumen",
titulo="Resultado de los metodo IQR y MAD para valores atipicos")
| variable | n_out_iqr | n_out_mad | pct_out_iqr | pct_out_mad |
|---|---|---|---|---|
| preciom | 0 | 537 | 0 | 6.46 |
| areaconst | 0 | 517 | 0 | 6.21 |
Aplicando diagramas de cajas y dispersión: El análisis de dispersión entre el área construida y el precio muestra la presencia de valores extremos, ligados a propiedades de mayor tamaño y valor, algo esperable por la existencia de segmentos premium. Dado que no se observan outliers claramente erróneos o inconsistentes, no se recomienda eliminar registros de forma masiva, ya que esto podría distorsionar la representación real del mercado. En su lugar, se sugiere conservar las observaciones y aplicar transformaciones logarítmicas a las variables de precio y área construida para estabilizar la varianza, disminuir la influencia de los valores extremos y fortalecer la robustez de los análisis multivariados posteriores, como PCA y clustering.
p_box1 <- ggplot(df_outliers, aes(x = "", y = preciom)) +
geom_boxplot() +
labs(x = NULL, y = "preciom")
p_box2 <- ggplot(df_outliers, aes(x = "", y = areaconst)) +
geom_boxplot() +
labs(x = NULL, y = "areaconst")
mi_figura(
p_box1,
id = "figura_resumen",
titulo = "Diagrama de caja del precio de ofertas inmobiliarias"
)
mi_figura(
p_box2,
id = "figura_resumen",
titulo = "Diagrama de caja del area de ofertas inmobiliarias"
)
p_dis_prec_area <- ggplot(df_outliers, aes(x = areaconst, y = preciom)) +
geom_point(aes(shape = outlier_any), alpha = 0.5) +
labs(
x = "areaconst",
y = "preciom"
)
mi_figura(
p_dis_prec_area,
id = "figura_resumen",
titulo = "Diagrama de dispersón del área versus precio (outliers resaltados)"
)
Se procede a la estandarización de variables (z-score) y a la creación de la variables logaritmicas de precio y área.
# 1) Crear variables log de forma segura
# Usamos log1p() = log(1 + x) para evitar problemas si hay 0.
# (Si tus variables son estrictamente > 0, también puedes usar log(x).)
df_vivienda_prep <- df_vivienda_clean %>%
mutate(
log_preciom = log1p(preciom),
log_areaconst = log1p(areaconst)
)
# 2) Seleccionar columnas numéricas a estandarizar
# Recomendación: excluir identificadores y coordenadas si no van al PCA/Clustering
cols_excluir <- intersect(names(df_vivienda_prep), c("id", "latitud", "longitud"))
num_cols <- df_vivienda_prep %>%
select(where(is.numeric)) %>%
select(-all_of(cols_excluir)) %>%
names()
# 3) Estandarización (z-score): (x - media) / sd
# scale() devuelve matriz, por eso lo convertimos a numeric con as.numeric
df_vivienda_std <- df_vivienda_prep %>%
mutate(across(
all_of(num_cols),
~ as.numeric(scale(.x))
))
# 4) Verificación rápida: medias ~0 y sd ~1 (aprox)
verif_std <- data.frame(
variable = num_cols,
media = round(sapply(df_vivienda_std[num_cols], mean, na.rm = TRUE), 3),
sd = round(sapply(df_vivienda_std[num_cols], sd, na.rm = TRUE), 3)
)
verif_std <- verif_std |>
tibble::as_tibble()
mi_tabla(
verif_std,
id="tabla_resumen",
titulo="Verificación de estandarización de variables")
| variable | media | sd |
|---|---|---|
| estrato | 0 | 1 |
| preciom | 0 | 1 |
| areaconst | 0 | 1 |
| parqueaderos | 0 | 1 |
| banios | 0 | 1 |
| habitaciones | 0 | 1 |
| log_preciom | 0 | 1 |
| log_areaconst | 0 | 1 |
df_vivienda_prep <- df_vivienda_prep |>
tibble::as_tibble()
# Vista rápida
mi_tabla(
head(df_vivienda_prep, 5),
id="tabla_resumen",
titulo="Tabla de ofertas inmobiliarias con datos preparados (registros iniciales)")
| id | zona | piso | estrato | preciom | areaconst | parqueaderos | banios | habitaciones | tipo | barrio | longitud | latitud | piso_missing | parqueaderos_missing | log_preciom | log_areaconst |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1147 | Zona Oriente | Sin informacion | 3 | 250 | 70 | 1 | 3 | 6 | Casa | 20 de julio | -76.512 | 3.434 | TRUE | FALSE | 5.525 | 4.263 |
| 1169 | Zona Oriente | Sin informacion | 3 | 320 | 120 | 1 | 2 | 3 | Casa | 20 de julio | -76.512 | 3.434 | TRUE | FALSE | 5.771 | 4.796 |
| 1350 | Zona Oriente | Sin informacion | 3 | 350 | 220 | 2 | 2 | 4 | Casa | 20 de julio | -76.515 | 3.436 | TRUE | FALSE | 5.861 | 5.398 |
| 5992 | Zona Sur | 02 | 4 | 400 | 280 | 3 | 5 | 3 | Casa | 3 de julio | -76.540 | 3.435 | FALSE | FALSE | 5.994 | 5.638 |
| 1212 | Zona Norte | 01 | 5 | 260 | 90 | 1 | 2 | 3 | Apartamento | acopi | -76.513 | 3.459 | FALSE | FALSE | 5.565 | 4.511 |
df_vivienda_std <- df_vivienda_std |>
tibble::as_tibble()
# Vista rápida
mi_tabla(
head(df_vivienda_std, 5),
id="tabla_resumen",
titulo="Tabla de ofertas inmobiliarias con variables estandarizadas (registros iniciales)")
| id | zona | piso | estrato | preciom | areaconst | parqueaderos | banios | habitaciones | tipo | barrio | longitud | latitud | piso_missing | parqueaderos_missing | log_preciom | log_areaconst |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1147 | Zona Oriente | Sin informacion | -1.587 | -0.560 | -0.734 | -0.668 | -0.078 | 1.641 | Casa | 20 de julio | -76.512 | 3.434 | TRUE | FALSE | -0.477 | -1.012 |
| 1169 | Zona Oriente | Sin informacion | -1.587 | -0.347 | -0.384 | -0.668 | -0.778 | -0.415 | Casa | 20 de julio | -76.512 | 3.434 | TRUE | FALSE | -0.109 | -0.205 |
| 1350 | Zona Oriente | Sin informacion | -1.587 | -0.255 | 0.315 | 0.273 | -0.778 | 0.270 | Casa | 20 de julio | -76.515 | 3.436 | TRUE | FALSE | 0.025 | 0.706 |
| 5992 | Zona Sur | 02 | -0.616 | -0.103 | 0.735 | 1.214 | 1.322 | -0.415 | Casa | 3 de julio | -76.540 | 3.435 | FALSE | FALSE | 0.225 | 1.069 |
| 1212 | Zona Norte | 01 | 0.356 | -0.529 | -0.594 | -0.668 | -0.778 | -0.415 | Apartamento | acopi | -76.513 | 3.459 | FALSE | FALSE | -0.418 | -0.636 |
La verificación posterior a la estandarización muestra que todas las variables numéricas presentan media cercana a cero y desviación estándar cercana a uno, lo que confirma la correcta aplicación del proceso de escalamiento y garantiza la comparabilidad entre variables para los análisis multivariados posteriores.
El reto principal consisten en realizar un análisis integral y multidimensional de la base de datos para obtener una comprensión del mercado inmobiliario urbano. Se requiere aplicar diversas técnicas de análisis de datos, incluyendo:
Reducir la dimensionalidad del conjunto de datos y visualizar la estructura de las variables en componentes principales para identificar características clave que influyen en la variación de precios y oferta del mercado.
Selecciona las variables numericas para realizar el analisis:
# 1) Selección de variables numéricas para PCA
# - Excluye identificadores y coordenadas
# - Excluye versiones originales si usarás log_*
excluir <- intersect(names(df_vivienda_std), c("id", "latitud", "longitud", "preciom", "areaconst"))
df_pca <- df_vivienda_std %>%
select(where(is.numeric)) %>%
select(-all_of(excluir))
# Vista rápida
mi_tabla(
data.frame(variable = names(df_pca)),
id="tabla_resumen",
titulo="Variables numericas seleccionadas para PCA")
| variable |
|---|
| estrato |
| parqueaderos |
| banios |
| habitaciones |
| log_preciom |
| log_areaconst |
Se ejecuta el análisis PCA pra determinar la importancia de los componentes:
# 2) PCA con prcomp (ya está estandarizado, por eso center/scale = FALSE)
pca_fit <- prcomp(df_pca, center = FALSE, scale. = FALSE)
# Resumen
var_explicada <- (pca_fit$sdev)^2
prop_var <- var_explicada / sum(var_explicada)
var_acum <- cumsum(prop_var)
tabla_pca <- data.frame(
Componente = paste0("PC", seq_along(prop_var)),
Desviacion_Estandar = pca_fit$sdev,
Varianza_Explicada = prop_var,
Varianza_Acumulada = var_acum
)
mi_tabla(
tabla_pca,
id="tabla_resumen",
titulo="Importancia de componentes de acuerdo con PCA")
| Componente | Desviacion_Estandar | Varianza_Explicada | Varianza_Acumulada |
|---|---|---|---|
| PC1 | 1.916 | 0.612 | 0.612 |
| PC2 | 1.106 | 0.204 | 0.816 |
| PC3 | 0.685 | 0.078 | 0.894 |
| PC4 | 0.553 | 0.051 | 0.945 |
| PC5 | 0.487 | 0.039 | 0.984 |
| PC6 | 0.308 | 0.016 | 1.000 |
Interpretación El análisis de Componentes Principales (PCA) evidencia que la mayor parte de la información del conjunto de datos se concentra en los primeros componentes. En particular, el PC1 explica por sí solo el 61.2% de la variabilidad, y al considerar hasta el PC4 se alcanza aproximadamente el 94.5% de la varianza acumulada. Esto indica que es posible reducir significativamente la dimensionalidad del conjunto original sin perder información relevante.
Agrupar las propiedades residenciales en segmentos homogéneos con características similares para entender las dinámicas de las ofertas específicas en diferentes partes de la ciudad y en diferentes estratos socioeconómicos.
Selecciona las variables para realizar el analisis:
# Variables numéricas estandarizadas para clustering
excluir <- intersect(names(df_vivienda_std), c("id", "latitud", "longitud", "preciom", "areaconst"))
df_cluster <- df_vivienda_std %>%
select(where(is.numeric)) %>%
select(-all_of(excluir))
# Vista rápida
mi_tabla(
data.frame(variable = names(df_cluster)),
id="tabla_resumen",
titulo="Variables numericas seleccionadas para clustering")
| variable |
|---|
| estrato |
| parqueaderos |
| banios |
| habitaciones |
| log_preciom |
| log_areaconst |
Se aplica los métodos de codo y silueta para determinar el número adecuado de clusteres para el análisis.
# Método del codo (WSS)
plot_codo <- fviz_nbclust(df_cluster, kmeans, method = "wss")
mi_figura(
plot_codo,
id = "figura_resumen",
titulo = "Método del codo para selección de número de clusteres"
)
# Silhouette
plot_silu <- fviz_nbclust(df_cluster, kmeans, method = "silhouette")
mi_figura(
plot_silu,
id = "figura_resumen",
titulo = "Método de la silueta para selección de número de clusteres"
)
El método de la silueta indica que la mejor segmentación se logra con k = 2 clusters, ya que en este punto se obtiene el mayor ancho promedio de silueta. Por su parte, el método del codo muestra un cambio de tendencia entre k = 3 y k = 4, lo que sugiere que a partir de allí las mejoras en la compactación de los grupos son cada vez menores. Al considerar ambos enfoques y buscando un modelo claro y fácil de interpretar, se decide trabajar con k = 3 como la alternativa para segmentar la oferta inmobiliaria.
Se segmenta el set de datos de oferta inmobiliarias (corregido) con k = 3. A continuación se presenta los clusteres con la frecuencia de ofertas inmobiliarias.
set.seed(1234)
k_opt <- 3 # <-- AJUSTA según Elbow/Silhouette
km_fit <- kmeans(df_cluster, centers = k_opt, nstart = 25)
# Añadir cluster al dataframe limpio (con categóricas)
df_vivienda_cluster <- df_vivienda_clean %>%
mutate(cluster = factor(km_fit$cluster))
tabla_clusters <- as.data.frame(table(df_vivienda_cluster$cluster))
names(tabla_clusters) <- c("cluster", "frecuencia")
# Vista rápida
mi_tabla(
tabla_clusters,
id="tabla_resumen",
titulo="Tamaño de cada segmento")
| cluster | frecuencia |
|---|---|
| 1 | 2683 |
| 2 | 4587 |
| 3 | 1049 |
Se realiza el perfilamiento de cada cluster con las variables típicas del caso con estadistiscos principales (mediana y media).
res_cluster_num <- df_vivienda_cluster %>%
group_by(cluster) %>%
summarise(
n = n(),
# Variables típicas del caso (ajusta si deseas)
preciom_mediana = median(preciom, na.rm = TRUE),
areaconst_mediana = median(areaconst, na.rm = TRUE),
parqueaderos_prom = mean(parqueaderos, na.rm = TRUE),
banios_prom = mean(banios, na.rm = TRUE),
habitaciones_prom = mean(habitaciones, na.rm = TRUE),
estrato_mediana = median(estrato, na.rm = TRUE)
) %>%
arrange(cluster)
mi_tabla(
res_cluster_num |> tibble::as_tibble(),
id="tabla_resumen",
titulo="Perfilamiento de cada cluster")
| cluster | n | preciom_mediana | areaconst_mediana | parqueaderos_prom | banios_prom | habitaciones_prom | estrato_mediana |
|---|---|---|---|---|---|---|---|
| 1 | 2683 | 650 | 227 | 2.679 | 4.264 | 3.804 | 6 |
| 2 | 4587 | 230 | 84 | 1.185 | 2.177 | 2.892 | 4 |
| 3 | 1049 | 380 | 260 | 1.525 | 4.247 | 6.217 | 4 |
Interpretación El análisis de clusters permitió identificar tres segmentos diferenciados en la oferta inmobiliaria: (i) un segmento premium de alto valor y estrato elevado, caracterizado por mayor área y nivel de dotación; (ii) un segmento medio–económico que concentra la mayor cantidad de viviendas, con menor tamaño y precio; y (iii) un segmento de viviendas familiares amplias, con alto número de habitaciones y área construida, pero ubicado en estratos intermedios. Esta segmentación evidencia la coexistencia de mercados con diferentes niveles de valorización y tipología habitacional.
Examinar la relación entre las variables categóricas (tipo de vivienda, zona y barrio), para identificar patrones de comportamiento de la oferta en mercado inmobiliario.
A continuación se presenta la distribución del tipo de inmueble, zona y barrio de las ofertas inmobliliarias (tabla corregida).
df_cat <- df_vivienda_clean %>%
transmute(
tipo = as.factor(str_squish(tipo)),
zona = as.factor(str_squish(zona)),
barrio = str_squish(barrio)
) %>%
mutate(
tipo = fct_explicit_na(tipo, na_level = "Sin informacion"),
zona = fct_explicit_na(zona, na_level = "Sin informacion"),
barrio = ifelse(is.na(barrio) | barrio == "", "Sin informacion", barrio)
)
# Reducir cardinalidad de barrio (Top N + Otros)
top_n <- 20 # ajusta 15-30 según tu preferencia
top_barrios <- df_cat %>%
count(barrio, sort = TRUE) %>%
slice_head(n = top_n) %>%
pull(barrio)
df_cat <- df_cat %>%
mutate(
barrio_rec = ifelse(barrio %in% top_barrios, barrio, "Otros"),
barrio_rec = as.factor(barrio_rec)
)
# Verificación rápida
mi_tabla(
df_cat %>% count(tipo, name = "frecuencia"),
id="tabla_resumen",
titulo="Distribución por tipo de inmueble")
| tipo | frecuencia |
|---|---|
| Apartamento | 5100 |
| Casa | 3219 |
mi_tabla(
df_cat %>% count(zona, name = "frecuencia"),
id="tabla_resumen",
titulo="Distribución por zona")
| zona | frecuencia |
|---|---|
| Zona Centro | 124 |
| Zona Norte | 1920 |
| Zona Oeste | 1198 |
| Zona Oriente | 351 |
| Zona Sur | 4726 |
mi_tabla(
df_cat %>% count(barrio_rec, name = "frecuencia"),
id="tabla_resumen",
titulo="Distribución por barrio")
| barrio_rec | frecuencia |
|---|---|
| acopi | 158 |
| aguacatal | 109 |
| brisas de los | 81 |
| caney | 88 |
| ciudad 2000 | 95 |
| ciudad jardín | 516 |
| cristales | 83 |
| el caney | 208 |
| el ingenio | 202 |
| el limonar | 135 |
| el refugio | 120 |
| la flora | 366 |
| la hacienda | 164 |
| los cristales | 154 |
| normandía | 154 |
| Otros | 3798 |
| pance | 409 |
| prados del norte | 126 |
| santa teresita | 262 |
| urbanización la flora | 83 |
| valle del lili | 1008 |
Se realiza el analisis cruzado entre el tipo del inmueble versus las zonas.
tab_tipo_zona <- table(df_cat$tipo, df_cat$zona)
tab_tipo_zona_wide <- as.data.frame(tab_tipo_zona) %>%
rename(tipo = Var1, zona = Var2, n = Freq) %>%
pivot_wider(names_from = zona, values_from = n, values_fill = 0)
mi_tabla(
tab_tipo_zona_wide,
id="tabla_resumen",
titulo="Tabla cruzada entre el tipo de inmueble versus las zonas")
| tipo | Zona Centro | Zona Norte | Zona Oeste | Zona Oriente | Zona Sur |
|---|---|---|---|---|---|
| Apartamento | 24 | 1198 | 1029 | 62 | 2787 |
| Casa | 100 | 722 | 169 | 289 | 1939 |
Interpretación Se realiza la prueba de independencia entre el tipo de inmueble y las zonas, donde el p-value tiende a cero, lo cual rechaza la prueba. Con esto, hay una dependencia fuerte entre el tipo de inmueble y las zonas.
chisq.test(tab_tipo_zona)
##
## Pearson's Chi-squared test
##
## data: tab_tipo_zona
## X-squared = 690.93, df = 4, p-value < 2.2e-16
Tambien Se realiza el analisis cruzado entre las zonas versus los barrios.
tab_zona_barrio <- table(df_cat$zona, df_cat$barrio_rec)
tab_zona_barrio_wide <- as.data.frame(tab_zona_barrio) %>%
rename(zona = Var1, barrio_rec = Var2, n = Freq) %>%
pivot_wider(names_from = barrio_rec, values_from = n, values_fill = 0)
mi_tabla(
tab_zona_barrio_wide,
id="tabla_resumen",
titulo="Tabla cruzada entre las zonas versus los barrios")
| zona | acopi | aguacatal | brisas de los | caney | ciudad 2000 | ciudad jardín | cristales | el caney | el ingenio | el limonar | el refugio | la flora | la hacienda | los cristales | normandía | Otros | pance | prados del norte | santa teresita | urbanización la flora | valle del lili |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Zona Centro | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 123 | 0 | 1 | 0 | 0 | 0 |
| Zona Norte | 157 | 0 | 81 | 0 | 0 | 3 | 1 | 0 | 0 | 0 | 0 | 365 | 0 | 0 | 0 | 1100 | 0 | 124 | 2 | 83 | 4 |
| Zona Oeste | 0 | 108 | 0 | 0 | 0 | 0 | 81 | 0 | 0 | 0 | 0 | 0 | 0 | 154 | 153 | 447 | 0 | 1 | 254 | 0 | 0 |
| Zona Oriente | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 350 | 1 | 0 | 0 | 0 | 0 |
| Zona Sur | 1 | 1 | 0 | 88 | 95 | 513 | 1 | 208 | 202 | 135 | 120 | 1 | 164 | 0 | 1 | 1778 | 408 | 0 | 6 | 0 | 1004 |
Interpretación Se realiza la prueba de independencia entre las zonas y los barrios, donde el p-value tiende a cero, lo cual rechaza la prueba. Con esto, hay una dependencia fuerte entre las zonas y los barrios.
chisq.test(tab_zona_barrio)
##
## Pearson's Chi-squared test
##
## data: tab_zona_barrio
## X-squared = 9699.9, df = 80, p-value < 2.2e-16
Presentar gráficos, mapas y otros recursos visuales para comunicar los hallazgos de manera clara y efectiva a la dirección de la empresa.
Interpretación de componentes principales.
A continuación, se grafica la varianza explicada para los componentes del PCA, el analisis bivariado de los componentes principales y la contribución de las variables para cada componente principal.
var_exp <- (pca_fit$sdev^2) / sum(pca_fit$sdev^2)
df_scree <- data.frame(
componente = seq_along(var_exp),
varianza = var_exp
)
plot_vec <- ggplot(df_scree, aes(x = componente, y = varianza)) +
geom_line() +
geom_point() +
scale_x_continuous(breaks = df_scree$componente) +
labs(
x = "Componente principal",
y = "Proporción de varianza explicada"
)
mi_figura(
plot_vec,
id = "figura_resumen",
titulo = "Varianza explicada por componente"
)
plot_pca_bi <- fviz_pca_biplot(
pca_fit,
col.var = "black",
col.ind = "gray70"
)
mi_figura(
plot_pca_bi,
id = "figura_resumen",
titulo = "Analisis bivariado de componentes PC1 versus PC2 para individuos y variables"
)
# Variables que más contribuyen a PC1 y PC2
plot_con_pc1 <- fviz_contrib(pca_fit, choice = "var", axes = 1, top = 10)
mi_figura(
plot_con_pc1,
id = "figura_resumen",
titulo = "Top variables que más contribuyen a PC1"
)
plot_con_pc2 <- fviz_contrib(pca_fit, choice = "var", axes = 2, top = 10)
mi_figura(
plot_con_pc2,
id = "figura_resumen",
titulo = "Top variables que más contribuyen a PC2"
)
Interpretación general El análisis de componentes principales (PCA) permitió reducir la dimensionalidad del conjunto de variables numéricas estandarizadas, identificando un conjunto pequeño de componentes que explican la mayor parte de la variabilidad observada en la oferta inmobiliaria.
Las contribuciones de variables a las primeras componentes sugieren cuáles características del inmueble y del mercado (por ejemplo, asociadas a tamaño, dotación y nivel socioeconómico) influyen con mayor peso en la variación de precios y en la estructura general de la oferta. La visualización en el plano PC1–PC2 facilita la identificación de patrones y posibles segmentos, los cuales serán explorados posteriormente mediante análisis de conglomerados.
Descripción de segmentos encontrados.
Con la tabla del analisis de conglomerados, se realiza la visualización de los clusteres con los dos componentes principales del analisis PCA.
# PCA solo para visualizar (sobre el mismo df_cluster usado en kmeans)
pca_vis <- prcomp(df_cluster, center = FALSE, scale. = FALSE)
p_clusteres_PCA <- fviz_pca_ind(
pca_vis,
geom.ind = "point",
habillage = df_vivienda_cluster$cluster,
addEllipses = TRUE,
ellipse.level = 0.95,
alpha.ind = 0.5
)
mi_figura(
p_clusteres_PCA,
id = "figura_resumen",
titulo = "Clusters proyectados en el plano PCA con las dos componentes principales"
)
Interpretación de la segmentación en plano PCA El plano PCA evidencia una segmentación coherente del mercado inmobiliario en tres grupos con perfiles diferenciados, aunque con solapamientos moderados propios de la heterogeneidad del mercado.
tab_zona <- df_vivienda_cluster %>%
count(zona, cluster) %>%
group_by(zona) %>%
mutate(pct = round(100 * n / sum(n), 2)) %>%
arrange(zona, desc(pct))
p_clusteres_zona <- ggplot(tab_zona, aes(x = zona, y = pct, fill = cluster)) +
geom_col() +
labs(
x = "Zona",
y = "% dentro de cada zona"
) +
theme(axis.text.x = element_text(angle = 25, hjust = 1))
mi_figura(
p_clusteres_zona,
id = "figura_resumen",
titulo = "Composición porcentual de clusters por zona"
)
Interpretación de los clusteres por zona La composición porcentual por zona muestra que el mercado inmobiliario presenta diferencias espaciales claras. La Zona Oeste concentra una mayor proporción de viviendas pertenecientes al cluster de mayor valorización, mientras que las Zonas Norte y Sur están principalmente dominadas por el segmento medio–económico. En contraste, las Zonas Centro y Oriente exhiben una distribución más equilibrada y diversa entre los distintos tipos de vivienda. En conjunto, estos hallazgos confirman que el comportamiento del mercado inmobiliario varía de manera importante según la ubicación geográfica.
tab_estrato <- df_vivienda_cluster %>%
count(estrato, cluster) %>%
group_by(estrato) %>%
mutate(pct = round(100 * n / sum(n), 2)) %>%
arrange(estrato, desc(pct))
p_clusteres_estrato <- ggplot(tab_estrato, aes(x = factor(estrato), y = pct, fill = cluster)) +
geom_col() +
labs(
x = "Estrato",
y = "% dentro de cada estrato"
)
mi_figura(
p_clusteres_estrato,
id = "figura_resumen",
titulo = "Composición porcentual de clusters por estrato"
)
Interpretación de los clusteres por estrato La composición porcentual por estrato evidencia una clara relación entre la segmentación obtenida y el nivel socioeconómico de las viviendas. El estrato 6 está dominado por el cluster de mayor valorización, mientras que los estratos 3 y 4 concentran principalmente el segmento medio–económico. Por su parte, el estrato 5 presenta una estructura de transición entre ambos mercados. Estos resultados confirman la coherencia del modelo de clustering con la estratificación socioeconómica del mercado inmobiliario.
Interpretación general El análisis de conglomerados permitió segmentar las propiedades en 3 grupos. Los perfiles por cluster evidencian diferencias en variables asociadas al tamaño del inmueble, dotación (baños, parqueaderos, habitaciones) y nivel socioeconómico (estrato), lo que sugiere la existencia de segmentos de mercado diferenciados.
La distribución de clusters por zona y estrato revela dinámicas específicas del mercado: ciertos segmentos se concentran en determinadas zonas y estratos, lo que puede orientar estrategias de valoración, compra/venta y focalización comercial.
Relaciones entre variables categóricas.
Con la tabla cruzada entre las zonas y los barrios, se visualiza el analisis de correspondencia de estas variables.
ca_zona_barrio <- CA(tab_zona_barrio, graph = FALSE)
p_bi_zona_barrio <- fviz_ca_biplot(
ca_zona_barrio,
repel = TRUE
)
mi_figura(
p_bi_zona_barrio,
id = "figura_resumen",
titulo = "Análisis de Correspondencia: Zona vs Barrio"
)
A continuación, se visualiza la contribución por zonas y barrios a la dimensión 1.
p_con_zona_dim1 <- fviz_contrib(ca_zona_barrio, choice = "row", axes = 1, top = 10)
mi_figura(
p_con_zona_dim1,
id = "figura_resumen",
titulo = "Contribución por zonas a dim 1"
)
p_con_barrio_dim1 <- fviz_contrib(ca_zona_barrio, choice = "col", axes = 1, top = 10)
mi_figura(
p_con_barrio_dim1,
id = "figura_resumen",
titulo = "Contribución por barrios a dim 1"
)
Interpretación El Análisis de Correspondencia permitió explorar asociaciones entre variables categóricas del mercado inmobiliario. Las pruebas chi-cuadrado complementarias evalúan si existe dependencia estadística entre las categorías.
En los mapas factoriales, categorías cercanas indican patrones de asociación (por ejemplo, ciertos barrios se asocian más fuertemente con zonas específicas). Las contribuciones a las dimensiones ayudan a identificar qué categorías explican la mayor parte de la variabilidad representada en el plano.
Síntesis de hallazgos clave y recomendaciones estratégicas para la empresa inmobiliaria.
El mercado inmobiliario analizado presenta una estructura claramente segmentada, donde el precio y el área construida actúan como ejes dominantes de diferenciación, mientras que la ubicación geográfica y el estrato socioeconómico determinan patrones espaciales de la oferta. La empresa dispone de suficiente información para implementar estrategias comerciales diferenciadas por segmento y territorio.
Implementar una estrategia comercial por segmentos
Acciones recomendadas
Diseñar portafolios específicos para:
Segmento premium
Segmento medio
Segmento económico
Personalizar mensajes y canales de venta por segmento.
Ajustar metas comerciales por tipo de cliente objetivo.
Priorizar la gestión territorial del inventario
Acciones recomendadas
Concentrar captación de inmuebles de alto valor según zonas.
Definir precios de referencia diferenciados por zona.
Implementar campañas de marketing hiperlocal.
Mensaje ejecutivo La empresa cuenta con una estructura de datos que permite evolucionar desde un enfoque descriptivo hacia una estrategia inmobiliaria basada en analítica avanzada, donde la segmentación, el pricing inteligente y la focalización geográfica serán los principales motores de ventaja competitiva.