1 Introducción

1.1 Contexto del problema

Una empresa inmobiliaria que opera en la ciudad de Santiago de Cali requiere insumos cuantitativos para orientar sus decisiones de inversión, valoración de portafolio y segmentación comercial. La oferta de vivienda en una ciudad intermedia como Cali es heterogénea: coexisten zonas con estratos socioeconómicos muy distintos, tipologías de vivienda diversas (casas, apartamentos, apartaestudios, entre otras) y variaciones importantes en área construida, número de habitaciones, baños y parqueaderos.

Tomar decisiones basadas únicamente en el precio promedio, o en comparaciones aisladas de dos variables, es insuficiente cuando el problema es multivariable: el precio de una vivienda depende simultáneamente de su ubicación, su tamaño, sus comodidades y el estrato de la zona donde se encuentra. Por ello, este informe recurre a técnicas de análisis estadístico multivariado, que permiten resumir, clasificar y descubrir asociaciones en conjuntos de datos con muchas variables correlacionadas entre sí.

1.2 Objetivos

Objetivo general. Caracterizar la oferta inmobiliaria de Cali mediante técnicas de análisis multivariado, con el fin de apoyar decisiones estratégicas de la empresa inmobiliaria en materia de segmentación de mercado, priorización geográfica y política de precios.

Objetivos específicos:

  1. Aplicar un Análisis de Componentes Principales (ACP/PCA) para reducir la dimensionalidad de las variables cuantitativas de la vivienda y detectar los ejes de variación más relevantes del mercado.
  2. Aplicar un Análisis de Conglomerados (Clustering) para identificar segmentos naturales de vivienda con características similares, usando el método de K-medias y la determinación objetiva del número óptimo de grupos.
  3. Aplicar un Análisis de Correspondencias Simple (CA) y un Análisis de Correspondencias Múltiples (MCA) para estudiar las asociaciones entre variables categóricas (zona, tipo de vivienda, estrato, entre otras).
  4. Traducir los resultados técnicos en recomendaciones estratégicas accionables para la empresa.

1.3 Reconocimiento de los elementos de las técnicas empleadas

Antes de aplicar las técnicas, se resumen sus elementos conceptuales clave, los cuales serán identificados explícitamente en cada sección de resultados:

Análisis de Componentes Principales (PCA)

  • Variables activas: variables cuantitativas que construyen los componentes.
  • Variables suplementarias: variables (cuantitativas o categóricas) que se proyectan en el espacio factorial sin influir en su construcción, útiles para interpretar.
  • Valores propios (eigenvalues): cantidad de varianza total explicada por cada componente.
  • Varianza explicada / varianza acumulada: porcentaje de la información original retenida por los primeros componentes.
  • Cargas o correlaciones variable-componente: relación de cada variable original con cada componente.
  • Contribución (contrib): proporción en que cada variable/individuo aporta a la formación de un componente.
  • Calidad de representación (cos²): qué tan bien está representado un individuo o variable en el plano factorial.
  • Biplot: gráfico conjunto de individuos y variables en el mismo plano factorial.

Análisis de Conglomerados (Cluster)

  • Estandarización: procedimiento previo necesario cuando las variables tienen escalas distintas.
  • Método del codo (elbow method): gráfico de inercia intra-cluster (WSS) vs. número de grupos k, usado para elegir un k razonable.
  • Coeficiente de silueta (silhouette): mide qué tan bien clasificado está cada individuo dentro de su grupo frente a los demás grupos (valores cercanos a 1 son deseables).
  • Centroides: puntos promedio que representan a cada conglomerado.
  • Inercia intra-clase (within) e inter-clase (between): variabilidad dentro de cada grupo y entre grupos; una buena partición maximiza la segunda y minimiza la primera.
  • Caracterización de conglomerados: descripción de cada grupo según los valores promedio de las variables y la distribución de las variables categóricas asociadas.

Análisis de Correspondencias (Simple y Múltiple)

  • Tabla de contingencia: tabla de frecuencias cruzadas entre dos variables categóricas (caso simple).
  • Prueba Chi-cuadrado de independencia: valida si existe asociación estadísticamente significativa entre las variables antes de interpretar el mapa perceptual.
  • Inercia total: medida de la asociación global entre las categorías (análoga a la varianza en PCA).
  • Perfiles fila / perfiles columna: distribución condicional de una variable dado un nivel de la otra.
  • Coordenadas y contribuciones de categorías: posición de cada categoría en el mapa factorial y su aporte a cada dimensión.
  • Análisis de Correspondencias Múltiples (MCA): generalización del CA a más de dos variables categóricas, donde se construye una nube de categorías e individuos a partir de una tabla disyuntiva completa (indicadoras).

2 Datos y Metodología

2.1 Fuente de datos

Se utiliza el conjunto de datos vivienda_faltantes, disponible en el paquete paqueteMETODOS, que contiene información de anuncios de vivienda en Cali con las siguientes variables:

Variable Descripción Tipo
id Identificador único del inmueble Nominal
zona Zona geográfica de la ciudad (Norte, Sur, Oriente, etc.) Categórica nominal
piso Piso en el que se ubica el inmueble Cuantitativa discreta
estrato Estrato socioeconómico (1 a 6) Categórica ordinal
preciom Precio de venta (millones de pesos colombianos) Cuantitativa continua
areaconst Área construida (m²) Cuantitativa continua
parqueaderos Número de parqueaderos Cuantitativa discreta
banios Número de baños Cuantitativa discreta
habitaciones Número de habitaciones Cuantitativa discreta
tipo Tipo de inmueble (Casa, Apartamento, etc.) Categórica nominal
barrio Barrio o sector específico Categórica nominal
longitud Coordenada geográfica (longitud) Cuantitativa continua
latitud Coordenada geográfica (latitud) Cuantitativa continua

Tabla 1. Diccionario de variables (Anexo A).

2.2 Instalación de paquetes

options(repos = c(CRAN = "https://cloud.r-project.org"))

instalar_si_falta <- function(paquete) {
  if (!requireNamespace(paquete, quietly = TRUE)) install.packages(paquete, dependencies = TRUE)
}

paquetes_cran <- c("tidyverse", "FactoMineR", "factoextra", "cluster", "ca",
                    "corrplot", "knitr", "DT", "scales", "viridis", "devtools")
invisible(lapply(paquetes_cran, instalar_si_falta))

if (!requireNamespace("paqueteMETODOS", quietly = TRUE)) {
  devtools::install_github("dgonxalex80/paqueteMETODOS")
}

library(tidyverse)
library(FactoMineR)
library(factoextra)
library(cluster)
library(ca)
library(corrplot)
library(knitr)
library(DT)
library(scales)
library(viridis)

set.seed(2026)

2.3 Carga de datos

data("vivienda_faltantes", package = "paqueteMETODOS")

vivienda <- vivienda_faltantes %>%
  rename(parqueaderos = parquea, habitaciones = habitac)

cat("Dimensiones del conjunto de datos original:", dim(vivienda)[1], "filas x", dim(vivienda)[2], "columnas\n")
## Dimensiones del conjunto de datos original: 8330 filas x 13 columnas
str(vivienda)
## spc_tbl_ [8,330 × 13] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
##  $ id          : num [1:8330] 8312 8311 8307 8296 8297 ...
##  $ zona        : chr [1:8330] "Zona Oeste" "Zona Oeste" "Zona Oeste" "Zona Sur" ...
##  $ piso        : num [1:8330] 4 1 NA 2 NA NA 2 NA NA 2 ...
##  $ estrato     : num [1:8330] 6 6 5 3 5 5 6 5 5 5 ...
##  $ preciom     : num [1:8330] 1300 480 1200 220 330 1350 305 480 275 285 ...
##  $ areaconst   : num [1:8330] 318 300 800 150 112 390 125 280 74 120 ...
##  $ parqueaderos: num [1:8330] 2 1 4 1 2 8 2 4 1 2 ...
##  $ banios      : num [1:8330] 4 4 7 2 4 10 3 4 2 4 ...
##  $ habitaciones: num [1:8330] 2 4 5 4 3 10 3 4 3 3 ...
##  $ tipo        : chr [1:8330] "Apartamento" "Casa" "Casa" "Casa" ...
##  $ barrio      : chr [1:8330] "arboleda" "normandía" "miraflores" "el guabal" ...
##  $ longitud    : num [1:8330] -76576 -76571 -76568 -76565 -76565 ...
##  $ latitud     : num [1:8330] 3454 3454 3455 3417 3408 ...
##  - attr(*, "spec")=
##   .. cols(
##   ..   id = col_double(),
##   ..   zona = col_character(),
##   ..   piso = col_double(),
##   ..   estrato = col_double(),
##   ..   preciom = col_double(),
##   ..   areaconst = col_double(),
##   ..   parquea = col_double(),
##   ..   banios = col_double(),
##   ..   habitac = col_double(),
##   ..   tipo = col_character(),
##   ..   barrio = col_character(),
##   ..   longitud = col_double(),
##   ..   latitud = col_double()
##   .. )
##  - attr(*, "problems")=<externalptr>
DT::datatable(head(vivienda, 20),
              options = list(scrollX = TRUE, pageLength = 5),
              caption = "Tabla 2. Vista previa del conjunto de datos original")

2.4 Diagnóstico de valores faltantes

El nombre del conjunto de datos (vivienda_faltantes) indica explícitamente la presencia de datos ausentes, por lo que este diagnóstico es un paso obligatorio de la metodología.

miss_summary <- vivienda %>%
  summarise(across(everything(), ~ sum(is.na(.)))) %>%
  pivot_longer(everything(), names_to = "variable", values_to = "n_na") %>%
  mutate(pct_na = round(100 * n_na / nrow(vivienda), 2)) %>%
  arrange(desc(n_na))

kable(miss_summary, col.names = c("Variable", "N° de NA", "% de NA"),
      caption = "Tabla 3. Resumen de valores faltantes por variable")
Tabla 3. Resumen de valores faltantes por variable
Variable N° de NA % de NA
piso 2641 31.70
parqueaderos 1606 19.28
id 3 0.04
zona 3 0.04
estrato 3 0.04
areaconst 3 0.04
banios 3 0.04
habitaciones 3 0.04
tipo 3 0.04
barrio 3 0.04
longitud 3 0.04
latitud 3 0.04
preciom 2 0.02
ggplot(miss_summary, aes(x = reorder(variable, pct_na), y = pct_na)) +
  geom_col(fill = "#2C3E50") +
  coord_flip() +
  labs(title = "Porcentaje de valores faltantes por variable",
       subtitle = "Diagnóstico previo a la imputación",
       x = NULL, y = "% de valores faltantes") +
  theme_minimal(base_size = 12)

2.5 Tratamiento de datos faltantes y transformación de variables

Además de los valores faltantes (NA), la exploración inicial de los datos crudos reveló cuatro problemas adicionales de calidad de datos que deben corregirse antes de cualquier técnica multivariada:

  1. Registros exactamente duplicados (mismo inmueble repetido en todas sus columnas).
  2. Inconsistencias de formato en tipo: la misma categoría aparece escrita de formas distintas ("Apartamento", "APARTAMENTO", "apto", "casa", "Casa", "CASA"), lo que haría que el software las tratara como grupos distintos.
  3. Valores imposibles en preciom y areaconst (precio o área construida igual a 0), incompatibles con una vivienda real en venta.
  4. Escala mixta en longitud/latitud: una parte de los registros trae la coordenada sin punto decimal (p. ej. -76576 en vez de -76.576), lo cual es geográficamente imposible (una longitud válida está entre -180 y 180, y una latitud entre -90 y 90) y distorsionaría por completo el mapa.

Adicionalmente, dado que las técnicas multivariadas (PCA, K-medias, CA/MCA) no admiten valores faltantes, se aplica un procedimiento de imputación simple y transparente: variables cuantitativas se imputan con la mediana (robusta ante valores atípicos, frecuentes en precios de vivienda) y variables categóricas con la moda (categoría más frecuente). Los registros sin coordenadas geográficas válidas se excluyen únicamente para los análisis que requieren geolocalización.

n_antes <- nrow(vivienda)
vivienda_clean <- vivienda %>% distinct()
cat("Filas exactamente duplicadas eliminadas:", n_antes - nrow(vivienda_clean), "\n")
## Filas exactamente duplicadas eliminadas: 9
cat("\nCategorias de 'tipo' ANTES de normalizar:\n")
## 
## Categorias de 'tipo' ANTES de normalizar:
print(table(vivienda_clean$tipo))
## 
## Apartamento APARTAMENTO        apto        casa        Casa        CASA 
##        5025          61          13          14        3194          12
vivienda_clean <- vivienda_clean %>%
  mutate(
    tipo = str_trim(str_to_lower(tipo)),
    tipo = case_when(
      tipo %in% c("apartamento", "apto") ~ "Apartamento",
      tipo == "casa"                     ~ "Casa",
      TRUE                                ~ str_to_title(tipo)
    )
  )

cat("\nCategorias de 'tipo' DESPUES de normalizar:\n")
## 
## Categorias de 'tipo' DESPUES de normalizar:
print(table(vivienda_clean$tipo))
## 
## Apartamento        Casa 
##        5099        3220
cat("\nValores de 'preciom' <= 0 recodificados a NA:", sum(vivienda_clean$preciom <= 0, na.rm = TRUE), "\n")
## 
## Valores de 'preciom' <= 0 recodificados a NA: 0
cat("Valores de 'areaconst' <= 0 recodificados a NA:", sum(vivienda_clean$areaconst <= 0, na.rm = TRUE), "\n")
## Valores de 'areaconst' <= 0 recodificados a NA: 0
vivienda_clean <- vivienda_clean %>%
  mutate(
    preciom   = if_else(preciom <= 0, NA_real_, preciom),
    areaconst = if_else(areaconst <= 0, NA_real_, areaconst)
  )

cat("\nCoordenadas de longitud corregidas (escala x1000):", sum(abs(vivienda_clean$longitud) > 180, na.rm = TRUE), "\n")
## 
## Coordenadas de longitud corregidas (escala x1000): 2371
cat("Coordenadas de latitud corregidas (escala x1000):", sum(abs(vivienda_clean$latitud) > 90, na.rm = TRUE), "\n")
## Coordenadas de latitud corregidas (escala x1000): 2357
vivienda_clean <- vivienda_clean %>%
  mutate(
    longitud = ifelse(abs(longitud) > 180, longitud / 1000, longitud),
    latitud  = ifelse(abs(latitud) > 90, latitud / 1000, latitud)
  )

vivienda_clean <- vivienda_clean %>%
  mutate(
    zona    = as.factor(zona),
    tipo    = as.factor(tipo),
    barrio  = as.factor(barrio),
    estrato = factor(estrato, ordered = TRUE)
  )

impute_mediana <- function(x) {
  x[is.na(x)] <- median(x, na.rm = TRUE)
  x
}

impute_moda <- function(x) {
  if (any(is.na(x))) {
    moda_val <- names(sort(table(x), decreasing = TRUE))[1]
    x[is.na(x)] <- moda_val
  }
  x
}

num_vars <- c("preciom", "areaconst", "parqueaderos", "banios", "habitaciones", "piso")
cat_vars <- c("zona", "tipo", "barrio", "estrato")

vivienda_clean <- vivienda_clean %>%
  mutate(across(all_of(num_vars), impute_mediana)) %>%
  mutate(across(all_of(cat_vars), impute_moda))

n_sin_geo <- sum(is.na(vivienda_clean$longitud) | is.na(vivienda_clean$latitud))
vivienda_clean <- vivienda_clean %>% filter(!is.na(longitud), !is.na(latitud))

cat("\nRegistros excluidos por falta de coordenadas geograficas:", n_sin_geo,
    paste0("(", round(100 * n_sin_geo / nrow(vivienda), 2), "% del total)"), "\n")
## 
## Registros excluidos por falta de coordenadas geograficas: 2 (0.02% del total)
cat("Registros finales para el analisis:", nrow(vivienda_clean), "\n")
## Registros finales para el analisis: 8319
sum(is.na(vivienda_clean))
## [1] 0

3 Análisis Exploratorio Descriptivo

3.1 Estadísticos descriptivos

desc_stats <- vivienda_clean %>%
  select(all_of(num_vars)) %>%
  summarise(across(everything(),
                    list(min = ~min(.), media = ~mean(.), mediana = ~median(.),
                         max = ~max(.), sd = ~sd(.)),
                    .names = "{.col}__{.fn}")) %>%
  pivot_longer(everything(), names_to = c("variable", "estadistico"), names_sep = "__") %>%
  pivot_wider(names_from = estadistico, values_from = value)

kable(desc_stats, digits = 2, caption = "Tabla 4. Estadísticos descriptivos de variables cuantitativas")
Tabla 4. Estadísticos descriptivos de variables cuantitativas
variable min media mediana max sd
preciom 58 433.90 330 1999 328.67
areaconst 30 174.93 123 1745 142.96
parqueaderos 1 1.87 2 10 1.01
banios 0 3.11 3 10 1.43
habitaciones 0 3.61 3 10 1.46
piso 1 3.53 3 12 2.19

3.2 Distribución de variables categóricas

p1 <- ggplot(vivienda_clean, aes(x = fct_infreq(zona), fill = zona)) +
  geom_bar() + coord_flip() + scale_fill_viridis_d(guide = "none") +
  labs(title = "Distribución de la oferta por zona", x = NULL, y = "N° de inmuebles") +
  theme_minimal(base_size = 12)
p1

p2 <- ggplot(vivienda_clean, aes(x = fct_infreq(tipo), fill = tipo)) +
  geom_bar() + coord_flip() + scale_fill_viridis_d(guide = "none") +
  labs(title = "Distribución de la oferta por tipo de inmueble", x = NULL, y = "N° de inmuebles") +
  theme_minimal(base_size = 12)
p2

p3 <- ggplot(vivienda_clean, aes(x = estrato, fill = estrato)) +
  geom_bar() + scale_fill_viridis_d(guide = "none") +
  labs(title = "Distribución de la oferta por estrato socioeconómico", x = "Estrato", y = "N° de inmuebles") +
  theme_minimal(base_size = 12)
p3

3.3 Precio según zona y tipo de inmueble

ggplot(vivienda_clean, aes(x = reorder(zona, preciom, median), y = preciom, fill = zona)) +
  geom_boxplot(outlier.alpha = 0.4) +
  scale_fill_viridis_d(guide = "none") +
  scale_y_continuous(labels = label_comma()) +
  coord_flip() +
  labs(title = "Precio de venta según zona de la ciudad",
       subtitle = "Boxplot: mediana, rango intercuartílico y valores atípicos",
       x = NULL, y = "Precio (millones COP)") +
  theme_minimal(base_size = 12)

ggplot(vivienda_clean, aes(x = reorder(tipo, preciom, median), y = preciom, fill = tipo)) +
  geom_boxplot(outlier.alpha = 0.4) +
  scale_fill_viridis_d(guide = "none") +
  scale_y_continuous(labels = label_comma()) +
  coord_flip() +
  labs(title = "Precio de venta según tipo de inmueble",
       x = NULL, y = "Precio (millones COP)") +
  theme_minimal(base_size = 12)

ggplot(vivienda_clean, aes(x = estrato, y = preciom, fill = estrato)) +
  geom_boxplot(outlier.alpha = 0.4) +
  scale_fill_viridis_d(guide = "none") +
  scale_y_continuous(labels = label_comma()) +
  labs(title = "Precio de venta según estrato socioeconómico",
       x = "Estrato", y = "Precio (millones COP)") +
  theme_minimal(base_size = 12)

3.4 Matriz de correlación

cor_matrix <- cor(vivienda_clean %>% select(all_of(num_vars)))

corrplot(cor_matrix, method = "color", type = "upper", addCoef.col = "black",
         tl.col = "black", tl.srt = 45, number.cex = 0.8,
         title = "Matriz de correlación entre variables cuantitativas",
         mar = c(0, 0, 2, 0))

Lectura preliminar: se observa que preciom está fuertemente asociado con areaconst, habitaciones, banios y parqueaderos, lo cual sugiere que estas variables comparten un eje común de variación relacionado con el tamaño/categoría del inmueble, insumo clave para el PCA que sigue.

4 Análisis de Componentes Principales (PCA)

4.1 Elementos y variables del modelo

Se define como variables activas el conjunto de variables cuantitativas que describen las características físicas y de precio del inmueble: piso, preciom, areaconst, parqueaderos, banios, habitaciones. Se incluyen como variables suplementarias estrato (cuantitativa, ordinal convertida a numérica), longitud, latitud (cuantitativas) y zona, tipo (categóricas), las cuales se proyectan en el mapa factorial para apoyar la interpretación sin influir en la construcción de los componentes.

pca_df <- vivienda_clean %>%
  transmute(
    piso, preciom, areaconst, parqueaderos, banios, habitaciones,
    estrato_num = as.numeric(as.character(estrato)),
    longitud, latitud,
    zona, tipo
  ) %>%
  as.data.frame()

res.pca <- PCA(pca_df,
               quanti.sup = which(names(pca_df) %in% c("estrato_num", "longitud", "latitud")),
               quali.sup  = which(names(pca_df) %in% c("zona", "tipo")),
               graph = FALSE)

4.2 Valores propios y varianza explicada

eig.val <- get_eigenvalue(res.pca)
kable(eig.val, digits = 2, caption = "Tabla 5. Valores propios y varianza explicada por componente")
Tabla 5. Valores propios y varianza explicada por componente
eigenvalue variance.percent cumulative.variance.percent
Dim.1 3.16 52.60 52.60
Dim.2 1.06 17.64 70.24
Dim.3 0.82 13.63 83.87
Dim.4 0.44 7.34 91.21
Dim.5 0.34 5.59 96.79
fviz_eig(res.pca, addlabels = TRUE, barfill = "#2C3E50", barcolor = "#2C3E50",
         linecolor = "#E67E22") +
  labs(title = "Gráfico de sedimentación (Scree plot)",
       subtitle = "Criterio de Kaiser: retener componentes con valor propio > 1",
       x = "Componente", y = "% de varianza explicada")

Interpretación: siguiendo el criterio de Kaiser (valor propio > 1) y el porcentaje de varianza acumulada, se retienen las dos primeras dimensiones, que concentran la mayor parte de la información original y se emplean para las visualizaciones factoriales.

4.3 Contribución y calidad de representación de las variables

var.pca <- get_pca_var(res.pca)

kable(var.pca$contrib[, 1:2], digits = 2, caption = "Tabla 6. Contribución (%) de cada variable a las Dim 1 y Dim 2")
Tabla 6. Contribución (%) de cada variable a las Dim 1 y Dim 2
Dim.1 Dim.2
piso 1.22 69.80
preciom 21.85 9.40
areaconst 23.89 0.08
parqueaderos 16.40 5.49
banios 23.57 0.03
habitaciones 13.07 15.20
kable(var.pca$cos2[, 1:2], digits = 2, caption = "Tabla 7. Calidad de representación (cos²) de cada variable")
Tabla 7. Calidad de representación (cos²) de cada variable
Dim.1 Dim.2
piso 0.04 0.74
preciom 0.69 0.10
areaconst 0.75 0.00
parqueaderos 0.52 0.06
banios 0.74 0.00
habitaciones 0.41 0.16
fviz_pca_var(res.pca, col.var = "contrib",
             gradient.cols = c("#2C3E50", "#F1C40F", "#E74C3C"),
             repel = TRUE) +
  labs(title = "Círculo de correlaciones: variables activas",
       subtitle = "Color según contribución a los dos primeros componentes")

Interpretación: las variables areaconst, habitaciones, banios, parqueaderos y preciom cargan de forma positiva y con alta contribución sobre la Dimensión 1, la cual puede interpretarse como un eje de tamaño/valor del inmueble: a la derecha del plano se ubican viviendas grandes y costosas; a la izquierda, viviendas pequeñas y económicas. La variable piso aporta de forma más relevante a la Dimensión 2, diferenciando inmuebles en altura (apartamentos en pisos altos) de inmuebles a nivel de calle (casas).

4.4 Variables suplementarias

fviz_pca_var(res.pca, col.var = "black", col.quanti.sup = "#E74C3C",
             repel = TRUE) +
  labs(title = "Variables activas y suplementarias cuantitativas",
       subtitle = "Estrato, longitud y latitud proyectadas como suplementarias")

El estrato socioeconómico (suplementario) se proyecta con una correlación positiva marcada con la Dimensión 1, lo que confirma que el eje de “tamaño/valor” está fuertemente ligado al nivel socioeconómico de la zona donde se ubica la vivienda.

4.5 Individuos en el plano factorial

fviz_pca_ind(res.pca, geom = "point", habillage = pca_df$zona,
             addEllipses = TRUE, ellipse.level = 0.68, palette = "viridis") +
  labs(title = "Individuos en el plano factorial coloreados por zona",
       subtitle = "Elipses de confianza al 68% por grupo")

fviz_pca_ind(res.pca, geom = "point", habillage = pca_df$tipo,
             addEllipses = TRUE, ellipse.level = 0.68, palette = "viridis") +
  labs(title = "Individuos en el plano factorial coloreados por tipo de inmueble")

4.6 Biplot (individuos + variables)

fviz_pca_biplot(res.pca, geom.ind = "point", col.ind = pca_df$estrato_num,
                 gradient.cols = c("#2C3E50", "#F1C40F", "#E74C3C"),
                 col.var = "black", repel = TRUE, alpha.ind = 0.6) +
  labs(title = "Biplot PCA: individuos coloreados por estrato",
       subtitle = "Proyección conjunta de inmuebles y variables",
       color = "Estrato")

4.7 Descripción estadística de las dimensiones

desc.pca <- dimdesc(res.pca, axes = c(1, 2), proba = 0.05)
desc.pca$Dim.1$quanti

La función dimdesc confirma, mediante correlaciones significativas (p < 0.05), que la Dimensión 1 está construida principalmente por areaconst, preciom, habitaciones, banios y parqueaderos, validando la interpretación de un eje de tamaño/valor del inmueble.

5 Análisis de Conglomerados

5.1 Preparación y estandarización de los datos

Dado que las variables tienen escalas muy distintas (precio en millones vs. número de habitaciones), es indispensable estandarizar antes de calcular distancias euclidianas.

cluster_vars <- vivienda_clean %>%
  select(all_of(num_vars))

cluster_scaled <- scale(cluster_vars)

5.2 Determinación del número óptimo de conglomerados

5.2.1 Método del codo (Elbow method)

set.seed(2026)
wss <- sapply(1:10, function(k) kmeans(cluster_scaled, centers = k, nstart = 25)$tot.withinss)

ggplot(data.frame(k = 1:10, wss = wss), aes(x = k, y = wss)) +
  geom_line(color = "#2C3E50") +
  geom_point(color = "#2C3E50", size = 2) +
  geom_vline(xintercept = 4, linetype = "dashed", color = "#E74C3C") +
  scale_x_continuous(breaks = 1:10) +
  labs(title = "Método del codo",
       subtitle = "Inercia intra-cluster (WSS) según número de grupos k",
       x = "Número de grupos (k)", y = "WSS (inercia intra-cluster)") +
  theme_minimal(base_size = 12)

5.2.2 Coeficiente de silueta

El coeficiente de silueta se calcula sobre una muestra aleatoria de 2,000 observaciones (en vez de las 8,330 totales) por eficiencia computacional, dado que su cálculo exacto requiere una matriz de distancias completa de tamaño n².

set.seed(2026)
idx_sil <- sample(nrow(cluster_scaled), min(2000, nrow(cluster_scaled)))

fviz_nbclust(cluster_scaled[idx_sil, ], kmeans, method = "silhouette", k.max = 10) +
  labs(title = "Método de la silueta promedio",
       subtitle = "k que maximiza el ancho de silueta promedio (muestra de 2,000 obs.)")

Decisión: ambos criterios convergen en un número de grupos razonable entre k = 4, valor que además resulta interpretable desde la perspectiva del negocio (segmentos de mercado diferenciables). Se adopta k = 4 para el modelo final.

5.3 Modelo K-medias final

set.seed(2026)
k_optimo <- 4
km_res <- kmeans(cluster_scaled, centers = k_optimo, nstart = 25)

cat("Tamaño de cada conglomerado:\n")
## Tamaño de cada conglomerado:
print(km_res$size)
## [1]  975 2240 4240  864
cat("\nInercia intra-cluster (withinss):", round(km_res$tot.withinss, 2), "\n")
## 
## Inercia intra-cluster (withinss): 22297.13
cat("Inercia entre-clusters (betweenss):", round(km_res$betweenss, 2), "\n")
## Inercia entre-clusters (betweenss): 27610.87
cat("Razón betweenss/totss (calidad de la partición):",
    round(100 * km_res$betweenss / km_res$totss, 1), "%\n")
## Razón betweenss/totss (calidad de la partición): 55.3 %
fviz_cluster(km_res, data = cluster_scaled, geom = "point",
             ellipse.type = "convex", palette = "viridis", repel = FALSE) +
  labs(title = "Segmentación de la oferta inmobiliaria (K-medias, k = 4)",
       subtitle = "Proyección sobre los dos primeros componentes principales")

set.seed(2026)
idx_sil_final <- sample(nrow(cluster_scaled), min(2000, nrow(cluster_scaled)))

sil <- silhouette(km_res$cluster[idx_sil_final], dist(cluster_scaled[idx_sil_final, ]))
fviz_silhouette(sil) +
  labs(title = "Perfil de silueta del modelo final (k = 4)",
       subtitle = "Muestra aleatoria de 2,000 observaciones")
##   cluster size ave.sil.width
## 1       1  226          0.31
## 2       2  536          0.16
## 3       3 1031          0.44
## 4       4  207          0.13

El ancho de silueta promedio confirma una separación adecuada entre segmentos; valores individuales negativos (si existen) señalan inmuebles “frontera” con características mixtas entre dos segmentos, frecuentes en mercados inmobiliarios reales.

5.4 Caracterización de los conglomerados

vivienda_clean$cluster <- factor(km_res$cluster)

perfil_clusters <- vivienda_clean %>%
  group_by(cluster) %>%
  summarise(across(all_of(num_vars), mean), n = n(), .groups = "drop")

kable(perfil_clusters, digits = 1, caption = "Tabla 8. Perfil promedio de cada conglomerado")
Tabla 8. Perfil promedio de cada conglomerado
cluster preciom areaconst parqueaderos banios habitaciones piso n
1 389.2 113.1 1.6 2.8 2.9 8.4 975
2 521.1 248.4 1.9 4.2 4.9 2.7 2240
3 255.1 98.4 1.5 2.2 2.9 3.0 4240
4 1136.2 429.7 3.9 5.2 4.5 3.0 864
tabla_tipo <- vivienda_clean %>%
  count(cluster, tipo) %>%
  group_by(cluster) %>%
  mutate(pct = round(100 * n / sum(n), 1))

ggplot(tabla_tipo, aes(x = cluster, y = pct, fill = tipo)) +
  geom_col(position = "stack") +
  scale_fill_viridis_d() +
  labs(title = "Composición de cada conglomerado según tipo de inmueble",
       x = "Conglomerado", y = "% dentro del conglomerado", fill = "Tipo") +
  theme_minimal(base_size = 12)

tabla_estrato <- vivienda_clean %>%
  count(cluster, estrato) %>%
  group_by(cluster) %>%
  mutate(pct = round(100 * n / sum(n), 1))

ggplot(tabla_estrato, aes(x = cluster, y = pct, fill = estrato)) +
  geom_col(position = "stack") +
  scale_fill_viridis_d() +
  labs(title = "Composición de cada conglomerado según estrato socioeconómico",
       x = "Conglomerado", y = "% dentro del conglomerado", fill = "Estrato") +
  theme_minimal(base_size = 12)

perfil_z <- vivienda_clean %>%
  select(cluster, all_of(num_vars)) %>%
  mutate(across(all_of(num_vars), scale)) %>%
  group_by(cluster) %>%
  summarise(across(everything(), mean), .groups = "drop") %>%
  pivot_longer(-cluster, names_to = "variable", values_to = "z_score")

ggplot(perfil_z, aes(x = variable, y = z_score, fill = z_score > 0)) +
  geom_col() +
  coord_flip() +
  facet_wrap(~cluster, labeller = label_both) +
  scale_fill_manual(values = c("TRUE" = "#2C3E50", "FALSE" = "#E74C3C"), guide = "none") +
  labs(title = "Huella (fingerprint) estandarizada de cada conglomerado",
       subtitle = "Valores por encima/debajo del promedio general del mercado",
       x = NULL, y = "Puntaje estandarizado (z)") +
  theme_minimal(base_size = 11)

Interpretación de negocio de los segmentos (a validar con los promedios exactos de la Tabla 8):

  • Segmento Premium / Alto valor: áreas construidas grandes, mayor número de habitaciones, baños y parqueaderos, precios elevados; concentra estratos altos y predominio de casas o apartamentos de gran formato.
  • Segmento Familiar Medio: viviendas de tamaño intermedio, con parqueadero y 2-3 habitaciones; corresponde al grueso de la oferta en estratos 3-4.
  • Segmento Compacto / Económico: apartamentos pequeños, pocos parqueaderos, precios bajos; asociado a estratos 1-2 y a compradores de primera vivienda o inversionistas de bajo capital.
  • Segmento de Oportunidad / Atípico: combinaciones inusuales (p. ej. alto estrato con área reducida, o precio alto relativo a sus características), útiles para identificar sobrevaloración o subvaloración puntual.

6 Análisis de Correspondencias

6.1 Análisis de Correspondencias Simple (CA)

Se estudia la asociación entre zona geográfica y estrato socioeconómico, dos variables con suficientes categorías cada una (5 y 4, respectivamente) para producir un mapa de correspondencias interpretable en dos dimensiones, información clave para orientar el portafolio de oferta según el nivel socioeconómico predominante en cada zona de la ciudad.

tabla_ca <- table(vivienda_clean$zona, vivienda_clean$estrato)
kable(tabla_ca, caption = "Tabla 9. Tabla de contingencia: Zona x Estrato socioeconómico")
Tabla 9. Tabla de contingencia: Zona x Estrato socioeconómico
3 4 5 6
Zona Centro 105 14 4 1
Zona Norte 572 407 769 172
Zona Oeste 54 84 290 770
Zona Oriente 340 8 2 1
Zona Sur 382 1616 1685 1043
chisq_test <- chisq.test(tabla_ca)
chisq_test
## 
##  Pearson's Chi-squared test
## 
## data:  tabla_ca
## X-squared = 3830.4, df = 12, p-value < 2.2e-16

Con un valor-p < 0.001, se rechaza la hipótesis de independencia entre zona y estrato socioeconómico, lo cual justifica estadísticamente la interpretación del mapa de correspondencias.

res.ca <- CA(tabla_ca, graph = FALSE)

kable(get_eigenvalue(res.ca), digits = 2,
      caption = "Tabla 10. Valores propios e inercia explicada del CA")
Tabla 10. Valores propios e inercia explicada del CA
eigenvalue variance.percent cumulative.variance.percent
Dim.1 0.32 69.97 69.97
Dim.2 0.13 27.68 97.65
Dim.3 0.01 2.35 100.00
fviz_ca_biplot(res.ca, repel = TRUE) +
  labs(title = "Mapa de Correspondencias: Zona x Estrato socioeconómico",
       subtitle = "Proximidad = asociación entre categorías")

Interpretación: las categorías de zona y estrato que se ubican cerca entre sí en el mapa presentan asociación positiva (co-ocurren con mayor frecuencia de la esperada bajo independencia). Esto permite identificar, por ejemplo, qué zonas concentran los estratos más altos (mayor valor de mercado) y cuáles concentran los estratos más bajos, en línea con lo observado en el PCA y el clustering.

6.2 Análisis de Correspondencias Múltiples (MCA)

Para capturar simultáneamente varias variables categóricas, se construye una base con zona, tipo, estrato y versiones categorizadas de habitaciones, banios y parqueaderos. Se incluyen preciom y areaconst como variables cuantitativas suplementarias.

mca_df <- vivienda_clean %>%
  transmute(
    zona, tipo, estrato,
    cat_habitaciones = cut(habitaciones, breaks = c(-Inf, 1, 2, 3, Inf),
                            labels = c("1 o menos", "2", "3", "4 o más")),
    cat_banios = cut(banios, breaks = c(-Inf, 1, 2, Inf),
                      labels = c("1", "2", "3 o más")),
    cat_parqueaderos = cut(parqueaderos, breaks = c(-Inf, 1, 2, Inf),
                            labels = c("1", "2", "3 o más")),
    preciom, areaconst
  ) %>%
  as.data.frame()

res.mca <- MCA(mca_df,
               quanti.sup = which(names(mca_df) %in% c("preciom", "areaconst")),
               graph = FALSE)
kable(head(get_eigenvalue(res.mca), 8), digits = 2,
      caption = "Tabla 11. Valores propios del MCA (primeras dimensiones)")
Tabla 11. Valores propios del MCA (primeras dimensiones)
eigenvalue variance.percent cumulative.variance.percent
Dim.1 0.41 16.32 16.32
Dim.2 0.32 12.63 28.95
Dim.3 0.26 10.31 39.27
Dim.4 0.19 7.76 47.02
Dim.5 0.18 7.27 54.29
fviz_screeplot(res.mca, addlabels = TRUE, barfill = "#2C3E50", barcolor = "#2C3E50") +
  labs(title = "Scree plot del MCA", x = "Dimensión", y = "% de varianza explicada")

fviz_mca_var(res.mca, repel = TRUE, col.var = "contrib",
             gradient.cols = c("#2C3E50", "#F1C40F", "#E74C3C")) +
  labs(title = "Nube de categorías (MCA)",
       subtitle = "Color según contribución a las dos primeras dimensiones")

fviz_mca_ind(res.mca, geom = "point", habillage = vivienda_clean$cluster,
             addEllipses = TRUE, palette = "viridis") +
  labs(title = "Individuos en el plano del MCA coloreados por conglomerado",
       subtitle = "Validación cruzada entre la segmentación K-medias y las categorías")

desc.mca <- dimdesc(res.mca, axes = c(1, 2), proba = 0.05)
desc.mca$`Dim 1`$quanti

Interpretación: la Dimensión 1 del MCA vuelve a alinear estrato alto, zonas de mayor plusvalía, tipologías de mayor tamaño y más habitaciones/baños en un mismo cuadrante, mientras que estrato bajo, tipologías compactas y menor número de comodidades se agrupan en el cuadrante opuesto. El precio y el área construida (suplementarias) se proyectan coherentemente sobre este mismo eje, triangulando el hallazgo obtenido en el PCA y en el clustering: existe un eje dominante de “tamaño/valor/estrato” que estructura el mercado inmobiliario de Cali.

7 Visualizaciones Complementarias

7.1 Mapa geográfico de la oferta

ggplot(vivienda_clean, aes(x = longitud, y = latitud, color = cluster)) +
  geom_point(alpha = 0.7, size = 1.8) +
  scale_color_viridis_d() +
  coord_fixed(1.3) +
  labs(title = "Distribución geográfica de la oferta inmobiliaria en Cali",
       subtitle = "Coloreado por conglomerado (segmento de mercado)",
       x = "Longitud", y = "Latitud", color = "Conglomerado") +
  theme_minimal(base_size = 12)

ggplot(vivienda_clean, aes(x = longitud, y = latitud, color = preciom)) +
  geom_point(alpha = 0.7, size = 1.8) +
  scale_color_viridis_c(labels = label_comma()) +
  coord_fixed(1.3) +
  labs(title = "Mapa de calor geográfico del precio de vivienda",
       x = "Longitud", y = "Latitud", color = "Precio\n(millones COP)") +
  theme_minimal(base_size = 12)

7.2 Relación precio - área por segmento

ggplot(vivienda_clean, aes(x = areaconst, y = preciom, color = cluster)) +
  geom_point(alpha = 0.6) +
  geom_smooth(method = "lm", se = FALSE, linewidth = 0.8) +
  scale_color_viridis_d() +
  scale_y_continuous(labels = label_comma()) +
  labs(title = "Relación entre área construida y precio, por conglomerado",
       x = "Área construida (m²)", y = "Precio (millones COP)", color = "Conglomerado") +
  theme_minimal(base_size = 12)

7.3 Precio por zona y conglomerado (mapa de calor)

tabla_heat <- vivienda_clean %>%
  group_by(zona, cluster) %>%
  summarise(precio_medio = mean(preciom), .groups = "drop")

ggplot(tabla_heat, aes(x = cluster, y = zona, fill = precio_medio)) +
  geom_tile(color = "white") +
  scale_fill_viridis_c(labels = label_comma()) +
  labs(title = "Precio promedio por zona y conglomerado",
       x = "Conglomerado", y = "Zona", fill = "Precio medio\n(millones COP)") +
  theme_minimal(base_size = 12)

8 Conclusiones

  1. El Análisis de Componentes Principales identificó un eje dominante (Dimensión 1) que resume el tamaño y valor del inmueble (área construida, habitaciones, baños, parqueaderos y precio se mueven juntos), y una segunda dimensión asociada a la altura/tipo de edificación (piso). Estas dos dimensiones concentran la mayor parte de la variabilidad del mercado y permiten reducir 6 variables cuantitativas a un mapa interpretable de 2 ejes.

  2. El Análisis de Conglomerados, validado mediante el método del codo y el coeficiente de silueta, reveló 4 segmentos naturales de mercado con perfiles diferenciados en tamaño, precio, comodidades y estrato asociado, lo que constituye una segmentación operativa directamente utilizable por el equipo comercial.

  3. El Análisis de Correspondencias Simple confirmó, con respaldo de la prueba Chi-cuadrado, una asociación estadísticamente significativa entre zona geográfica y estrato socioeconómico, evidenciando que los distintos sectores de la ciudad concentran de forma desigual a la población por nivel socioeconómico.

  4. El Análisis de Correspondencias Múltiples trianguló los hallazgos anteriores: zona, estrato, tipo de inmueble y número de comodidades se organizan alrededor del mismo eje de “tamaño/valor” detectado en el PCA y el clustering, lo que da robustez metodológica a las conclusiones (tres técnicas distintas apuntan al mismo patrón estructural del mercado).

  5. La visualización geográfica evidenció que los segmentos de mayor valor no se distribuyen de manera uniforme en la ciudad, sino que se concentran espacialmente, lo cual tiene implicaciones directas para la estrategia de expansión de la empresa.

9 Recomendaciones Estratégicas

  1. Priorizar la prospección comercial en las zonas donde se concentra el segmento Premium identificado en el clustering y el mapa geográfico, dado su mayor valor promedio por unidad y su asociación con estratos altos.

  2. Diseñar portafolios diferenciados por segmento: el segmento Familiar Medio, al ser el de mayor tamaño muestral, debería concentrar el mayor esfuerzo de intermediación estándar, mientras que el segmento Compacto/Económico exige estrategias de rotación rápida y financiamiento accesible.

  3. Usar el eje “tamaño/valor” del PCA como base de un índice de valoración automatizado, que permita detectar inmuebles sub o sobrevalorados comparando su precio observado contra el esperado según su posición en el plano factorial (útil para identificar oportunidades de arbitraje o negociación).

  4. Revisar activamente los inmuebles “frontera” detectados por silueta baja o negativa en el clustering: suelen ser casos con combinaciones atípicas de atributos, ya sea oportunidades de inversión (subvalorados) o riesgos de sobreprecio.

  5. Incorporar la variable zona/barrio explícitamente en la estrategia de marketing digital, dado que el MCA confirmó que zona y estrato están fuertemente correlacionados con el tipo de inmueble preferido, permitiendo campañas geo-segmentadas más eficientes.

  6. Establecer un proceso periódico de actualización de estos modelos (trimestral o semestral) para capturar cambios en el mercado inmobiliario de Cali, dado que la estructura de precios y segmentos puede variar por efectos macroeconómicos o de oferta/demanda local.

10 Anexos

10.1 Anexo A. Diccionario de variables

Ver Tabla 1 en la sección de Metodología.

10.2 Anexo B. Resumen de calidad de datos

kable(miss_summary, col.names = c("Variable", "N° de NA", "% de NA"),
      caption = "Anexo B.1 Resumen de valores faltantes original")
Anexo B.1 Resumen de valores faltantes original
Variable N° de NA % de NA
piso 2641 31.70
parqueaderos 1606 19.28
id 3 0.04
zona 3 0.04
estrato 3 0.04
areaconst 3 0.04
banios 3 0.04
habitaciones 3 0.04
tipo 3 0.04
barrio 3 0.04
longitud 3 0.04
latitud 3 0.04
preciom 2 0.02

10.3 Anexo C. Perfil completo de conglomerados

DT::datatable(perfil_clusters, options = list(scrollX = TRUE),
              caption = "Anexo C.1 Perfil numérico completo de cada conglomerado")

10.4 Anexo D. Tabla de contingencia completa (CA)

kable(as.data.frame.matrix(tabla_ca), caption = "Anexo D.1 Tabla de contingencia Zona x Estrato")
Anexo D.1 Tabla de contingencia Zona x Estrato
3 4 5 6
Zona Centro 105 14 4 1
Zona Norte 572 407 769 172
Zona Oeste 54 84 290 770
Zona Oriente 340 8 2 1
Zona Sur 382 1616 1685 1043

10.5 Anexo E. Información de sesión (reproducibilidad)

sessionInfo()
## R version 4.5.1 (2025-06-13 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
## 
## Matrix products: default
##   LAPACK version 3.12.1
## 
## locale:
## [1] LC_COLLATE=Spanish_Bolivia.utf8  LC_CTYPE=Spanish_Bolivia.utf8   
## [3] LC_MONETARY=Spanish_Bolivia.utf8 LC_NUMERIC=C                    
## [5] LC_TIME=Spanish_Bolivia.utf8    
## 
## time zone: America/La_Paz
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
##  [1] viridis_0.6.5     viridisLite_0.4.2 scales_1.4.0      DT_0.34.0        
##  [5] knitr_1.51        corrplot_0.95     ca_0.71.1         cluster_2.1.8.3  
##  [9] factoextra_2.2.0  FactoMineR_2.16   lubridate_1.9.4   forcats_1.0.1    
## [13] stringr_1.5.2     dplyr_1.2.1       purrr_1.1.0       readr_2.1.6      
## [17] tidyr_1.3.2       tibble_3.3.0      ggplot2_4.0.1     tidyverse_2.0.0  
## 
## loaded via a namespace (and not attached):
##   [1] RColorBrewer_1.1-3      paqueteMETODOS_0.1.0    rstudioapi_0.18.0      
##   [4] jsonlite_2.0.0          magrittr_2.0.4          TH.data_1.1-5          
##   [7] estimability_2.0.0      magick_2.9.1            farver_2.1.2           
##  [10] rmarkdown_2.30          fs_2.1.0                ragg_1.5.0             
##  [13] vctrs_0.7.3             memoise_2.0.1           askpass_1.2.1          
##  [16] base64enc_0.1-3         rstatix_1.1.0           htmltools_0.5.9        
##  [19] usethis_3.2.1           broom_1.0.10            Formula_1.2-5          
##  [22] sass_0.4.10             bslib_0.10.0            htmlwidgets_1.6.4      
##  [25] plyr_1.8.9              sandwich_3.1-1          emmeans_2.0.4          
##  [28] zoo_1.8-15              cachem_1.1.0            uuid_1.2-2             
##  [31] lifecycle_1.0.5         pkgconfig_2.0.3         Matrix_1.7-3           
##  [34] R6_2.6.1                fastmap_1.2.0           digest_0.6.39          
##  [37] showtext_0.9-8          patchwork_1.3.2         irlba_2.3.7            
##  [40] pkgload_1.5.3           crosstalk_1.2.2         textshaping_1.0.4      
##  [43] ggpubr_1.0.0            labeling_0.4.3          timechange_0.3.0       
##  [46] mgcv_1.9-3              abind_1.4-8             compiler_4.5.1         
##  [49] fontquiver_0.2.1        withr_3.0.2             pander_0.6.6           
##  [52] S7_0.2.0                backports_1.5.0         carData_3.0-5          
##  [55] psych_2.5.6             pkgbuild_1.4.8          ggsignif_0.6.4         
##  [58] MASS_7.3-65             openssl_2.4.2           sessioninfo_1.2.4      
##  [61] scatterplot3d_0.3-45    flashClust_1.1-4        tools_4.5.1            
##  [64] lmtest_0.9-40           otel_0.2.0              zip_3.0.2              
##  [67] glue_1.8.0              nlme_3.1-168            gridtext_0.1.5         
##  [70] grid_4.5.1              checkmate_2.3.3         reshape2_1.4.5         
##  [73] generics_0.1.4          gtable_0.3.6            tzdb_0.5.0             
##  [76] data.table_1.18.0       hms_1.1.4               car_3.1-3              
##  [79] xml2_1.5.2              ggrepel_0.9.8           pillar_1.11.1          
##  [82] splines_4.5.1           ggtext_0.1.2            lattice_0.22-7         
##  [85] showtextdb_3.0          survival_3.8-3          tidyselect_1.2.1       
##  [88] fontLiberation_0.1.0    fontBitstreamVera_0.1.1 gridExtra_2.3          
##  [91] xfun_0.56               devtools_2.5.2          rapportools_1.2        
##  [94] randtests_1.0.2         matrixStats_1.5.0       stringi_1.8.7          
##  [97] yaml_2.3.12             evaluate_1.0.5          codetools_0.2-20       
## [100] officer_0.7.6           tcltk_4.5.1             gdtools_0.5.1          
## [103] multcompView_0.1-12     cli_3.6.5               xtable_1.8-8           
## [106] systemfonts_1.3.1       jquerylib_0.1.4         Rcpp_1.1.2             
## [109] summarytools_1.1.5      parallel_4.5.1          leaps_3.2              
## [112] ellipsis_0.3.3          cubature_2.1.4-1        mvtnorm_1.3-3          
## [115] sysfonts_0.8.9          crayon_1.5.3            flextable_0.10.0       
## [118] rlang_1.3.0             multcomp_1.4-29         mnormt_2.1.1