El mercado inmobiliario urbano se caracteriza por una alta heterogeneidad: propiedades con atributos físicos, ubicaciones y precios muy diversos coexisten en un mismo mercado. Para una empresa inmobiliaria, comprender la estructura subyacente de esa heterogeneidad es condición necesaria para tomar decisiones informadas sobre compra, venta y valoración de inmuebles.
El presente informe analiza una base de 8.322 ofertas de vivienda urbana, descritas por trece variables que combinan atributos físicos, localización y precio. El volumen y la dimensionalidad del conjunto hacen inviable el análisis variable por variable: se requieren técnicas capaces de tratar simultáneamente múltiples atributos y revelar patrones que el análisis univariado no detecta.
Con ese propósito se aplican tres técnicas de análisis multivariado. El análisis de componentes principales reduce la dimensionalidad del conjunto e identifica las dimensiones latentes que explican la mayor parte de la variabilidad de la oferta. El análisis de conglomerados segmenta las propiedades en grupos homogéneos que permiten caracterizar submercados. El análisis de correspondencia examina las asociaciones entre los atributos categóricos de las viviendas y su localización. Los resultados se integran en un conjunto de conclusiones y recomendaciones dirigidas a la dirección de la empresa.
Caracterizar la estructura del mercado de vivienda urbana mediante técnicas de análisis multivariado, con el fin de identificar patrones, relaciones y segmentaciones que sustenten decisiones estratégicas de compra, venta y valoración de propiedades.
Para llegar a los resultados, el informe sigue estos pasos:
vivienda del
paquete paqueteMODELOS.Los datos provienen del portal OLX y fueron recolectados mediante un
procedimiento de web scraping. El conjunto se distribuye en el
paquete paqueteMODELOS bajo el nombre
vivienda, y contiene 8.322 registros descritos por trece
variables.
data("vivienda")
tabla_str <- data.frame(
Variable = names(vivienda),
Tipo = sapply(vivienda, function(x) class(x)[1]),
Ejemplo = sapply(vivienda, function(x) as.character(x[1])),
NAs = sapply(vivienda, function(x) sum(is.na(x)))
)
knitr::kable(
tabla_str,
caption = "Tabla 1. Estructura de la base de datos vivienda",
row.names = FALSE
)
| Variable | Tipo | Ejemplo | NAs |
|---|---|---|---|
| id | numeric | 1147 | 3 |
| zona | character | Zona Oriente | 3 |
| piso | character | NA | 2638 |
| estrato | numeric | 3 | 3 |
| preciom | numeric | 250 | 2 |
| areaconst | numeric | 70 | 3 |
| parqueaderos | numeric | 1 | 1605 |
| banios | numeric | 3 | 3 |
| habitaciones | numeric | 6 | 3 |
| tipo | character | Casa | 3 |
| barrio | character | 20 de julio | 3 |
| longitud | numeric | -76.51168 | 3 |
| latitud | numeric | 3.43382 | 3 |
La Tabla 1 confirma la estructura de las 13 variables. Se observa
además que id no tiene significado estadístico, y que
longitud y latitud describen posición
geográfica más que atributos del inmueble, por lo que las tres se
reservan para la representación cartográfica de los resultados y no se
incluirán en el ACP. En cuanto a los datos faltantes, la tabla evidencia
mayor concentración en piso y parqueaderos; su
tratamiento se aborda en detalle en la sección de depuración de
datos.
Antes de aplicar cualquier técnica multivariada se examina la distribución de las variables numéricas. Esta revisión persigue tres fines: dimensionar las escalas de medición, cuantificar los valores faltantes, y detectar observaciones extremas. Los tres aspectos condicionan directamente los resultados posteriores, dado que el análisis de componentes principales y el de conglomerados operan sobre varianzas y distancias, y son por tanto sensibles tanto a las diferencias de escala como a la presencia de valores atípicos. Iniciamos este proceso con la variables númericas del dataset
num <- vivienda[sapply(vivienda, is.numeric)]
resumen <- data.frame(
Variable = names(num),
Minimo = sapply(num, min, na.rm = TRUE),
Q1 = sapply(num, quantile, probs = 0.25, na.rm = TRUE),
Mediana = sapply(num, median, na.rm = TRUE),
Media = sapply(num, mean, na.rm = TRUE),
Q3 = sapply(num, quantile, probs = 0.75, na.rm = TRUE),
Maximo = sapply(num, max, na.rm = TRUE),
NAs = sapply(num, function(x) sum(is.na(x))),
row.names = NULL
)
resumen[, -1] <- round(resumen[, -1], 2)
knitr::kable(resumen, caption = " Tabla 2. Resumen estadístico de las variables numéricas")
| Variable | Minimo | Q1 | Mediana | Media | Q3 | Maximo | NAs |
|---|---|---|---|---|---|---|---|
| id | 1.00 | 2080.50 | 4160.00 | 4160.00 | 6239.50 | 8319.00 | 3 |
| estrato | 3.00 | 4.00 | 5.00 | 4.63 | 5.00 | 6.00 | 3 |
| preciom | 58.00 | 220.00 | 330.00 | 433.89 | 540.00 | 1999.00 | 2 |
| areaconst | 30.00 | 80.00 | 123.00 | 174.93 | 229.00 | 1745.00 | 3 |
| parqueaderos | 1.00 | 1.00 | 2.00 | 1.84 | 2.00 | 10.00 | 1605 |
| banios | 0.00 | 2.00 | 3.00 | 3.11 | 4.00 | 10.00 | 3 |
| habitaciones | 0.00 | 3.00 | 3.00 | 3.61 | 4.00 | 10.00 | 3 |
| longitud | -76.59 | -76.54 | -76.53 | -76.53 | -76.52 | -76.46 | 3 |
| latitud | 3.33 | 3.38 | 3.42 | 3.42 | 3.45 | 3.50 | 3 |
La Tabla 2, revela cuatro hallazgos relevantes.
Escalas heterogéneas. Las variables operan en
órdenes de magnitud muy distintos: preciom alcanza valores
cercanos a 2.000 y areaconst supera los 1.700, mientras que
banios, habitaciones y
parqueaderos no exceden 10. Sin una estandarización previa,
las dos primeras dominarían por completo el cálculo de componentes
principales y de distancias, no por su relevancia sino por su escala. Se
confirma así la necesidad de escalar las variables antes del
análisis.
Distribuciones asimétricas. En preciom
la media (433,9) supera ampliamente a la mediana (330), y lo mismo
ocurre en areaconst (174,9 frente a 123). Este patrón
indica asimetría positiva: la mayoría de las propiedades se concentra en
valores bajos y una minoría de inmuebles de alto valor desplaza la media
hacia arriba. El máximo de areaconst equivale a 14 veces la
mediana, y el de preciom a 6 veces.
Valores faltantes de dos naturalezas. Ocho variables
presentan exactamente tres faltantes, coincidencia que sugiere un patrón
común. La variable parqueaderos, en contraste, presenta
1.605 (19,3%), lo que constituye un problema de otra índole.
Cobertura del estrato. La variable
estrato toma valores entre 3 y 6, sin presencia de los
estratos 1 y 2. La base describe por tanto el segmento medio y alto del
mercado, lo que delimita el alcance de las conclusiones: los hallazgos
no son extrapolables a la vivienda de interés social.
El análisis de valores faltantes se realiza en dos niveles: por
variable, para dimensionar la magnitud del problema, y por registro,
para determinar si los faltantes se concentran en observaciones
específicas o se distribuyen aleatoriamente en la base. Se inicia por
las variables con pocos datos faltantes (id,
zona, estrato, preciom,
areaconst, banios, habitaciones,
tipo, barrio, longitud y
latitud), ya que en estos casos es más viable identificar
un patrón o parámetro que explique su ausencia y así justificar la
decisión de tratamiento antes de abordar los casos más críticos:
piso y parqueaderos..
vars_pocos_na <- c("id", "zona", "estrato", "preciom", "areaconst",
"banios", "habitaciones", "tipo", "barrio",
"longitud", "latitud")
# Filas donde al menos una de estas variables tiene NA
filas_na <- which(rowSums(is.na(vivienda[, vars_pocos_na])) > 0)
# Tabla con esas filas, agregando el número de registro como primera columna
tabla_na <- vivienda[filas_na, vars_pocos_na]
tabla_na <- cbind(registro = filas_na, tabla_na)
knitr::kable(
tabla_na,
caption = "Tabla 3.Registros con datos faltantes en variables con pocos NA",
row.names = FALSE
)
| registro | id | zona | estrato | preciom | areaconst | banios | habitaciones | tipo | barrio | longitud | latitud |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 8320 | NA | NA | NA | NA | NA | NA | NA | NA | NA | NA | NA |
| 8321 | NA | NA | NA | NA | NA | NA | NA | NA | NA | NA | NA |
| 8322 | NA | NA | NA | 330 | NA | NA | NA | NA | NA | NA | NA |
La inspección confirma el diagnóstico: los tres registros están
vacíos en la totalidad de sus campos, incluido el identificador. Un
único registro conserva el valor de preciom (330 millones),
pero al carecer del resto de atributos no resulta utilizable en ninguna
de las técnicas previstas. El análisis de componentes principales y el
de conglomerados requieren el vector completo de variables numéricas
para calcular varianzas y distancias; el análisis de correspondencia
opera sobre las variables categóricas, todas ausentes en este caso.
Se eliminan los tres registros, con base en tres criterios:
La eliminación se implementa filtrando los registros sin identificador, criterio robusto frente a eventuales cambios en el orden de las filas.
vivienda_limpia <- vivienda[!is.na(vivienda$id), ]
print(paste("La base de datos después de la limpieza es de", nrow(vivienda_limpia), "registros"))
## [1] "La base de datos después de la limpieza es de 8319 registros"
Queda pendiente el tratamiento de parqueaderos, cuyos
1.605 valores faltantes no admiten la misma solución: su eliminación
implicaría descartar casi una quinta parte de la muestra.En este caso
primero de concerá mejor el pomportmiento comportamiento de la la
variable, para esto seelaborará una tabla de fecuncia sin tener e ncunta
los NA.
tabla_parqueaderos <- table(vivienda_limpia$parqueaderos, useNA = "ifany")
knitr::kable(
as.data.frame(tabla_parqueaderos),
col.names = c("Parqueaderos", "Frecuencia"),
caption = "Tabla 4.Distribución de valores de la variable parqueaderos"
)
| Parqueaderos | Frecuencia |
|---|---|
| 1 | 3155 |
| 2 | 2475 |
| 3 | 520 |
| 4 | 384 |
| 5 | 68 |
| 6 | 68 |
| 7 | 18 |
| 8 | 17 |
| 9 | 4 |
| 10 | 8 |
| NA | 1602 |
La Tabla 4 muestra la distribución de valores que toma la variable
parqueaderos: los registros van de 1 a 10, y en ningún caso
aparece el valor 0. Esta ausencia es la base de la hipótesis de trabajo:
los 1.602 valores faltantes no corresponden a información perdida al
azar, sino a viviendas que efectivamente no cuentan con parqueadero, y
que por esa razón no fueron registradas con ningún valor numérico.
Para confirmar esta hipótesis se analiza el patrón de los datos faltantes comparando el área construida, el estrato y el precio entre los registros sin dato y los niveles 1, 2 y 3 de parqueaderos (los de mayor tamaño de muestra). La expectativa es que, si el faltante corresponde efectivamente a 0 parqueaderos, sus valores se ubiquen de forma consistente por debajo del grupo con 1 parqueadero, siguiendo la misma tendencia creciente que se observa entre 1, 2 y 3. Se excluyen las categorías de 4 parqueaderos en adelante por su tamaño de muestra reducido, que haría poco confiables sus estadísticos.
Es importante aclarar que este análisis es descriptivo y no constituye una prueba estadística formal de tendencia; su propósito es reforzar, con evidencia adicional, la hipótesis planteada.
library(dplyr)
vivienda_tendencia <- vivienda_limpia %>%
filter(parqueaderos %in% c(1, 2, 3) | is.na(parqueaderos)) %>%
mutate(grupo_parq = ifelse(is.na(parqueaderos), "Sin dato", as.character(parqueaderos))) %>%
mutate(grupo_parq = factor(grupo_parq, levels = c("Sin dato", "1", "2", "3")))
resumen_tendencia <- vivienda_tendencia %>%
group_by(grupo_parq) %>%
summarise(
n = n(),
area_mediana = median(areaconst, na.rm = TRUE),
estrato_medio = mean(estrato, na.rm = TRUE),
precio_mediano = median(preciom, na.rm = TRUE)
)
knitr::kable(
resumen_tendencia,
caption = "Tabla 5. Área, estrato y precio según número de parqueaderos (incluye grupo sin dato)"
)
| grupo_parq | n | area_mediana | estrato_medio | precio_mediano |
|---|---|---|---|---|
| Sin dato | 1602 | 90 | 3.808365 | 179 |
| 1 | 3155 | 86 | 4.303328 | 250 |
| 2 | 2475 | 150 | 5.214950 | 450 |
| 3 | 520 | 259 | 5.436539 | 800 |
La tabla 5 muestran que, en estrato y precio, el grupo sin dato se ubica de forma consistente por debajo del grupo con 1 parqueadero, siguiendo la misma tendencia creciente que se observa entre 1, 2 y 3 parqueaderos (estrato: 3.8 < 4.3 < 5.2 < 5.4; precio mediano: 179 < 250 450 < 800). El área construida, sin embargo, no sigue el mismo patrón: el grupo sin dato (90 m² de mediana) es ligeramente mayor que el grupo con 1 parqueadero (86 m²). Lejos de contradecir la hipótesis, este resultado la refuerza: si la diferencia en precio se debiera únicamente al tamaño de la vivienda, se esperaría que el grupo sin dato también tuviera menor área. Al tratarse de viviendas de tamaño similar pero con precio y estrato consistentemente más bajos, la caída de valor parece explicarse por la ausencia del parqueadero en sí, y no por otras características de la propiedad.
Con esta evidencia (ausencia del valor 0, relación con el tipo de
propiedad y tendencia por estrato y precio), se considera
suficientemente sustentada la hipótesis de que los valores faltantes en
parqueaderos corresponden a viviendas sin parqueadero. En
consecuencia, se procede a recodificar estos valores como 0. Esta
operación no constituye una imputación estadística —no se está estimando
un valor desconocido—, sino la corrección de una categoría real que no
fue registrada explícitamente en la base de datos.
vivienda_recod <- vivienda_limpia
vivienda_recod$parqueaderos[is.na(vivienda_recod$parqueaderos)] <- 0
Para cerrar el tratamiento de valores faltantes, se analiza la
variable piso, con 2.638 valores ausentes (31.7% del
total), la unica vatiable categóriaca con datos faltates idetificadas e
la Table 1, y la de mayor proporción de faltantes en la base de datos. A
diferencia de parqueaderos, aún no se cuenta con una
hipótesis sobre el origen de estos datos, por lo que el análisis inicia
con la exploración de la distribución de valores que toma la variable,
con el fin de identificar posibles patrones que orienten la decisión de
tratamiento.
#Se elabora la tabla de frecuncia
tabla_piso <- table(vivienda_recod$piso, useNA = "ifany")
knitr::kable(
as.data.frame(tabla_piso),
col.names = c("Piso", "Frecuencia"),
caption = "Tabla 6. Distribución de valores de la variable piso"
)
| Piso | Frecuencia |
|---|---|
| 01 | 860 |
| 02 | 1450 |
| 03 | 1097 |
| 04 | 607 |
| 05 | 567 |
| 06 | 245 |
| 07 | 204 |
| 08 | 211 |
| 09 | 146 |
| 10 | 130 |
| 11 | 84 |
| 12 | 83 |
| NA | 2635 |
La Tabla 6 de frecuencias de piso muestra 12 categorías
(de “01” a “12”), consistentes con el número de piso del apartamento
dentro de un edificio. Bajo esta interpretación, se plantea como primera
hipótesis que piso es una característica propia de los
apartamentos y no aplica a las casas, al tratarse de construcciones
independientes sin niveles dentro de un edificio. De confirmarse, los
2.635 valores faltantes (31.7% del total) corresponderían principalmente
a registros de tipo Casa, y no a información perdida. Esta
hipótesis se evalúa a continuación cruzando la variable con el tipo de
vivienda.
vivienda_recod %>%
group_by(tipo) %>%
summarise(
n = n(),
sin_piso = sum(is.na(piso)),
pct_sin_piso = round(100 * mean(is.na(piso)), 1)
) %>%
kable(caption = "Tabla 7. Ausencia de dato en piso según tipo de vivienda") %>%
kable_styling(full_width = FALSE)
| tipo | n | sin_piso | pct_sin_piso |
|---|---|---|---|
| Apartamento | 5100 | 1381 | 27.1 |
| Casa | 3219 | 1254 | 39.0 |
La tabla 7 La Tabla 7, no brinda información clara sobre un posible patrón de datos que explican los datos faltantes y permita establece una estrategia de imputación, por lo tanto, se amplía las variables a comparar, incluyendo tipo de tipo, área construida, estrato y precio en función de tener dato o no.
library(dplyr)
resumen_piso_completo <- vivienda_recod %>%
mutate(grupo = case_when(
tipo == "Casa" & !is.na(piso) ~ "Casa con piso",
tipo == "Casa" & is.na(piso) ~ "Casa sin piso",
tipo == "Apartamento" & !is.na(piso) ~ "Apartamento con piso",
tipo == "Apartamento" & is.na(piso) ~ "Apartamento sin piso"
)) %>%
group_by(grupo) %>%
summarise(
n = n(),
area_mediana = median(areaconst, na.rm = TRUE),
estrato_medio = mean(estrato, na.rm = TRUE),
precio_mediano = median(preciom, na.rm = TRUE)
)
knitr::kable(
resumen_piso_completo,
caption = "Tabla 8. Resumen de área, estrato y precio según tipo de vivienda y disponibilidad de dato en piso"
)
| grupo | n | area_mediana | estrato_medio | precio_mediano |
|---|---|---|---|---|
| Apartamento con piso | 3719 | 90.0 | 4.719279 | 270 |
| Apartamento sin piso | 1381 | 91.0 | 4.748733 | 290 |
| Casa con piso | 1965 | 245.0 | 4.544020 | 440 |
| Casa sin piso | 1254 | 230.5 | 4.393142 | 400 |
La Tabla 8 tampoco revela un patrón concluyente: los cuatro grupos presentan perfiles de área, estrato y precio sin separación sistemática. No se logra identificar el mecanismo de ausencia: no es completamente aleatorio, pero ninguna variable observada lo explica.
• No se imputa. Toda imputación exige sustentar que la ausencia depende de variables observadas, condición que aquí no se cumple. A ello se suma que ninguna variable de la base determina el piso de un apartamento —ni el área, ni el precio, ni el estrato—, por lo que una imputación por vecinos más cercanos asignaría el valor más frecuente al 31,7% de los registros, introduciendo precisión ficticia.
• No se eliminan registros. Resultaría innecesario: excluir la variable conserva íntegros los 8.319 registros para las tres técnicas.
• Se recodifica la ausencia como categoría explícita, “Sin registro”, aplicando la técnica del indicador de faltante. La base queda sin valores nulos y la no-respuesta documentada, sin fabricar información.
• Se excluye la variable del análisis. Del ACP y de los conglomerados, por ser categórica. Del análisis de correspondencia, porque las variables definidas para ese análisis son tipo, zona y barrio, y porque “Sin registro” no es una modalidad del mercado sino del proceso de recolección: con un peso del 31,7% dominaría los ejes factoriales, reflejando prácticas de publicación antes que patrones de oferta.
vivienda_recod$piso_cat <- ifelse(is.na(vivienda_recod$piso),
"Sin registro",
vivienda_recod$piso)
A manera de cierre de la identifiación y manejo de los datos faltantes, se corre nuevametes el código para idenificar datos faltante el dataset.
data.frame(
Variable = names(vivienda_recod),
NAs = sapply(vivienda_recod, function(x) sum(is.na(x)))
) %>%
kable(caption = "Tabla 9. Verificación de valores faltantes tras la depuración",
row.names = FALSE) %>%
kable_styling(full_width = FALSE)
| Variable | NAs |
|---|---|
| id | 0 |
| zona | 0 |
| piso | 2635 |
| estrato | 0 |
| preciom | 0 |
| areaconst | 0 |
| parqueaderos | 0 |
| banios | 0 |
| habitaciones | 0 |
| tipo | 0 |
| barrio | 0 |
| longitud | 0 |
| latitud | 0 |
| piso_cat | 0 |
Se conservan ambas versiones de la variable: piso, en su
forma original, para preservar la trazabilidad del dato de origen, y
piso_cat, con la ausencia recodificada como categoría
explícita. Las 2.635 ausencias que registra la primera corresponden por
tanto a una decisión deliberada de preservación, no a un tratamiento
pendiente. Ninguna de las dos interviene en las técnicas multivariadas
aplicadas.
Con esta verificación se corrobora que las tres situaciones de datos
faltantes identificadas en la base han sido tratadas y documentadas: los
registros vacíos fueron eliminados, las ausencias de
parqueaderos recodificadas como valor cero, y las de
piso recodificadas como categoría explícita y excluidas del
análisis. La base de trabajo no presenta valores faltantes en ninguna de
las variables que intervienen en las técnicas previstas, por lo que se
da por cerrada esta etapa.
Resta un último aspecto de la preparación de los datos: la
identificación y el tratamiento de los valores atípicos, anticipados en
el análisis de la Tabla 2 al constatar la asimetría de las
distribuciones de preciom y areaconst. Dado
que el análisis de componentes principales opera sobre varianzas y el de
conglomerados sobre distancias, ambas técnicas resultan sensibles a las
observaciones extremas, lo que hace necesario examinar su naturaleza
antes de decidir si corresponde conservarlas o descartarlas.
b1 <- ggplot(vivienda_recod, aes(y = preciom)) +
geom_boxplot(fill = "steelblue") +
labs(title = "Fig1.Precio (millones)", y = "preciom") +
theme_minimal()
b2 <- ggplot(vivienda_recod, aes(y = areaconst)) +
geom_boxplot(fill = "darkgreen") +
labs(title = "Fig 2.Área construida (m²)", y = "areaconst") +
theme_minimal()
b3 <- ggplot(vivienda_recod, aes(x = factor(estrato), y = preciom)) +
geom_boxplot(fill = "steelblue") +
labs(title = "Fig3. Precio por estrato", x = "Estrato", y = "preciom") +
theme_minimal()
b4 <- ggplot(vivienda_recod, aes(x = factor(estrato), y = areaconst)) +
geom_boxplot(fill = "darkgreen") +
labs(title = "Fig 4.Área por estrato", x = "Estrato", y = "areaconst") +
theme_minimal()
grid.arrange(b1, b2, b3, b4, ncol = 2)
La inspección visual revela dos patrones. Al desagregar por estrato, las cajas del precio se desplazan hacia arriba de forma sostenida, mientras que las del área construida mantienen posiciones similares: el estrato parece incidir más sobre el precio que sobre el tamaño. Los valores extremos también difieren: en precio se concentran en el estrato 5, y en área alcanzan sus máximos en el estrato 3, pese a que este presenta una de las cajas más bajas.
Estas son impresiones visuales, no mediciones. El diagrama de caja representa cuartiles mediante posiciones, por lo que permite reconocer patrones pero no cuantificarlos. Para sustentar las afirmaciones anteriores se calculan a continuación los estadísticos correspondientes.
# Estadísticos de posición de precio y área, calculados dentro de cada estrato.
# group_by parte la base en 4 grupos; summarise resume cada grupo en una fila.
vivienda_recod %>%
group_by(estrato) %>%
summarise(
n = n(),
precio_mediana = median(preciom),
precio_Q1 = quantile(preciom, 0.25),
precio_Q3 = quantile(preciom, 0.75),
precio_max = max(preciom),
area_mediana = median(areaconst),
area_Q1 = quantile(areaconst, 0.25),
area_Q3 = quantile(areaconst, 0.75),
area_max = max(areaconst)
) %>%
kable(caption = "Tabla 10. Precio y área construida por estrato") %>%
kable_styling(full_width = FALSE)
| estrato | n | precio_mediana | precio_Q1 | precio_Q3 | precio_max | area_mediana | area_Q1 | area_Q3 | area_max |
|---|---|---|---|---|---|---|---|---|---|
| 3 | 1453 | 160 | 120 | 270 | 1600 | 91 | 60 | 190.00 | 1745 |
| 4 | 2129 | 235 | 165 | 335 | 1800 | 83 | 64 | 159.42 | 1000 |
| 5 | 2750 | 350 | 270 | 470 | 1999 | 115 | 90 | 217.03 | 1200 |
| 6 | 1987 | 700 | 515 | 990 | 1950 | 198 | 136 | 304.50 | 1600 |
Para evaluar la coherencia interna de los registros extremos se calcula el precio por metro cuadrado, indicador que permite distinguir entre errores de digitación y propiedades efectivamente valorizadas. La variable se emplea con fines diagnósticos y no se incorpora al análisis de componentes principales, por tratarse de un cociente entre dos variables ya incluidas en él.
# mutate agrega una columna nueva a la base.
# Al asignar con <- sobre el mismo objeto, la columna queda guardada.
vivienda_recod <- vivienda_recod %>%
mutate(precio_m2 = preciom / areaconst)
# Cuenta cuántos registros superan el bigote superior del boxplot en cada
# variable. El umbral (Q3 + 1.5*IQR) se recalcula dentro de cada estrato,
# de modo que cada vivienda se compara contra su propio grupo.
vivienda_recod %>%
group_by(estrato) %>%
summarise(
n = n(),
atip_precio = sum(preciom > quantile(preciom, 0.75) + 1.5 * IQR(preciom)),
pct_precio = round(100 * atip_precio / n, 1),
atip_area = sum(areaconst > quantile(areaconst, 0.75) + 1.5 * IQR(areaconst)),
pct_area = round(100 * atip_area / n, 1),
atip_m2 = sum(precio_m2 > quantile(precio_m2, 0.75) + 1.5 * IQR(precio_m2)),
pct_m2 = round(100 * atip_m2 / n, 1)
) %>%
kable(caption = "Tabla 11. Valores atípicos por estrato") %>%
kable_styling(full_width = FALSE)
| estrato | n | atip_precio | pct_precio | atip_area | pct_area | atip_m2 | pct_m2 |
|---|---|---|---|---|---|---|---|
| 3 | 1453 | 50 | 3.4 | 55 | 3.8 | 32 | 2.2 |
| 4 | 2129 | 86 | 4.0 | 166 | 7.8 | 20 | 0.9 |
| 5 | 2750 | 155 | 5.6 | 178 | 6.5 | 9 | 0.3 |
| 6 | 1987 | 49 | 2.5 | 97 | 4.9 | 13 | 0.7 |
# Rango del precio por metro cuadrado en cada estrato.
vivienda_recod %>%
group_by(estrato) %>%
summarise(
minimo = round(min(precio_m2), 2),
Q1 = round(quantile(precio_m2, 0.25), 2),
mediana = round(median(precio_m2), 2),
Q3 = round(quantile(precio_m2, 0.75), 2),
maximo = round(max(precio_m2), 2)
) %>%
kable(caption = "Tabla 12. Precio por metro cuadrado según estrato (millones)") %>%
kable_styling(full_width = FALSE)
| estrato | minimo | Q1 | mediana | Q3 | maximo |
|---|---|---|---|---|---|
| 3 | 0.15 | 1.32 | 1.71 | 2.10 | 7.00 |
| 4 | 0.38 | 1.96 | 2.50 | 2.90 | 7.74 |
| 5 | 0.28 | 2.10 | 2.82 | 3.42 | 9.47 |
| 6 | 0.85 | 2.83 | 3.66 | 4.37 | 8.30 |
El precio mediano crece de manera sostenida entre estratos —160, 235, 350 y 700 millones—, mientras que el área mediana lo hace de forma irregular: 91, 83, 115 y 198 m², con un descenso en el estrato 4. Entre los extremos, el precio se multiplica por más de cuatro y el área apenas por dos: la valorización responde más al estrato y la localización que a los metros construidos.
Los valores atípicos representan una proporción reducida en todos los estratos: entre 2,5% y 5,6% en precio, y entre 3,8% y 7,8% en área. El estrato 5, que en el gráfico parecía concentrar los extremos de precio, presenta 155 casos equivalentes al 5,6% de sus 2.750 registros: la densidad observada respondía al mayor tamaño del grupo y no a una proporción superior. El estrato 6 registra la menor proporción en precio (2,5%), consistente con que allí los valores elevados constituyen el comportamiento habitual.
El precio por metro cuadrado asciende de forma consistente con el estrato —1,71, 2,50, 2,82 y 3,66 millones—, lo que confirma que la clasificación socioeconómica ordena de manera coherente el valor unitario del suelo. Los registros extremos en este indicador son escasos y decrecen al ascender de estrato, del 2,2% en el estrato 3 al 0,3% en el estrato 5. Este patrón sugiere que en los estratos bajos una vivienda con atributos superiores se distingue con facilidad de su grupo, mientras que en los altos la calidad está ya incorporada al precio típico. Las causas de esa valorización diferencial —calidad de acabados, estado de conservación, ubicación específica dentro del barrio— corresponden a factores no observados en la base.
Decisión: se conservan los valores atípicos. El análisis por estrato muestra que no se trata de errores de registro sino de propiedades de alto valor y gran tamaño efectivamente presentes en el mercado. El precio por metro cuadrado de estos registros resulta coherente con el de su estrato, lo que descarta inconsistencias de digitación. Eliminarlas supondría excluir el segmento premium, de mayor margen y evidente interés estratégico para la empresa. Su influencia se atenúa por vía metodológica: el análisis de componentes principales se realizará sobre variables estandarizadas, lo que reduce el peso de las escalas extremas sin descartar información. Es previsible que estas propiedades conformen un segmento diferenciado en el análisis de conglomerados.
Cabe señalar, por último, la posibilidad de que la base agrupe
inmuebles de usos distintos —vivienda unifamiliar, edificaciones
divididas o propiedades de uso mixto— bajo las dos categorías de la
variable tipo. La base no incluye información que permita
verificarlo directamente.
Con esto concluye la etapa de preparación de los datos. La base cuenta con 8.319 registros sin valores faltantes en las variables de análisis, con los valores extremos identificados, dimensionados y conservados de forma justificada.
El análisis de componentes principales es una técnica de reducción de dimensionalidad que transforma un conjunto de variables correlacionadas en un número menor de variables nuevas, denominadas componentes principales, que son combinaciones lineales de las originales y no están correlacionadas entre sí. Cada componente se construye de modo que recoja la mayor cantidad posible de la varianza no explicada por las anteriores: la primera captura la máxima variabilidad del conjunto, la segunda la máxima variabilidad restante, y así sucesivamente.
Su utilidad en este estudio es doble. Por una parte, permite identificar qué combinaciones de atributos concentran la variación observada en la oferta inmobiliaria, respondiendo a la pregunta de qué características diferencian realmente a unas propiedades de otras. Por otra, produce un conjunto reducido de variables no correlacionadas que sirve de insumo al análisis de conglomerados, evitando que la redundancia entre atributos distorsione la medición de distancias entre propiedades.
La técnica requiere que las variables sean numéricas y que exista correlación apreciable entre ellas: si fueran mutuamente independientes, no habría redundancia que resumir y el procedimiento carecería de objeto. Por ello el análisis se inicia con el examen de la matriz de correlaciones.
Se incorporan al análisis las seis variables numéricas de la base:
estrato, preciom, areaconst,
parqueaderos, banios y
habitaciones. Se excluyen id, por tratarse de
un identificador sin contenido informativo, y longitud y
latitud, que corresponden a coordenadas geográficas y no a
atributos del inmueble. Se excluye asimismo precio_m2,
calculada con fines diagnósticos, por ser un cociente entre dos
variables ya incluidas.
La variable estrato, de naturaleza ordinal, se trata
como numérica dado que sus niveles guardan un orden y una distancia
interpretable en términos socioeconómicos. Las variables
parqueaderos, banios y
habitaciones son discretas; su inclusión es admisible
porque las diferencias entre sus valores son interpretables de manera
uniforme.
datos_acp <- vivienda_recod %>%
select(estrato, preciom, areaconst, parqueaderos, banios, habitaciones)
data.frame(
Variable = names(datos_acp),
NAs = colSums(is.na(datos_acp))
) %>%
kable(caption = "Tabla 13. Verificación de valores faltantes en las variables del ACP",
row.names = FALSE) %>%
kable_styling(full_width = FALSE)
| Variable | NAs |
|---|---|
| estrato | 0 |
| preciom | 0 |
| areaconst | 0 |
| parqueaderos | 0 |
| banios | 0 |
| habitaciones | 0 |
# Correlación de Pearson: mide asociación lineal. Es la que emplea el ACP.
round(cor(datos_acp, method = "pearson"), 3) %>%
kable(caption = "Tabla 13. Matriz de correlaciones de Pearson") %>%
kable_styling(full_width = FALSE)
| estrato | preciom | areaconst | parqueaderos | banios | habitaciones | |
|---|---|---|---|---|---|---|
| estrato | 1.000 | 0.610 | 0.274 | 0.513 | 0.420 | -0.071 |
| preciom | 0.610 | 1.000 | 0.687 | 0.640 | 0.669 | 0.264 |
| areaconst | 0.274 | 0.687 | 1.000 | 0.482 | 0.648 | 0.517 |
| parqueaderos | 0.513 | 0.640 | 0.482 | 1.000 | 0.523 | 0.199 |
| banios | 0.420 | 0.669 | 0.648 | 0.523 | 1.000 | 0.590 |
| habitaciones | -0.071 | 0.264 | 0.517 | 0.199 | 0.590 | 1.000 |
# Correlación de Spearman: se calcula sobre rangos, por lo que es robusta
# frente a los valores extremos que se decidió conservar.
round(cor(datos_acp, method = "spearman"), 3) %>%
kable(caption = "Tabla 14. Matriz de correlaciones de Spearman") %>%
kable_styling(full_width = FALSE)
| estrato | preciom | areaconst | parqueaderos | banios | habitaciones | |
|---|---|---|---|---|---|---|
| estrato | 1.000 | 0.710 | 0.395 | 0.586 | 0.471 | 0.040 |
| preciom | 0.710 | 1.000 | 0.822 | 0.660 | 0.768 | 0.427 |
| areaconst | 0.395 | 0.822 | 1.000 | 0.507 | 0.771 | 0.651 |
| parqueaderos | 0.586 | 0.660 | 0.507 | 1.000 | 0.547 | 0.250 |
| banios | 0.471 | 0.768 | 0.771 | 0.547 | 1.000 | 0.621 |
| habitaciones | 0.040 | 0.427 | 0.651 | 0.250 | 0.621 | 1.000 |
Se examinan las correlaciones mediante dos coeficientes complementarios. El de Pearson mide asociación lineal y constituye la base del análisis de componentes principales. El de Spearman, calculado sobre rangos, es robusto frente a los valores extremos conservados en la etapa anterior, por lo que permite verificar que la estructura de asociación no dependa de ellos.
El análisis se realiza sobre variables estandarizadas, es decir, sobre la matriz de correlaciones y no sobre la de covarianzas. Esta decisión responde a la heterogeneidad de las escalas documentada en el análisis descriptivo: el precio se expresa en millones de pesos y alcanza valores del orden de dos mil, mientras que el número de baños o habitaciones no supera las dos cifras. Sin estandarizar, las variables de mayor magnitud dominarían por completo las componentes, no por su relevancia sino por su unidad de medida. La estandarización iguala el peso inicial de todas las variables y tiene el efecto adicional de atenuar la influencia de los valores extremos.
# prcomp calcula las componentes principales.
# scale. = TRUE estandariza las variables antes del cálculo.
acp <- prcomp(datos_acp, scale. = TRUE)
# Tabla de varianza explicada.
# acp$sdev son las desviaciones estándar de cada componente;
# al elevarlas al cuadrado se obtiene la varianza (el autovalor).
varianza <- data.frame(
Componente = paste0("CP", 1:length(acp$sdev)),
Autovalor = round(acp$sdev^2, 3),
Prop = round(100 * acp$sdev^2 / sum(acp$sdev^2), 2),
Acumulada = round(100 * cumsum(acp$sdev^2) / sum(acp$sdev^2), 2)
)
kable(varianza, caption = "Tabla 15. Varianza explicada por componente") %>%
kable_styling(full_width = FALSE)
| Componente | Autovalor | Prop | Acumulada |
|---|---|---|---|
| CP1 | 3.415 | 56.92 | 56.92 |
| CP2 | 1.259 | 20.99 | 77.91 |
| CP3 | 0.460 | 7.66 | 85.57 |
| CP4 | 0.435 | 7.24 | 92.82 |
| CP5 | 0.241 | 4.02 | 96.83 |
| CP6 | 0.190 | 3.17 | 100.00 |
# Gráfico de sedimentación: barras = varianza de cada componente,
# línea roja = varianza acumulada.
ggplot(varianza, aes(x = Componente, y = Prop, group = 1)) +
geom_col(fill = "steelblue") +
geom_line(aes(y = Acumulada), color = "darkred", linewidth = 1) +
geom_point(aes(y = Acumulada), color = "darkred") +
geom_hline(yintercept = 100 / ncol(datos_acp), linetype = "dashed") +
labs(title = "Fig 5. Gráfico de sedimentación",
subtitle = "La línea punteada marca el criterio de Kaiser (autovalor = 1)",
x = "", y = "% de varianza") +
theme_minimal()
La decisión sobre cuántas componentes conservar se apoya en tres criterios de uso extendido, que conviene contrastar entre sí:
# Cargas: peso de cada variable original en cada componente.
# Valores altos en magnitud (positivos o negativos) indican que la variable
# contribuye fuertemente a esa componente.
round(acp$rotation, 3) %>%
kable(caption = "Tabla 16. Cargas de las variables en cada componente") %>%
kable_styling(full_width = FALSE)
| PC1 | PC2 | PC3 | PC4 | PC5 | PC6 | |
|---|---|---|---|---|---|---|
| estrato | 0.331 | -0.582 | 0.555 | 0.077 | 0.461 | -0.159 |
| preciom | 0.478 | -0.188 | -0.036 | -0.361 | -0.220 | 0.746 |
| areaconst | 0.442 | 0.239 | -0.288 | -0.618 | 0.279 | -0.453 |
| parqueaderos | 0.410 | -0.261 | -0.657 | 0.571 | 0.048 | -0.070 |
| banios | 0.467 | 0.192 | 0.363 | 0.185 | -0.679 | -0.344 |
| habitaciones | 0.285 | 0.681 | 0.211 | 0.349 | 0.445 | 0.300 |
Las cargas indican el peso de cada variable original en la construcción de cada componente. Su lectura permite asignar un significado sustantivo a dimensiones que en principio son constructos matemáticos: una componente en la que todas las variables cargan con el mismo signo y magnitud similar representa un efecto de tamaño general, mientras que una en la que unas variables cargan positivamente y otras negativamente representa un contraste entre atributos.
# Representación de las variables en el plano de las dos primeras componentes.
# Cada flecha es una variable: su dirección indica con qué componente se
# asocia y su longitud, cuán bien representada queda en este plano.
cargas <- as.data.frame(acp$rotation[, 1:2])
cargas$variable <- rownames(cargas)
ggplot(cargas, aes(x = PC1, y = PC2)) +
geom_segment(aes(x = 0, y = 0, xend = PC1, yend = PC2),
arrow = arrow(length = unit(0.25, "cm")), color = "steelblue") +
geom_text(aes(label = variable), vjust = -0.6, size = 3.5) +
geom_hline(yintercept = 0, linetype = "dashed", color = "grey60") +
geom_vline(xintercept = 0, linetype = "dashed", color = "grey60") +
labs(title = "Fig 6. Variables en el plano de las dos primeras componentes",
x = "Componente 1", y = "Componente 2") +
theme_minimal()
En este gráfico, las variables representadas por flechas próximas entre sí se encuentran correlacionadas positivamente; las que apuntan en direcciones opuestas, negativamente; y las perpendiculares resultan independientes. La longitud de cada flecha indica el grado en que la variable queda representada en el plano de las dos primeras componentes.
El análisis de conglomerados agrupa observaciones en segmentos de modo que las propiedades pertenecientes a un mismo grupo resulten similares entre sí y diferentes de las de los demás grupos. A diferencia de las técnicas de clasificación supervisada, no parte de categorías conocidas: los segmentos emergen de la estructura de los propios datos.
Se emplea el algoritmo de k-medias, que procede de forma iterativa. Fijado un número k de grupos, el algoritmo sitúa k centros iniciales, asigna cada observación al centro más próximo, recalcula la posición de cada centro como el promedio de las observaciones asignadas, y repite el proceso hasta que las asignaciones se estabilizan. Se selecciona por su eficiencia computacional, adecuada para una base de 8.319 registros, frente a los métodos jerárquicos, que requieren calcular y almacenar la matriz de distancias entre todos los pares de observaciones.
El análisis se aplica sobre las componentes principales retenidas y no sobre las variables originales. Esta decisión obedece a que las distancias euclidianas resultan distorsionadas cuando las variables están correlacionadas: los atributos redundantes se cuentan varias veces y pesan en exceso en la conformación de los grupos. Las componentes principales, al ser ortogonales entre sí, eliminan ese problema.
# Se toman las puntuaciones de las observaciones en las componentes retenidas.
# acp$x contiene las coordenadas de cada vivienda en cada componente.
# AJUSTAR el número 2 según las componentes que se hayan decidido retener.
puntuaciones <- acp$x[, 1:2]
El algoritmo de k-medias requiere fijar de antemano el número de grupos, que no se deriva de los datos sino que constituye una decisión del analista. Para sustentarla se emplea el método del codo, que evalúa cómo disminuye la suma de cuadrados dentro de los grupos a medida que aumenta k. Esta medida siempre decrece al añadir grupos, de modo que el criterio no es minimizarla sino identificar el punto a partir del cual las reducciones adicionales se vuelven marginales.
# set.seed fija la semilla aleatoria para que el resultado sea reproducible:
# k-medias parte de centros aleatorios, y sin semilla el resultado varía.
set.seed(123)
# Se calcula la suma de cuadrados intragrupo para k de 1 a 10.
inercia <- numeric(10)
for (k in 1:10) {
modelo <- kmeans(puntuaciones, centers = k, nstart = 10)
inercia[k] <- modelo$tot.withinss
}
data.frame(k = 1:10, inercia = inercia) %>%
ggplot(aes(x = k, y = inercia)) +
geom_line(color = "steelblue", linewidth = 1) +
geom_point(color = "steelblue", size = 2) +
scale_x_continuous(breaks = 1:10) +
labs(title = "Fig 7. Método del codo",
x = "Número de conglomerados (k)",
y = "Suma de cuadrados intragrupo") +
theme_minimal()
# AJUSTAR centers según el número de conglomerados decidido.
set.seed(123)
modelo_km <- kmeans(puntuaciones, centers = 4, nstart = 25)
# Se incorpora la asignación de cada vivienda a la base.
vivienda_recod$cluster <- factor(modelo_km$cluster)
# Tamaño de cada conglomerado.
vivienda_recod %>%
count(cluster, name = "Viviendas") %>%
mutate(Porcentaje = round(100 * Viviendas / sum(Viviendas), 1)) %>%
kable(caption = "Tabla 17. Tamaño de los conglomerados") %>%
kable_styling(full_width = FALSE)
| cluster | Viviendas | Porcentaje |
|---|---|---|
| 1 | 2634 | 31.7 |
| 2 | 3787 | 45.5 |
| 3 | 869 | 10.4 |
| 4 | 1029 | 12.4 |
La utilidad de una segmentación depende de que los grupos obtenidos admitan una descripción sustantiva. Para ello se examinan los valores típicos de cada atributo en cada conglomerado, empleando la mediana por su robustez frente a los valores extremos conservados.
vivienda_recod %>%
group_by(cluster) %>%
summarise(
n = n(),
estrato = round(median(estrato), 1),
precio = round(median(preciom), 1),
area = round(median(areaconst), 1),
precio_m2 = round(median(precio_m2), 2),
habitaciones = round(median(habitaciones), 1),
banios = round(median(banios), 1),
parqueaderos = round(median(parqueaderos), 1)
) %>%
kable(caption = "Tabla 18. Perfil de los conglomerados (valores medianos)") %>%
kable_styling(full_width = FALSE)
| cluster | n | estrato | precio | area | precio_m2 | habitaciones | banios | parqueaderos |
|---|---|---|---|---|---|---|---|---|
| 1 | 2634 | 5 | 450 | 149 | 3.19 | 3 | 3 | 2 |
| 2 | 3787 | 4 | 210 | 77 | 2.56 | 3 | 2 | 1 |
| 3 | 869 | 4 | 400 | 280 | 1.45 | 6 | 4 | 1 |
| 4 | 1029 | 6 | 1050 | 348 | 2.92 | 4 | 5 | 3 |
# Representación de las viviendas en el plano de las dos primeras componentes,
# coloreadas según el conglomerado al que fueron asignadas.
data.frame(
CP1 = acp$x[, 1],
CP2 = acp$x[, 2],
cluster = vivienda_recod$cluster
) %>%
ggplot(aes(x = CP1, y = CP2, color = cluster)) +
geom_point(alpha = 0.3, size = 0.8) +
labs(title = "Fig 8. Conglomerados en el plano de las componentes principales",
x = "Componente 1", y = "Componente 2", color = "Grupo") +
theme_minimal()
# Distribución de los conglomerados por zona.
table(vivienda_recod$cluster, vivienda_recod$zona) %>%
kable(caption = "Tabla 19. Conglomerados según zona de la ciudad") %>%
kable_styling(full_width = FALSE)
| Zona Centro | Zona Norte | Zona Oeste | Zona Oriente | Zona Sur |
|---|---|---|---|---|
| 2 | 533 | 694 | 3 | 1402 |
| 71 | 1080 | 166 | 194 | 2276 |
| 50 | 197 | 45 | 153 | 424 |
| 1 | 110 | 293 | 1 | 624 |
# Distribución de los conglomerados por tipo de vivienda.
table(vivienda_recod$cluster, vivienda_recod$tipo) %>%
kable(caption = "Tabla 20. Conglomerados según tipo de vivienda") %>%
kable_styling(full_width = FALSE)
| Apartamento | Casa |
|---|---|
| 1762 | 872 |
| 3008 | 779 |
| 17 | 852 |
| 313 | 716 |
El cruce de los conglomerados con las variables de localización y tipo permite establecer si los segmentos identificados presentan una expresión territorial diferenciada, aspecto de interés directo para las decisiones de inversión y comercialización de la empresa.
El análisis de correspondencia examina la asociación entre variables categóricas a partir de una tabla de contingencia, representando sus categorías en un espacio de pocas dimensiones. Su lógica es análoga a la del análisis de componentes principales, pero opera sobre frecuencias en lugar de sobre varianzas: descompone la inercia de la tabla, medida que cuantifica cuánto se apartan las frecuencias observadas de las que cabría esperar si las variables fueran independientes.
El resultado es un mapa en el que la proximidad entre categorías de una misma variable indica perfiles similares, y la proximidad entre categorías de variables distintas indica asociación. La técnica es descriptiva: no contrasta hipótesis, sino que revela la estructura de las relaciones. Por ello suele acompañarse de la prueba chi-cuadrado, que sí permite establecer si la asociación observada es estadísticamente significativa.
En este estudio se aplica a las variables tipo,
zona y barrio, con el propósito de identificar
patrones de localización de la oferta según el tipo de inmueble.
# Ejecutar UNA VEZ en la consola, no en el documento:
# install.packages("ca")
library(ca)
# Tabla de contingencia: frecuencias observadas de cada combinación.
tabla_tz <- table(vivienda_recod$tipo, vivienda_recod$zona)
tabla_tz %>%
kable(caption = "Tabla 21. Distribución de la oferta por tipo y zona") %>%
kable_styling(full_width = FALSE)
| Zona Centro | Zona Norte | Zona Oeste | Zona Oriente | Zona Sur | |
|---|---|---|---|---|---|
| Apartamento | 24 | 1198 | 1029 | 62 | 2787 |
| Casa | 100 | 722 | 169 | 289 | 1939 |
# Perfiles fila: composición porcentual de cada tipo entre las zonas.
# round(prop.table(tabla, 1), 3) calcula porcentajes por fila.
round(100 * prop.table(tabla_tz, 1), 1) %>%
kable(caption = "Tabla 22. Perfiles fila: distribución zonal de cada tipo (%)") %>%
kable_styling(full_width = FALSE)
| Zona Centro | Zona Norte | Zona Oeste | Zona Oriente | Zona Sur | |
|---|---|---|---|---|---|
| Apartamento | 0.5 | 23.5 | 20.2 | 1.2 | 54.6 |
| Casa | 3.1 | 22.4 | 5.3 | 9.0 | 60.2 |
# Prueba chi-cuadrado de independencia.
chisq.test(tabla_tz)
##
## Pearson's Chi-squared test
##
## data: tabla_tz
## X-squared = 690.93, df = 4, p-value < 2.2e-16
La prueba chi-cuadrado contrasta la hipótesis nula de independencia entre las dos variables. Debe advertirse que, con un tamaño muestral de 8.319 registros, la prueba detecta como significativas asociaciones de magnitud reducida, por lo que el resultado se interpreta junto con los perfiles de la Tabla 22 y no de manera aislada.
Cabe señalar una limitación de orden técnico. El número máximo de
dimensiones que puede extraer un análisis de correspondencia equivale al
menor valor entre el número de filas menos uno y el número de columnas
menos uno. Dado que tipo presenta únicamente dos
categorías, el análisis de esta tabla admite una sola dimensión,
insuficiente para construir un mapa factorial. La asociación entre tipo
y zona se interpreta, por tanto, mediante los perfiles porcentuales y la
prueba chi-cuadrado.
Para obtener una representación factorial que admita interpretación
bidimensional se requiere que ambas variables presenten al menos tres
categorías. Se analiza en consecuencia la relación entre
barrio y estrato, que permite caracterizar la
composición socioeconómica de la oferta por localización.
# Número de barrios distintos en la base.
n_distinct(vivienda_recod$barrio)
## [1] 436
Dado el elevado número de barrios, incluirlos todos produciría un mapa ilegible y otorgaría el mismo peso visual a barrios con centenares de registros y a otros con unos pocos. Se restringe el análisis a los quince barrios con mayor número de propiedades ofertadas, que concentran una proporción sustantiva de la oferta y resultan relevantes para las decisiones comerciales de la empresa.
# Se identifican los 15 barrios con más registros.
top_barrios <- vivienda_recod %>%
count(barrio, sort = TRUE) %>%
head(15) %>%
pull(barrio)
# Se filtra la base a esos barrios y se construye la tabla de contingencia.
datos_ca <- vivienda_recod %>%
filter(barrio %in% top_barrios)
tabla_be <- table(datos_ca$barrio, datos_ca$estrato)
tabla_be %>%
kable(caption = "Tabla 23. Oferta por barrio y estrato (quince barrios principales)") %>%
kable_styling(full_width = FALSE)
| 3 | 4 | 5 | 6 | |
|---|---|---|---|---|
| acopi | 21 | 51 | 59 | 27 |
| aguacatal | 20 | 24 | 4 | 61 |
| ciudad jardín | 2 | 4 | 66 | 444 |
| el caney | 9 | 165 | 34 | 0 |
| el ingenio | 0 | 9 | 156 | 37 |
| el limonar | 6 | 36 | 91 | 2 |
| el refugio | 5 | 87 | 28 | 0 |
| la flora | 1 | 33 | 326 | 6 |
| la hacienda | 2 | 29 | 132 | 1 |
| los cristales | 0 | 3 | 44 | 107 |
| normandía | 1 | 0 | 22 | 131 |
| pance | 0 | 2 | 20 | 387 |
| prados del norte | 3 | 74 | 49 | 0 |
| santa teresita | 0 | 4 | 27 | 231 |
| valle del lili | 4 | 576 | 423 | 5 |
# Proporción de la oferta total que representan estos barrios.
round(100 * nrow(datos_ca) / nrow(vivienda_recod), 1)
## [1] 49.2
# ca() calcula el análisis de correspondencia.
ca_be <- ca(tabla_be)
# summary muestra la inercia de cada dimensión y las coordenadas.
summary(ca_be)
##
## Principal inertias (eigenvalues):
##
## dim value % cum% scree plot
## 1 0.740922 71.2 71.2 ******************
## 2 0.223668 21.5 92.6 *****
## 3 0.076716 7.4 100.0 **
## -------- -----
## Total: 1.041306 100.0
##
##
## Rows:
## name mass qlt inr k=1 cor ctr k=2 cor ctr
## 1 | acop | 39 183 31 | -339 138 6 | -194 45 6 |
## 2 | agct | 27 303 50 | 456 107 7 | -616 196 45 |
## 3 | cddj | 126 996 139 | 1071 996 195 | -25 1 0 |
## 4 | elcn | 51 999 74 | -872 499 52 | -873 500 173 |
## 5 | elng | 49 999 36 | -238 76 4 | 832 924 153 |
## 6 | ellm | 33 896 20 | -637 643 18 | 399 253 23 |
## 7 | elrf | 29 1000 34 | -846 593 28 | -701 407 64 |
## 8 | lflr | 89 998 105 | -574 269 40 | 944 728 356 |
## 9 | lhcn | 40 994 35 | -626 428 21 | 719 566 93 |
## 10 | lscr | 38 993 22 | 749 935 28 | 186 57 6 |
## 11 | nrmn | 38 999 40 | 1056 999 57 | 0 0 0 |
## 12 | panc | 100 992 149 | 1235 982 206 | -127 10 7 |
## 13 | prds | 31 995 22 | -796 862 26 | -314 134 14 |
## 14 | sntt | 64 992 76 | 1107 989 106 | -63 3 1 |
## 15 | vlld | 246 948 167 | -785 874 205 | -228 74 57 |
##
## Columns:
## name mass qlt inr k=1 cor ctr k=2 cor ctr
## 1 | 3 | 18 128 79 | -284 18 2 | -711 111 41 |
## 2 | 4 | 268 986 273 | -830 649 249 | -598 337 429 |
## 3 | 5 | 362 999 195 | -491 430 118 | 565 570 518 |
## 4 | 6 | 352 999 452 | 1153 993 631 | -89 6 13 |
# Mapa factorial. Los barrios y los estratos se representan en el mismo plano.
plot(ca_be,
main = "Fig 9. Mapa de correspondencia: barrio y estrato",
col = c("steelblue", "darkred"))
En el mapa, los barrios situados próximos entre sí presentan una composición por estrato semejante, y los que aparecen cercanos a un estrato determinado concentran su oferta en ese nivel. La distancia respecto del origen indica el grado en que una categoría se aparta del perfil promedio: las categorías próximas al centro presentan una distribución similar a la del conjunto, mientras que las alejadas corresponden a perfiles distintivos.
El código empleado en la elaboración de este informe se encuentra disponible en el propio documento. Puede visualizarse mediante el botón “Code” situado en el extremo superior derecho de cada bloque, o desplegarse en su totalidad desde el menú “Code” del encabezado del documento.
# Versiones de R y de los paquetes empleados, para garantizar
# la reproducibilidad del análisis.
sessionInfo()
## R version 4.5.2 (2025-10-31 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
##
## Matrix products: default
## LAPACK version 3.12.1
##
## locale:
## [1] LC_COLLATE=Spanish_Colombia.utf8 LC_CTYPE=Spanish_Colombia.utf8
## [3] LC_MONETARY=Spanish_Colombia.utf8 LC_NUMERIC=C
## [5] LC_TIME=Spanish_Colombia.utf8
##
## time zone: America/Bogota
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] ca_0.71.1 kableExtra_1.4.1 dplyr_1.2.0
## [4] paqueteMODELOS_0.1.0 summarytools_1.1.5 knitr_1.51
## [7] gridExtra_2.3.1 GGally_2.4.0 ggplot2_4.0.2
## [10] broom_1.0.13 boot_1.3-32
##
## loaded via a namespace (and not attached):
## [1] sass_0.4.10 generics_0.1.4 tidyr_1.3.2 xml2_1.5.2
## [5] tcltk_4.5.2 stringi_1.8.7 digest_0.6.39 magrittr_2.0.4
## [9] evaluate_1.0.5 grid_4.5.2 timechange_0.4.0 RColorBrewer_1.1-3
## [13] fastmap_1.2.0 plyr_1.8.9 jsonlite_2.0.0 backports_1.5.0
## [17] rapportools_1.2 purrr_1.2.1 pander_0.6.6 viridisLite_0.4.3
## [21] scales_1.4.0 textshaping_1.0.5 jquerylib_0.1.4 cli_3.6.6
## [25] rlang_1.3.0 base64enc_0.1-6 withr_3.0.2 cachem_1.1.0
## [29] yaml_2.3.12 otel_0.2.0 tools_4.5.2 reshape2_1.4.5
## [33] checkmate_2.3.4 ggstats_0.13.0 vctrs_0.7.1 R6_2.6.1
## [37] matrixStats_1.5.0 lifecycle_1.0.5 lubridate_1.9.5 magick_2.9.1
## [41] stringr_1.6.0 MASS_7.3-65 pkgconfig_2.0.3 pillar_1.11.1
## [45] bslib_0.10.0 gtable_0.3.6 glue_1.8.0 Rcpp_1.1.1
## [49] systemfonts_1.3.2 xfun_0.56 tibble_3.3.1 tidyselect_1.2.1
## [53] rstudioapi_0.18.0 farver_2.1.2 htmltools_0.5.9 labeling_0.4.3
## [57] svglite_2.2.2 rmarkdown_2.30 compiler_4.5.2 S7_0.2.1