Este informe analiza 8.322 ofertas de vivienda publicadas en el portal OLX para la ciudad de Cali, con el fin de caracterizar la estructura del mercado de oferta y apoyar las decisiones de compra, venta y valoración de una empresa inmobiliaria. Se aplicaron tres técnicas de análisis multivariante: componentes principales, conglomerados y correspondencias múltiples.
El precio de oferta se explica principalmente por el tamaño del inmueble. El área construida, el número de baños y la disponibilidad de parqueaderos acompañan el precio con mayor intensidad que el número de habitaciones, atributo que suele destacarse en los anuncios.
La oferta se organiza en dos segmentos. El primero, que reúne el 29,9 % de los inmuebles, corresponde a vivienda amplia con precio mediano de 695 millones y predominio de casas. El segundo, con el 70,1 % restante, corresponde a vivienda compacta con precio mediano de 260 millones y predominio de apartamentos. El precio por metro cuadrado resulta ligeramente superior en el segmento compacto.
Ambos segmentos conviven en las mismas zonas de la ciudad. La segmentación por características del inmueble no coincide con su localización, de modo que la zona no permite anticipar el tipo de producto que se encontrará en ella. La estructura territorial, en cambio, sí ordena la oferta según el estrato socioeconómico.
Los datos corresponden a precios solicitados por los vendedores y no a operaciones cerradas, por lo que los resultados describen la estructura de la oferta publicada y no el valor de mercado de los inmuebles.
Apoyar las decisiones de compra, venta y valoración de una empresa inmobiliaria, mediante la caracterización de la estructura del mercado de oferta de vivienda urbana con técnicas de análisis multivariante.
Determinar qué atributos concentran la variabilidad de la oferta mediante análisis de componentes principales, a partir de una base previamente depurada.
Segmentar la oferta en grupos homogéneos y caracterizar cada segmento según su precio, dimensiones y localización.
Establecer los patrones de asociación entre zona, tipo de vivienda y estrato mediante análisis de correspondencias múltiples, incorporando el barrio como variable ilustrativa.
Formular recomendaciones de captación, comercialización y valoración para la empresa, señalando los límites de lo que los datos permiten concluir.
El presente informe analiza los datos de una base extraída mediante webscraping de OLX, plataforma en línea de anuncios de vivienda. La base se distribuye en el paquete paqueteMODELOS y contiene 8.322 ofertas de vivienda en Cali, descritas por 13 variables y georreferenciadas por longitud y latitud.
Conviene precisar qué tipo de información recogen estos registros. Se trata de inmuebles publicados y no de operaciones cerradas, de modo que preciom corresponde al valor que solicita el vendedor y no al precio en que finalmente se negocia la vivienda. La base permite entonces establecer en qué rangos se está pidiendo por inmuebles con determinadas características, pero no estimar cuánto vale realmente un inmueble particular.
A continuación se construye el diccionario de las variables que conforman la base de datos, en el que se señala el tipo de dato, la escala de medición, la unidad en que está expresada, el número de valores distintos que toman y el conteo de registros faltantes. La escala de medición se incluye de manera explícita porque de ella depende el tratamiento estadístico que cada variable admite.
meta <- data.frame(
Variable = c("id", "zona", "piso", "estrato", "preciom", "areaconst",
"parqueaderos", "banios", "habitaciones", "tipo", "barrio",
"longitud", "latitud"),
Escala = c("Nominal (identificador)", "Nominal", "Ordinal", "Ordinal",
"Razón (continua)", "Razón (continua)", "Razón (discreta)",
"Razón (discreta)", "Razón (discreta)", "Nominal", "Nominal",
"Intervalo (coordenada)", "Intervalo (coordenada)"),
Unidad = c("—", "—", "n.º de piso", "categoría", "millones COP", "m²",
"conteo", "conteo", "conteo", "—", "—",
"grados decimales", "grados decimales"),
Descripcion = c(
"Identificador del registro",
"Zona de la ciudad donde se ubica el inmueble",
"Piso de ubicación del inmueble",
"Estrato socioeconómico del predio",
"Precio de oferta del inmueble",
"Área construida",
"Número de parqueaderos",
"Número de baños",
"Número de habitaciones",
"Tipo de vivienda",
"Barrio de ubicación",
"Coordenada de longitud",
"Coordenada de latitud")
)
# sapply() aplica una función a cada elemento de una lista o data frame
# y simplifica el resultado a vector. Aquí recorre las columnas de la base
# y devuelve, de una sola vez, tres atributos de cada variable.
# class(x)[1] toma solo la primera clase declarada: los tibbles importados
# arrastran varias clases por columna y sin el [1] la tabla saldría rota.
meta$`Tipo en R` <- sapply(vivienda[meta$Variable], function(x) class(x)[1])
# unique() lista los valores distintos; na.omit() los depura de faltantes
# antes de contarlos, para que NA no se cuente como un nivel más.
meta$Niveles <- sapply(vivienda[meta$Variable], function(x) length(unique(na.omit(x))))
# is.na() devuelve TRUE/FALSE por celda; sum() cuenta los TRUE.
meta$Faltantes <- sapply(vivienda[meta$Variable], function(x) sum(is.na(x)))
meta$`% NA` <- round(100 * meta$Faltantes / nrow(vivienda), 2)
diccionario <- meta[, c("Variable", "Tipo en R", "Escala", "Unidad",
"Niveles", "Faltantes", "% NA", "Descripcion")]
tabla(diccionario, caption = "Tabla 1. Diccionario de variables de la base vivienda")
| Variable | Tipo en R | Escala | Unidad | Niveles | Faltantes | % NA | Descripcion |
|---|---|---|---|---|---|---|---|
| id | numeric | Nominal (identificador) | — | 8,319 | 3 | 0.04 | Identificador del registro |
| zona | character | Nominal | — | 5 | 3 | 0.04 | Zona de la ciudad donde se ubica el inmueble |
| piso | character | Ordinal | n.º de piso | 12 | 2,638 | 31.70 | Piso de ubicación del inmueble |
| estrato | numeric | Ordinal | categoría | 4 | 3 | 0.04 | Estrato socioeconómico del predio |
| preciom | numeric | Razón (continua) | millones COP | 539 | 2 | 0.02 | Precio de oferta del inmueble |
| areaconst | numeric | Razón (continua) | m² | 652 | 3 | 0.04 | Área construida |
| parqueaderos | numeric | Razón (discreta) | conteo | 10 | 1,605 | 19.29 | Número de parqueaderos |
| banios | numeric | Razón (discreta) | conteo | 11 | 3 | 0.04 | Número de baños |
| habitaciones | numeric | Razón (discreta) | conteo | 11 | 3 | 0.04 | Número de habitaciones |
| tipo | character | Nominal | — | 2 | 3 | 0.04 | Tipo de vivienda |
| barrio | character | Nominal | — | 436 | 3 | 0.04 | Barrio de ubicación |
| longitud | numeric | Intervalo (coordenada) | grados decimales | 2,928 | 3 | 0.04 | Coordenada de longitud |
| latitud | numeric | Intervalo (coordenada) | grados decimales | 3,679 | 3 | 0.04 | Coordenada de latitud |
A continuación se examina la distribución de las variables categóricas de la base, reportando para cada categoría su frecuencia y el porcentaje que representa sobre el total de registros. Esta revisión se realiza porque la frecuencia de cada categoría condiciona su tratamiento en el análisis de correspondencias múltiples.
# --- Distribución de las variables categóricas --
resumen_cat <- function(x, nombre) {
t <- table(x, useNA = "ifany") # frecuencias; useNA fuerza a mostrar los NA
data.frame(
Variable = nombre,
Categoria = names(t), # nombres de las categorías
Frecuencia = as.integer(t), # conteos
Porcentaje = round(100 * as.integer(t) / length(x), 1),
row.names = NULL
)
}
# rbind() apila data frames con las mismas columnas, uno debajo del otro.
cat_resumen <- rbind(
resumen_cat(vivienda$zona, "Zona"),
resumen_cat(vivienda$tipo, "Tipo"),
resumen_cat(vivienda$estrato, "Estrato")
)
tabla(cat_resumen, caption = "Tabla 2. Distribución de las variables categóricas")
| Variable | Categoria | Frecuencia | Porcentaje |
|---|---|---|---|
| Zona | Zona Centro | 124 | 1.5 |
| Zona | Zona Norte | 1,920 | 23.1 |
| Zona | Zona Oeste | 1,198 | 14.4 |
| Zona | Zona Oriente | 351 | 4.2 |
| Zona | Zona Sur | 4,726 | 56.8 |
| Zona | NA | 3 | 0.0 |
| Tipo | Apartamento | 5,100 | 61.3 |
| Tipo | Casa | 3,219 | 38.7 |
| Tipo | NA | 3 | 0.0 |
| Estrato | 3 | 1,453 | 17.5 |
| Estrato | 4 | 2,129 | 25.6 |
| Estrato | 5 | 2,750 | 33.0 |
| Estrato | 6 | 1,987 | 23.9 |
| Estrato | NA | 3 | 0.0 |
La base presenta datos faltantes concentrados en dos variables. En lugar de eliminar los registros incompletos o imputar los valores ausentes de manera uniforme, se examinó cada variable por separado, con el propósito de identificar su naturaleza, dado que un campo vacío no siempre significa lo mismo: en unos casos corresponde a información no reportada y en otros a una característica que el anuncio no registra. ## Datos faltantes
A continuación se examina la distribución de los registros según el número de campos faltantes que presentan.
# La Tabla 1 cuenta faltantes por variable; aquí se clasifican los registros
# según cuántas variables les faltan, que es la unidad sobre la que recae
# la decisión de eliminar.
vacios <- rowSums(is.na(vivienda)) # campos vacíos de cada registro
# cut() convierte un vector numérico en categorías definidas por cortes.
# breaks fija los límites y labels les pone nombre; right = FALSE hace que
# cada intervalo incluya su límite inferior y excluya el superior.
estado <- cut(vacios,
breaks = c(0, 1, 2, 3, 14),
labels = c("Completo",
"Falta una variable",
"Faltan dos variables",
"Sin información"),
right = FALSE)
completitud <- as.data.frame(table(estado))
names(completitud) <- c("Estado del registro", "Registros")
completitud$Porcentaje <- round(100 * completitud$Registros / nrow(vivienda), 1)
tabla(completitud, caption = "Tabla 3. Estado de completitud de los registros")
| Estado del registro | Registros | Porcentaje |
|---|---|---|
| Completo | 4,808 | 57.8 |
| Falta una variable | 2,785 | 33.5 |
| Faltan dos variables | 726 | 8.7 |
| Sin información | 3 | 0.0 |
Como se observa en la Tabla 3, al examinar los registros de manera individual se encuentra que solo el 57.8% del conjunto de datos cuenta con información en las trece variables. El 42.2% restante presenta al menos un campo vacío, cuyo tratamiento se aborda en los apartados siguientes.
piso presenta 2.638 datos faltantes, el 31,70 % de la base de datos. La ausencia podría explicarse porque las casas no tienen número de piso, pero como se observa en la Tabla 4, esa explicación se descarta: 1.381 apartamentos carecen del dato y 1.965 casas sí lo registran.
cruce_piso <- as.data.frame.matrix(
table(vivienda$tipo, is.na(vivienda$piso), useNA = "ifany")
)
names(cruce_piso) <- c("Con dato", "Sin dato")
cruce_piso <- cbind(`Tipo de vivienda` = rownames(cruce_piso), cruce_piso)
rownames(cruce_piso) <- NULL
cruce_piso$`Tipo de vivienda`[is.na(cruce_piso$`Tipo de vivienda`)] <- "Sin tipo registrado"
tabla(cruce_piso, caption = "Tabla 4. Registros con y sin dato de piso según tipo de vivienda")
| Tipo de vivienda | Con dato | Sin dato |
|---|---|---|
| Apartamento | 3,719 | 1,381 |
| Casa | 1,965 | 1,254 |
| NA. | 0 | 3 |
El dato consignado en las casas indica que piso no mide lo mismo en todos los registros. Por esa razón, y por el porcentaje de faltantes, se toma la decisión de excluir la variable retirando la columna sin eliminar registros.
La variable parqueaderos presenta 1.605 datos faltantes, el 19,29 % de la base de datos. El vacío podría indicar que el inmueble no tiene parqueadero, en lugar de un dato no reportado. Para verificarlo se examinan los valores observados de la variable.
frec_parq <- as.data.frame(table(vivienda$parqueaderos, useNA = "ifany"))
names(frec_parq) <- c("Parqueaderos", "Registros")
frec_parq$Porcentaje <- round(100 * frec_parq$Registros / nrow(vivienda), 1)
frec_parq$Parqueaderos <- as.character(frec_parq$Parqueaderos)
frec_parq$Parqueaderos[is.na(frec_parq$Parqueaderos)] <- "Sin dato"
tabla(frec_parq, caption = "Tabla 5. Distribución de los valores observados de parqueaderos")
| Parqueaderos | Registros | Porcentaje |
|---|---|---|
| 1 | 3,155 | 37.9 |
| 2 | 2,475 | 29.7 |
| 3 | 520 | 6.2 |
| 4 | 384 | 4.6 |
| 5 | 68 | 0.8 |
| 6 | 68 | 0.8 |
| 7 | 18 | 0.2 |
| 8 | 17 | 0.2 |
| 9 | 4 | 0.0 |
| 10 | 8 | 0.1 |
| Sin dato | 1,605 | 19.3 |
La variable toma valores entre 1 y 10 y el cero no aparece en ninguno de los 6.717 registros con dato. Esto indica que el anuncio no dispone de una forma de registrar numéricamente la ausencia de parqueadero, condición que queda representada por el campo vacío.
Los 1.605 datos faltantes se recodifican entonces como cero, decisión que conserva la totalidad de los registros. Se trata de un supuesto sustentado en la codificación observada, no de un hecho verificado.
Estas dos variables registran el valor cero: 45 casos en banios y 66 en habitaciones. Un inmueble residencial no puede carecer de baño, de modo que ese cero corresponde a un campo sin diligenciar. En habitaciones, en cambio, el cero podría ser legítimo si se tratara de apartaestudios, por lo que se examina el detalle de esos registros.
frec_ceros <- rbind(
data.frame(Variable = "Banios", Valor = vivienda$banios),
data.frame(Variable = "Habitaciones", Valor = vivienda$habitaciones)
)
frec_ceros <- subset(frec_ceros, !is.na(Valor))
ggplot(frec_ceros, aes(x = factor(Valor), fill = Valor == 0)) +
geom_bar() +
facet_wrap(~ Variable, ncol = 1, scales = "free_y") +
scale_fill_manual(values = c("FALSE" = AZUL, "TRUE" = NARANJA), guide = "none") +
labs(x = "Valor registrado", y = "Número de registros")
Figura 1. Distribución de banios y habitaciones
Como se observa en la Figura 1, el valor cero aparece aislado en las distribuciones de las dos varibles. Se puede inferir que ese patrón corresponde a un campo que, durante el proceso de web scraping se registró como cero cuando el anuncio lo dejó vacío, y no a un valor efectivamente reportado.
El detalle de los registros descarta además la hipótesis del apartaestudio: de los 66 casos con cero habitaciones, 45 corresponden a casas y 35 presentan también cero baños. Ninguna tipología de vivienda carece de habitaciones y de baños a la vez.
Por lo tanto, se toma la decisión de eliminar los 76 registros con cero en cualquiera de las dos variables.
vivienda_dep <- vivienda
# 1. Se retira piso
vivienda_dep$piso <- NULL
# 2. Los faltantes de parqueaderos se recodifican como cero
vivienda_dep$parqueaderos[is.na(vivienda_dep$parqueaderos)] <- 0
# 3. Se eliminan los registros con cero baños o cero habitaciones
vivienda_dep <- subset(vivienda_dep,
!(banios == 0 | habitaciones == 0) |
is.na(banios) | is.na(habitaciones))
# 4. Se eliminan los registros sin información
vivienda_dep <- vivienda_dep[rowSums(is.na(vivienda_dep)) < 10, ]
balance <- data.frame(
Concepto = c("Registros iniciales",
"Eliminados por cero en banios o habitaciones",
"Eliminados por ausencia de información",
"Registros finales"),
Registros = c(nrow(vivienda), 76, 3, nrow(vivienda_dep))
)
balance$Porcentaje <- round(100 * balance$Registros / nrow(vivienda), 2)
tabla(balance, caption = "Tabla 6. Balance de la depuración")
| Concepto | Registros | Porcentaje |
|---|---|---|
| Registros iniciales | 8,322 | 100.00 |
| Eliminados por cero en banios o habitaciones | 76 | 0.91 |
| Eliminados por ausencia de información | 3 | 0.04 |
| Registros finales | 8,243 | 99.05 |
El tratamiento diferenciado por variable permite conservar 8.243 registros, el 99,05 % del total original. La exclusión de piso se resolvió retirando la columna y no los registros, de modo que los 2.638 inmuebles sin ese dato permanecen en la base con el resto de sus atributos completos. Los 1.605 faltantes de parqueaderos se recodificaron como cero y tampoco supusieron pérdida alguna. Las eliminaciones se limitaron a los 76 registros con valor cero en banios o en habitaciones, que no corresponden a inmuebles reales, y a los 3 sin información generados en la extracción. La eliminación por lista, en cambio, habría descartado el 42,2 % de la base.
Antes de aplicar las técnicas multivariadas se examina el comportamiento de las cinco variables cuantitativas que participarán en el análisis de componentes principales y en el de conglomerados. Ambas técnicas operan sobre distancias calculadas a partir de la varianza, de modo que la dispersión y la asimetría de cada variable condicionan sus resultados.
descriptivos <- data.frame(
Variable = vars_cuant,
Media = sapply(vivienda_dep[vars_cuant], mean, na.rm = TRUE),
Mediana = sapply(vivienda_dep[vars_cuant], median, na.rm = TRUE),
Desv = sapply(vivienda_dep[vars_cuant], sd, na.rm = TRUE),
Minimo = sapply(vivienda_dep[vars_cuant], min, na.rm = TRUE),
Maximo = sapply(vivienda_dep[vars_cuant], max, na.rm = TRUE),
row.names = NULL
)
descriptivos$CV <- round(100 * descriptivos$Desv / descriptivos$Media, 1)
asimetria <- function(x) {
x <- x[!is.na(x)]
mean((x - mean(x))^3) / sd(x)^3
}
descriptivos$Asimetria <- round(sapply(vivienda_dep[vars_cuant], asimetria), 2)
tabla(descriptivos, caption = "Tabla 7. Estadísticos descriptivos de las variables cuantitativas")
| Variable | Media | Mediana | Desv | Minimo | Maximo | CV | Asimetria |
|---|---|---|---|---|---|---|---|
| preciom | 433.27 | 330 | 328.93 | 58 | 1,999 | 75.9 | 1.85 |
| areaconst | 174.04 | 122 | 142.32 | 30 | 1,745 | 81.8 | 2.73 |
| parqueaderos | 1.49 | 1 | 1.24 | 0 | 10 | 83.2 | 1.63 |
| banios | 3.13 | 3 | 1.41 | 1 | 10 | 45.2 | 0.98 |
| habitaciones | 3.63 | 3 | 1.43 | 1 | 10 | 39.3 | 1.82 |
La Tabla 7 evidencia asimetría positiva en las cinco variables. areaconst presenta el coeficiente más alto, con 2,73, seguida de preciom con 1,85. Solo banios, con 0,98, se ubica en un rango moderado.
Esta asimetría se refleja en la relación entre la media y la mediana. En preciom, la media asciende a 433 millones frente a una mediana de 330, y en areaconst a 174 m² frente a 122. En ambos casos el promedio se desplaza por encima del valor típico, de modo que la mediana describe mejor la oferta corriente que la media.
Los valores máximos confirman la presencia de observaciones extremas: 1.999 millones en preciom y 1.745 m² en areaconst, entre seis y catorce veces por encima de sus respectivas medianas. Su tratamiento se aborda en el apartado siguiente.
Nota: el coeficiente de variación de parqueaderos, de 83,2 %, no refleja la dispersión real del atributo en el mercado. La recodificación de los 1.605 datos faltantes como cero reduce la media y amplía la desviación, elevando el coeficiente por ambas vías.
La detección de valores atípicos se realizó atendiendo a la
naturaleza de cada variable. En preciom y
areaconst, continuas y de rango amplio, se aplicó el método
del rango intercuartílico, que considera atípicos los valores situados
por debajo de \(Q_1 - 1{,}5 \times
IQR\) o por encima de \(Q_3 + 1{,}5
\times IQR\), donde
\[IQR = Q_3 - Q_1\]
El método se apoya en cuartiles y no en la media, por lo que no asume normalidad ni se ve afectado por los propios valores extremos que busca identificar.
calc_stats <- function(x, nombre) {
Q1 <- quantile(x, 0.25, na.rm = TRUE)
Q3 <- quantile(x, 0.75, na.rm = TRUE)
IQR_val <- Q3 - Q1
lim_inf <- Q1 - 1.5 * IQR_val
lim_sup <- Q3 + 1.5 * IQR_val
n_atip <- sum(x < lim_inf | x > lim_sup, na.rm = TRUE)
data.frame(
Variable = nombre,
Q1 = Q1, Q3 = Q3, IQR = IQR_val,
`Límite superior` = lim_sup,
Atipicos = n_atip,
`% Atípicos` = round(100 * n_atip / sum(!is.na(x)), 1),
check.names = FALSE, row.names = NULL
)
}
atipicos <- rbind(
calc_stats(vivienda_dep$preciom, "preciom"),
calc_stats(vivienda_dep$areaconst, "areaconst")
)
tabla(atipicos, caption = "Tabla 8. Detección de valores atípicos por el método IQR")
| Variable | Q1 | Q3 | IQR | Límite superior | Atipicos | % Atípicos |
|---|---|---|---|---|---|---|
| preciom | 220 | 540 | 320 | 1,020.0 | 547 | 6.6 |
| areaconst | 80 | 227 | 147 | 447.5 | 428 | 5.2 |
El método identifica 547 registros atípicos en preciom,
el 6,6 % de la base, y 428 en areaconst, el 5,2 %. La
proporción es consistente con la asimetría reportada en la Tabla 7 y se
concentra en la cola derecha de ambas distribuciones.
p1 <- ggplot(vivienda_dep, aes(x = "", y = preciom)) +
geom_boxplot(fill = ACENTO, alpha = 0.7, outlier.colour = NARANJA) +
labs(title = "a) Precio de oferta", y = "Millones COP", x = "")
p2 <- ggplot(vivienda_dep, aes(x = "", y = areaconst)) +
geom_boxplot(fill = ACENTO, alpha = 0.7, outlier.colour = NARANJA) +
labs(title = "b) Área construida", y = "m²", x = "")
p3 <- ggplot(vivienda_dep, aes(x = preciom)) +
geom_histogram(bins = 40, fill = AZUL, colour = "white") +
labs(title = "c) Distribución del precio", x = "Millones COP", y = "Frecuencia")
(p1 + p2) / p3
Figura 2. Distribución de preciom y areaconst
Como se observa en la Figura 2, ambas variables presentan una concentración de observaciones en los valores bajos y una cola derecha extensa. El histograma del precio muestra además un escalonamiento propio de la fuente: los anuncios reportan cifras redondeadas, lo que genera acumulaciones en valores como 250, 300 y 350 millones.
La revisión de los registros situados por encima del límite superior descarta que se trate de errores de captura. Los registros examinados corresponden a inmuebles de estratos 5 y 6, ubicados principalmente en la Zona Oeste y la Zona Norte, con áreas construidas amplias y entre tres y seis baños. El perfil es internamente coherente y corresponde al segmento alto del mercado.
Por consiguiente, estos registros se conservan. Su eliminación supondría excluir el segmento de mayor valor de la oferta, precisamente uno de los que el análisis busca caracterizar, y resultaría inconsistente con el criterio adoptado en la depuración, donde solo se eliminaron los registros que no correspondían a inmuebles reales.
Nota: la conservación de estos valores tiene una consecuencia para las técnicas siguientes. El análisis de componentes principales y el de conglomerados operan sobre distancias euclidianas, en las que los valores extremos ejercen una influencia desproporcionada. La estandarización por puntaje Z no corrige esa influencia, dado que la desviación estándar empleada como divisor se encuentra a su vez elevada por dichos valores. La conveniencia de aplicar una transformación logarítmica se evalúa en el apartado de componentes principales.
En parqueaderos, banios y
habitaciones el método del rango intercuartílico no resulta
apropiado. Al tomar entre diez y once valores distintos, su recorrido es
corto y el criterio clasificaría como atípica una proporción
considerable de observaciones sin que ello represente una anomalía real.
Estas variables se examinan mediante tablas de frecuencia.
frec_discretas <- rbind(
resumen_cat(vivienda_dep$parqueaderos, "parqueaderos"),
resumen_cat(vivienda_dep$banios, "banios"),
resumen_cat(vivienda_dep$habitaciones, "habitaciones")
)
tabla(frec_discretas, caption = "Tabla 9. Distribución de frecuencias de las variables discretas")
| Variable | Categoria | Frecuencia | Porcentaje |
|---|---|---|---|
| parqueaderos | 0 | 1,555 | 18.9 |
| parqueaderos | 1 | 3,144 | 38.1 |
| parqueaderos | 2 | 2,462 | 29.9 |
| parqueaderos | 3 | 519 | 6.3 |
| parqueaderos | 4 | 382 | 4.6 |
| parqueaderos | 5 | 68 | 0.8 |
| parqueaderos | 6 | 67 | 0.8 |
| parqueaderos | 7 | 18 | 0.2 |
| parqueaderos | 8 | 17 | 0.2 |
| parqueaderos | 9 | 4 | 0.0 |
| parqueaderos | 10 | 7 | 0.1 |
| banios | 1 | 494 | 6.0 |
| banios | 2 | 2,935 | 35.6 |
| banios | 3 | 1,988 | 24.1 |
| banios | 4 | 1,448 | 17.6 |
| banios | 5 | 888 | 10.8 |
| banios | 6 | 312 | 3.8 |
| banios | 7 | 107 | 1.3 |
| banios | 8 | 47 | 0.6 |
| banios | 9 | 15 | 0.2 |
| banios | 10 | 9 | 0.1 |
| habitaciones | 1 | 58 | 0.7 |
| habitaciones | 2 | 926 | 11.2 |
| habitaciones | 3 | 4,092 | 49.6 |
| habitaciones | 4 | 1,728 | 21.0 |
| habitaciones | 5 | 678 | 8.2 |
| habitaciones | 6 | 318 | 3.9 |
| habitaciones | 7 | 172 | 2.1 |
| habitaciones | 8 | 138 | 1.7 |
| habitaciones | 9 | 83 | 1.0 |
| habitaciones | 10 | 50 | 0.6 |
La Tabla 9 muestra distribuciones concentradas en los valores bajos, con modas en un parqueadero, dos baños y tres habitaciones. Las categorías superiores presentan frecuencias reducidas pero decrecientes de forma gradual, sin discontinuidades que sugieran errores de captura.
La aplicación del método IQR resultaría inadecuada en estas
variables. En habitaciones, con \(Q_1 = 3\) y \(Q_3
= 4\), el límite superior se situaría en 5,5 y clasificaría como
atípicos 761 registros, el 9,2 % de la base, entre los que se encuentran
viviendas de seis o más habitaciones que corresponden a inmuebles
corrientes en los estratos altos.
En consecuencia, los valores de las tres variables se conservan en su totalidad.
El análisis de componentes principales se construye sobre la matriz de correlaciones. El coeficiente de Pearson, empleado habitualmente para estimarla, se calcula a partir de la media y la desviación estándar, por lo que supone que las variables siguen una distribución normal. De no cumplirse ese supuesto, corresponde emplear un coeficiente no paramétrico como el de Spearman.
La verificación se realiza mediante la prueba de Anderson-Darling. La elección obedece a dos razones. En primer lugar, la prueba de Shapiro-Wilk, de uso frecuente, admite un máximo de 5.000 observaciones y la base cuenta con 8.243. En segundo lugar, el estadístico de Anderson-Darling pondera con mayor peso el comportamiento de las colas de la distribución, que es precisamente donde las variables de este conjunto presentan mayor apartamiento, según lo evidenciado por los coeficientes de asimetría de la Tabla 7.
Las hipótesis contrastadas son:
\[H_0: \text{la variable sigue una distribución normal}\]
\[H_1: \text{la variable no sigue una distribución normal}\]
Se adopta un nivel de significancia de 0,05.
normalidad <- data.frame(
Variable = vars_cuant,
Estadistico = sapply(vivienda_dep[vars_cuant],
function(x) round(nortest::ad.test(x)$statistic, 2)),
`Valor p` = sapply(vivienda_dep[vars_cuant],
function(x) format.pval(nortest::ad.test(x)$p.value,
digits = 3, eps = 0.001)),
check.names = FALSE, row.names = NULL
)
tabla(normalidad, caption = "Tabla 10. Prueba de normalidad de Anderson-Darling")
| Variable | Estadistico | Valor p |
|---|---|---|
| preciom | 455.88 | <0.001 |
| areaconst | 505.25 | <0.001 |
| parqueaderos | 401.73 | <0.001 |
| banios | 334.81 | <0.001 |
| habitaciones | 652.60 | <0.001 |
La Tabla 10 evidencia el rechazo de la hipótesis nula en las cinco
variables, con valores p inferiores a 0,001. Los estadísticos oscilan
entre 334,81 en banios y 652,60 en
habitaciones, muy por encima del valor crítico de
referencia, que se sitúa alrededor de 0,75 para un nivel de
significancia de 0,05. Ninguna de las cinco variables cuantitativas
sigue una distribución normal.
Con 8.243 observaciones, cualquier prueba de normalidad tiende a
rechazar la hipótesis nula, incluso cuando el apartamiento es pequeño.
Por eso el resultado se contrasta con los coeficientes de asimetría de
la Tabla 7: 1,85 en preciom y 2,73 en
areaconst. Ambas medidas coinciden, de modo que el rechazo
no es un efecto del tamaño de la muestra.
La matriz de correlaciones se estima entonces con el coeficiente de Spearman, que no exige normalidad porque se calcula sobre los rangos de las observaciones y no sobre sus valores.
Trabajar con rangos aporta además una ventaja frente a los valores atípicos conservados en el apartado anterior. Un inmueble de 1.999 millones ocupa la última posición del ordenamiento, con independencia de qué tan alejado esté del resto, de modo que su magnitud no distorsiona la estimación.
El análisis de componentes principales opera reduciendo la información redundante entre variables, de modo que su aplicación solo resulta pertinente cuando existe correlación entre ellas. Se examina entonces la matriz de correlaciones de las cinco variables cuantitativas, estimada mediante el coeficiente de Spearman conforme a lo establecido en el apartado anterior.
mat_cor <- cor(vivienda_dep[vars_cuant], method = "spearman", use = "complete.obs")
tabla(round(mat_cor, 3), caption = "Tabla 11. Matriz de correlaciones de Spearman")
| preciom | areaconst | parqueaderos | banios | habitaciones | |
|---|---|---|---|---|---|
| preciom | 1.00 | 0.82 | 0.67 | 0.78 | 0.44 |
| areaconst | 0.82 | 1.00 | 0.52 | 0.79 | 0.67 |
| parqueaderos | 0.67 | 0.52 | 1.00 | 0.55 | 0.24 |
| banios | 0.78 | 0.79 | 0.55 | 1.00 | 0.62 |
| habitaciones | 0.44 | 0.67 | 0.24 | 0.62 | 1.00 |
corrplot(mat_cor, method = "ellipse", type = "upper",
addCoef.col = "black", tl.col = AZUL, number.cex = 0.8)
Figura 3. Matriz de correlaciones de las variables cuantitativas
La Tabla 11 evidencia correlaciones positivas entre las cinco variables, con valores que oscilan entre 0,24 y 0,82. La existencia de asociación entre ellas es la condición que justifica la aplicación del análisis de componentes principales.
El precio de oferta se asocia principalmente con el tamaño del
inmueble. La correlación más alta corresponde a preciom con
areaconst, con 0,82, seguida de banios con
0,78 y parqueaderos con 0,67. La asociación con
habitaciones, de 0,44, resulta considerablemente menor.
Como se observa en la Figura 3, la asociación más débil se presenta
entre parqueaderos y habitaciones, con 0,24.
El número de habitaciones responde al tamaño del grupo familiar,
mientras que la disponibilidad de parqueaderos depende de las
características de la construcción, de modo que ambos atributos varían
con relativa independencia.
La matriz de Spearman se emplea para verificar la existencia de asociación entre las variables, que es la condición de aplicación del análisis de componentes principales. El cálculo de los componentes se realiza sobre las variables estandarizadas y no sobre esta matriz.
Conviene precisar el alcance del supuesto de normalidad. El análisis de componentes principales es una técnica descriptiva de reducción de dimensión y no requiere normalidad para su aplicación; el supuesto condiciona la interpretación del coeficiente de Pearson como medida de asociación, razón por la cual la matriz se reporta con el coeficiente de Spearman.
Las cinco variables cuantitativas se estandarizan mediante el puntaje
Z, dado que están expresadas en unidades distintas. Sin esa
transformación preciom, con desviación de 328,93 millones,
dominaría el análisis frente a banios, con desviación de
1,41, por efecto de la escala y no de su relevancia.
\[Z = \frac{X - \bar{X}}{s}\]
No se aplicó la transformación logarítmica anunciada en el apartado
de valores atípicos. La transformación reduciría la asimetría de
preciom y areaconst, pero las cargas de los
componentes quedarían expresadas sobre el logaritmo de esas variables,
lo que dificulta su lectura en términos del mercado. Se conserva
entonces la escala original y se reconoce la influencia de los valores
extremos como una limitación, que se retoma en el apartado final.
vivienda_z <- scale(vivienda_dep[vars_cuant])
acp <- prcomp(vivienda_z)
varianza <- data.frame(
Componente = paste0("PC", 1:5),
`Desviación estándar` = round(acp$sdev, 3),
`Valor propio` = round(acp$sdev^2, 3),
`% Varianza` = round(100 * acp$sdev^2 / sum(acp$sdev^2), 2),
`% Acumulado` = round(cumsum(100 * acp$sdev^2 / sum(acp$sdev^2)), 2),
check.names = FALSE, row.names = NULL
)
tabla(varianza, caption = "Tabla 12. Varianza explicada por los componentes principales")
| Componente | Desviación estándar | Valor propio | % Varianza | % Acumulado |
|---|---|---|---|---|
| PC1 | 1.78 | 3.17 | 63.43 | 63.43 |
| PC2 | 0.96 | 0.92 | 18.39 | 81.83 |
| PC3 | 0.64 | 0.41 | 8.14 | 89.96 |
| PC4 | 0.56 | 0.31 | 6.28 | 96.24 |
| PC5 | 0.43 | 0.19 | 3.76 | 100.00 |
La elección del número de componentes se apoya en tres criterios: el valor propio asociado a cada componente, el porcentaje de varianza acumulada y la inspección del gráfico de sedimentación.
fviz_eig(acp, addlabels = TRUE, barfill = ACENTO, barcolor = ACENTO) +
labs(title = "", x = "Componentes", y = "Porcentaje de varianza explicada")
Figura 4. Gráfico de sedimentación de los componentes principales
El primer componente concentra el 63,4 % de la varianza y el segundo un 18,4 % adicional, de modo que ambos acumulan el 81,8 %, cifra que supera el rango de referencia del 70 % al 80 %. Como se observa en la Figura 4, la pendiente desciende de forma pronunciada entre el primer y el segundo componente y se aplana a partir del tercero, cuyo aporte es de 8,1 %.
El criterio del valor propio mayor que uno retendría únicamente el primer componente, dado que el segundo alcanza 0,92. No obstante, ese valor se sitúa muy próximo al umbral y su exclusión impediría representar los individuos en un plano factorial, elemento necesario para el análisis posterior de conglomerados.
Se retienen entonces dos componentes, que conservan el 81,8 % de la varianza total.
Las cargas indican la influencia de cada variable sobre cada componente y permiten atribuirles un significado.
cargas <- as.data.frame(round(acp$rotation[, 1:2], 3))
cargas <- cbind(Variable = rownames(cargas), cargas)
rownames(cargas) <- NULL
tabla(cargas, caption = "Tabla 13. Cargas de las variables en los dos primeros componentes")
| Variable | PC1 | PC2 |
|---|---|---|
| preciom | 0.48 | 0.35 |
| areaconst | 0.49 | -0.11 |
| parqueaderos | 0.41 | 0.54 |
| banios | 0.50 | -0.14 |
| habitaciones | 0.36 | -0.75 |
En el primer componente las cinco variables presentan cargas positivas y de magnitud similar, entre 0,355 y 0,497. El componente ordena los inmuebles según su tamaño y dotación: valores altos corresponden a viviendas amplias, con mayor número de baños, habitaciones y parqueaderos, y de mayor precio. Se denomina entonces tamaño y capacidad de la vivienda.
El segundo componente opone signos. habitaciones
presenta la carga más elevada en magnitud, con −0,746, frente a
parqueaderos con 0,536 y preciom con 0,354.
Las variables areaconst y banios registran
cargas próximas a cero, de modo que no intervienen en su definición.
Este componente diferencia inmuebles de tamaño comparable según la distribución de su espacio. Valores negativos corresponden a viviendas que reparten el área construida en un mayor número de habitaciones, mientras que los valores positivos corresponden a inmuebles con menos habitaciones, más parqueaderos y precio superior. Se denomina distribución del espacio interior.
A continuación se examina la contribución de cada variable a los dos componentes retenidos y su calidad de representación en el plano factorial. La contribución corresponde a la carga al cuadrado normalizada sobre la suma del componente, de modo que los aportes de cada eje suman 100 %. La calidad de representación, o \(\cos^2\), compara la longitud de la proyección de la variable con su distancia original.
\[\text{Contribución de } X_j \text{ a } PC_i = \frac{a_{ij}^2}{\sum_k a_{ik}^2}\]
acp_fm <- PCA(vivienda_dep[vars_cuant], scale.unit = TRUE, ncp = 2, graph = FALSE)
contrib <- data.frame(
Variable = rownames(acp_fm$var$contrib),
`Contrib. PC1` = round(acp_fm$var$contrib[, 1], 2),
`Contrib. PC2` = round(acp_fm$var$contrib[, 2], 2),
`cos2 PC1` = round(acp_fm$var$cos2[, 1], 3),
`cos2 PC2` = round(acp_fm$var$cos2[, 2], 3),
check.names = FALSE, row.names = NULL
)
tabla(contrib, caption = "Tabla 14. Contribución y calidad de representación de las variables")
| Variable | Contrib. PC1 | Contrib. PC2 | cos2 PC1 | cos2 PC2 |
|---|---|---|---|---|
| preciom | 22.67 | 12.56 | 0.72 | 0.12 |
| areaconst | 23.64 | 1.20 | 0.75 | 0.01 |
| parqueaderos | 16.43 | 28.71 | 0.52 | 0.26 |
| banios | 24.68 | 1.88 | 0.78 | 0.02 |
| habitaciones | 12.57 | 55.65 | 0.40 | 0.51 |
banios, areaconst y preciom
aportan cada una alrededor de la cuarta parte del primer componente, sin
que ninguna predomine, lo que corresponde a un eje que resume el tamaño
general del inmueble.
El segundo componente lo define habitaciones, con una
contribución de 55,65 %, seguida de parqueaderos con 28,71
%. Las aportaciones de areaconst y banios son
inferiores al 2 %.
La calidad de representación muestra que habitaciones es
la variable menos explicada por el primer componente, con un \(\cos^2\) de 0,40, y la mejor explicada por
el segundo, con 0,51. Retener el segundo componente permite entonces
representar adecuadamente un atributo que el primero recoge de forma
parcial.
fviz_pca_var(acp, col.var = "contrib",
gradient.cols = c(GRIS, ACENTO, AZUL),
repel = TRUE) +
labs(title = "")
Figura 5. Círculo de correlaciones de las variables
La Figura 5 representa las variables en el plano factorial. La cercanía de cada vector al borde del círculo indica su calidad de representación, mientras que el color corresponde a su contribución. Ambos indicadores son distintos y se leen sobre el mismo gráfico.
Los 8.243 inmuebles se proyectan sobre el plano definido por los dos
componentes. La nube se colorea según estrato y según
tipo, que no intervinieron en el cálculo de los
componentes.
fviz_pca_ind(acp,
geom.ind = "point",
col.ind = factor(vivienda_dep$estrato),
addEllipses = TRUE,
pointsize = 0.8, alpha.ind = 0.3,
legend.title = "Estrato") +
labs(title = "")
Figura 6. Individuos en el plano factorial según estrato
En la Figura 6 las elipses se desplazan hacia la derecha a medida que aumenta el estrato. El estrato 3 se ubica en los valores bajos del primer componente y el estrato 6 en los altos. Esto es consistente con la forma en que se asigna el estrato, que depende de las características de la vivienda y de su entorno.
El segundo componente también los diferencia. El estrato 6 se desplaza hacia los valores positivos, donde están los parqueaderos, y el estrato 3 hacia los negativos, donde están las habitaciones.
Las elipses de los estratos 3, 4 y 5 se superponen bastante. El estrato desplaza los perfiles, no los separa.
fviz_pca_ind(acp,
geom.ind = "point",
col.ind = vivienda_dep$tipo,
addEllipses = TRUE,
pointsize = 0.8, alpha.ind = 0.3,
palette = c(ACENTO, NARANJA),
legend.title = "Tipo") +
labs(title = "")
Figura 7. Individuos en el plano factorial según tipo de vivienda
La Figura 7 muestra una diferencia de dispersión y no de posición. La elipse de los apartamentos es pequeña y se concentra cerca del origen. La de las casas es mucho más amplia en los dos componentes.
Los apartamentos son un producto homogéneo. Las casas van desde inmuebles parecidos a un apartamento hasta los valores más altos de la nube, y se extienden hacia el lado de las habitaciones en el segundo componente.
El tipo de vivienda informa sobre el perfil del inmueble cuando se trata de un apartamento, pero no cuando se trata de una casa.
El análisis de componentes principales reduce las cinco variables a
dos componentes que conservan el 81,8 % de la varianza. El primero,
definido por banios, areaconst y
preciom con contribuciones cercanas al 23 % cada una,
resume el tamaño y la dotación del inmueble y concentra por sí solo el
63,4 % de la varianza. El segundo lo define habitaciones,
con una contribución de 55,65 %, en oposición a
parqueaderos con 28,71 %, y distingue entre inmuebles de
tamaño comparable según cómo se reparte el espacio construido.
preciom presenta un \(\cos^2\) de 0,72 en el primer componente,
de modo que el precio de oferta queda explicado principalmente por la
dimensión de tamaño. habitaciones es la única variable cuya
información reside mayoritariamente en el segundo componente, con un
\(\cos^2\) de 0,51 frente a 0,40 en el
primero.
Los atributos de tamaño concentran entonces la mayor parte de la información sobre el precio de oferta, mientras que el número de habitaciones aporta una dimensión de diferenciación independiente de la magnitud del
Con el fin de precisar el sentido de los componentes, se examinan los inmuebles situados en los extremos de cada eje.
coord <- as.data.frame(acp$x[, 1:2])
extremos <- rbind(
vivienda_dep[order(coord$PC1)[1:2], ],
vivienda_dep[order(-coord$PC1)[1:2], ],
vivienda_dep[order(coord$PC2)[1:2], ],
vivienda_dep[order(-coord$PC2)[1:2], ]
)
extremos_tab <- data.frame(
Posición = c("PC1 mínimo", "PC1 mínimo", "PC1 máximo", "PC1 máximo",
"PC2 mínimo", "PC2 mínimo", "PC2 máximo", "PC2 máximo"),
extremos[c("tipo", "zona", "estrato", "preciom", "areaconst",
"banios", "habitaciones", "parqueaderos")],
row.names = NULL
)
tabla(extremos_tab, caption = "Tabla 15. Inmuebles situados en los extremos de los componentes")
| Posición | tipo | zona | estrato | preciom | areaconst | banios | habitaciones | parqueaderos |
|---|---|---|---|---|---|---|---|---|
| PC1 mínimo | Apartamento | Zona Sur | 4 | 108 | 40 | 1 | 1 | 0 |
| PC1 mínimo | Apartamento | Zona Sur | 5 | 93 | 48 | 1 | 1 | 0 |
| PC1 máximo | Casa | Zona Sur | 6 | 1,800 | 1,586 | 4 | 5 | 10 |
| PC1 máximo | Casa | Zona Sur | 6 | 1,600 | 1,600 | 6 | 6 | 3 |
| PC2 mínimo | Casa | Zona Sur | 3 | 470 | 366 | 10 | 10 | 0 |
| PC2 mínimo | Casa | Zona Centro | 4 | 610 | 750 | 8 | 10 | 0 |
| PC2 máximo | Casa | Zona Sur | 6 | 1,600 | 330 | 5 | 3 | 10 |
| PC2 máximo | Casa | Zona Sur | 6 | 1,200 | 360 | 6 | 4 | 10 |
Los inmuebles situados en el extremo inferior del primer componente son apartamentos de 40 y 48 m², con un baño, una habitación y sin parqueadero. En el extremo superior aparecen casas de estrato 6 con áreas cercanas a 1.600 m², hasta diez parqueaderos y precios entre 1.600 y 1.800 millones. Todos los atributos aumentan de forma conjunta, lo que confirma la interpretación del componente como una medida de tamaño y capacidad.
El segundo componente diferencia inmuebles de dimensiones comparables. En su extremo inferior se ubican casas de 366 y 750 m² con diez habitaciones y ningún parqueadero. En el superior, casas de 330 y 360 m² con tres o cuatro habitaciones y diez parqueaderos. Las cuatro corresponden a viviendas amplias y de precio elevado, de modo que la diferencia entre ellas no reside en su magnitud sino en la distribución del espacio construido.
El análisis de conglomerados agrupa los inmuebles en segmentos internamente homogéneos y diferenciados entre sí. Se emplea el algoritmo k-medias, que asigna cada observación al centroide más cercano y recalcula los centroides hasta alcanzar la convergencia, minimizando la varianza dentro de cada grupo.
Se utilizan las mismas cinco variables cuantitativas del análisis de componentes principales, estandarizadas mediante el puntaje Z, dado que el algoritmo opera sobre distancias euclidianas y es sensible a las diferencias de escala.
El algoritmo requiere fijar el número de grupos de antemano. Se aplican dos criterios: el método del codo, que examina la reducción de la suma de cuadrados dentro de los conglomerados a medida que aumenta el número de grupos, y el coeficiente de silueta, que mide para cada observación la diferencia entre su distancia media al propio grupo y al grupo vecino más cercano.
\[s(i) = \frac{b(i) - a(i)}{\max\{a(i), b(i)\}}\]
donde \(a(i)\) corresponde a la distancia media respecto de las observaciones del mismo conglomerado y \(b(i)\) a la del conglomerado más próximo. Valores cercanos a 1 indican una asignación inequívoca.
set.seed(1234)
g1 <- fviz_nbclust(vivienda_z, kmeans, method = "wss", k.max = 8) +
labs(title = "a) Método del codo", subtitle = "",
x = "Número de conglomerados", y = "Suma de cuadrados intra")
g2 <- fviz_nbclust(vivienda_z, kmeans, method = "silhouette", k.max = 8) +
labs(title = "b) Coeficiente de silueta", subtitle = "",
x = "Número de conglomerados", y = "Silueta promedio")
g1 / g2
Figura 8. Criterios para la selección del número de conglomerados
Como se observa en la Figura 8, la suma de cuadrados desciende de forma pronunciada al pasar de uno a dos conglomerados y la reducción se atenúa a partir del cuarto. El coeficiente de silueta alcanza su valor máximo en dos conglomerados, con 0,46, y disminuye de manera sostenida hasta 0,24 en ocho.
Ambos criterios convergen en dos conglomerados, que es el valor adoptado.
Se ajusta el modelo con dos conglomerados y se caracteriza cada uno en las unidades originales de las variables, lo que permite interpretar los segmentos en términos del mercado y no de puntuaciones estandarizadas.
set.seed(1234)
km <- kmeans(vivienda_z, centers = 2, nstart = 50)
vivienda_dep$cluster <- factor(km$cluster)
perfil <- aggregate(vivienda_dep[vars_cuant],
by = list(Conglomerado = vivienda_dep$cluster),
FUN = median)
perfil$n <- as.integer(table(vivienda_dep$cluster))
perfil$`%` <- round(100 * perfil$n / nrow(vivienda_dep), 1)
tabla(perfil, caption = "Tabla 16. Perfil de los conglomerados en unidades originales (medianas)")
| Conglomerado | preciom | areaconst | parqueaderos | banios | habitaciones | n | % |
|---|---|---|---|---|---|---|---|
| 1 | 695 | 296 | 2 | 5 | 4 | 2,461 | 29.9 |
| 2 | 260 | 93 | 1 | 2 | 3 | 5,782 | 70.1 |
Los perfiles se describen mediante la mediana y no la media, dado que las variables presentan asimetría positiva, según lo establecido en la Tabla 7.
El primer conglomerado agrupa 2.461 inmuebles, el 29,9 % de la base, con precio mediano de 695 millones, 296 m² de área construida, cinco baños y dos parqueaderos. El segundo reúne 5.782 inmuebles, el 70,1 %, con precio mediano de 260 millones, 93 m², dos baños y un parqueadero.
La diferencia de precio entre ambos es de 2,7 veces y la de área construida de 3,2 veces. Todas las variables se ordenan en el mismo sentido, sin inversiones: el conglomerado de mayor precio presenta también mayor área y mejor dotación en los tres atributos de conteo.
No obstante, el precio por metro cuadrado invierte esa relación. El primer conglomerado registra una mediana de 2,43 millones por m² y el segundo de 2,68, de modo que los inmuebles de menor tamaño se ofertan a un valor unitario superior.
La partición reproduce la estructura identificada en el análisis de componentes principales: los dos conglomerados se sitúan a cada lado del eje de tamaño y capacidad. Dado que ese componente concentra el 63,4 % de la varianza, los grupos obtenidos corresponden a tramos de un continuo de gama y no a poblaciones separadas.
Los conglomerados se construyeron únicamente con las variables cuantitativas. Se examina a continuación su composición según tipo de vivienda, estrato y zona, que no intervinieron en la segmentación.
g_tipo <- ggplot(vivienda_dep, aes(x = cluster, fill = tipo)) +
geom_bar(position = "fill") +
scale_fill_manual(values = c(ACENTO, NARANJA), name = "Tipo") +
labs(title = "a) Tipo", x = "Conglomerado", y = "Proporción")
g_estrato <- ggplot(vivienda_dep, aes(x = cluster, fill = factor(estrato))) +
geom_bar(position = "fill") +
scale_fill_brewer(palette = "Blues", name = "Estrato") +
labs(title = "b) Estrato", x = "Conglomerado", y = "")
g_zona <- ggplot(vivienda_dep, aes(x = cluster, fill = zona)) +
geom_bar(position = "fill") +
scale_fill_brewer(palette = "Greys", name = "Zona") +
labs(title = "c) Zona", x = "Conglomerado", y = "")
g_tipo + g_estrato + g_zona
Figura 9. Composición de los conglomerados según tipo, estrato y zona
Como se observa en la Figura 9, los dos conglomerados presentan composiciones opuestas en tipo de vivienda y en estrato, pero similares en zona.
El primer conglomerado está compuesto en un 71 % por casas, mientras que el segundo lo está en un 75 % por apartamentos. En cuanto al estrato, el primer conglomerado concentra el 49,3 % de sus inmuebles en estrato 6, proporción que en el segundo desciende al 13,2 %. Este último se distribuye principalmente entre los estratos 5 y 4, con 34,9 % y 31,4 % respectivamente.
La distribución por zona es equivalente en Zona Sur, que reúne cerca del 56 % de los inmuebles en ambos conglomerados, y en Zona Centro y Zona Oriente, con proporciones prácticamente iguales. Zona Oeste, en cambio, concentra el 22,1 % del primer conglomerado frente al 11,1 % del segundo, mientras que Zona Norte presenta la relación inversa, con 16,3 % y 25,7 % respectivamente.
La segmentación por características del inmueble no coincide con la división geográfica de la oferta, aunque Zona Oeste y Zona Norte muestran una composición diferenciada. Ambos segmentos coexisten en las mismas zonas de la ciudad, de modo que la localización no permite anticipar por sí sola a qué segmento pertenece un inmueble.
Los conglomerados se proyectan sobre el plano de los dos primeros componentes principales, que resume el 81,8 % de la varianza de las variables empleadas en la segmentación.
fviz_cluster(km, data = vivienda_z,
geom = "point", pointsize = 0.8, alpha = 0.3,
ellipse.type = "convex",
palette = c(NARANJA, ACENTO),
ggtheme = tema_informe) +
labs(title = "", x = "PC1 — Tamaño y capacidad", y = "PC2 — Distribución del espacio")
Figura 10. Conglomerados en el plano de los componentes principales
Como se observa en la Figura 10, la división entre los dos conglomerados es una línea recta que corta la nube a lo largo del primer componente. Las dos regiones se tocan sin que exista un espacio vacío entre ellas, y a ambos lados del corte la concentración de inmuebles sigue siendo alta.
Esto significa que el algoritmo no encontró dos grupos separados, sino que dividió en dos una nube continua. Si existieran dos mercados diferenciados, la zona de la frontera estaría despoblada. Los inmuebles situados a un lado y otro del corte se parecen entre sí más de lo que la clasificación sugiere.
El resultado es consistente con el análisis de componentes principales, donde el primer componente concentraba el 63,4 % de la varianza. Si los inmuebles varían principalmente a lo largo de una sola dimensión, lo esperable es un continuo y no grupos aislados.
Antes de representar las asociaciones entre las variables categóricas se verifica su existencia mediante la prueba chi-cuadrado de Pearson, que compara las frecuencias observadas en la tabla de contingencia con las esperadas bajo el supuesto de independencia.
\[\chi^2 = \sum \frac{(O - E)^2}{E}\]
Las hipótesis contrastadas para cada par de variables son:
\[H_0: \text{las dos variables son independientes}\]
\[H_1: \text{las dos variables no son independientes}\]
Se adopta un nivel de significancia de 0,05. Dado que el estadístico chi-cuadrado aumenta con el tamaño de la muestra, se acompaña de la V de Cramér como medida del tamaño del efecto:
\[V = \sqrt{\frac{\chi^2}{n \cdot \min(r-1,\, c-1)}}\]
que toma valores entre 0 y 1 con independencia del número de observaciones.
prueba_chi <- function(x, y, nombre) {
t <- table(x, y)
p <- chisq.test(t)
v <- sqrt(as.numeric(p$statistic) / (sum(t) * (min(dim(t)) - 1)))
data.frame(
Cruce = nombre,
`Chi-cuadrado` = round(as.numeric(p$statistic), 1),
gl = as.integer(p$parameter),
`Valor p` = format.pval(p$p.value, digits = 3, eps = 0.001),
`V de Cramér` = round(v, 3),
check.names = FALSE, row.names = NULL
)
}
contingencia <- rbind(
prueba_chi(vivienda_dep$zona, vivienda_dep$estrato, "Zona × Estrato"),
prueba_chi(vivienda_dep$zona, vivienda_dep$tipo, "Zona × Tipo"),
prueba_chi(vivienda_dep$tipo, vivienda_dep$estrato, "Tipo × Estrato")
)
tabla(contingencia, caption = "Tabla 17. Pruebas de independencia entre las variables categóricas")
| Cruce | Chi-cuadrado | gl | Valor p | V de Cramér |
|---|---|---|---|---|
| Zona × Estrato | 3,801.8 | 12 | <0.001 | 0.39 |
| Zona × Tipo | 688.2 | 4 | <0.001 | 0.29 |
| Tipo × Estrato | 215.4 | 3 | <0.001 | 0.16 |
Las tres pruebas rechazan la hipótesis de independencia con valores p inferiores a 0,001, de modo que existe asociación entre las tres variables categóricas.
La V de Cramér permite ordenar la intensidad de esas asociaciones. La
más fuerte corresponde a zona y estrato, con
0,39, resultado consistente con el hecho de que la estratificación
socioeconómica se asigna por sectores geográficos. Le siguen
zona y tipo con 0,29, y tipo y
estrato con 0,16.
Cabe señalar que, con 8.243 registros, la prueba chi-cuadrado rechaza la independencia ante desviaciones de magnitud reducida. Los valores de la V de Cramér indican que las asociaciones, aun siendo estadísticamente significativas, son de intensidad moderada: ninguna de las tres variables determina a las otras.
El análisis de correspondencias múltiples se aplica sobre
zona, tipo y estrato como
variables activas. La variable barrio se incorpora como
ilustrativa, dado que sus 433 categorías impedirían la lectura del mapa
factorial y las modalidades con pocos registros ejercerían una
influencia desproporcionada sobre la construcción de los ejes.
Se conservan con su denominación original los barrios con al menos 100 registros y las restantes categorías se agrupan bajo una etiqueta común.
frec_barrio <- table(vivienda_dep$barrio)
barrios_frec <- names(frec_barrio[frec_barrio >= 100])
vivienda_dep$barrio_agr <- factor(
ifelse(vivienda_dep$barrio %in% barrios_frec, vivienda_dep$barrio, "Otros")
)
vivienda_dep$zona <- factor(vivienda_dep$zona)
vivienda_dep$tipo <- factor(vivienda_dep$tipo)
vivienda_dep$estrato_f <- factor(vivienda_dep$estrato)
resumen_barrio <- data.frame(
Concepto = c("Barrios en la base",
"Barrios con 100 o más registros",
"Cobertura de la oferta (%)"),
Valor = c(length(frec_barrio),
length(barrios_frec),
round(100 * sum(vivienda_dep$barrio %in% barrios_frec) / nrow(vivienda_dep), 1))
)
tabla(resumen_barrio, caption = "Tabla 18. Agrupamiento de la variable barrio")
| Concepto | Valor |
|---|---|
| Barrios en la base | 433.0 |
| Barrios con 100 o más registros | 15.0 |
| Cobertura de la oferta (%) | 49.2 |
El análisis de correspondencias múltiples representa las modalidades
de las variables categóricas en un espacio de baja dimensión, en el que
la proximidad entre categorías indica que tienden a presentarse
conjuntamente en los mismos inmuebles. Se aplica sobre
zona, tipo y estrato como
variables activas, e incorpora barrio y el conglomerado
obtenido en el apartado anterior como variables ilustrativas, que se
proyectan sobre los ejes sin intervenir en su construcción.
levels(vivienda_dep$estrato_f) <- paste("Estrato", 3:6)
levels(vivienda_dep$cluster) <- c("Conglomerado 1", "Conglomerado 2")
acm <- MCA(vivienda_dep[c("zona", "tipo", "estrato_f", "barrio_agr", "cluster")],
quali.sup = 4:5,
graph = FALSE)
inercia <- data.frame(
Dimensión = paste("Dim", 1:5),
`Valor propio` = round(acm$eig[1:5, 1], 4),
`% Inercia` = round(acm$eig[1:5, 2], 2),
`% Acumulado` = round(acm$eig[1:5, 3], 2),
check.names = FALSE, row.names = NULL
)
tabla(inercia, caption = "Tabla 19. Inercia explicada por dimensión")
| Dimensión | Valor propio | % Inercia | % Acumulado |
|---|---|---|---|
| Dim 1 | 0.56 | 21.05 | 21.05 |
| Dim 2 | 0.45 | 17.00 | 38.05 |
| Dim 3 | 0.38 | 14.27 | 52.32 |
| Dim 4 | 0.33 | 12.51 | 64.83 |
| Dim 5 | 0.32 | 12.16 | 76.99 |
Los dos primeros ejes concentran el 21,05 % y el 17,00 % de la inercia, con un acumulado del 38,05 %. Estos porcentajes no son comparables con los del análisis de componentes principales: en un análisis de correspondencias múltiples la inercia total depende del número de modalidades y no del grado de asociación entre las variables. Con 11 modalidades activas y 3 variables, la inercia total asciende a 2,67 y se distribuye entre 8 dimensiones, de modo que ninguna concentra una proporción elevada aun cuando la asociación sea fuerte.
Se retienen los dos primeros ejes, que permiten representar las modalidades en un plano factorial.
fviz_mca_var(acm, repel = TRUE,
col.var = AZUL, col.quali.sup = NARANJA,
labelsize = 3, max.overlaps = 30) +
labs(title = "")
Figura 11. Mapa factorial de las variables categóricas
A continuación se examina la contribución de cada categoría a la formación de los ejes y su calidad de representación en el plano factorial.
contrib_acm <- data.frame(
Categoría = rownames(acm$var$contrib),
`Contrib. Dim1` = round(acm$var$contrib[, 1], 2),
`Contrib. Dim2` = round(acm$var$contrib[, 2], 2),
`cos2 Dim1` = round(acm$var$cos2[, 1], 3),
`cos2 Dim2` = round(acm$var$cos2[, 2], 3),
check.names = FALSE, row.names = NULL
)
contrib_acm <- contrib_acm[order(-contrib_acm$`Contrib. Dim1`), ]
tabla(contrib_acm, caption = "Tabla 20. Contribución y calidad de representación de las categorías")
| Categoría | Contrib. Dim1 | Contrib. Dim2 | cos2 Dim1 | cos2 Dim2 | |
|---|---|---|---|---|---|
| 8 | Estrato 3 | 30.65 | 4.92 | 0.62 | 0.08 |
| 4 | Zona Oriente | 23.82 | 6.32 | 0.42 | 0.09 |
| 3 | Zona Oeste | 9.89 | 32.75 | 0.20 | 0.52 |
| 7 | Casa | 9.42 | 0.23 | 0.26 | 0.00 |
| 11 | Estrato 6 | 8.26 | 23.53 | 0.18 | 0.42 |
| 1 | Zona Centro | 6.39 | 1.03 | 0.11 | 0.01 |
| 6 | Apartamento | 5.88 | 0.14 | 0.26 | 0.00 |
| 2 | Zona Norte | 2.83 | 0.92 | 0.06 | 0.02 |
| 5 | Zona Sur | 1.47 | 9.69 | 0.06 | 0.31 |
| 10 | Estrato 5 | 0.78 | 5.41 | 0.02 | 0.11 |
| 9 | Estrato 4 | 0.60 | 15.06 | 0.01 | 0.28 |
La primera dimensión está definida principalmente por Estrato 3 y Zona Oriente, que aportan el 30,65 % y el 23,82 % de su inercia. Sumada la contribución de Zona Centro, con 6,39 %, la periferia de estrato bajo explica cerca del 61 % de este eje. Estas tres modalidades se sitúan en los valores positivos y quedan separadas del resto de la ciudad.
La segunda dimensión la definen Zona Oeste con 32,75 % y Estrato 6 con 23,53 %, en los valores positivos, frente a Estrato 4 con 15,06 % y Zona Sur con 9,69 % en el extremo opuesto. Distingue la concentración de estrato alto de la oferta de estratos intermedios.
Las modalidades Casa y Apartamento
contribuyen de forma moderada a la primera dimensión, con 9,42 % y 5,88
%, y resultan irrelevantes en la segunda, con aportes inferiores al 0,25
%. El tipo de vivienda participa entonces en la separación de la
periferia respecto del resto de la ciudad, pero no en la distinción
entre estratos altos e intermedios. El resultado es coherente con la V
de Cramér obtenida en la Tabla 16, la más baja de los tres cruces.
Zona Sur presenta una contribución reducida en la primera dimensión, de 1,47 %. Al concentrar el 56,8 % de la oferta, su composición por tipo y estrato coincide en buena medida con la del conjunto de la ciudad, de modo que se sitúa cerca del origen del plano.
La calidad de representación indica qué categorías admiten una lectura confiable. Estrato 3 alcanza un cos² de 0,62 en la primera dimensión y Zona Oeste de 0,52 en la segunda. Zona Norte, en cambio, registra valores de 0,06 y 0,02, de modo que su posición en el mapa no resulta interpretable.
Los barrios proyectados se distribuyen de forma consistente con la estructura descrita. Normandía, Santa Teresita, Los Cristales y Aguacatal se sitúan próximos a Zona Oeste y al estrato 6, mientras que Valle del Lili, El Refugio, El Caney y El Limonar lo hacen cerca de Zona Sur y de los estratos 4 y 5.
Los dos conglomerados se proyectan cerca del origen, con una separación reducida entre sí frente a la de las modalidades activas, que alcanzan valores superiores a 3 en la primera dimensión. La segmentación construida a partir de los atributos físicos y del precio no se diferencia de manera apreciable en el espacio de las variables categóricas.
Los resultados de los apartados anteriores se representan sobre la localización geográfica de los inmuebles, con el fin de examinar la distribución espacial de la segmentación obtenida. Se emplean las coordenadas de longitud y latitud, reservadas desde el diccionario de variables para este propósito.
El mapa representa una muestra aleatoria de 2.000 inmuebles. La restricción responde al tamaño del archivo generado y no afecta la lectura, dado que el mapa cumple una función ilustrativa de los hallazgos ya establecidos.
set.seed(1234)
muestra_mapa <- vivienda_dep[sample(nrow(vivienda_dep), 2000), ]
pal <- colorFactor(c(NARANJA, ACENTO), domain = muestra_mapa$cluster)
leaflet(muestra_mapa) |>
addTiles() |>
addCircleMarkers(
lng = ~longitud, lat = ~latitud,
color = ~pal(cluster), radius = 3,
stroke = FALSE, fillOpacity = 0.6,
popup = ~paste0("Precio: ", preciom, " millones<br>",
"Área: ", areaconst, " m²<br>",
"Estrato: ", estrato)
) |>
addLegend(position = "bottomright", pal = pal,
values = ~cluster, title = "Conglomerado")
Figura 12. Distribución espacial de los conglomerados.
La Figura 11 muestra los inmuebles de ambos conglomerados entremezclados en toda la extensión de la ciudad. No se identifican sectores en los que uno de los segmentos aparezca de forma exclusiva.
La oferta se distribuye siguiendo un eje norte-sur, con mayor densidad en el centro y el sur. El conglomerado 1 presenta una concentración relativamente mayor en el sector occidental, consistente con el 22,1 % que registra en Zona Oeste frente al 11,1 % del conglomerado 2.
El mapa confirma visualmente lo establecido en la Figura 9: la segmentación construida a partir de los atributos del inmueble no se corresponde con una división territorial de la oferta.
La variabilidad de la oferta se concentra en el tamaño del inmueble. Dos componentes principales resumen el 81,8 % de la varianza de las cinco variables cuantitativas. El primero, que reúne área construida, número de baños y precio con contribuciones cercanas al 23 % cada una, explica por sí solo el 63,4 % y ordena los inmuebles según su tamaño y dotación. El precio de oferta queda explicado casi enteramente por esa dimensión, con un \(\cos^2\) de 0,72. El segundo componente, definido por el número de habitaciones, distingue inmuebles de tamaño comparable según cómo reparten su espacio construido.
La oferta se organiza en dos segmentos diferenciados. El primero reúne el 29,9 % de los inmuebles, con precio mediano de 695 millones, 296 m² y predominio de casas en un 71 %. El segundo concentra el 70,1 % restante, con 260 millones, 93 m² y predominio de apartamentos en un 75 %. La diferencia de precio entre ambos es de 2,7 veces, aunque el precio por metro cuadrado resulta superior en el segmento de menor tamaño, con 2,68 millones frente a 2,43.
Las variables categóricas presentan asociaciones de intensidad moderada. Las tres pruebas de independencia se rechazan, pero la V de Cramér ubica la asociación más fuerte entre zona y estrato, con 0,39, y la más débil entre tipo de vivienda y estrato, con 0,16. El análisis de correspondencias múltiples organiza el territorio en dos dimensiones: la primera aísla la periferia de estrato bajo, conformada por Zona Oriente y Zona Centro, y la segunda separa la concentración de estrato 6 en Zona Oeste de los estratos intermedios de Zona Sur.
La segmentación por atributos del inmueble no coincide con la estructura territorial de la oferta. Los dos conglomerados presentan distribuciones por zona equivalentes, se proyectan próximos al origen en el plano del análisis de correspondencias y aparecen entremezclados en el mapa. La gama del inmueble y su localización constituyen entonces dos dimensiones independientes del mercado, y ninguna de las dos por separado describe adecuadamente la oferta.
El tratamiento diferenciado de los datos faltantes permitió conservar
el 99,05 % de los registros. La verificación del origen de las ausencias
mostró que la falta de dato en piso no responde al tipo de
vivienda y que el valor cero nunca se registra en
parqueaderos, lo que sustentó decisiones distintas para
cada variable. La eliminación por lista habría reducido la base al 57,8
%.
Dar más peso al área y a los baños que al número de habitaciones al fijar precios. En esta oferta el precio va de la mano sobre todo con el tamaño del inmueble y con la cantidad de baños. El número de habitaciones, que suele destacarse en los anuncios, acompaña mucho menos al precio: su correlación es de 0,44, frente a 0,82 del área construida. Al sugerir un precio de lista conviene mirar primero los metros cuadrados.
Valorar de forma distinta las casas y los apartamentos. Los apartamentos se parecen bastante entre sí, de modo que comparar con inmuebles similares funciona bien. Las casas son muy distintas unas de otras: bajo esa misma etiqueta hay desde viviendas pequeñas hasta las más grandes y costosas del mercado. Conviene revisar cada casa individualmente en lugar de aplicar un criterio general.
Organizar los equipos comerciales por tipo de producto y no por zona. Los dos grupos de inmuebles conviven en los mismos barrios. Un equipo asignado a una zona termina manejando al mismo tiempo apartamentos de 260 millones y casas de 695, que se venden a públicos distintos y con argumentos distintos. Especializar por segmento aprovecha mejor el conocimiento del vendedor.
No abandonar el segmento de vivienda compacta. Concentra el 70,1 % de los inmuebles ofertados y su precio por metro cuadrado es incluso algo mayor que el del segmento amplio: 2,68 millones frente a 2,43. El segmento de gama alta deja más dinero por operación, pero el volumen está del otro lado.
Buscar inmuebles de gama alta en la Zona Oeste. Es la única zona donde la composición de los dos segmentos difiere de forma apreciable: 22,1 % del segmento amplio frente a 11,1 % del compacto. La Zona Sur, con el 56,8 % de toda la oferta, es el mercado de volumen antes que el de producto premium.
Revisar qué pasa en la Zona Oriente y la Zona Centro. Entre las dos apenas reúnen el 5,7 % de los avisos y concentran el estrato 3. No se sabe si es porque hay poca vivienda en venta o porque esos propietarios no publican en este portal. Vale la pena averiguarlo con información propia: si es lo segundo, hay un mercado sin atender.
Registrar el cero cuando un inmueble no tenga parqueadero. En el 19,29 % de los avisos ese dato no aparece, y el cero no se escribe en ninguno de los 6.717 que sí lo reportan. Dejarlo en blanco hace que un inmueble sin parqueadero se vea igual que uno del que no se sabe.
Kassambara, A., & Mundt, F. (2020). factoextra: Extract and visualize the results of multivariate data analyses. R package version 1.0.7.
Lê, S., Josse, J., & Husson, F. (2008). FactoMineR: An R package for multivariate analysis. Journal of Statistical Software, 25(1), 1–18.
Ortega Lenis, D. (2026). Modelos estadísticos para la toma de decisiones [material del curso]. Pontificia Universidad Javeriana Cali.
R Core Team (2026). R: A language and environment for statistical computing. R Foundation for Statistical Computing.
Rousseeuw, P. J. (1987). Silhouettes: A graphical aid to the interpretation and validation of cluster analysis. Journal of Computational and Applied Mathematics, 20, 53–65.
La categoría residual del análisis de correspondencias múltiples reúne los barrios con menos de 100 registros. Se listan a continuación los veinte de mayor frecuencia dentro de ese conjunto.
otros <- sort(frec_barrio[frec_barrio < 100], decreasing = TRUE)[1:20]
anexo_b <- data.frame(
Barrio = names(otros),
Registros = as.integer(otros),
row.names = NULL
)
tabla(anexo_b, caption = "Tabla A1. Barrios más frecuentes agrupados en la categoría Otros")
| Barrio | Registros |
|---|---|
| ciudad 2000 | 95 |
| caney | 87 |
| cristales | 83 |
| urbanización la flora | 83 |
| brisas de los | 81 |
| zona sur | 74 |
| nueva tequendama | 73 |
| quintas de don | 72 |
| versalles | 70 |
| santa isabel | 63 |
| parcelaciones pance | 61 |
| el peñon | 60 |
| el lido | 59 |
| torres de comfandi | 57 |
| capri | 56 |
| san fernando | 53 |
| juanamb√∫ | 52 |
| melendez | 51 |
| santa monica | 51 |
| villa del prado | 50 |
Se documenta el entorno de cómputo empleado, con el fin de permitir la reproducción de los resultados.
sesion <- data.frame(
Elemento = c("Versión de R", "Sistema operativo", "Semilla aleatoria"),
Valor = c(R.version.string,
Sys.info()[["sysname"]],
"1234")
)
tabla(sesion, caption = "Tabla A2. Entorno de cómputo")
| Elemento | Valor |
|---|---|
| Versión de R | R version 4.6.1 (2026-06-24 ucrt) |
| Sistema operativo | Windows |
| Semilla aleatoria | 1234 |