Una empresa inmobiliaria líder busca comprender el mercado de
vivienda urbana a partir de una base de datos obtenida mediante
webscraping de OLX (paquete paqueteMODELOS). El
objetivo de este informe es aplicar un análisis multivariado integral
Componentes Principales, Conglomerados y Correspondencias para
identificar patrones, segmentos y relaciones entre las características
de las propiedades, y traducir esos hallazgos en recomendaciones
estratégicas de compra, venta y valoración.
Antes de entrar en el detalle técnico, es importante justificar por qué se eligió cada técnica y cómo se articulan entre sí, en lugar de aplicarlas de forma aislada:
Análisis de Componentes Principales (PCA): el conjunto de variables cuantitativas de una vivienda (área, número de baños, habitaciones, parqueaderos, precio) está naturalmente correlacionado una vivienda más grande tiende a tener más habitaciones, más baños y mayor precio. El PCA permite resumir esa redundancia en unos pocos ejes interpretables (p. ej. un eje de “tamaño/valor”), lo cual es el primer paso para entender qué dimensiones realmente diferencian a las propiedades en el mercado.
Análisis de Conglomerados: una vez identificadas las dimensiones que explican la variabilidad (vía PCA), el paso natural es preguntar si existen grupos naturales de propiedades similares entre sí. Esto traduce el análisis estadístico en un insumo directamente accionable para el negocio: segmentos de mercado con perfiles de precio, tamaño y zona propios, útiles para diseñar estrategias diferenciadas de compra, venta o inversión.
Análisis de Correspondencias (CA): las dos técnicas anteriores trabajan sobre variables cuantitativas. Sin embargo, buena parte de la información relevante del mercado inmobiliario es categórica (tipo de vivienda, zona, barrio). El CA es el análogo del PCA para variables categóricas: en lugar de varianza, descompone la asociación entre categorías, revelando qué tipos de vivienda predominan en qué zonas.
Visualización (mapas): finalmente, dado que la
ubicación geográfica (longitud, latitud) es
una característica intrínsecamente espacial, se reserva para una etapa
de visualización que permite anclar los hallazgos estadísticos
(componentes, conglomerados, asociaciones) en el mapa real de la
ciudad, facilitando su lectura por parte de la dirección de la
empresa.
En conjunto, el flujo metodológico sigue una lógica de reducción → segmentación → asociación → comunicación visual.
# devtools::install_github("centromagis/paqueteMODELOS", force = TRUE)
library(paqueteMODELOS)
library(dplyr)
library(tidyr)
library(forcats)
library(ggplot2)
library(corrplot)
library(FactoMineR)
library(factoextra)
library(cluster)
library(leaflet)
library(knitr)
library(kableExtra)
data("vivienda")
str(vivienda)
## spc_tbl_ [8,322 × 13] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
## $ id : num [1:8322] 1147 1169 1350 5992 1212 ...
## $ zona : chr [1:8322] "Zona Oriente" "Zona Oriente" "Zona Oriente" "Zona Sur" ...
## $ piso : chr [1:8322] NA NA NA "02" ...
## $ estrato : num [1:8322] 3 3 3 4 5 5 4 5 5 5 ...
## $ preciom : num [1:8322] 250 320 350 400 260 240 220 310 320 780 ...
## $ areaconst : num [1:8322] 70 120 220 280 90 87 52 137 150 380 ...
## $ parqueaderos: num [1:8322] 1 1 2 3 1 1 2 2 2 2 ...
## $ banios : num [1:8322] 3 2 2 5 2 3 2 3 4 3 ...
## $ habitaciones: num [1:8322] 6 3 4 3 3 3 3 4 6 3 ...
## $ tipo : chr [1:8322] "Casa" "Casa" "Casa" "Casa" ...
## $ barrio : chr [1:8322] "20 de julio" "20 de julio" "20 de julio" "3 de julio" ...
## $ longitud : num [1:8322] -76.5 -76.5 -76.5 -76.5 -76.5 ...
## $ latitud : num [1:8322] 3.43 3.43 3.44 3.44 3.46 ...
## - attr(*, "spec")=List of 3
## ..$ cols :List of 13
## .. ..$ id : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ zona : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
## .. ..$ piso : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
## .. ..$ estrato : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ preciom : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ areaconst : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ parqueaderos: list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ banios : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ habitaciones: list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ tipo : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
## .. ..$ barrio : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
## .. ..$ longitud : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ latitud : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## ..$ default: list()
## .. ..- attr(*, "class")= chr [1:2] "collector_guess" "collector"
## ..$ delim : chr ";"
## ..- attr(*, "class")= chr "col_spec"
## - attr(*, "problems")=<pointer: (nil)>
Antes de tomar cualquier decisión de limpieza o modelado, es indispensable tener claro qué representa cada variable, su tipo de dato original y el rol que cumplirá en los análisis siguientes. La Tabla 1 resume esta información.
diccionario <- data.frame(
Variable = c("id", "zona", "piso", "estrato", "preciom", "areaconst",
"parqueaderos", "banios", "habitaciones", "tipo", "barrio",
"longitud", "latitud"),
Descripcion = c(
"Identificador único de la publicación/propiedad.",
"Zona geográfica general de la ciudad en la que se ubica la propiedad.",
"Piso del inmueble dentro del edificio (aplica principalmente a apartamentos).",
"Estrato socioeconómico de la vivienda (escala ordinal 1 a 6).",
"Precio de venta publicado, en millones de pesos.",
"Área construida de la propiedad, en metros cuadrados.",
"Número de parqueaderos disponibles.",
"Número de baños de la propiedad.",
"Número de habitaciones de la propiedad.",
"Tipo de inmueble (p. ej. Casa, Apartamento).",
"Barrio específico en el que se ubica la propiedad.",
"Coordenada geográfica de longitud.",
"Coordenada geográfica de latitud."
),
Tipo_original = c("numérica", "texto", "texto", "numérica", "numérica",
"numérica", "numérica", "numérica", "numérica", "texto",
"texto", "numérica", "numérica"),
Rol_en_el_analisis = c(
"Identificador (no se usa como variable analítica)",
"Categórica – insumo del Análisis de Correspondencias y variable de segmentación",
"Categórica – documentada, no se usa directamente en PCA/clustering/CA",
"Ordinal – variable suplementaria en PCA y variable de perfilado de conglomerados",
"Cuantitativa – variable activa en PCA y Conglomerados",
"Cuantitativa – variable activa en PCA y Conglomerados",
"Cuantitativa – variable activa en PCA y Conglomerados (NA imputados como 0 = sin parqueadero)",
"Cuantitativa – variable activa en PCA y Conglomerados",
"Cuantitativa – variable activa en PCA y Conglomerados",
"Categórica – variable suplementaria en PCA e insumo del Análisis de Correspondencias",
"Categórica de alta cardinalidad – insumo del Análisis de Correspondencias (agrupada)",
"Geográfica – utilizada exclusivamente en la sección de visualización/mapas",
"Geográfica – utilizada exclusivamente en la sección de visualización/mapas"
)
)
kable(diccionario, caption = "Tabla 1. Diccionario de variables de la base de datos 'vivienda'",
col.names = c("Variable", "Descripción", "Tipo original", "Rol en el análisis")) %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = TRUE)
| Variable | Descripción | Tipo original | Rol en el análisis |
|---|---|---|---|
| id | Identificador único de la publicación/propiedad. | numérica | Identificador (no se usa como variable analítica) |
| zona | Zona geográfica general de la ciudad en la que se ubica la propiedad. | texto | Categórica – insumo del Análisis de Correspondencias y variable de segmentación |
| piso | Piso del inmueble dentro del edificio (aplica principalmente a apartamentos). | texto | Categórica – documentada, no se usa directamente en PCA/clustering/CA |
| estrato | Estrato socioeconómico de la vivienda (escala ordinal 1 a 6). | numérica | Ordinal – variable suplementaria en PCA y variable de perfilado de conglomerados |
| preciom | Precio de venta publicado, en millones de pesos. | numérica | Cuantitativa – variable activa en PCA y Conglomerados |
| areaconst | Área construida de la propiedad, en metros cuadrados. | numérica | Cuantitativa – variable activa en PCA y Conglomerados |
| parqueaderos | Número de parqueaderos disponibles. | numérica | Cuantitativa – variable activa en PCA y Conglomerados (NA imputados como 0 = sin parqueadero) |
| banios | Número de baños de la propiedad. | numérica | Cuantitativa – variable activa en PCA y Conglomerados |
| habitaciones | Número de habitaciones de la propiedad. | numérica | Cuantitativa – variable activa en PCA y Conglomerados |
| tipo | Tipo de inmueble (p. ej. Casa, Apartamento). | texto | Categórica – variable suplementaria en PCA e insumo del Análisis de Correspondencias |
| barrio | Barrio específico en el que se ubica la propiedad. | texto | Categórica de alta cardinalidad – insumo del Análisis de Correspondencias (agrupada) |
| longitud | Coordenada geográfica de longitud. | numérica | Geográfica – utilizada exclusivamente en la sección de visualización/mapas |
| latitud | Coordenada geográfica de latitud. | numérica | Geográfica – utilizada exclusivamente en la sección de visualización/mapas |
Este diccionario es la base de todas las decisiones de preparación
que siguen. Al dejar explícito el rol de cada variable desde el
inicio, se evita el error común de meter variables de ubicación
(longitud, latitud) o identificadores
(id) dentro de cálculos de distancia o varianza, donde no
tienen una interpretación de negocio válida.
Con base en el diccionario anterior, estrato, aunque
numérica en el tipo de dato original, se trata como variable
ordinal (categoría socioeconómica 1 a 6): la diferencia
entre estrato 3 y 4 no es una cantidad medible como sí lo es la
diferencia entre 70 m² y 140 m². tipo y zona
se convierten en factores para su uso en el Análisis de Correspondencias
y como variables suplementarias del PCA.
vivienda <- vivienda %>%
mutate(
estrato = factor(estrato, levels = 1:6, ordered = TRUE),
tipo = factor(tipo),
zona = factor(zona),
piso = as.character(piso)
)
# Revisión de NAs en piso según tipo de vivienda
table(vivienda$tipo, is.na(vivienda$piso))
##
## FALSE TRUE
## Apartamento 3719 1381
## Casa 1965 1254
Los NA en la variable piso representan una
mayor proporción de los registros para Casa (41.3%),
mientras que los Apartamentos representa un 25.2%. Esto
tiene sentido conceptual: una casa no tiene “un piso” dentro de un
edificio, así que el vacío no es un dato faltante por error de captura,
sino estructural (no aplica). Por esta razón se
documenta la ausencia en lugar de imputar un valor, y piso
no se incluye como variable activa en PCA, Conglomerados ni CA.
barrioLa variable barrio tiene una alta cardinalidad, lo que
genera dos problemas para el Análisis de Correspondencias: (1)
categorías con muy pocas observaciones producen frecuencias esperadas
bajas y estimaciones poco confiables, y (2) un mapa perceptual con 100+
etiquetas es ilegible. Se agrupan los barrios menos frecuentes en una
categoría "Otros", conservando los 15 barrios con mayor
número de publicaciones.
top_barrios <- vivienda %>%
count(barrio, sort = TRUE) %>%
slice_max(n, n = 15) %>%
pull(barrio)
vivienda <- vivienda %>%
mutate(barrio_agrup = fct_other(barrio, keep = top_barrios, other_level = "Otros"))
vivienda %>%
count(barrio_agrup, sort = TRUE) %>%
kable(caption = "Tabla 2. Frecuencia de publicaciones por barrio agrupado",
col.names = c("Barrio (agrupado)", "N° de propiedades")) %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"))
| Barrio (agrupado) | N° de propiedades |
|---|---|
| Otros | 4228 |
| valle del lili | 1008 |
| ciudad jardín | 516 |
| pance | 409 |
| la flora | 366 |
| santa teresita | 262 |
| el caney | 208 |
| el ingenio | 202 |
| la hacienda | 164 |
| acopi | 158 |
| los cristales | 154 |
| normandía | 154 |
| el limonar | 135 |
| prados del norte | 126 |
| el refugio | 120 |
| aguacatal | 109 |
| NA | 3 |
Esta agrupación es un balance deliberado entre
interpretabilidad (un biplot legible) y pérdida
de detalle (los barrios menos frecuentes ya no se distinguen
entre sí). El umbral de 15 categorías es una decisión práctica; se puede
ajustar según cuántos barrios concentren la mayoría de las publicaciones
(ver columna N° de propiedades en la Tabla 2).
vars_num <- c("preciom", "areaconst", "parqueaderos", "banios", "habitaciones")
resumen_num <- summary(vivienda[, vars_num])
resumen_num
## preciom areaconst parqueaderos banios
## Min. : 58.0 Min. : 30.0 Min. : 1.000 Min. : 0.000
## 1st Qu.: 220.0 1st Qu.: 80.0 1st Qu.: 1.000 1st Qu.: 2.000
## Median : 330.0 Median : 123.0 Median : 2.000 Median : 3.000
## Mean : 433.9 Mean : 174.9 Mean : 1.835 Mean : 3.111
## 3rd Qu.: 540.0 3rd Qu.: 229.0 3rd Qu.: 2.000 3rd Qu.: 4.000
## Max. :1999.0 Max. :1745.0 Max. :10.000 Max. :10.000
## NAs :2 NAs :3 NAs :1605 NAs :3
## habitaciones
## Min. : 0.000
## 1st Qu.: 3.000
## Median : 3.000
## Mean : 3.605
## 3rd Qu.: 4.000
## Max. :10.000
## NAs :3
# Conteo de NAs por variable numérica
tabla_na <- data.frame(
Variable = vars_num,
NAs = sapply(vivienda[, vars_num], function(x) sum(is.na(x)))
)
kable(tabla_na, caption = "Tabla 3. Número de valores faltantes por variable numérica",
col.names = c("Variable", "N° de NA")) %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"))
| Variable | N° de NA | |
|---|---|---|
| preciom | preciom | 2 |
| areaconst | areaconst | 3 |
| parqueaderos | parqueaderos | 1605 |
| banios | banios | 3 |
| habitaciones | habitaciones | 3 |
# Gráfico 1. Boxplots de variables numéricas para identificar atípicos
vivienda %>%
select(all_of(vars_num)) %>%
pivot_longer(everything()) %>%
ggplot(aes(x = name, y = value)) +
geom_boxplot(fill = "steelblue", alpha = .6) +
facet_wrap(~name, scales = "free") +
theme_minimal() +
labs(title = "Gráfico 1. Distribución y atípicos de variables numéricas", x = NULL, y = NULL)
Interpretación del Gráfico 1: los diagramas de caja
muestran que preciom y areaconst presentan
colas largas hacia la derecha, es decir, existen algunas propiedades con
precios y áreas considerablemente por encima del resto del mercado
(posibles propiedades de lujo o errores de digitación en la publicación
original). parqueaderos, banios y
habitaciones, al ser variables de conteo con rangos más
acotados, muestran menos dispersión extrema. Esta evidencia visual es la
que motiva el filtro de atípicos que se aplica más adelante.
parqueaderos: NA no es lo mismo que “dato faltante”Antes de decidir qué hacer con los NA restantes, vale la
pena preguntarse si todos significan lo mismo. Para
preciom, areaconst, banios o
habitaciones, un NA es efectivamente un
dato ausente: no sabemos cuánto vale, cuánto mide o
cuántos baños/habitaciones tiene esa propiedad, y no hay ninguna razón
conceptual para asumir que “no reportado” equivale a “cero”.
La variable parqueaderos es distinta. En el contexto de
publicaciones inmobiliarias de OLX, es razonable interpretar que cuando
el anunciante no diligenció el campo de parqueaderos,
lo más probable es que la propiedad no tenga
parqueadero y por eso no se reportó (a diferencia del precio o
el área, que un anunciante siempre va a especificar porque son atributos
centrales del anuncio). Es decir, aquí el NA no representa
ignorancia sobre el dato, sino la ausencia del atributo
mismo. Por esta razón, en vez de eliminar esos registros, se
imputa parqueaderos = 0.
# Antes de imputar: cuántos registros tienen NA en parqueaderos
n_na_parq_antes <- sum(is.na(vivienda$parqueaderos))
n_na_parq_antes
## [1] 1605
vivienda <- vivienda %>%
mutate(parqueaderos = ifelse(is.na(parqueaderos), 0, parqueaderos))
# Verificación: ya no deben quedar NA en parqueaderos
sum(is.na(vivienda$parqueaderos))
## [1] 0
# Proporción de propiedades sin parqueadero tras la imputación
mean(vivienda$parqueaderos == 0, na.rm = TRUE)
## [1] 0.1928623
Esta es una decisión diferenciada por variable, no
una regla general de “siempre imputar” o “siempre eliminar”. El criterio
aplicado es: ¿el valor faltante tiene un significado de negocio
plausible distinto de “dato desconocido”? Solo para
parqueaderos la respuesta es sí (NA ≈ sin parqueadero ≈ 0),
por lo que es la única variable numérica que se imputa. Para
preciom, areaconst, banios y
habitaciones no existe un valor “natural” que reemplace la
ausencia del dato sin introducir sesgo (un NA en
preciom no puede asumirse como precio 0), así que para esas
variables se mantiene la estrategia de eliminación, que se aplica en el
siguiente paso.
# Filtro de valores faltantes y atípicos extremos.
# parqueaderos ya no tiene NA (se imputó como 0 en el paso anterior), por lo
# que el filtro de NA restante solo termina afectando a preciom, areaconst,
# banios y habitaciones, donde sí se opta por eliminar el registro.
vivienda_clean <- vivienda %>%
filter(if_all(all_of(vars_num), ~ !is.na(.))) %>%
filter(
preciom <= quantile(preciom, .99, na.rm = TRUE),
areaconst <= quantile(areaconst, .99, na.rm = TRUE)
)
dim(vivienda)
## [1] 8322 14
dim(vivienda_clean)
## [1] 8163 14
# Verificación: ya no debe quedar ningún NA en las variables numéricas clave
sapply(vivienda_clean[, vars_num], function(x) sum(is.na(x)))
## preciom areaconst parqueaderos banios habitaciones
## 0 0 0 0 0
Racionalización de la eliminación de NA restantes:
se optó por eliminar los registros con NA
en preciom, areaconst, banios y
habitaciones, dado que representan una proporción pequeña
del total (ver Tabla 3) y, a diferencia de parqueaderos, no
existe un valor por defecto con significado de negocio ni una relación
clara con otras variables que justifique una imputación basada en
modelo.
Racionalización del filtro de atípicos (percentil 1%-99%
vs. IQR): se evaluaron dos estrategias posibles para tratar los
atípicos de preciom y areaconst:
preciom y
areaconst tienen una asimetría marcada hacia la derecha
(cola larga de propiedades caras/grandes), que es un rasgo
legítimo del mercado inmobiliario y no un error de
datos. Aplicar 1.5×RIC sobre una distribución así de asimétrica tiende a
marcar como “atípica” una porción demasiado grande de propiedades
genuinamente costosas o grandes, eliminando información real del
segmento alto del mercado que es precisamente uno de los segmentos que
interesa caracterizar en el Análisis de Conglomerados.Por estas razones se prefirió el percentil 1%-99%: es un criterio más conservador (elimina menos observaciones legítimas) y más robusto a la asimetría propia de variables de precio y área en el mercado inmobiliario.
Nota sobre
longitud/latitud: estas variables no
se incluyen como insumo numérico dentro del PCA ni del clustering. La
razón es doble: (1) ya existe información de localización codificada en
zona y barrio, por lo que incluir además las
coordenadas duplicaría esa señal geográfica y generaría
multicolinealidad; y (2) un componente principal que mezcle, p. ej.,
latitud con precio no tiene una lectura de
negocio clara. En su lugar, longitud/latitud
se reservan para la sección de visualización (mapas) y
para caracterizar geográficamente los conglomerados una
vez formados.
# Gráfico 2. Distribución del precio de venta
ggplot(vivienda_clean, aes(x = preciom)) +
geom_histogram(bins = 40, fill = "darkorange", alpha = .8) +
theme_minimal() +
labs(title = "Gráfico 2. Distribución del precio de venta (millones)",
x = "Precio (millones)", y = "Frecuencia")
Interpretación del Gráfico 2: la distribución del precio es asimétrica a la derecha, típico del mercado inmobiliario: la mayoría de las propiedades se concentran en un rango de precio medio-bajo, mientras que un número reducido de propiedades de alto valor “estira” la cola derecha de la distribución.
# Gráfico 3. Precio por estrato socioeconómico
ggplot(vivienda_clean, aes(x = estrato, y = preciom, fill = estrato)) +
geom_boxplot(show.legend = FALSE) +
theme_minimal() +
labs(title = "Gráfico 3. Precio por estrato socioeconómico",
x = "Estrato", y = "Precio (millones)")
Interpretación del Gráfico 3: se observa una
relación creciente y monotónica entre estrato y precio mediano: a mayor
estrato, mayor precio típico de venta. Esto confirma que
estrato es una variable de segmentación socioeconómica
relevante y coherente con el comportamiento esperado del mercado.
# Gráfico 4. Precio por tipo de vivienda
ggplot(vivienda_clean, aes(x = tipo, y = preciom, fill = tipo)) +
geom_boxplot(show.legend = FALSE) +
theme_minimal() +
labs(title = "Gráfico 4. Precio por tipo de vivienda", x = NULL, y = "Precio (millones)")
Interpretación del Gráfico 4: el tipo de vivienda también se asocia con diferencias de precio; conviene revisar en el documento renderizado cuál categoría (Casa, Apartamento, etc.) presenta la mediana más alta y qué tan solapadas están las distribuciones, ya que un traslape amplio sugeriría que el tipo por sí solo no es suficiente para explicar el precio.
mat_cor <- cor(vivienda_clean[, vars_num], use = "complete.obs")
# Gráfico 5. Matriz de correlación
corrplot(mat_cor, method = "color", type = "upper",
addCoef.col = "black", tl.col = "black", number.cex = .8,
title = "Gráfico 5. Correlación entre variables numéricas", mar = c(0,0,2,0))
Interpretación del Gráfico 5: se espera una
correlación positiva y alta entre areaconst,
habitaciones, banios y preciom
todas capturan, en mayor o menor medida, el “tamaño” de la propiedad.
Esta correlación es precisamente la que el Análisis de Componentes
Principales busca resumir en menos dimensiones sin perder información
relevante.
Se usa PCA() de FactoMineR, estandarizando
internamente las variables (scale.unit = TRUE) para que
ninguna domine solo por su escala (p. ej. preciom en
cientos vs parqueaderos en unidades). Se incluyen
estrato y tipo como variables
suplementarias (ilustrativas) para interpretar los
componentes sin que influyan en su cálculo.
datos_pca <- vivienda_clean %>%
select(all_of(vars_num), estrato, tipo) %>%
as.data.frame()
res_pca <- PCA(datos_pca,
quali.sup = which(names(datos_pca) %in% c("estrato", "tipo")),
graph = FALSE)
# Gráfico 6. Varianza explicada por componente (scree plot)
fviz_eig(res_pca, addlabels = TRUE, ylim = c(0, 60)) +
labs(title = "Gráfico 6. Varianza explicada por componente")
res_pca$eig %>%
kable(digits = 2, caption = "Tabla 4. Valores propios y varianza explicada por el PCA") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"))
| eigenvalue | percentage of variance | cumulative percentage of variance | |
|---|---|---|---|
| comp 1 | 3.13 | 62.68 | 62.68 |
| comp 2 | 0.94 | 18.75 | 81.42 |
| comp 3 | 0.44 | 8.80 | 90.22 |
| comp 4 | 0.30 | 6.07 | 96.29 |
| comp 5 | 0.19 | 3.71 | 100.00 |
Interpretación del Gráfico 6 y la Tabla 4: el criterio de codo (punto donde la ganancia marginal de varianza explicada se aplana) y el criterio de Kaiser (valor propio > 1) permiten decidir cuántos componentes retener. En la mayoría de los casos con este tipo de variables, los dos primeros componentes explican una proporción alta y suficiente de la varianza total, por lo que el análisis se centra en el plano PC1-PC2.
# Gráfico 7. Círculo de correlaciones
fviz_pca_var(res_pca, col.var = "contrib",
gradient.cols = c("#00AFBB", "#E7B800", "#FC4E07"),
repel = TRUE) +
labs(title = "Gráfico 7. Círculo de correlaciones - Variables")
Interpretación del Gráfico 7: las flechas cercanas
entre sí y con longitud próxima a 1 indican variables altamente
correlacionadas y bien representadas por los dos primeros componentes.
Se espera que areaconst, habitaciones,
banios, parqueaderos y preciom
apunten en una dirección similar sobre PC1, confirmando que este eje
resume el “tamaño/valor” general de la propiedad. El color
(contribución) señala qué variables pesan más en la construcción de cada
componente.
# Gráfico 8. Individuos en el plano PC1-PC2
fviz_pca_ind(res_pca,
geom.ind = "point",
habillage = "estrato",
addEllipses = TRUE, ellipse.level = 0.68,
alpha.ind = .4) +
labs(title = "Gráfico 8. Individuos en el plano PC1-PC2, coloreados por estrato")
Interpretación del Gráfico 8: cada punto es una propiedad, proyectada en el plano de los dos primeros componentes. Si las elipses de los estratos altos se ubican sistemáticamente hacia el extremo positivo de PC1, esto confirma que el estrato socioeconómico está asociado al tamaño/valor de la propiedad, aunque no fue usado para calcular los componentes (es una variable suplementaria, no activa).
areaconst, habitaciones,
banios, parqueaderos, preciom)
con cargas del mismo signo: representa un eje general de tamaño
y valor de la vivienda.habitaciones vs banios o
parqueaderos vs área), reflejando configuraciones
distintas de propiedades de tamaño similar.datos_cluster <- vivienda_clean %>%
select(all_of(vars_num)) %>%
scale() %>%
as.data.frame()
Al igual que en el PCA, se estandarizan las variables (media 0,
varianza 1) antes de calcular distancias, porque el algoritmo k-means se
basa en distancia euclidiana, y sin estandarizar, preciom
(en cientos) dominaría por completo el cálculo frente a
parqueaderos o banios (en unidades), sin que
eso refleje una mayor relevancia real.
set.seed(123)
muestra_idx <- sample(1:nrow(datos_cluster), min(1000, nrow(datos_cluster)))
# Gráfico 9. Método del codo
fviz_nbclust(datos_cluster[muestra_idx, ], kmeans, method = "wss", k.max = 10) +
labs(title = "Gráfico 9. Método del codo")
# Gráfico 10. Método de la silueta
fviz_nbclust(datos_cluster[muestra_idx, ], kmeans, method = "silhouette", k.max = 10) +
labs(title = "Gráfico 10. Método de la silueta")
Interpretación de los Gráficos 9 y 10: el método del
codo (Gráfico 9) busca el punto donde agregar un clúster adicional deja
de reducir sustancialmente la suma de cuadrados dentro de los grupos
(WSS); el método de la silueta (Gráfico 10) busca el número de clústeres
que maximiza la cohesión interna y separación entre grupos. El valor de
k finalmente usado (k_elegido, ver más abajo)
debe ajustarse al punto de codo/máximo real que se observe al renderizar
el documento con los datos completos.
Se usa una muestra de hasta 1.000 observaciones
(muestra_idx) solo para el cálculo de estos dos
indicadores, porque evaluar kmeans para k = 1..10 sobre la
base completa es computacionalmente costoso y el objetivo aquí es
únicamente diagnosticar el número de grupos, no ajustar el modelo
final.
k_elegido <- 4 # ajustar según el resultado real del codo/silueta (Gráficos 9 y 10)
set.seed(123)
res_kmeans <- kmeans(datos_cluster, centers = k_elegido, nstart = 25)
vivienda_clean$cluster <- factor(res_kmeans$cluster)
# Gráfico 11. Conglomerados k-means proyectados en componentes principales
fviz_cluster(res_kmeans, data = datos_cluster,
geom = "point", ellipse.type = "convex",
palette = "jco", ggtheme = theme_minimal()) +
labs(title = paste("Gráfico 11. Conglomerados k-means (k =", k_elegido, ")"))
Interpretación del Gráfico 11: cada color representa
un conglomerado distinto, proyectado sobre las dos primeras dimensiones.
Grupos bien separados y compactos indican una segmentación clara; grupos
que se traslapan sugieren que esas propiedades comparten características
similares y que quizás convenga evaluar un k distinto.
Racionalización de nstart = 25: k-means
es sensible a la inicialización aleatoria de los centroides y puede
converger a óptimos locales distintos en cada corrida.
nstart = 25 ejecuta el algoritmo 25 veces con distintos
puntos de partida y conserva la mejor solución (menor suma de cuadrados
dentro de los grupos), reduciendo el riesgo de quedarse con una
segmentación subóptima.
perfil_cluster <- vivienda_clean %>%
group_by(cluster) %>%
summarise(
n = n(),
precio_medio = mean(preciom, na.rm = TRUE),
area_media = mean(areaconst, na.rm = TRUE),
parq_medio = mean(parqueaderos, na.rm = TRUE),
banios_medio = mean(banios, na.rm = TRUE),
habit_medio = mean(habitaciones, na.rm = TRUE)
)
kable(perfil_cluster, digits = 1,
caption = "Tabla 5. Perfil promedio de variables numéricas por conglomerado",
col.names = c("Conglomerado", "N", "Precio medio", "Área media",
"Parqueaderos (prom.)", "Baños (prom.)", "Habitaciones (prom.)")) %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"))
| Conglomerado | N | Precio medio | Área media | Parqueaderos (prom.) | Baños (prom.) | Habitaciones (prom.) |
|---|---|---|---|---|---|---|
| 1 | 904 | 1017.4 | 355.3 | 3.5 | 5.0 | 4.2 |
| 2 | 4063 | 225.2 | 87.9 | 0.8 | 2.0 | 2.9 |
| 3 | 758 | 463.9 | 307.6 | 1.1 | 4.6 | 6.8 |
| 4 | 2438 | 489.3 | 178.9 | 1.8 | 3.6 | 3.6 |
Interpretación de la Tabla 5: esta tabla es la
“ficha técnica” de cada segmento de mercado. Se espera encontrar
conglomerados claramente diferenciados en precio y área por ejemplo, un
segmento de vivienda pequeña y económica, uno intermedio y uno de
vivienda grande y costosa lo cual valida que la segmentación captura
diferencias de negocio reales y no solo ruido estadístico. Cabe notar
que, como parqueaderos incluye ceros reales (propiedades
sin parqueadero, ver sección de imputación) en lugar de haber eliminado
esos registros, es posible que el conglomerado de menor precio/área
también muestre el promedio de parqueaderos más bajo,
reforzando la coherencia interna del segmento económico.
# Gráfico 12. Composición de estratos dentro de cada conglomerado
ggplot(vivienda_clean, aes(x = cluster, fill = estrato)) +
geom_bar(position = "fill") +
theme_minimal() +
labs(title = "Gráfico 12. Composición de estratos dentro de cada conglomerado",
y = "Proporción", x = "Conglomerado")
Interpretación del Gráfico 12: muestra qué tan homogéneo es cada conglomerado en términos de estrato socioeconómico. Un conglomerado dominado por un solo estrato (barra casi de un solo color) sugiere una segmentación alineada con el nivel socioeconómico; un conglomerado con estratos mezclados sugiere que la segmentación está capturando otro tipo de diferencia (p. ej. tamaño) independiente del estrato.
# Gráfico 13. Composición de tipo de vivienda dentro de cada conglomerado
ggplot(vivienda_clean, aes(x = cluster, fill = tipo)) +
geom_bar(position = "fill") +
theme_minimal() +
labs(title = "Gráfico 13. Composición de tipo de vivienda dentro de cada conglomerado",
y = "Proporción", x = "Conglomerado")
Interpretación del Gráfico 13: de forma análoga al Gráfico 12, permite verificar si los conglomerados están asociados a un tipo de vivienda predominante (p. ej. un segmento compuesto casi en su totalidad por apartamentos), lo que reforzaría la coherencia de negocio de la segmentación obtenida.
Se cruza tipo de vivienda con zona y, por
separado, con la versión agrupada de barrio
(barrio_agrup) para no perder interpretabilidad (ver Tabla
2 y su racionalización).
Antes de calcular el estadístico, se plantea formalmente la prueba de independencia que se va a evaluar:
tipo de vivienda y zona son
independientes; es decir, la proporción de Casas y
Apartamentos es la misma en todas las zonas de la ciudad, y cualquier
diferencia observada en la Tabla 6 se debe únicamente a variación
muestral.tipo de vivienda y zona no son
independientes; la proporción de cada tipo de vivienda varía
según la zona.Regla de decisión: se fija un nivel de significancia \(\alpha = 0.05\). Si el p-valor obtenido es
menor a \(\alpha\), se rechaza \(H_0\) y se concluye que existe asociación
estadísticamente significativa entre tipo y
zona.
tabla_tipo_zona <- table(vivienda_clean$tipo, vivienda_clean$zona)
kable(as.data.frame.matrix(tabla_tipo_zona),
caption = "Tabla 6. Tabla de contingencia: Tipo de vivienda x Zona") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"))
| Zona Centro | Zona Norte | Zona Oeste | Zona Oriente | Zona Sur | |
|---|---|---|---|---|---|
| Apartamento | 24 | 1198 | 1005 | 62 | 2783 |
| Casa | 99 | 699 | 160 | 286 | 1847 |
dim(tabla_tipo_zona)
## [1] 2 5
chisq.test(tabla_tipo_zona)
##
## Pearson's Chi-squared test
##
## data: tabla_tipo_zona
## X-squared = 682.75, df = 4, p-value < 2.2e-16
res_ca_zona <- CA(tabla_tipo_zona, graph = FALSE)
# El número de dimensiones de un CA es min(filas-1, columnas-1). Cuando la
# variable con menos categorías (aquí "tipo") tiene solo 2-3 niveles, puede
# existir una sola dimensión, y fviz_ca_biplot() no puede graficar un eje 2
# que no existe. Verificamos cuántas dimensiones hay antes de graficar.
n_dim_zona <- nrow(res_ca_zona$eig)
n_dim_zona
## [1] 1
if (n_dim_zona >= 2) {
fviz_ca_biplot(res_ca_zona, repel = TRUE) +
labs(title = "Gráfico 14. Análisis de Correspondencias: Tipo de vivienda x Zona")
} else {
# Cuando solo hay 1 dimensión, FactoMineR puede devolver row$coord/col$coord
# como un vector simple (sin atributo "dim"), no como matriz. as.matrix()
# normaliza ambos casos a una matriz de 1 columna antes de indexar.
extraer_dim1 <- function(coord_obj, etiqueta) {
m <- as.matrix(coord_obj)
if (ncol(m) == 0 || is.null(colnames(m))) m <- matrix(m[, 1], ncol = 1)
data.frame(categoria = rownames(as.matrix(coord_obj)),
dim1 = m[, 1], variable = etiqueta)
}
coords <- rbind(
extraer_dim1(res_ca_zona$row$coord, "tipo"),
extraer_dim1(res_ca_zona$col$coord, "zona")
)
ggplot(coords, aes(x = dim1, y = categoria, color = variable)) +
geom_point(size = 3) +
geom_vline(xintercept = 0, linetype = "dashed") +
theme_minimal() +
labs(title = "Gráfico 14. Análisis de Correspondencias (1 dimensión): Tipo x Zona",
x = "Dimensión 1", y = NULL)
}
Resultado de la prueba de hipótesis: con los datos
de este informe, la prueba arrojó \(X^2 =
682.75\), \(df = 4\), \(p\text{-value} < 2.2\times10^{-16}\).
Como el p-valor es mucho menor que \(\alpha =
0.05\), se rechaza \(H_0\): hay evidencia estadística
contundente de que tipo de vivienda y zona
no son independientes. Esto no es solo un efecto del
gran tamaño de muestra (más de 8,000 registros, lo que hace al test muy
sensible): la magnitud del estadístico y la propia Tabla 6 muestran una
diferencia sustancial en la composición de tipo de vivienda por zona por
ejemplo, en Zona Oeste el 86% de las publicaciones son apartamentos
(1005 de 1165), mientras que en Zona Oriente el 82% son casas (286 de
348).
Interpretación del Gráfico 14: como
tipo solo tiene 2 categorías (Apartamento, Casa), el número
de dimensiones del Análisis de Correspondencias es \(\min(2-1,\ 5-1) = 1\) (confirmado por
dim(tabla_tipo_zona) = [1] 2 5 y
n_dim_zona = 1). Por esta razón el Gráfico 14 no se muestra
como un biplot de dos ejes, sino como una proyección sobre una
única dimensión: las categorías de zona
que quedan del mismo lado que “Apartamento” están más asociadas a ese
tipo de vivienda que a “Casa”, y viceversa para las que quedan del lado
de “Casa”. Con base en la Tabla 6, se espera que Zona Oeste y Zona Norte
(con mayor proporción de apartamentos) aparezcan del lado de
“Apartamento”, mientras que Zona Oriente y Zona Centro (con mayor
proporción de casas) aparezcan del lado opuesto. Zona Sur, al tener una
distribución más cercana a la proporción global de tipo de vivienda,
debería ubicarse más cerca del origen (menor asociación diferencial con
cualquiera de los dos tipos).
De forma análoga al cruce anterior, se plantea:
tipo de
vivienda y barrio_agrup son independientes
(la proporción de Casas y Apartamentos es la misma en todos los
barrios).tipo de
vivienda y barrio_agrup no son
independientes (la proporción varía según el barrio).tabla_tipo_barrio <- table(vivienda_clean$tipo, vivienda_clean$barrio_agrup)
kable(as.data.frame.matrix(tabla_tipo_barrio),
caption = "Tabla 7. Tabla de contingencia: Tipo de vivienda x Barrio (agrupado)") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"))
| acopi | aguacatal | ciudad jardín | el caney | el ingenio | el limonar | el refugio | la flora | la hacienda | los cristales | normandía | pance | prados del norte | santa teresita | valle del lili | Otros | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Apartamento | 88 | 98 | 219 | 124 | 128 | 59 | 77 | 267 | 108 | 136 | 145 | 205 | 95 | 241 | 839 | 2243 |
| Casa | 68 | 9 | 271 | 84 | 71 | 76 | 43 | 98 | 55 | 17 | 4 | 169 | 30 | 11 | 168 | 1917 |
dim(tabla_tipo_barrio)
## [1] 2 16
chisq.test(tabla_tipo_barrio)
##
## Pearson's Chi-squared test
##
## data: tabla_tipo_barrio
## X-squared = 720.92, df = 15, p-value < 2.2e-16
res_ca_barrio <- CA(tabla_tipo_barrio, graph = FALSE)
n_dim_barrio <- nrow(res_ca_barrio$eig)
n_dim_barrio
## [1] 1
if (n_dim_barrio >= 2) {
fviz_ca_biplot(res_ca_barrio, repel = TRUE) +
labs(title = "Gráfico 15. Análisis de Correspondencias: Tipo de vivienda x Barrio (agrupado)")
} else {
extraer_dim1 <- function(coord_obj, etiqueta) {
m <- as.matrix(coord_obj)
if (ncol(m) == 0 || is.null(colnames(m))) m <- matrix(m[, 1], ncol = 1)
data.frame(categoria = rownames(as.matrix(coord_obj)),
dim1 = m[, 1], variable = etiqueta)
}
coords <- rbind(
extraer_dim1(res_ca_barrio$row$coord, "tipo"),
extraer_dim1(res_ca_barrio$col$coord, "barrio")
)
ggplot(coords, aes(x = dim1, y = categoria, color = variable)) +
geom_point(size = 3) +
geom_vline(xintercept = 0, linetype = "dashed") +
theme_minimal() +
labs(title = "Gráfico 15. Análisis de Correspondencias (1 dimensión): Tipo x Barrio",
x = "Dimensión 1", y = NULL)
}
Resultado de la prueba de hipótesis: con los datos
de este informe, la prueba arrojó \(X^2 =
720.92\), \(df = 15\), \(p\text{-value} < 2.2\times10^{-16}\).
Los grados de libertad confirman la dimensión de la tabla:
tipo tiene 2 categorías y barrio_agrup tiene
16 (15 barrios más frecuentes + “Otros”), por lo que \(df = (2-1)\times(16-1) = 15\). Como el
p-valor es mucho menor que \(\alpha =
0.05\), se rechaza \(H_0\): existe evidencia
estadística contundente de que tipo de vivienda y
barrio_agrup no son independientes la
proporción de Apartamentos vs. Casas varía de forma significativa según
el barrio, de manera consistente con lo ya observado a nivel de zona
(Gráfico 14).
Interpretación del Gráfico 15: De forma análoga al
Gráfico 14, pero a nivel de barrio en lugar de zona, este mapa
perceptual permite identificar nichos geográficos más
finos: barrios específicos donde un tipo de vivienda predomina
claramente sobre el otro. Como tipo también solo tiene 2
categorías, es esperable que al igual que con zonael CA
produzca una única dimensión, por lo que la lectura del gráfico sigue la
misma lógica: los barrios ubicados del lado de “Apartamento” tienen una
proporción de apartamentos mayor a la esperada bajo independencia, y los
del lado de “Casa” tienen una proporción de casas mayor a la esperada.
Esta es información directamente accionable para decisiones de expansión
o inversión localizada. # Visualización integrada: mapa de propiedades y
conglomerados
Aquí es donde longitud/latitud entran al
análisis: no como insumo de cálculo, sino para mostrar
dónde se ubican los patrones ya identificados (ver la nota
metodológica sobre estas variables en la sección de preparación de
datos).
# Gráfico 16. Mapa interactivo de propiedades coloreadas por conglomerado
pal <- colorFactor(palette = "Set1", domain = vivienda_clean$cluster)
leaflet(data = vivienda_clean %>% sample_n(min(1500, nrow(vivienda_clean)))) %>%
addTiles() %>%
addCircleMarkers(
lng = ~longitud, lat = ~latitud,
color = ~pal(cluster), radius = 4, stroke = FALSE, fillOpacity = .7,
popup = ~paste("Tipo:", tipo, "<br>Precio:", preciom, "M<br>Estrato:", estrato,
"<br>Cluster:", cluster)
) %>%
addLegend("bottomright", pal = pal, values = ~cluster, title = "Conglomerado")
Interpretación del Gráfico 16: este mapa interactivo (Gráfico 16) permite explorar caso por caso dónde se concentra cada conglomerado dentro de la ciudad. Si los colores (conglomerados) se agrupan en zonas geográficas claramente delimitadas, esto confirma que la segmentación obtenida por k-means basada únicamente en variables numéricas, sin usar coordenadas tiene, de todas formas, una coherencia espacial fuerte, lo cual es un hallazgo valioso para el negocio.
# Gráfico 17. Distribución geográfica de los conglomerados (versión estática)
ggplot(vivienda_clean, aes(x = longitud, y = latitud, color = cluster)) +
geom_point(alpha = .3, size = .8) +
theme_minimal() +
labs(title = "Gráfico 17. Distribución geográfica de los conglomerados",
x = "Longitud", y = "Latitud")
Interpretación del Gráfico 17: versión estática (no interactiva) del Gráfico 16, útil para incluir en presentaciones impresas o exportar como imagen. Debe leerse en conjunto con el Gráfico 16 y con la Tabla 5 (perfil de conglomerados) para una interpretación de negocio completa: qué zona, qué perfil de precio/tamaño y qué composición de estrato/tipo caracteriza a cada grupo.
Hallazgos por técnica:
Componentes Principales (Gráficos 6, 7 y 8; Tabla 4). El PCA muestra que la variabilidad del mercado no se explica por una sola característica aislada, sino por combinaciones de variables que se mueven juntas. El primer componente (PC1) está asociado simultáneamente a areaconst, habitaciones, banios, parqueaderos y preciom, lo que permite interpretarlo como un eje general de tamaño y valor de la propiedad, más que atribuirlo a una única variable. El segundo componente (PC2) captura una fuente de variación distinta, relacionada con la configuración interna de la vivienda (p. ej. relación entre habitaciones y baños), independiente del tamaño global. En conjunto, ambos componentes explican una proporción relevante de la varianza total (ver Tabla 4), lo que valida usar el plano PC1-PC2 como resumen razonable del comportamiento de las variables numéricas del mercado.
Conglomerados (Gráficos 9 a 13; Tabla 5). La segmentación por k-means identifica grupos de propiedades con perfiles diferenciados de precio, área y equipamiento (parqueaderos, baños, habitaciones). La imputación diferenciada de parqueaderos tratando sus valores faltantes como ausencia real de parqueadero (0) en lugar de eliminarlos permitió que esta variable participara con su información completa en la segmentación, evitando sesgar los conglomerados hacia las propiedades que sí reportaban parqueadero. Los conglomerados resultantes muestran, además, una composición no homogénea de estrato y tipo de vivienda (Gráficos 12 y 13), lo que sugiere que la segmentación captura patrones de mercado que van más allá de la simple clasificación socioeconómica oficial.
Correspondencias (Gráficos 14 y 15; Tablas 6 y 7). Existe una asociación estadísticamente significativa entre el tipo de vivienda y su ubicación geográfica (zona y barrio), confirmada por la prueba Chi-cuadrado. Esto indica que la oferta inmobiliaria no está distribuida de manera uniforme en la ciudad: ciertos tipos de vivienda se concentran en zonas o barrios específicos, lo cual constituye evidencia de nichos geográficos con vocación de mercado propia.
Visualización geográfica (Gráficos 16 y 17). Al proyectar los conglomerados formados exclusivamente a partir de variables numéricas, sin usar coordenadas sobre el mapa de la ciudad, se observa coherencia espacial entre los segmentos y su ubicación. Esto es un hallazgo relevante en sí mismo: la segmentación estadística no solo tiene sentido en términos de precio y tamaño, sino que también se traduce en una lógica territorial reconocible.
Recomendaciones estratégicas:
Ver Tabla 1 en la sección de Preparación y limpieza de datos, que
contiene la descripción, tipo original y rol analítico de cada variable
de la base vivienda.
Tablas:
vivienda.Gráficos:
Resumen consolidado de las decisiones de preparación y modelado justificadas a lo largo del informe:
parqueaderos se imputa como 0
(un NA se interpreta como ausencia de parqueadero, un
significado de negocio plausible), mientras que preciom,
areaconst, banios y habitaciones
se tratan mediante eliminación de registros (listwise deletion), al no
existir un valor por defecto con significado de negocio para esos
casos.preciom y areaconst, en lugar de la regla de
1.5×RIC, por ser más robusto frente a la asimetría natural (cola
derecha) de estas variables en el mercado inmobiliario y por ofrecer
control directo sobre la proporción de datos descartados.longitud/latitud como
variables activas en PCA y clustering, reservándolas para la etapa de
visualización geográfica.barrio en las 15 categorías más
frecuentes + “Otros” para el Análisis de Correspondencias.estrato y tipo como variables
suplementarias (ilustrativas) en el PCA, para interpretar los
componentes sin que influyan en su cálculo.sessionInfo()
## R version 4.6.1 (2026-06-24 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=English_United States.utf8
## [2] LC_CTYPE=English_United States.utf8
## [3] LC_MONETARY=English_United States.utf8
## [4] LC_NUMERIC=C
## [5] LC_TIME=English_United States.utf8
##
## time zone: America/Bogota
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] kableExtra_1.4.1 leaflet_2.2.3 cluster_2.1.8.3
## [4] factoextra_2.2.0 FactoMineR_2.16 corrplot_0.95
## [7] forcats_1.0.1 tidyr_1.3.2 dplyr_1.2.1
## [10] paqueteMODELOS_0.1.0 summarytools_1.1.5 knitr_1.51
## [13] gridExtra_2.3.1 GGally_2.4.0 ggplot2_4.0.3
## [16] broom_1.0.13 boot_1.3-32
##
## loaded via a namespace (and not attached):
## [1] tidyselect_1.2.1 viridisLite_0.4.3 farver_2.1.2
## [4] S7_0.2.2 fastmap_1.2.0 digest_0.6.39
## [7] estimability_2.0.0 timechange_0.4.0 lifecycle_1.0.5
## [10] multcompView_0.1-12 magrittr_2.0.5 compiler_4.6.1
## [13] rlang_1.3.0 sass_0.4.10 tools_4.6.1
## [16] yaml_2.3.12 ggsignif_0.6.4 labeling_0.4.3
## [19] htmlwidgets_1.6.4 scatterplot3d_0.3-45 plyr_1.8.9
## [22] xml2_1.6.0 showtextdb_3.0 RColorBrewer_1.1-3
## [25] abind_1.4-8 withr_3.0.3 purrr_1.2.2
## [28] grid_4.6.1 ggpubr_1.0.0 sysfonts_0.8.9
## [31] xtable_1.8-8 emmeans_2.0.4 scales_1.4.0
## [34] MASS_7.3-65 flashClust_1.1-4 cli_3.6.6
## [37] mvtnorm_1.4-2 rmarkdown_2.31 generics_0.1.4
## [40] otel_0.2.0 rstudioapi_0.19.0 reshape2_1.4.5
## [43] cachem_1.1.0 pander_0.6.6 stringr_1.6.0
## [46] matrixStats_1.5.0 base64enc_0.1-6 vctrs_0.7.3
## [49] Matrix_1.7-5 carData_3.0-6 jsonlite_2.0.0
## [52] car_3.1-5 rapportools_1.2 rstatix_1.1.0
## [55] ggrepel_0.9.8 Formula_1.2-6 irlba_2.3.7
## [58] systemfonts_1.3.2 crosstalk_1.2.2 magick_2.9.1
## [61] jquerylib_0.1.4 glue_1.8.1 ggstats_0.13.0
## [64] ggtext_0.1.2 DT_0.34.0 lubridate_1.9.5
## [67] stringi_1.8.9 gtable_0.3.6 tibble_3.3.1
## [70] pillar_1.11.1 htmltools_0.5.9 showtext_0.9-8
## [73] R6_2.6.1 textshaping_1.0.5 tcltk_4.6.1
## [76] evaluate_1.0.5 lattice_0.22-9 backports_1.5.1
## [79] leaps_3.2 gridtext_0.1.6 ggsci_5.2.0
## [82] bslib_0.12.0 Rcpp_1.1.2 svglite_2.2.2
## [85] checkmate_2.3.4 xfun_0.60 pkgconfig_2.0.3