Una empresa inmobiliaria requiere comprender la dinámica del mercado de vivienda urbano con el fin de optimizar sus decisiones de inversión y comercialización. Para ello se dispone de una base de datos con NA observaciones de ofertas de vivienda (variable a confirmar tras la carga de datos), que incluye características físicas del inmueble (área, número de habitaciones, baños, parqueaderos), su ubicación (zona, barrio, coordenadas geográficas), su clasificación socioeconómica (estrato) y el precio de oferta.
El presente informe desarrolla:
# Carga oficial del paquete (ejecutar tal cual en su equipo)
# devtools::install_github("centromagis/paqueteMODELOS", force = TRUE)
cargado <- FALSE
if (requireNamespace("paqueteMODELOS", quietly = TRUE)) {
library(paqueteMODELOS)
data("vivienda")
cargado <- TRUE
}
# Alternativa de respaldo (solo se activa si el paquete no está disponible
# en el equipo donde se compila el documento)
if (!cargado && file.exists("vivienda.rda")) {
load("vivienda.rda")
}
vivienda <- as.data.frame(vivienda)
str(vivienda)## 'data.frame': 8322 obs. of 13 variables:
## $ id : num 1147 1169 1350 5992 1212 ...
## $ zona : chr "Zona Oriente" "Zona Oriente" "Zona Oriente" "Zona Sur" ...
## $ piso : chr NA NA NA "02" ...
## $ estrato : num 3 3 3 4 5 5 4 5 5 5 ...
## $ preciom : num 250 320 350 400 260 240 220 310 320 780 ...
## $ areaconst : num 70 120 220 280 90 87 52 137 150 380 ...
## $ parqueaderos: num 1 1 2 3 1 1 2 2 2 2 ...
## $ banios : num 3 2 2 5 2 3 2 3 4 3 ...
## $ habitaciones: num 6 3 4 3 3 3 3 4 6 3 ...
## $ tipo : chr "Casa" "Casa" "Casa" "Casa" ...
## $ barrio : chr "20 de julio" "20 de julio" "20 de julio" "3 de julio" ...
## $ longitud : num -76.5 -76.5 -76.5 -76.5 -76.5 ...
## $ latitud : num 3.43 3.43 3.44 3.44 3.46 ...
## - attr(*, "spec")=List of 3
## ..$ cols :List of 13
## .. ..$ id : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ zona : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
## .. ..$ piso : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
## .. ..$ estrato : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ preciom : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ areaconst : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ parqueaderos: list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ banios : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ habitaciones: list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ tipo : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
## .. ..$ barrio : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
## .. ..$ longitud : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ latitud : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## ..$ default: list()
## .. ..- attr(*, "class")= chr [1:2] "collector_guess" "collector"
## ..$ delim : chr ";"
## ..- attr(*, "class")= chr "col_spec"
## - attr(*, "problems")=<externalptr>
La base de datos contiene 8,322 observaciones (inmuebles ofertados) y 13 variables.
diccionario <- data.frame(
Variable = c("id", "zona", "piso", "estrato", "preciom", "areaconst",
"parqueaderos", "banios", "habitaciones", "tipo", "barrio",
"longitud", "latitud"),
Descripcion = c(
"Identificador único del registro / inmueble",
"Zona geográfica de la ciudad en la que se ubica el inmueble",
"Piso en el que se encuentra el inmueble (aplica principalmente a apartamentos)",
"Estrato socioeconómico del inmueble (escala 1 a 6)",
"Precio de oferta del inmueble, en millones de pesos colombianos (COP)",
"Área construida del inmueble, en metros cuadrados (m²)",
"Número de parqueaderos / garajes con los que cuenta el inmueble",
"Número de baños del inmueble",
"Número de habitaciones (alcobas) del inmueble",
"Tipo de inmueble: Casa o Apartamento",
"Barrio en el que se localiza el inmueble",
"Coordenada geográfica de longitud",
"Coordenada geográfica de latitud"
),
Tipo_de_variable = c(
"Identificador", "Cualitativa nominal", "Cualitativa ordinal",
"Cuantitativa discreta (ordinal)", "Cuantitativa continua",
"Cuantitativa continua", "Cuantitativa discreta", "Cuantitativa discreta",
"Cuantitativa discreta", "Cualitativa nominal", "Cualitativa nominal",
"Cuantitativa continua", "Cuantitativa continua"
),
check.names = FALSE
)
# Se asignan los nombres de columna visibles por separado (como cadenas de
# texto, no como identificadores de R) para evitar errores de codificación
# al compilar el documento en sistemas donde el archivo no se lea como UTF-8.
names(diccionario) <- c("Variable", "Descripci\u00f3n", "Tipo de variable")
tabla(diccionario, caption = "Diccionario de variables de la base *vivienda*")| Variable | Descripción | Tipo de variable |
|---|---|---|
| id | Identificador único del registro / inmueble | Identificador |
| zona | Zona geográfica de la ciudad en la que se ubica el inmueble | Cualitativa nominal |
| piso | Piso en el que se encuentra el inmueble (aplica principalmente a apartamentos) | Cualitativa ordinal |
| estrato | Estrato socioeconómico del inmueble (escala 1 a 6) | Cuantitativa discreta (ordinal) |
| preciom | Precio de oferta del inmueble, en millones de pesos colombianos (COP) | Cuantitativa continua |
| areaconst | Área construida del inmueble, en metros cuadrados (m²) | Cuantitativa continua |
| parqueaderos | Número de parqueaderos / garajes con los que cuenta el inmueble | Cuantitativa discreta |
| banios | Número de baños del inmueble | Cuantitativa discreta |
| habitaciones | Número de habitaciones (alcobas) del inmueble | Cuantitativa discreta |
| tipo | Tipo de inmueble: Casa o Apartamento | Cualitativa nominal |
| barrio | Barrio en el que se localiza el inmueble | Cualitativa nominal |
| longitud | Coordenada geográfica de longitud | Cuantitativa continua |
| latitud | Coordenada geográfica de latitud | Cuantitativa continua |
La Tabla siguiente presenta las primeras 10 observaciones de la base de datos:
| id | zona | piso | estrato | preciom | areaconst | parqueaderos | banios | habitaciones | tipo | barrio | longitud | latitud |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1,147 | Zona Oriente | NA | 3 | 250 | 70 | 1 | 3 | 6 | Casa | 20 de julio | -76.51 | 3.43 |
| 1,169 | Zona Oriente | NA | 3 | 320 | 120 | 1 | 2 | 3 | Casa | 20 de julio | -76.51 | 3.43 |
| 1,350 | Zona Oriente | NA | 3 | 350 | 220 | 2 | 2 | 4 | Casa | 20 de julio | -76.52 | 3.44 |
| 5,992 | Zona Sur | 02 | 4 | 400 | 280 | 3 | 5 | 3 | Casa | 3 de julio | -76.54 | 3.44 |
| 1,212 | Zona Norte | 01 | 5 | 260 | 90 | 1 | 2 | 3 | Apartamento | acopi | -76.51 | 3.46 |
| 1,724 | Zona Norte | 01 | 5 | 240 | 87 | 1 | 3 | 3 | Apartamento | acopi | -76.52 | 3.37 |
| 2,326 | Zona Norte | 01 | 4 | 220 | 52 | 2 | 2 | 3 | Apartamento | acopi | -76.52 | 3.43 |
| 4,386 | Zona Norte | 01 | 5 | 310 | 137 | 2 | 3 | 4 | Apartamento | acopi | -76.53 | 3.38 |
| 1,209 | Zona Norte | 02 | 5 | 320 | 150 | 2 | 4 | 6 | Casa | acopi | -76.51 | 3.48 |
| 1,592 | Zona Norte | 02 | 5 | 780 | 380 | 2 | 3 | 3 | Casa | acopi | -76.52 | 3.49 |
resumen_estructura <- data.frame(
Variable = names(vivienda),
Tipo = sapply(vivienda, function(x) class(x)[1]),
`Valores únicos` = sapply(vivienda, function(x) length(unique(x))),
`Valores faltantes` = sapply(vivienda, function(x) sum(is.na(x))),
`% Faltantes` = round(sapply(vivienda, function(x) mean(is.na(x))) * 100, 2),
check.names = FALSE
)
tabla(resumen_estructura, caption = "Estructura general y valores faltantes por variable")| Variable | Tipo | Valores únicos | Valores faltantes | % Faltantes | |
|---|---|---|---|---|---|
| id | id | numeric | 8,320 | 3 | 0.04 |
| zona | zona | character | 6 | 3 | 0.04 |
| piso | piso | character | 13 | 2,638 | 31.70 |
| estrato | estrato | numeric | 5 | 3 | 0.04 |
| preciom | preciom | numeric | 540 | 2 | 0.02 |
| areaconst | areaconst | numeric | 653 | 3 | 0.04 |
| parqueaderos | parqueaderos | numeric | 11 | 1,605 | 19.29 |
| banios | banios | numeric | 12 | 3 | 0.04 |
| habitaciones | habitaciones | numeric | 12 | 3 | 0.04 |
| tipo | tipo | character | 3 | 3 | 0.04 |
| barrio | barrio | character | 437 | 3 | 0.04 |
| longitud | longitud | numeric | 2,929 | 3 | 0.04 |
| latitud | latitud | numeric | 3,680 | 3 | 0.04 |
ggplot(resumen_estructura[resumen_estructura$`% Faltantes` > 0, ],
aes(x = reorder(Variable, `% Faltantes`), y = `% Faltantes`)) +
geom_col(fill = paleta[2]) +
coord_flip() +
geom_text(aes(label = paste0(`% Faltantes`, "%")), hjust = -0.15, size = 3.5) +
labs(title = "Porcentaje de valores faltantes por variable",
x = NULL, y = "% de valores faltantes") +
expand_limits(y = max(resumen_estructura$`% Faltantes`) * 1.15)Interpretación: la base de datos tiene un tamaño
considerable (8,322 registros) y, en general, un bajo nivel de datos
faltantes. La variable con mayor proporción de valores ausentes es
parqueaderos (cerca del 19% de los registros), lo cual es
razonable ya que muchas viviendas —sobre todo casas de barrios
populares— no ofrecen parqueadero, por lo que la ausencia podría no ser
aleatoria sino informativa. La variable piso presenta cerca
de un 32% de valores faltantes, explicado porque este campo solo aplica
de forma natural a apartamentos y no a casas. El resto de variables
(estrato, preciom, areaconst,
banios, habitaciones, tipo,
barrio, coordenadas) presentan una proporción marginal de
datos faltantes (menor al 0.1%), por lo que su efecto sobre los análisis
posteriores es prácticamente nulo. Para los análisis multivariados (PCA,
conglomerados y correspondencias) se trabajará con los registros
completos en las variables involucradas en cada técnica.
vars_num <- c("estrato", "preciom", "areaconst", "parqueaderos", "banios", "habitaciones")
desc <- vivienda %>%
select(all_of(vars_num)) %>%
summarise(across(everything(), list(
n = ~sum(!is.na(.)),
Media = ~mean(., na.rm = TRUE),
Mediana = ~median(., na.rm = TRUE),
DE = ~sd(., na.rm = TRUE),
CV_pct = ~sd(., na.rm = TRUE) / mean(., na.rm = TRUE) * 100,
Minimo = ~min(., na.rm = TRUE),
Q1 = ~quantile(., 0.25, na.rm = TRUE),
Q3 = ~quantile(., 0.75, na.rm = TRUE),
Maximo = ~max(., na.rm = TRUE),
Asimetria = ~ (mean((. - mean(., na.rm = TRUE))^3, na.rm = TRUE)) /
(sd(., na.rm = TRUE)^3)
), .names = "{.col}__{.fn}")) %>%
pivot_longer(everything(), names_to = c("Variable", ".value"), names_sep = "__")
# Nombres de columna con tilde asignados como texto (no como identificadores),
# para evitar errores de codificación al compilar en distintos sistemas.
names(desc)[names(desc) == "Minimo"] <- "M\u00ednimo"
names(desc)[names(desc) == "Maximo"] <- "M\u00e1ximo"
names(desc)[names(desc) == "Asimetria"] <- "Asimetr\u00eda"
tabla(desc, digits = 2, caption = "Estadísticas descriptivas de las variables numéricas")| Variable | n | Media | Mediana | DE | CV_pct | Mínimo | Q1 | Q3 | Máximo | Asimetría |
|---|---|---|---|---|---|---|---|---|---|---|
| estrato | 8,319 | 4.63 | 5 | 1.03 | 22.21 | 3 | 4 | 5 | 6 | -0.18 |
| preciom | 8,320 | 433.89 | 330 | 328.65 | 75.74 | 58 | 220 | 540 | 1,999 | 1.85 |
| areaconst | 8,319 | 174.93 | 123 | 142.96 | 81.72 | 30 | 80 | 229 | 1,745 | 2.69 |
| parqueaderos | 6,717 | 1.84 | 2 | 1.12 | 61.30 | 1 | 1 | 2 | 10 | 2.33 |
| banios | 8,319 | 3.11 | 3 | 1.43 | 45.90 | 0 | 2 | 4 | 10 | 0.93 |
| habitaciones | 8,319 | 3.61 | 3 | 1.46 | 40.48 | 0 | 3 | 4 | 10 | 1.63 |
Interpretación: el precio de oferta
(preciom) presenta una media de aproximadamente 434
millones de pesos, pero una mediana notablemente inferior (330
millones), lo que junto con un coeficiente de variación cercano al 76% y
una asimetría positiva (1.85) evidencia una distribución sesgada a la
derecha: existen pocas propiedades de muy alto valor que elevan el
promedio. Un patrón aún más marcado se observa en el área construida
(areaconst), con un coeficiente de variación de casi 82% y
la mayor asimetría de todas las variables numéricas (2.69), coherente
con la coexistencia de vivienda de interés social y propiedades
campestres o de lujo en la muestra. Variables como banios,
habitaciones y estrato muestran una dispersión
moderada y distribuciones más simétricas, típicas de conteos acotados.
parqueaderos también exhibe asimetría positiva, reflejo de
que la mayoría de inmuebles cuenta con 1 o 2 parqueaderos, mientras que
unos pocos (posiblemente casas campestres) tienen hasta 10.
p_list <- lapply(vars_num, function(v) {
ggplot(vivienda, aes_string(x = v)) +
geom_histogram(fill = paleta[2], color = "white", bins = 30) +
labs(title = v, x = NULL, y = "Frecuencia")
})
do.call(grid.arrange, c(p_list, ncol = 2))Interpretación: los histogramas confirman lo
detectado en las estadísticas descriptivas: preciom y
areaconst presentan colas largas hacia la derecha (pocos
inmuebles de precio/área muy alta), mientras que estrato,
banios, habitaciones y
parqueaderos son variables discretas con rangos acotados y
concentración en pocos valores (por ejemplo, la mayoría de inmuebles
tiene entre 2 y 4 habitaciones y 2 a 3 baños). Esta asimetría sugiere
que, para algunos análisis estadísticos (como PCA), puede ser
conveniente estandarizar las variables e incluso evaluar
transformaciones (p. ej. logarítmica) sobre preciom y
areaconst en análisis posteriores más formales
(regresión).
b1 <- ggplot(vivienda, aes(x = "", y = preciom)) +
geom_boxplot(fill = paleta[2], outlier.color = paleta[4], outlier.alpha = 0.5) +
labs(title = "Precio (millones COP)", x = NULL, y = "Precio")
b2 <- ggplot(vivienda, aes(x = "", y = areaconst)) +
geom_boxplot(fill = paleta[2], outlier.color = paleta[4], outlier.alpha = 0.5) +
labs(title = "Área construida (m²)", x = NULL, y = "Área (m²)")
b3 <- ggplot(vivienda %>% filter(!is.na(tipo)), aes(x = tipo, y = preciom, fill = tipo)) +
geom_boxplot(outlier.alpha = 0.4) +
scale_fill_manual(values = paleta) +
labs(title = "Precio según tipo de inmueble", x = NULL, y = "Precio (millones COP)") +
theme(legend.position = "none")
b4 <- ggplot(vivienda %>% filter(!is.na(zona)), aes(x = reorder(zona, preciom, median),
y = preciom, fill = zona)) +
geom_boxplot(outlier.alpha = 0.4) +
scale_fill_manual(values = paleta) +
labs(title = "Precio según zona de la ciudad", x = NULL, y = "Precio (millones COP)") +
theme(legend.position = "none", axis.text.x = element_text(angle = 20, hjust = 1))
b5 <- ggplot(vivienda %>% filter(!is.na(estrato)), aes(x = factor(estrato), y = preciom,
fill = factor(estrato))) +
geom_boxplot(outlier.alpha = 0.4) +
scale_fill_manual(values = paleta) +
labs(title = "Precio según estrato socioeconómico", x = "Estrato", y = "Precio (millones COP)") +
theme(legend.position = "none")
grid.arrange(b1, b2, b3, b4, b5, ncol = 2)Interpretación: los boxplots univariados de
preciom y areaconst muestran numerosos valores
atípicos por encima del bigote superior, consistentes con la asimetría
ya identificada; estos “outliers” corresponden probablemente a
propiedades de lujo o campestres y deben tenerse en cuenta (no
necesariamente eliminarse) en análisis posteriores. Al comparar el
precio por tipo de inmueble, las casas presentan una
mediana y una dispersión mayores que los apartamentos, lo que es
coherente con la existencia de casas campestres de gran valor. Por
zona, la Zona Sur y la Zona Oeste muestran las medianas
de precio más altas, mientras que la Zona Centro y la Zona Oriente
presentan precios sustancialmente menores, reflejando la segregación
socioespacial típica de las ciudades colombianas. De forma consistente,
el precio crece de manera monótona con el estrato
socioeconómico, validando que el estrato es un buen proxy del
nivel socioeconómico y del valor de la vivienda. Sin embargo, como se
muestra en la siguiente sección, esta comparación agregada por zona
puede ser engañosa, pues mezcla dos efectos distintos: el precio real de
un inmueble dado su estrato, y la simple composición de estratos que
tiene cada zona.
Cuando se compara el precio promedio de una zona sin más, se mezclan dos efectos: (i) qué tan caro es, en promedio, un inmueble de un estrato dado en esa zona, y (ii) qué proporción de la oferta de esa zona corresponde a estratos altos o bajos. Para separar ambos efectos es necesario comparar el precio dentro de un mismo estrato entre zonas.
comp_estrato <- vivienda %>%
filter(!is.na(estrato), !is.na(zona)) %>%
count(zona, estrato) %>%
group_by(zona) %>%
mutate(pct = n / sum(n) * 100) %>%
ungroup()
tabla(
comp_estrato %>% filter(estrato == 6) %>% select(Zona = zona, `n° inmuebles` = n, `% de la oferta` = pct),
digits = 1,
caption = "Participación del estrato 6 dentro de la oferta de cada zona"
)| Zona | n° inmuebles | % de la oferta |
|---|---|---|
| Zona Centro | 1 | 0.8 |
| Zona Norte | 172 | 9.0 |
| Zona Oeste | 770 | 64.3 |
| Zona Oriente | 1 | 0.3 |
| Zona Sur | 1,043 | 22.1 |
zonas_robustas <- c("Zona Norte", "Zona Oeste", "Zona Sur") # únicas con muestra suficiente en todos los estratos
precio_ctrl <- vivienda %>%
filter(zona %in% zonas_robustas, estrato %in% 3:6) %>%
group_by(Estrato = factor(estrato), zona) %>%
summarise(n = n(), Precio_mediano = round(median(preciom, na.rm = TRUE), 0), .groups = "drop")
tabla(
precio_ctrl %>% pivot_wider(names_from = zona, values_from = c(n, Precio_mediano)),
caption = "Precio mediano (millones COP) por estrato, solo en zonas con muestra robusta"
)| Estrato | n_Zona Norte | n_Zona Oeste | n_Zona Sur | Precio_mediano_Zona Norte | Precio_mediano_Zona Oeste | Precio_mediano_Zona Sur |
|---|---|---|---|---|---|---|
| 3 | 572 | 54 | 382 | 135 | 165 | 170 |
| 4 | 407 | 84 | 1,616 | 250 | 215 | 230 |
| 5 | 769 | 290 | 1,685 | 370 | 445 | 325 |
| 6 | 172 | 770 | 1,043 | 675 | 699 | 745 |
ggplot(precio_ctrl, aes(x = Estrato, y = Precio_mediano, fill = zona, group = zona)) +
geom_col(position = "dodge") +
scale_fill_manual(values = paleta) +
labs(title = "Precio mediano por estrato, controlando la zona",
subtitle = "Comparación dentro de un mismo estrato (Norte, Oeste y Sur)",
x = "Estrato", y = "Precio mediano (millones COP)", fill = "Zona")Interpretación: la Zona Sur concentra 22.1% de su oferta en estrato 6, muy por encima de la Zona Norte (9.0%) y prácticamente inexistente en Centro y Oriente (menos de 1%). Esta mayor proporción de inmuebles de estrato alto es la que eleva su precio promedio agregado, y explica en buena parte por qué Zona Sur aparecía, en la comparación simple, como la segunda zona más “cara” del mercado.
Sin embargo, al comparar el precio dentro de un mismo estrato (usando solo Norte, Oeste y Sur, las tres zonas con muestra suficiente en todos los estratos), el panorama cambia: en los estratos 4 y 5, Zona Sur y Zona Norte tienen precios medianos muy similares —e incluso Sur queda ligeramente por debajo de Norte—, mientras que Zona Oeste es sistemáticamente la más cara en todos los estratos, con precios medianos entre 30% y 90% superiores a los de Norte y Sur para el mismo nivel socioeconómico.
Esto significa que la conclusión “Zona Sur agrupa los estratos y precios más altos del mercado, junto con Zona Oeste” debe matizarse: Zona Oeste es la que concentra un valor real por inmueble consistentemente más alto; el precio elevado de Zona Sur es, en gran medida, un efecto de composición —tiene más inmuebles de estrato alto en su oferta—, no necesariamente un mayor valor por unidad frente a Norte. Para efectos de definir una estrategia de pricing, la empresa debería usar como referencia el precio por estrato y zona (la tabla anterior), y no únicamente el precio promedio agregado por zona, que puede llevar a sobrestimar el atractivo relativo de Zona Sur frente a Zona Norte.
c1 <- vivienda %>% filter(!is.na(zona)) %>% count(zona) %>%
ggplot(aes(x = reorder(zona, n), y = n)) +
geom_col(fill = paleta[1]) + coord_flip() +
geom_text(aes(label = n), hjust = -0.1, size = 3.3) +
labs(title = "Oferta de vivienda por zona", x = NULL, y = "N° de inmuebles") +
expand_limits(y = max(table(vivienda$zona)) * 1.15)
c2 <- vivienda %>% filter(!is.na(tipo)) %>% count(tipo) %>%
ggplot(aes(x = tipo, y = n, fill = tipo)) +
geom_col() + scale_fill_manual(values = paleta) +
geom_text(aes(label = n), vjust = -0.3, size = 3.3) +
labs(title = "Oferta de vivienda por tipo", x = NULL, y = "N° de inmuebles") +
theme(legend.position = "none")
c3 <- vivienda %>% filter(!is.na(estrato)) %>% count(estrato) %>%
ggplot(aes(x = factor(estrato), y = n)) +
geom_col(fill = paleta[3]) +
geom_text(aes(label = n), vjust = -0.3, size = 3.3) +
labs(title = "Oferta de vivienda por estrato", x = "Estrato", y = "N° de inmuebles")
c4 <- vivienda %>% filter(!is.na(zona), !is.na(tipo)) %>%
count(zona, tipo) %>%
ggplot(aes(x = zona, y = n, fill = tipo)) +
geom_col(position = "fill") +
scale_fill_manual(values = paleta) +
scale_y_continuous(labels = percent) +
labs(title = "Composición tipo de inmueble por zona", x = NULL, y = "Proporción", fill = "Tipo") +
theme(axis.text.x = element_text(angle = 20, hjust = 1))
grid.arrange(c1, c2, c3, c4, ncol = 2)Interpretación: la Zona Sur concentra cerca del 57% de toda la oferta de vivienda de la ciudad, seguida por la Zona Norte y la Zona Oeste, mientras que la Zona Centro representa una fracción marginal de la oferta, lo cual sugiere que el crecimiento inmobiliario reciente se concentra al sur de la ciudad. En cuanto al tipo de inmueble, los apartamentos representan cerca del 61% de la oferta total frente a un 39% de casas. Los estratos 4 y 5 concentran la mayor parte de la oferta, mientras que el estrato 3 (más económico) y el 6 (más alto) tienen una participación menor. El gráfico de composición por zona evidencia que la oferta de casas es proporcionalmente mayor en la Zona Centro y la Zona Oriente (más del 80% de la oferta en ambas corresponde a casas), mientras que en la Zona Oeste predominan claramente los apartamentos (86%), seguida de la Zona Norte y la Zona Sur (ambas con más apartamentos que casas, aunque de forma menos marcada). Este patrón es relevante para la estrategia comercial de la empresa: la Zona Centro/Oriente tiene un perfil de casas —probablemente más pequeñas y económicas, dado que son zonas de precio mediano más bajo—, mientras que las zonas de mayor precio (Sur, Oeste) están dominadas por apartamentos y por un segmento más reducido de casas de mayor valor.
mat_cor <- cor(vivienda %>% select(all_of(vars_num)), use = "pairwise.complete.obs")
corrplot(mat_cor, method = "color", type = "upper", addCoef.col = "black",
number.cex = 0.8, tl.col = "black", tl.srt = 45,
col = colorRampPalette(c(paleta[4], "white", paleta[1]))(200),
title = "Correlación entre variables numéricas", mar = c(0,0,2,0))Interpretación: el precio (preciom)
presenta su correlación más fuerte y positiva con el área
construida (areaconst), lo cual es esperable pues
el tamaño del inmueble es uno de los principales determinantes de su
valor. También se observan correlaciones positivas moderadas entre
preciom y banios, habitaciones y
estrato, indicando que inmuebles más grandes, con más
habitaciones/baños y de estratos más altos tienden a tener mayor precio.
Las variables banios, habitaciones,
areaconst y parqueaderos están correlacionadas
entre sí, lo que anticipa que en el PCA estas variables tenderán a
agruparse en un mismo componente asociado al “tamaño/calidad” del
inmueble.
ggplot(vivienda %>% filter(!is.na(zona)),
aes(x = longitud, y = latitud, color = zona)) +
geom_point(alpha = 0.4, size = 1) +
scale_color_manual(values = paleta) +
coord_fixed() +
labs(title = "Distribución geográfica de la oferta de vivienda",
subtitle = "Coloreado por zona de la ciudad",
x = "Longitud", y = "Latitud", color = "Zona")Interpretación: el mapa de dispersión de coordenadas confirma que las zonas constituyen agrupaciones geográficas claramente diferenciadas y contiguas, sin solapamientos relevantes entre ellas. La Zona Sur, además de concentrar la mayor oferta, ocupa también la mayor extensión geográfica, mientras que la Zona Centro es la más compacta. Esta información es un insumo directo para complementar los análisis con herramientas de geomercadeo (mapas de calor de precio por metro cuadrado, identificación de submercados, etc.).
El objetivo del PCA es reducir la dimensionalidad de las variables numéricas del inmueble (precio, área, parqueaderos, baños, habitaciones y estrato) para identificar los factores latentes que explican la mayor parte de la variabilidad de la oferta inmobiliaria, y así facilitar la interpretación conjunta de estas variables.
Se trabaja con 6,717 observaciones completas en las 6 variables
numéricas. Dado que las variables tienen unidades y escalas distintas
(millones de pesos, metros cuadrados, conteos), el PCA se realiza sobre
la matriz de correlación (variables estandarizadas),
evitando que preciom o areaconst dominen
artificialmente los componentes por tener mayor varianza.
res_pca <- PCA(datos_pca, scale.unit = TRUE, graph = FALSE)
tabla(res_pca$eig, digits = 2, caption = "Valores propios y varianza explicada por componente")| eigenvalue | percentage of variance | cumulative percentage of variance | |
|---|---|---|---|
| comp 1 | 3.48 | 58.06 | 58.06 |
| comp 2 | 1.22 | 20.38 | 78.45 |
| comp 3 | 0.50 | 8.32 | 86.77 |
| comp 4 | 0.36 | 5.99 | 92.76 |
| comp 5 | 0.24 | 4.07 | 96.84 |
fviz_eig(res_pca, addlabels = TRUE, barfill = paleta[2], barcolor = paleta[1],
linecolor = paleta[4]) +
labs(title = "Porcentaje de varianza explicada por componente")Interpretación: los dos primeros componentes principales explican en conjunto cerca del 78.4% de la variabilidad total de las seis variables originales, lo cual es un porcentaje satisfactorio para trabajar en un plano de dos dimensiones sin una pérdida sustancial de información. A partir del tercer componente la varianza explicada adicional disminuye considerablemente (“codo” en el gráfico de sedimentación), por lo que retener dos componentes resulta una decisión razonable para los fines interpretativos y descriptivos de este informe.
fviz_pca_var(res_pca, col.var = "contrib",
gradient.cols = c(paleta[4], paleta[2], paleta[1]),
repel = TRUE) +
labs(title = "Círculo de correlaciones - variables en el plano PC1-PC2")tabla(res_pca$var$contrib, digits = 2,
caption = "Contribución (%) de cada variable a los primeros componentes")| Dim.1 | Dim.2 | Dim.3 | Dim.4 | Dim.5 | |
|---|---|---|---|---|---|
| estrato | 8.73 | 40.67 | 29.50 | 0.50 | 18.92 |
| preciom | 22.14 | 5.73 | 1.17 | 8.47 | 7.07 |
| areaconst | 20.27 | 4.18 | 6.73 | 43.19 | 9.23 |
| parqueaderos | 18.30 | 2.29 | 37.97 | 38.50 | 1.42 |
| banios | 21.65 | 2.56 | 13.66 | 1.98 | 46.01 |
| habitaciones | 8.92 | 44.57 | 10.98 | 7.36 | 17.35 |
Interpretación: el primer componente
(PC1), que explica la mayor parte de la varianza, está asociado
positivamente y de forma similar con areaconst,
habitaciones, banios,
parqueaderos y preciom; puede interpretarse
como un eje de “tamaño y valor del inmueble”: a mayor
puntaje en PC1, mayor es el inmueble en área, número de espacios y
precio. El segundo componente (PC2) está determinado
principalmente por estrato, y en menor medida por
preciom, diferenciándose del PC1 al capturar más el
nivel socioeconómico/ubicación que el tamaño físico del
inmueble; esto sugiere que existen inmuebles grandes en estratos no
necesariamente altos (y viceversa), es decir, tamaño y estrato no son
completamente redundantes.
fviz_pca_ind(res_pca, geom = "point", alpha.ind = 0.4,
col.ind = vivienda$tipo[complete.cases(vivienda %>% select(all_of(vars_num)))],
palette = paleta, addEllipses = TRUE, legend.title = "Tipo") +
labs(title = "Individuos en el plano PC1-PC2, coloreados por tipo de inmueble")Interpretación: al proyectar los inmuebles en el plano principal y colorearlos por tipo, se observa que las casas tienden a ubicarse hacia valores más altos de PC1 (mayor tamaño/valor), mientras que los apartamentos se concentran en valores más bajos y moderados de PC1, aunque con una zona de traslape considerable. Esto es consistente con lo observado en el EDA: las casas, en promedio, tienen mayor área construida y precio que los apartamentos. La elipse de confianza de cada grupo, sin embargo, se superpone parcialmente, indicando que el tipo de inmueble por sí solo no segmenta perfectamente el mercado y que conviene complementar este análisis con una segmentación por conglomerados.
El propósito de este análisis es agrupar las propiedades en segmentos homogéneos según sus características numéricas, de forma que la empresa pueda diseñar estrategias comerciales diferenciadas por segmento (p. ej. vivienda familiar de estrato medio, apartamentos compactos, propiedades de alto valor, etc.).
fviz_nbclust(datos_cluster, kmeans, method = "wss", k.max = 8) +
labs(title = "Método del codo para seleccionar el número de conglomerados")Interpretación: el gráfico del método del codo muestra una reducción marcada de la inercia intra-grupo hasta k = 4, punto a partir del cual las ganancias adicionales al incrementar el número de grupos son mucho más pequeñas. Por esta razón, y buscando un balance entre poder explicativo y facilidad de interpretación y accionabilidad comercial, se seleccionan 4 conglomerados.
set.seed(123)
k_final <- 4
res_km <- kmeans(datos_cluster, centers = k_final, nstart = 25)
fviz_cluster(res_km, data = datos_cluster, geom = "point", alpha = 0.4,
palette = paleta, ellipse.type = "convex", ggtheme = theme_minimal()) +
labs(title = "Conglomerados de vivienda en el plano de componentes principales")datos_pca$cluster <- factor(res_km$cluster)
perfil <- datos_pca %>%
group_by(cluster) %>%
summarise(
n = n(),
`%` = round(n() / nrow(datos_pca) * 100, 1),
Precio_prom = round(mean(preciom), 0),
Area_prom = round(mean(areaconst), 0),
Habitaciones_prom = round(mean(habitaciones), 1),
Banios_prom = round(mean(banios), 1),
Parqueaderos_prom = round(mean(parqueaderos), 1),
Estrato_prom = round(mean(estrato), 1)
)
# Se renombran las columnas con tilde/ñ como texto, evitando identificadores
# no-ASCII que puedan causar errores de codificación al compilar.
names(perfil)[names(perfil) == "Area_prom"] <- "\u00c1rea_prom"
names(perfil)[names(perfil) == "Banios_prom"] <- "Ba\u00f1os_prom"
tabla(perfil, caption = "Perfil promedio de cada conglomerado")| cluster | n | % | Precio_prom | Área_prom | Habitaciones_prom | Baños_prom | Parqueaderos_prom | Estrato_prom |
|---|---|---|---|---|---|---|---|---|
| 1 | 2,073 | 30.9 | 533 | 170 | 3.4 | 3.6 | 2.0 | 5.6 |
| 2 | 756 | 11.3 | 460 | 298 | 6.1 | 4.5 | 1.8 | 4.1 |
| 3 | 3,062 | 45.6 | 245 | 95 | 2.9 | 2.2 | 1.2 | 4.3 |
| 4 | 826 | 12.3 | 1,146 | 420 | 4.4 | 5.2 | 3.9 | 5.7 |
idx_completos <- complete.cases(vivienda %>% select(all_of(vars_num)))
tab_cluster_tipo <- table(
Cluster = datos_pca$cluster,
Tipo = vivienda$tipo[idx_completos]
)
tabla(as.data.frame.matrix(tab_cluster_tipo),
caption = "Composición de cada conglomerado según tipo de inmueble")| Apartamento | Casa |
|---|---|
| 1,466 | 607 |
| 31 | 725 |
| 2,500 | 562 |
| 234 | 592 |
Interpretación: los cuatro conglomerados identificados representan segmentos de mercado claramente diferenciados:
Esta segmentación es directamente accionable para la empresa: permite priorizar inventario, definir rangos de precio de referencia por segmento y diseñar mensajes de mercadeo diferenciados por perfil de comprador.
Se busca examinar la asociación entre variables categóricas (tipo de vivienda, zona, estrato y barrio) para identificar patrones de oferta en el mercado, complementando lo observado en el EDA univariado.
tab_zona_tipo <- table(vivienda$zona, vivienda$tipo)
tabla(as.data.frame.matrix(tab_zona_tipo), caption = "Tabla de contingencia Zona × Tipo")| Apartamento | Casa | |
|---|---|---|
| Zona Centro | 24 | 100 |
| Zona Norte | 1,198 | 722 |
| Zona Oeste | 1,029 | 169 |
| Zona Oriente | 62 | 289 |
| Zona Sur | 2,787 | 1,939 |
La prueba Chi-cuadrado de independencia arroja un estadístico
χ² = 690.9 con 4 grados de libertad y
un valor p <2e-16, lo que indica una
asociación estadísticamente significativa entre la zona
de la ciudad y el tipo de inmueble ofertado. Como la variable
tipo solo tiene dos categorías, la tabla de contingencia
tiene rango 1, por lo que el análisis de correspondencias se resume en
un único eje (no es posible un mapa de dos dimensiones); la Tabla
siguiente muestra las coordenadas de cada zona sobre ese eje:
coord_ca1 <- data.frame(Zona = names(res_ca1$row$coord), Coord_Dim1 = as.numeric(res_ca1$row$coord))
tabla(coord_ca1, caption = "Coordenadas de cada zona en el único eje de correspondencias (Zona × Tipo)")| Zona | Coord_Dim1 |
|---|---|
| Zona Centro | -0.86 |
| Zona Norte | 0.02 |
| Zona Oeste | 0.50 |
| Zona Oriente | -0.90 |
| Zona Sur | -0.05 |
ggplot(coord_ca1, aes(x = reorder(Zona, Coord_Dim1), y = Coord_Dim1)) +
geom_col(fill = paleta[2]) + coord_flip() +
labs(title = "Eje de correspondencias Zona × Tipo de inmueble",
subtitle = "Valores positivos: mayor afinidad relativa con 'Apartamento' · Valores negativos: mayor afinidad con 'Casa'",
x = NULL, y = "Coordenada en el eje 1")Interpretación: el mapa unidimensional confirma la asociación detectada en el EDA: la Zona Centro y la Zona Oriente presentan las coordenadas más negativas, indicando una fuerte afinidad relativa con la categoría “Casa” (recordemos que en ambas zonas más del 80% de la oferta corresponde a casas). En el extremo opuesto, la Zona Oeste presenta la coordenada más positiva, asociada a una mayor afinidad con “Apartamento” (86% de la oferta de esa zona), seguida de la Zona Norte con una afinidad positiva más moderada. La Zona Sur se ubica muy cerca del origen: esto no significa que carezca de un tipo predominante, sino que su composición (59% apartamentos / 41% casas) es muy similar al promedio general de la ciudad, por lo que el análisis de correspondencias no la distingue como atípica en este eje.
tab_zona_estrato <- table(vivienda$zona, vivienda$estrato)
tabla(as.data.frame.matrix(tab_zona_estrato), caption = "Tabla de contingencia Zona × Estrato")| 3 | 4 | 5 | 6 | |
|---|---|---|---|---|
| Zona Centro | 105 | 14 | 4 | 1 |
| Zona Norte | 572 | 407 | 769 | 172 |
| Zona Oeste | 54 | 84 | 290 | 770 |
| Zona Oriente | 340 | 8 | 2 | 1 |
| Zona Sur | 382 | 1,616 | 1,685 | 1,043 |
De nuevo, la prueba Chi-cuadrado (χ² = 3830.4, valor p <2e-16) confirma una asociación significativa entre zona y estrato socioeconómico.
res_ca2 <- CA(tab_zona_estrato, graph = FALSE)
fviz_ca_biplot(res_ca2, repel = TRUE, col.row = paleta[1], col.col = paleta[4]) +
labs(title = "Mapa de correspondencias: Zona × Estrato socioeconómico")Interpretación: el mapa de correspondencias evidencia una clara gradación espacial del estrato socioeconómico: la Zona Sur y la Zona Oeste se asocian con los estratos más altos (5 y 6), la Zona Norte ocupa una posición intermedia (estratos 4-5) y la Zona Centro y la Zona Oriente se asocian más con el estrato 3, el más bajo presente en la base. Este patrón replica la conocida segregación socioespacial de las ciudades colombianas y es un insumo clave para que la empresa oriente su portafolio de producto (vivienda social, vivienda media o vivienda premium) según la zona de intervención.
top_barrios <- vivienda %>% count(barrio, sort = TRUE) %>% slice_head(n = 15) %>% pull(barrio)
datos_barrio <- vivienda %>% filter(barrio %in% top_barrios, !is.na(zona))
tab_barrio_zona <- table(datos_barrio$barrio, datos_barrio$zona)
tabla(as.data.frame.matrix(tab_barrio_zona),
caption = "Tabla de contingencia Barrio (15 más frecuentes) × Zona")| Zona Centro | Zona Norte | Zona Oeste | Zona Oriente | Zona Sur | |
|---|---|---|---|---|---|
| acopi | 0 | 157 | 0 | 0 | 1 |
| aguacatal | 0 | 0 | 108 | 0 | 1 |
| ciudad jardín | 0 | 3 | 0 | 0 | 513 |
| el caney | 0 | 0 | 0 | 0 | 208 |
| el ingenio | 0 | 0 | 0 | 0 | 202 |
| el limonar | 0 | 0 | 0 | 0 | 135 |
| el refugio | 0 | 0 | 0 | 0 | 120 |
| la flora | 0 | 365 | 0 | 0 | 1 |
| la hacienda | 0 | 0 | 0 | 0 | 164 |
| los cristales | 0 | 0 | 154 | 0 | 0 |
| normandía | 0 | 0 | 153 | 0 | 1 |
| pance | 0 | 0 | 0 | 1 | 408 |
| prados del norte | 1 | 124 | 1 | 0 | 0 |
| santa teresita | 0 | 2 | 254 | 0 | 6 |
| valle del lili | 0 | 4 | 0 | 0 | 1,004 |
La prueba Chi-cuadrado (χ² = 8052, valor p <2e-16) confirma que, como es de esperarse, existe una asociación altamente significativa entre barrio y zona (cada barrio pertenece predominantemente a una zona específica).
res_ca3 <- CA(tab_barrio_zona, graph = FALSE)
fviz_ca_biplot(res_ca3, repel = TRUE, col.row = paleta[1], col.col = paleta[4],
labelsize = 3) +
labs(title = "Mapa de correspondencias: Barrio (top 15) × Zona")Interpretación: el mapa de correspondencias agrupa
visualmente los barrios más frecuentes de la oferta alrededor de la zona
a la que pertenecen, validando la consistencia interna de la variable
zona como agregación de barrio. Este mapa es
útil para la empresa porque permite identificar, dentro de cada zona,
cuáles son los barrios con mayor volumen de oferta (y
por tanto de mayor liquidez o rotación de inventario) y explorar
oportunidades de negocio en barrios cercanos entre sí en el mapa pero
pertenecientes a zonas distintas, que podrían compartir dinámicas de
mercado similares.
Hallazgos principales
Recomendaciones para la empresa inmobiliaria
parqueaderos y
piso presentan una proporción relevante de datos faltantes,
se recomienda reforzar los protocolos de captura de
información en el proceso de levantamiento de la oferta, ya que
estas variables son relevantes para explicar el precio.paqueteMODELOS, Centro de Investigación en
Matemáticas Aplicadas y Ciencia de Datos (CentroMAGIS). Disponible en:
https://github.com/centromagis/paqueteMODELOSFactoMineR)factoextra)