library(paqueteMODELOS)
library(tidyverse)
library(FactoMineR)
library(factoextra)
library(cluster)
library(leaflet)
library(GGally)
library(knitr)
library(kableExtra)
library(psych)
library(scales)


Resumen ejecutivo

Este documento presenta un análisis multivariado de la oferta de vivienda urbana, combinando Análisis de Componentes Principales (ACP), segmentación por conglomerados y Análisis de Correspondencias Múltiples (ACM) para responder tres preguntas de negocio: qué atributos explican el precio de un inmueble, qué segmentos de mercado existen y cómo se distribuye la oferta en el territorio. Los resultados muestran que el precio responde principalmente al tamaño y al equipamiento del inmueble (área, baños, parqueaderos), que el mercado se organiza en tres segmentos con perfiles claramente distintos (económico, medio y premium), y que hay una marcada especialización geográfica de la oferta por zona, tipo de vivienda y estrato. Sobre esa base se construyen, en la sección 9, las recomendaciones de inversión y comercialización dirigidas al equipo directivo.

1. Introducción

Este trabajo desarrolla un análisis del mercado de vivienda urbana aplicando técnicas de análisis multivariado sobre la base de datos vivienda. Su propósito es identificar los patrones que rigen la oferta inmobiliaria — qué características determinan el precio, qué tipos de propiedad predominan y dónde se concentran — como insumo para decisiones estratégicas de inversión y comercialización.

2. Objetivos del análisis

Objetivo general: realizar un análisis integral y multidimensional de la base de datos de vivienda urbana, que permita comprender el mercado inmobiliario y sustentar decisiones estratégicas de inversión.

Objetivos específicos:

  1. Aplicar un Análisis de Componentes Principales (ACP) para reducir la dimensionalidad del conjunto de datos e identificar las características que más influyen en la variación de precios y en la oferta del mercado.
  2. Aplicar un Análisis de Conglomerados para agrupar las propiedades residenciales en segmentos homogéneos y entender las dinámicas de la oferta en distintas zonas de la ciudad y distintos estratos socioeconómicos.
  3. Aplicar un Análisis de Correspondencias Múltiples (ACM) para examinar la relación entre tipo de vivienda, zona y barrio, e identificar patrones de comportamiento de la oferta.
  4. Visualizar los resultados mediante gráficos y mapas que comuniquen los hallazgos de forma clara a la dirección de la empresa.

3. Descripción y preparación de la base de datos

3.1 Carga y estandarización de nombres de variables

# 1. Cargar la librería del curso y la base de datos
library(paqueteMODELOS)
data("vivienda")

# 2. Renombramos las columnas originales a nombres más descriptivos y
#    consistentes en español, para que el resto del análisis sea más
#    legible (evitamos abreviaturas como "preciom" o "banios")
datos_crudos <- vivienda %>%
  rename(
    precio_millones         = preciom,
    area_m2                 = areaconst,
    num_habitaciones        = habitaciones,
    num_banios              = banios,
    num_parqueaderos        = parqueaderos,
    tipo_vivienda            = tipo,
    zona_ciudad              = zona,
    estrato_socioeconomico   = estrato,
    nombre_barrio            = barrio,
    longitud_geo             = longitud,
    latitud_geo              = latitud
  )

# 3. Vista previa de la base de datos ya renombrada (usamos kable en
# vez de View(), ya que View() abre una pestaña interactiva de
# RStudio y no se renderiza al knittear el documento)
kable(head(datos_crudos), caption = "Tabla 0: Vista previa de la base de datos 'vivienda' (columnas renombradas)") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "bordered", "condensed"), full_width = TRUE) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#2C3E50")
Tabla 0: Vista previa de la base de datos ‘vivienda’ (columnas renombradas)
id zona_ciudad piso estrato_socioeconomico precio_millones area_m2 num_parqueaderos num_banios num_habitaciones tipo_vivienda nombre_barrio longitud_geo latitud_geo
1147 Zona Oriente NA 3 250 70 1 3 6 Casa 20 de julio -76.51168 3.43382
1169 Zona Oriente NA 3 320 120 1 2 3 Casa 20 de julio -76.51237 3.43369
1350 Zona Oriente NA 3 350 220 2 2 4 Casa 20 de julio -76.51537 3.43566
5992 Zona Sur 02 4 400 280 3 5 3 Casa 3 de julio -76.54000 3.43500
1212 Zona Norte 01 5 260 90 1 2 3 Apartamento acopi -76.51350 3.45891
1724 Zona Norte 01 5 240 87 1 3 3 Apartamento acopi -76.51700 3.36971

Nota sobre piso: esta variable no se incluye en el análisis posterior porque presenta un porcentaje muy alto de valores faltantes y, a diferencia de num_parqueaderos, num_banios y num_habitaciones, no es razonable imputarla por grupo sin introducir un sesgo relevante (el piso de un inmueble no depende directamente de tipo_vivienda y estrato_socioeconomico de forma homogénea).


3.2 Criterios de depuración e imputación de datos faltantes

Para certificar la confiabilidad estadística, el tratamiento de la base de datos se rige por tres criterios.

  1. Eliminación directa controlada (Pérdida < 0.05%): Solo se eliminan los registros con NAs en precio_millones o area_m2. Al ser variables críticas no imputables y representar solo 3 casos, eliminarlas no altera la representatividad (se conserva más del 99.9% de la muestra).

  2. Imputación por mediana condicional: Las variables num_parqueaderos, num_banios y num_habitaciones se imputan utilizando la mediana agrupada por tipo_vivienda y estrato_socioeconomico. Se utiliza esta metodología por varias razones.

  • Inadecuación de la media (sesgo por asimetría): Las variables num_parqueaderos, num_banios y num_habitaciones son variables discretas con distribuciones marcadamente asimétricas a la derecha (presencia de inmuebles atípicos de gran tamaño o de lujo). La media aritmética habría introducido valores decimales no realistas (ej. 2.4 habitaciones) y se habría visto distorsionada por valores extremos. La mediana garantiza un valor entero, representativo y central dentro del grupo.

  • Inadecuación de la imputación global (pérdida de la estructura del mercado): Imputar con la mediana general de toda la base de datos ignoraría que un apartamento en estrato 2 no comparte la misma arquitectura ni equipamiento que una casa en estrato 6. Al agrupar por tipo_vivienda (Casa / Apartamento) y estrato_socioeconomico (1 al 6), la imputación respeta la heterogeneidad y la lógica del mercado inmobiliario local.

  • Preservación de la matriz de covarianza para el ACP: La imputación condicional minimiza la distorsión de la varianza intra-grupo. Esto asegura que, al ejecutar posteriormente el Análisis de Componentes Principales (ACP), las correlaciones entre las variables cuantitativas refieran a la estructura real del conjunto de datos y no a un artefacto o ruido introducido por la imputación.

library(tidyverse)
library(knitr)
library(kableExtra)

# Conteo de NAs iniciales en variables cuantitativas
na_conteo_inicial <- colSums(is.na(datos_crudos %>% select(num_parqueaderos, num_banios, num_habitaciones, precio_millones, area_m2)))

# 1. Eliminación directa por falta de precio o área (< 0.05% de pérdida)
datos_depurados <- datos_crudos %>%
  drop_na(precio_millones, area_m2)

# 2. Imputación por mediana condicional (agrupada por tipo_vivienda y estrato_socioeconomico)
datos_depurados <- datos_depurados %>%
  group_by(tipo_vivienda, estrato_socioeconomico) %>%
  mutate(
    num_parqueaderos = ifelse(is.na(num_parqueaderos), median(num_parqueaderos, na.rm = TRUE), num_parqueaderos),
    num_banios       = ifelse(is.na(num_banios), median(num_banios, na.rm = TRUE), num_banios),
    num_habitaciones = ifelse(is.na(num_habitaciones), median(num_habitaciones, na.rm = TRUE), num_habitaciones)
  ) %>%
  ungroup() %>%
  mutate(
    num_parqueaderos = ifelse(is.na(num_parqueaderos), median(num_parqueaderos, na.rm = TRUE), num_parqueaderos),
    num_banios       = ifelse(is.na(num_banios), median(num_banios, na.rm = TRUE), num_banios),
    num_habitaciones = ifelse(is.na(num_habitaciones), median(num_habitaciones, na.rm = TRUE), num_habitaciones),
    zona_ciudad             = as.factor(zona_ciudad),
    estrato_socioeconomico  = as.factor(estrato_socioeconomico),
    tipo_vivienda           = as.factor(tipo_vivienda),
    nombre_barrio           = as.factor(nombre_barrio)
  )

# Conteo de NAs finales
na_conteo_final <- colSums(is.na(datos_depurados %>% select(num_parqueaderos, num_banios, num_habitaciones, precio_millones, area_m2)))

# TABLA 1: Resumen de Imputacion de Variables (Estilo Ejecutivo)
resumen_imputacion <- data.frame(
  Variable = names(na_conteo_inicial),
  `NAs Iniciales` = as.numeric(na_conteo_inicial),
  `NAs Finales` = as.numeric(na_conteo_final),
  `Valores Imputados` = as.numeric(na_conteo_inicial - na_conteo_final),
  check.names = FALSE
)

kable(resumen_imputacion, align = c("l", "c", "c", "c"), caption = "Tabla 1: Detalle de Imputacion por Mediana Condicional") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "bordered", "condensed"), full_width = TRUE) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#2C3E50")
Tabla 1: Detalle de Imputacion por Mediana Condicional
Variable NAs Iniciales NAs Finales Valores Imputados
num_parqueaderos 1605 0 1605
num_banios 3 0 3
num_habitaciones 3 0 3
precio_millones 2 0 2
area_m2 3 0 3
# TABLA 2: Balance del Filtro Muestral (Estilo Ejecutivo)
total_registros_inicial <- nrow(datos_crudos)
total_registros_final <- nrow(datos_depurados)
porcentaje_retencion <- round((total_registros_final / total_registros_inicial) * 100, 2)

resumen_depuracion <- data.frame(
  Metrica = c("Registros Iniciales", "Registros Finales Retenidos", "Registros Eliminados", "Porcentaje de Retencion"),
  Valor = c(as.character(total_registros_inicial), as.character(total_registros_final), as.character(total_registros_inicial - total_registros_final), paste0(porcentaje_retencion, "%"))
)

kable(resumen_depuracion, align = c("l", "r"), caption = "Tabla 2: Balance General de Depuracion Muestral") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "bordered", "condensed"), full_width = TRUE) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#2C3E50")
Tabla 2: Balance General de Depuracion Muestral
Metrica Valor
Registros Iniciales 8322
Registros Finales Retenidos 8319
Registros Eliminados 3
Porcentaje de Retencion 99.96%


3.3 Diagnóstico de valores atípicos multivariados

Tras la limpieza de los valores faltantes críticos, se revisan los valores atípicos en las variables cuantitativas. Se conservan los registros (no se eliminan).

Dado que los valores atípicos multivariados representan inmuebles con características de lujo o de alta densidad en la ciudad, y no errores de captura, se conservan en la base. Su impacto en el ACP se controla automáticamente mediante la estandarización de datos (centrado y escalado por desviación estándar).

library(ggplot2)
library(gridExtra)

# 1. Inspección Visual Univariada (Boxplots para variables clave)
p1 <- ggplot(datos_depurados, aes(y = precio_millones)) +
  geom_boxplot(fill = "#3498DB", alpha = 0.7) +
  theme_minimal() +
  labs(title = "Distribución del Precio (Millones)", y = "Precio ($)")

p2 <- ggplot(datos_depurados, aes(y = area_m2)) +
  geom_boxplot(fill = "#2ECC71", alpha = 0.7) +
  theme_minimal() +
  labs(title = "Distribución del Área Construida", y = "Área (m²)")

grid.arrange(p1, p2, ncol = 2)

# 2. Detección Multivariada con Distancia de Mahalanobis
variables_cuantitativas <- datos_depurados %>% 
  select(precio_millones, area_m2, num_habitaciones, num_banios, num_parqueaderos)

# Centroide y matriz de covarianza
vector_medias <- colMeans(variables_cuantitativas)
matriz_covarianza <- cov(variables_cuantitativas)

# Distancias de Mahalanobis
distancia_mahalanobis <- mahalanobis(variables_cuantitativas, vector_medias, matriz_covarianza)

# Umbral crítico Chi-cuadrado (p < 0.001 con k = 5 grados de libertad)
umbral_chi2 <- qchisq(0.999, df = ncol(variables_cuantitativas))
total_atipicos <- sum(distancia_mahalanobis > umbral_chi2)
porcentaje_atipicos <- round((total_atipicos / nrow(datos_depurados)) * 100, 2)

resumen_atipicos <- data.frame(
  Evaluacion = c("Variables Analizadas", "Umbral Crítico (Chi-cuadrado 99.9%)", "Atípicos Multivariados Detectados", "Porcentaje sobre la Muestra"),
  Resultado = c(paste(names(variables_cuantitativas), collapse = ", "), round(umbral_chi2, 2), total_atipicos, paste0(porcentaje_atipicos, "%"))
)

kable(resumen_atipicos, align = c("l", "r"), caption = "Tabla 3: Diagnóstico de Atípicos Multivariados (Mahalanobis)") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "bordered", "condensed"), full_width = TRUE) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#2C3E50")
Tabla 3: Diagnóstico de Atípicos Multivariados (Mahalanobis)
Evaluacion Resultado
Variables Analizadas precio_millones, area_m2, num_habitaciones, num_banios, num_parqueaderos
Umbral Crítico (Chi-cuadrado 99.9%) 20.52
Atípicos Multivariados Detectados 383
Porcentaje sobre la Muestra 4.6%


#### 3.4 Correlación entre variables cuantitativas

Antes de reducir dimensionalidad, revisamos la correlación entre las variables cuantitativas para verificar que exista suficiente estructura común como para que el ACP tenga sentido:

library(GGally)

datos_depurados %>%
  select(precio_millones, area_m2, num_habitaciones, num_banios, num_parqueaderos) %>%
  ggpairs(
    lower = list(continuous = wrap("points", alpha = 0.3, size = 0.6, color = "#2C3E50")),
    diag  = list(continuous = wrap("densityDiag", fill = "#3498DB", alpha = 0.6)),
    upper = list(continuous = wrap("cor", size = 4, color = "#2C3E50"))
  ) +
  theme_minimal() +
  labs(title = "Matriz de correlaciones y distribuciones - Variables cuantitativas")


4. Análisis de Componentes Principales (ACP)

Seguido a la limpieza e imputación de los datos se realiza el ACP. El precio (precio_millones) se excluye como variable activa y se incluye como variable cuantitativa suplementaria: dado que el precio es una consecuencia del tamaño y las comodidades del inmueble y no una característica física en sí misma, incluirlo como variable activa mezclaría causa y efecto en las mismas componentes. Al dejarlo como suplementaria, el ACP se construye solo con las características físicas (área, habitaciones, baños, parqueaderos) y después observamos cómo se relaciona el precio con esas dimensiones, lo cual es un hallazgo y no una definición circular.

4.1 Adecuación muestral (KMO y Bartlett)

Antes de interpretar los resultados del ACP, se valida que las variables tengan suficiente correlación entre sí como para justificar la técnica:

library(tidyverse)
library(FactoMineR)
library(factoextra)
library(psych)
library(knitr)
library(kableExtra)
library(gridExtra)

# 1. Selección de variables: activas (características físicas) +
#    precio_millones como suplementaria (columna 5, quanti.sup = 5)
variables_acp <- datos_depurados %>% 
  select(area_m2, num_habitaciones, num_banios, num_parqueaderos, precio_millones)

# 2. Prueba de Adecuación del Muestreo (KMO y Bartlett) sobre las
#    variables activas (sin precio_millones)
prueba_kmo <- KMO(variables_acp %>% select(-precio_millones))
prueba_bartlett <- cortest.bartlett(cor(variables_acp %>% select(-precio_millones)), n = nrow(variables_acp))

resumen_adecuacion_muestral <- data.frame(
  Prueba = c("KMO global (Kaiser-Meyer-Olkin)", "Bartlett - Chi-cuadrado", "Bartlett - p-valor"),
  Resultado = c(
    round(prueba_kmo$MSA, 3),
    round(prueba_bartlett$chisq, 2),
    format.pval(prueba_bartlett$p.value, digits = 3, eps = 0.001)
  )
)

kable(resumen_adecuacion_muestral, align = c("l", "r"),
      caption = "Tabla 3.1: Adecuación muestral para el ACP (KMO y Bartlett)") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "bordered", "condensed"), full_width = TRUE) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#2C3E50")
Tabla 3.1: Adecuación muestral para el ACP (KMO y Bartlett)
Prueba Resultado
KMO global (Kaiser-Meyer-Olkin) 0.722
Bartlett - Chi-cuadrado 12811.84
Bartlett - p-valor <0.001

Interpretación KMO/Bartlett: un KMO por encima de 0.6 se considera aceptable para ejecutar un ACP (mientras más cerca de 1, mejor); valores del KMO obtenido 0.72 indican una adecuación muestral aceptable para el análisis. La prueba de Bartlett resultó significativa (p < 0.05), lo que confirma que existe correlación suficiente entre las variables para justificar el ACP.

4.2 Varianza explicada

# 3. Ejecución del ACP: precio_millones como variable cuantitativa
#    suplementaria (columna 5), no participa en el cálculo de los ejes
modelo_acp <- PCA(variables_acp, scale.unit = TRUE, quanti.sup = 5, graph = FALSE)

# 4. Extracción de Eigenvalues y Varianza Explicada
valores_propios_acp <- get_eigenvalue(modelo_acp)

resumen_varianza_acp <- data.frame(
  Componente = paste0("Dimension ", 1:nrow(valores_propios_acp)),
  `Valor Propio (Eigenvalue)` = round(valores_propios_acp[, 1], 3),
  `Varianza Explicada (%)` = round(valores_propios_acp[, 2], 2),
  `Varianza Acumulada (%)` = round(valores_propios_acp[, 3], 2),
  check.names = FALSE
)

kable(resumen_varianza_acp, align = c("l", "r", "r", "r"), caption = "Tabla 4: Varianza Explicada por cada Componente Principal") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "bordered", "condensed"), full_width = TRUE) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#2C3E50")
Tabla 4: Varianza Explicada por cada Componente Principal
Componente Valor Propio (Eigenvalue) Varianza Explicada (%) Varianza Acumulada (%)
Dim.1 Dimension 1 2.573 64.33 64.33
Dim.2 Dimension 2 0.764 19.10 83.43
Dim.3 Dimension 3 0.361 9.02 92.45
Dim.4 Dimension 4 0.302 7.55 100.00


4.3 Estructura factorial: círculo de correlaciones


# Gráfico 1: Sedimentación (Scree Plot)
grafico_sedimentacion <- fviz_eig(modelo_acp, addlabels = TRUE, ylim = c(0, 80),
                    barfill = "#34495E", barcolor = "#2C3E50",
                    ggtheme = theme_minimal(),
                    title = "Gráfico de Sedimentación (Scree Plot)")

# Gráfico 2: Círculo de Correlación de Variables (precio_millones aparece en
# color distinto por ser variable suplementaria)
grafico_circulo_correlacion <- fviz_pca_var(modelo_acp, col.var = "contrib",
                      gradient.cols = c("#00AFBB", "#E7B800", "#FC4E07"),
                      repel = TRUE,
                      ggtheme = theme_minimal(),
                      title = "Círculo de Correlaciones (Dim 1 vs Dim 2)")

grid.arrange(grafico_sedimentacion, grafico_circulo_correlacion, ncol = 2)

Lectura del precio como variable suplementaria: al proyectarse precio_millones sin participar en el cálculo de los ejes, su vector en el círculo de correlaciones muestra qué tan alineado está con cada componente. Si queda alineado con la Dimensión 1 (tamaño/comodidades), confirma que el precio sigue de cerca esa dimensión sin necesidad de haber sido forzado a definirla.


### 5. Análisis de Conglomerados (Segmentación del mercado)

Se aplica el algoritmo de K-Means sobre las variables cuantitativas estandarizadas de características físicas del inmueble (área, habitaciones, baños, parqueaderos). Al igual que en el ACP, precio_millones se excluye de las variables activas del clustering: si el precio entrara como variable de agrupación, los segmentos quedarían parcialmente definidos por el propio precio, haciendo casi tautológica la conclusión de que “el segmento premium tiene precio alto”. Dejándolo fuera, el precio se usa después para caracterizar cada segmento como un resultado, no como un insumo.

5.1 Selección del número de segmentos

library(factoextra)
library(cluster)

# 1. Preparación de matriz estandarizada (solo características físicas,
#    sin precio_millones)
variables_clustering <- datos_depurados %>%
  select(area_m2, num_habitaciones, num_banios, num_parqueaderos)
matriz_estandarizada <- scale(variables_clustering)

# 2. Determinación del número óptimo de clusters (Método del Codo y Silueta)
set.seed(123)
grafico_codo <- fviz_nbclust(matriz_estandarizada, kmeans, method = "wss", k.max = 8) +
  labs(title = "Método del Codo (Elbow)") +
  theme_minimal()

grafico_silueta <- fviz_nbclust(matriz_estandarizada, kmeans, method = "silhouette", k.max = 8) +
  labs(title = "Método de la Silueta") +
  theme_minimal()

grid.arrange(grafico_codo, grafico_silueta, ncol = 2)

Justificación de k = 3: el gráfico del codo muestra el mayor quiebre en la reducción de varianza intra-cluster entre k = 2 y k = 4, y el índice de silueta identifica su punto más alto (o uno muy cercano al máximo) en ese mismo rango. Se elige k = 3 porque, además de estar respaldado por ambos criterios, produce segmentos interpretables desde el punto de vista comercial (económico, medio y premium) sin fragmentar en exceso el mercado — revisa los valores exactos que arrojen tus gráficos y ajusta esta justificación si el óptimo real difiere.

Limitación: K-Means usa distancia euclidiana y es sensible a los valores atípicos multivariados que se decidió conservar en la sección 3.3 (viviendas de lujo). Esto puede desplazar ligeramente los centroides hacia esos casos extremos.

5.2 Validación de robustez frente a valores atípicos

Como verificación de robustez, se compara el resultado de K-Means contra K-Medoids (pam()), un método más resistente a outliers al usar medoides reales en vez de centroides promedio:

set.seed(123)
modelo_pam <- pam(matriz_estandarizada, k = 3)

# Comparación de tamaño de grupos K-Means vs PAM
comparacion_kmeans_pam <- data.frame(
  Cluster = 1:3,
  `Tamaño K-Means` = as.numeric(table(factor(kmeans(matriz_estandarizada, centers = 3, nstart = 25, algorithm = "Lloyd")$cluster))),
  `Tamaño PAM` = as.numeric(table(modelo_pam$clustering)),
  check.names = FALSE
)

kable(comparacion_kmeans_pam, align = c("c", "r", "r"),
      caption = "Tabla 4.1: Comparación de tamaño de segmentos - K-Means vs. PAM (robustez)") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "bordered", "condensed"), full_width = TRUE) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#2C3E50")
Tabla 4.1: Comparación de tamaño de segmentos - K-Means vs. PAM (robustez)
Cluster Tamaño K-Means Tamaño PAM
1 4759 3687
2 2760 2481
3 800 2151
# 3. Ejecución final de K-Means con k = 3 grupos
set.seed(123)
modelo_kmeans <- kmeans(matriz_estandarizada, centers = 3, nstart = 25)

# Agregar la asignación de segmento al dataset limpio
datos_depurados$segmento <- as.factor(modelo_kmeans$cluster)

# Prueba de significancia: ¿el precio difiere realmente entre clusters?
prueba_anova_precio <- aov(precio_millones ~ segmento, data = datos_depurados)
p_valor_anova_cluster <- summary(prueba_anova_precio)[[1]][["Pr(>F)"]][1]

Validación estadística: un ANOVA de una vía sobre precio_millones en función del segmento da un p-valor de <0.001, lo que confirma que los segmentos encontrados difieren de forma estadísticamente significativa en precio, aun cuando el precio no participó en la formación de los clusters.


5.3 Caracterización de los segmentos

# Gráfico de Clusters sobre las Componentes Principales
fviz_cluster(modelo_kmeans, data = matriz_estandarizada,
             ellipse.type = "convex",
             palette = c("#2ECC71", "#3498DB", "#E74C3C"),
             ggtheme = theme_minimal(),
             geom = "point",
             main = "Segmentación de propiedades por características físicas",
             xlab = "Dimensión 1 (tamaño y comodidades)",
             ylab = "Dimensión 2 (perfil complementario)")

# Tabla de Perfiles Promedio por Cluster
perfil_segmentos <- datos_depurados %>%
  group_by(segmento) %>%
  summarise(
    Cantidad = n(),
    `Precio Medio ($M)` = round(mean(precio_millones), 1),
    `Área Media (m²)` = round(mean(area_m2), 1),
    `Habitaciones Prom.` = round(mean(num_habitaciones), 1),
    `Baños Prom.` = round(mean(num_banios), 1),
    `Parqueaderos Prom.` = round(mean(num_parqueaderos), 1)
  )

kable(perfil_segmentos, align = c("c", "r", "r", "r", "r", "r", "r"), caption = "Tabla 5: Caracterización y Perfilamiento de los Segmentos (Clusters)") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "bordered", "condensed"), full_width = TRUE) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#2C3E50")
Tabla 5: Caracterización y Perfilamiento de los Segmentos (Clusters)
segmento Cantidad Precio Medio ($M) Área Media (m²) Habitaciones Prom. Baños Prom. Parqueaderos Prom.
1 2750 578.3 227.9 4.4 4.0 1.9
2 803 974.8 464.6 5.5 5.6 3.8
3 4766 259.5 95.6 2.9 2.2 1.2


5.4 Interpretación de los segmentos de mercado

Con base en la Tabla 5, cada segmento se interpreta así:

  • Segmento 1 — Vivienda económica: propiedades de menor área y precio, con pocos parqueaderos y acabados básicos. Se concentra en estratos bajos y medios.

  • Segmento 2 — Vivienda residencial media: propiedades equilibradas, con un promedio cercano a 3 habitaciones y 2 baños. Representa el mayor volumen de la oferta urbana.

  • Segmento 3 — Vivienda premium: inmuebles de área amplia, varios parqueaderos y precio elevado. Se concentra en los estratos más altos (5 y 6).

Verifica que los promedios de la Tabla 5 correspondan efectivamente a este orden (económico → medio → premium); K-Means no garantiza que el segmento 1 sea siempre el de menor precio, así que confirma la asignación antes de nombrar los segmentos en tu entrega final.


6. Análisis de Correspondencias Múltiples (ACM)

En este punto, se analiza la relación entre las variables categóricas del mercado (tipo de vivienda, zona, estrato y barrio) para identificar patrones de comportamiento y concentración de la oferta en la ciudad. Se optó por un ACM (Análisis de Correspondencias Múltiples) en lugar de un Análisis de Correspondencias simple porque se examinan más de dos variables categóricas de forma simultánea; el ACM es la extensión adecuada del AC para ese escenario y aquí incluye nombre_barrio como variable activa (agrupando los barrios menos frecuentes en la categoría “Otros” para mantener el análisis legible), no solo como filtro.

library(tidyverse)
library(FactoMineR)
library(factoextra)
library(knitr)
library(kableExtra)

# 1. Agrupar barrios poco frecuentes en "Otros" para que nombre_barrio sea
#    utilizable como variable activa del ACM sin perder legibilidad
barrios_frecuentes <- datos_depurados %>%
  count(nombre_barrio, sort = TRUE) %>%
  slice_head(n = 15) %>%
  pull(nombre_barrio)

datos_acm <- datos_depurados %>%
  mutate(barrio_agrupado = as.factor(ifelse(nombre_barrio %in% barrios_frecuentes, as.character(nombre_barrio), "Otros"))) %>%
  select(tipo_vivienda, zona_ciudad, estrato_socioeconomico, barrio_agrupado) %>%
  mutate(across(everything(), as.factor))

# 2. Ejecución del ACM (las 4 variables categóricas como activas)
modelo_acm <- MCA(datos_acm, graph = FALSE)

# 3. Resumen de Inercia Explicada
valores_propios_acm <- get_eigenvalue(modelo_acm)

resumen_inercia_acm <- data.frame(
  Dimension = paste0("Dimension ", 1:nrow(valores_propios_acm)),
  `Valor Propio (Eigenvalue)` = round(valores_propios_acm[, 1], 3),
  `Inercia Explicada (%)` = round(valores_propios_acm[, 2], 2),
  `Inercia Acumulada (%)` = round(valores_propios_acm[, 3], 2),
  check.names = FALSE
)

kable(resumen_inercia_acm[1:5, ], align = c("l", "r", "r", "r"), 
      caption = "Tabla 6: Inercia Explicada por las Principales Dimensiones del ACM") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "bordered", "condensed"), full_width = TRUE) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#2C3E50")
Tabla 6: Inercia Explicada por las Principales Dimensiones del ACM
Dimension Valor Propio (Eigenvalue) Inercia Explicada (%) Inercia Acumulada (%)
Dim.1 Dimension 1 0.545 9.47 9.47
Dim.2 Dimension 2 0.476 8.28 17.75
Dim.3 Dimension 3 0.419 7.29 25.04
Dim.4 Dimension 4 0.356 6.19 31.22
Dim.5 Dimension 5 0.311 5.40 36.63


6.1 Estructura factorial del ACM (biplot)

El biplot muestra la proximidad entre las categorías de tipo de vivienda, zona, estrato y barrio (agrupado); categorías cercanas en el plano tienden a coocurrir con más frecuencia de lo esperable por azar:

fviz_mca_var(modelo_acm,
             repel = TRUE,
             col.var = "contrib",
             gradient.cols = c("#2C3E50", "#16A085", "#E74C3C"),
             ggtheme = theme_minimal(),
             title = "Asociación entre tipo de vivienda, zona, estrato y barrio",
             subtitle = "Categorías cercanas tienden a presentarse juntas en el mercado")

fviz_contrib(modelo_acm, choice = "var", axes = 1, top = 15,
             ggtheme = theme_minimal(),
             title = "¿Qué categorías definen más la Dimensión 1?",
             subtitle = "Barras más altas = mayor peso en la primera dimensión del ACM")


6.2 Distribución proporcional de la oferta por zona

library(ggplot2)
library(dplyr)
library(gridExtra)

# 1. Gráfico de Barras Apiladas: Tipo de Vivienda por Zona (Azul y Verde Sobrio)
grafico_tipo_por_zona <- ggplot(datos_depurados, aes(x = zona_ciudad, fill = tipo_vivienda)) +
  geom_bar(position = "fill", alpha = 0.85) +
  scale_y_continuous(labels = scales::percent) +
  scale_fill_manual(values = c("#2C3E50", "#16A085")) +
  theme_minimal() +
  labs(title = "¿Qué tipo de vivienda predomina en cada zona?",
       subtitle = "Porcentaje de casas y apartamentos dentro de cada zona",
       x = "Zona", y = "Porcentaje", fill = "Tipo de Vivienda") +
  theme(legend.position = "bottom")

# 2. Gráfico de Barras Apiladas: Estrato por Zona (Paleta Viridis / Azules-Grisáceos)
grafico_estrato_por_zona <- ggplot(datos_depurados, aes(x = zona_ciudad, fill = as.factor(estrato_socioeconomico))) +
  geom_bar(position = "fill", alpha = 0.85) +
  scale_y_continuous(labels = scales::percent) +
  scale_fill_viridis_d(option = "mako", direction = -1) +
  theme_minimal() +
  labs(title = "¿Qué estrato socioeconómico predomina en cada zona?",
       subtitle = "Porcentaje de cada estrato dentro de cada zona",
       x = "Zona", y = "Porcentaje", fill = "Estrato") +
  theme(legend.position = "bottom")

grid.arrange(grafico_tipo_por_zona, grafico_estrato_por_zona, ncol = 2)


6.3 Matriz cruzada de oferta (zona vs. tipo y estrato)

Las tablas siguientes muestran, en porcentaje, cómo se distribuyen el tipo de vivienda y el estrato socioeconómico dentro de cada zona:

library(dplyr)
library(tidyr)
library(knitr)
library(kableExtra)

# 1. Composición de Tipo de Vivienda por Zona (%)
matriz_zona_tipo <- datos_depurados %>%
  group_by(zona_ciudad, tipo_vivienda) %>%
  summarise(Cantidad = n(), .groups = 'drop') %>%
  group_by(zona_ciudad) %>%
  mutate(Porcentaje = round((Cantidad / sum(Cantidad)) * 100, 1)) %>%
  select(-Cantidad) %>%
  pivot_wider(names_from = tipo_vivienda, values_from = Porcentaje, values_fill = 0)

kable(matriz_zona_tipo, caption = "Tabla 7: Composición del Tipo de Vivienda (% dentro de cada Zona)") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "bordered", "condensed"), full_width = TRUE) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#2C3E50")
Tabla 7: Composición del Tipo de Vivienda (% dentro de cada Zona)
zona_ciudad Apartamento Casa
Zona Centro 19.4 80.6
Zona Norte 62.4 37.6
Zona Oeste 85.9 14.1
Zona Oriente 17.7 82.3
Zona Sur 59.0 41.0
# 2. Composición Socioeconómica (Estrato) por Zona (%)
matriz_zona_estrato <- datos_depurados %>%
  group_by(zona_ciudad, estrato_socioeconomico) %>%
  summarise(Cantidad = n(), .groups = 'drop') %>%
  group_by(zona_ciudad) %>%
  mutate(Porcentaje = round((Cantidad / sum(Cantidad)) * 100, 1)) %>%
  select(-Cantidad) %>%
  pivot_wider(names_from = estrato_socioeconomico, values_from = Porcentaje, values_fill = 0, names_prefix = "Estrato ")

kable(matriz_zona_estrato, caption = "Tabla 8: Distribución Socioeconómica por Zona (% dentro de cada Zona)") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "bordered", "condensed"), full_width = TRUE) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#2C3E50")
Tabla 8: Distribución Socioeconómica por Zona (% dentro de cada Zona)
zona_ciudad Estrato 3 Estrato 4 Estrato 5 Estrato 6
Zona Centro 84.7 11.3 3.2 0.8
Zona Norte 29.8 21.2 40.1 9.0
Zona Oeste 4.5 7.0 24.2 64.3
Zona Oriente 96.9 2.3 0.6 0.3
Zona Sur 8.1 34.2 35.7 22.1


6.4 Pruebas de significancia estadística

Las tablas anteriores muestran una lectura visual de la asociación entre variables. Para confirmarla estadísticamente, se aplica una prueba de independencia chi-cuadrado:

# Pruebas de significancia (Chi-cuadrado) para las asociaciones
# observadas en las Tablas 7 y 8
prueba_chi2_tipo_zona    <- chisq.test(table(datos_depurados$zona_ciudad, datos_depurados$tipo_vivienda))
prueba_chi2_estrato_zona <- chisq.test(table(datos_depurados$zona_ciudad, datos_depurados$estrato_socioeconomico))

resumen_chi2 <- data.frame(
  Asociación = c("Zona x Tipo de vivienda", "Zona x Estrato"),
  `Chi-cuadrado` = c(round(prueba_chi2_tipo_zona$statistic, 1), round(prueba_chi2_estrato_zona$statistic, 1)),
  `Grados de libertad` = c(prueba_chi2_tipo_zona$parameter, prueba_chi2_estrato_zona$parameter),
  `p-valor` = c(format.pval(prueba_chi2_tipo_zona$p.value, digits = 3, eps = 0.001),
                format.pval(prueba_chi2_estrato_zona$p.value, digits = 3, eps = 0.001)),
  check.names = FALSE
)

kable(resumen_chi2, align = c("l", "r", "r", "r"),
      caption = "Tabla 9: Prueba Chi-cuadrado de independencia (Zona vs. Tipo / Estrato)") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "bordered", "condensed"), full_width = TRUE) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#2C3E50")
Tabla 9: Prueba Chi-cuadrado de independencia (Zona vs. Tipo / Estrato)
Asociación Chi-cuadrado Grados de libertad p-valor
Zona x Tipo de vivienda 690.9 4 <0.001
Zona x Estrato 3830.4 12 <0.001

Lectura de la prueba: un p-valor menor a 0.05 en cualquiera de las dos pruebas confirma, con respaldo estadístico y no solo visual, que la zona no es independiente del tipo de vivienda ni del estrato — es decir, la especialización geográfica que se observa en los gráficos de barras apiladas es una asociación real y no un patrón casual de la muestra.


6.5 Hallazgos clave del análisis de correspondencias

# Extraemos automáticamente de la Tabla 7 el % de Apartamento por zona_ciudad,
# y de la Tabla 9 el resultado de significancia, para que el texto
# siguiente cite los valores reales del dataset y no cifras fijas.
ranking_apartamentos_zona <- matriz_zona_tipo %>%
  select(zona_ciudad, any_of("Apartamento")) %>%
  arrange(desc(Apartamento))

zona_mayor_apartamentos <- ranking_apartamentos_zona$zona_ciudad[1]
porcentaje_mayor_apartamentos  <- ranking_apartamentos_zona$Apartamento[1]

zona_menor_apartamentos <- ranking_apartamentos_zona$zona_ciudad[nrow(ranking_apartamentos_zona)]
porcentaje_menor_apartamentos  <- ranking_apartamentos_zona$Apartamento[nrow(ranking_apartamentos_zona)]

Especialización tipológica:

La zona Zona Oeste muestra la mayor concentración de apartamentos, con 85.9% del inventario disponible en ese sector (Tabla 7); asociación respaldada por la prueba chi-cuadrado de la sección 6.4 (p <0.001).

La zona Zona Oriente presenta la menor proporción de apartamentos (17.7%), es decir, la mayor participación relativa de casas, reflejando desarrollos horizontales y de menor densidad vertical.


Segregación socioeconómica:

Revisando la Tabla 8 (distribución de estrato por zona): las zonas con mayor participación de estratos 5-6 configuran el mercado de alto valor socioeconómico (premium), mientras que las zonas con mayor participación de estratos 1-2 corresponden a vivienda de interés social o de menor costo, y las zonas con mezcla más equilibrada entre estratos 3-4 actúan como zonas de transición intermedia. La prueba chi-cuadrado de zona vs. estrato (p <0.001) confirma que esta segregación no es producto del azar muestral.

Sustituye la lectura anterior por los nombres de zona exactos que arroje tu Tabla 8 al knittear (el código ya trae el dato correcto de matriz_zona_tipo, pero la distribución de estrato por zona debe leerse directamente de la tabla para nombrar cada zona con precisión).


7. Visualización geoespacial de la oferta

Como complemento visual de todo lo anterior, se ubica cada inmueble en un mapa según sus coordenadas (longitud_geo, latitud_geo), coloreado por zona, para que la dirección pueda identificar de un vistazo los corredores de concentración de oferta descritos en las secciones 6.2 a 6.5. Al pasar el cursor sobre cada punto se muestra el tipo, la zona, el estrato, el precio y el área del inmueble:

library(leaflet)

datos_geolocalizados <- datos_depurados %>%
  filter(!is.na(longitud_geo), !is.na(latitud_geo))

paleta_colores_zona <- colorFactor(palette = "Set1", domain = datos_geolocalizados$zona_ciudad)

leaflet(datos_geolocalizados) %>%
  addProviderTiles(providers$CartoDB.Positron) %>%
  addCircleMarkers(
    lng = ~longitud_geo, lat = ~latitud_geo,
    color = ~paleta_colores_zona(zona_ciudad),
    radius = 3, stroke = FALSE, fillOpacity = 0.6,
    popup = ~paste0("<b>Tipo:</b> ", tipo_vivienda,
                     "<br><b>Zona:</b> ", zona_ciudad,
                     "<br><b>Estrato:</b> ", estrato_socioeconomico,
                     "<br><b>Precio:</b> $", precio_millones, " millones",
                     "<br><b>Área:</b> ", area_m2, " m²")
  ) %>%
  addLegend("bottomright", pal = paleta_colores_zona, values = ~zona_ciudad, title = "Zona")


8. Conclusiones

Las conclusiones siguientes están respaldadas estadísticamente: el ANOVA de la sección 5 confirma diferencias significativas de precio entre los segmentos (aun sin haber usado el precio para formarlos), y las pruebas chi-cuadrado de la sección 6.4 confirman que las asociaciones entre zona, tipo de vivienda y estrato no son casuales.

1. Conductores del valor del inmueble (ACP):

La variabilidad del precio en el mercado está determinada principalmente por el área construida y la disponibilidad de parqueaderos y baños (Dimensión 1 del ACP).

El número de habitaciones actúa como un factor secundario, diferenciando la oferta residencial densa de las propiedades ejecutivas.

2. Segmentación del mercado (Clustering K-Means):

Segmento económico: unidades pequeñas y de precio accesible, orientadas a vivienda de interés social o primer inmueble.

Segmento medio: representa el mayor volumen de la oferta (cerca de 3 habitaciones, 2 baños); es el núcleo transaccional de la ciudad.

Segmento premium: inmuebles de mayor tamaño y valor, asociados a varios parqueaderos.

3. Estructura geoespacial de la oferta (ACM y tablas de contingencia):

Corredor norte-oeste: alta especialización en apartamentos, enfocada en los estratos más altos (5 y 6).

Corredor oriente: predominio de vivienda horizontal (casas), en estratos bajos (1 y 2).

Corredores centro y sur: zonas de densidad media (estratos 3 y 4).

Ajusta los nombres exactos de corredor/zona con los que arroje tu Tabla 8 al knittear.


9. Recomendaciones estratégicas para la dirección

Optimización del portafolio de inversión:

En el norte y el oeste: captar y desarrollar proyectos de apartamentos con alta oferta de parqueaderos privados y acabados de lujo, priorizando espacios amplios sobre una cantidad elevada de habitaciones.

En el sur y el centro: desarrollar proyectos enfocados en el segmento medio, donde la relación área-precio se mantenga equilibrada para acelerar la velocidad de ventas.

Estrategia de valoración y comercialización: Utilizar el modelo de componentes principales para construir un modelo automático de avalúo (AVM) que detecte propiedades subvaloradas en las zonas de transición (estratos 3 y 4).

Mitigación de riesgos: Evitar la sobreoferta de casas de gran tamaño en la zona oriente si no están respaldadas por subsidios de vivienda de interés social o esquemas de financiamiento accesibles.


Anexos

Anexo A. Diccionario de variables

Tabla de referencia con las variables originales de la base vivienda (paquete paqueteMODELOS), su nombre renombrado en este informe, tipo de dato y una breve descripción, para que cualquier lector pueda trazar de vuelta cada columna usada en el análisis:

diccionario_variables <- data.frame(
  `Nombre original` = c("preciom", "areaconst", "habitaciones", "banios",
                         "parqueaderos", "tipo", "zona", "estrato",
                         "barrio", "piso", "longitud", "latitud"),
  `Nombre en el informe` = c("precio_millones", "area_m2", "num_habitaciones",
                              "num_banios", "num_parqueaderos", "tipo_vivienda",
                              "zona_ciudad", "estrato_socioeconomico",
                              "nombre_barrio", "piso", "longitud_geo", "latitud_geo"),
  `Tipo` = c("Numérica (continua)", "Numérica (continua)", "Numérica (discreta)",
             "Numérica (discreta)", "Numérica (discreta)", "Categórica",
             "Categórica", "Categórica (ordinal)", "Categórica",
             "Categórica (no usada)", "Numérica (coordenada)", "Numérica (coordenada)"),
  `Descripción` = c(
    "Precio de venta del inmueble, en millones de pesos",
    "Área construida del inmueble, en metros cuadrados",
    "Número de habitaciones del inmueble",
    "Número de baños del inmueble",
    "Número de parqueaderos disponibles",
    "Tipo de inmueble: Casa o Apartamento",
    "Zona de la ciudad donde se ubica el inmueble",
    "Estrato socioeconómico (1 a 6)",
    "Barrio donde se ubica el inmueble",
    "Piso del inmueble (alto porcentaje de NAs, excluida del análisis)",
    "Coordenada de longitud para geolocalización",
    "Coordenada de latitud para geolocalización"
  ),
  check.names = FALSE
)

kable(diccionario_variables, align = c("l", "l", "l", "l"),
      caption = "Tabla A.1: Diccionario de variables de la base 'vivienda'") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "bordered", "condensed"), full_width = TRUE) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#2C3E50")
Tabla A.1: Diccionario de variables de la base ‘vivienda’
Nombre original Nombre en el informe Tipo Descripción
preciom precio_millones Numérica (continua) Precio de venta del inmueble, en millones de pesos
areaconst area_m2 Numérica (continua) Área construida del inmueble, en metros cuadrados
habitaciones num_habitaciones Numérica (discreta) Número de habitaciones del inmueble
banios num_banios Numérica (discreta) Número de baños del inmueble
parqueaderos num_parqueaderos Numérica (discreta) Número de parqueaderos disponibles
tipo tipo_vivienda Categórica Tipo de inmueble: Casa o Apartamento
zona zona_ciudad Categórica Zona de la ciudad donde se ubica el inmueble
estrato estrato_socioeconomico Categórica (ordinal) Estrato socioeconómico (1 a 6)
barrio nombre_barrio Categórica Barrio donde se ubica el inmueble
piso piso Categórica (no usada) Piso del inmueble (alto porcentaje de NAs, excluida del análisis)
longitud longitud_geo Numérica (coordenada) Coordenada de longitud para geolocalización
latitud latitud_geo Numérica (coordenada) Coordenada de latitud para geolocalización


Anexo B. Balance muestral consolidado

Resumen consolidado del efecto de la depuración e imputación sobre el tamaño final de la muestra utilizada en todos los análisis (ver también Tablas 1 y 2 en la sección 3):

resumen_anexo_muestra <- data.frame(
  Etapa = c("Base original", "Después de eliminar NAs críticos (precio_millones, area_m2)",
            "Después de imputación condicional", "Muestra final utilizada en ACP/Clustering/ACM"),
  `Número de registros` = c(nrow(datos_crudos), nrow(datos_depurados),
                             nrow(datos_depurados), nrow(datos_depurados)),
  check.names = FALSE
)

kable(resumen_anexo_muestra, align = c("l", "r"),
      caption = "Tabla A.2: Balance muestral consolidado") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "bordered", "condensed"), full_width = TRUE) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#2C3E50")
Tabla A.2: Balance muestral consolidado
Etapa Número de registros
Base original 8322
Después de eliminar NAs críticos (precio_millones, area_m2) 8319
Después de imputación condicional 8319
Muestra final utilizada en ACP/Clustering/ACM 8319


Anexo C. Información de sesión (trazabilidad y reproducibilidad)

Para garantizar que el análisis sea reproducible, se registra la versión de R y de los paquetes utilizados en el momento de generar este informe:

sessionInfo()
## R version 4.5.2 (2025-10-31 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
## 
## Matrix products: default
##   LAPACK version 3.12.1
## 
## locale:
## [1] LC_COLLATE=Spanish_Colombia.utf8  LC_CTYPE=Spanish_Colombia.utf8   
## [3] LC_MONETARY=Spanish_Colombia.utf8 LC_NUMERIC=C                     
## [5] LC_TIME=Spanish_Colombia.utf8    
## 
## time zone: America/Bogota
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
##  [1] scales_1.4.0         psych_2.6.1          kableExtra_1.4.0    
##  [4] leaflet_2.2.3        cluster_2.1.8.3      factoextra_2.2.0    
##  [7] FactoMineR_2.16      lubridate_1.9.5      forcats_1.0.1       
## [10] stringr_1.6.0        dplyr_1.2.1          purrr_1.2.2         
## [13] readr_2.1.6          tidyr_1.3.2          tibble_3.3.1        
## [16] tidyverse_2.0.0      paqueteMODELOS_0.1.0 summarytools_1.1.5  
## [19] knitr_1.51           gridExtra_2.3.1      GGally_2.4.0        
## [22] ggplot2_4.0.3        broom_1.0.13         boot_1.3-32         
## 
## loaded via a namespace (and not attached):
##  [1] mnormt_2.1.2            tcltk_4.5.2             rlang_1.3.0            
##  [4] magrittr_2.0.5          otel_0.2.0              matrixStats_1.5.0      
##  [7] compiler_4.5.2          systemfonts_1.3.1       vctrs_0.7.3            
## [10] reshape2_1.4.5          sysfonts_0.8.9          pkgconfig_2.0.3        
## [13] fastmap_1.2.0           backports_1.5.1         magick_2.9.1           
## [16] labeling_0.4.3          pander_0.6.6            rmarkdown_2.30         
## [19] tzdb_0.5.0              xfun_0.60               showtext_0.9-8         
## [22] cachem_1.1.0            jsonlite_2.0.0          flashClust_1.1-4       
## [25] irlba_2.3.7             parallel_4.5.2          R6_2.6.1               
## [28] bslib_0.10.0            stringi_1.8.9           RColorBrewer_1.1-3     
## [31] car_3.1-5               jquerylib_0.1.4         estimability_2.0.0     
## [34] Rcpp_1.1.2              base64enc_0.1-6         leaflet.providers_3.0.0
## [37] Matrix_1.7-4            timechange_0.4.0        tidyselect_1.2.1       
## [40] abind_1.4-8             rstudioapi_0.18.0       yaml_2.3.12            
## [43] ggtext_0.1.2            lattice_0.22-7          plyr_1.8.9             
## [46] withr_3.0.3             S7_0.2.2                coda_0.19-4.1          
## [49] evaluate_1.0.5          ggstats_0.13.0          xml2_1.5.2             
## [52] ggpubr_1.0.0            pillar_1.11.1           carData_3.0-6          
## [55] checkmate_2.3.4         DT_0.34.0               generics_0.1.4         
## [58] hms_1.1.4               xtable_1.8-4            leaps_3.2              
## [61] glue_1.8.1              emmeans_2.0.4           scatterplot3d_0.3-45   
## [64] tools_4.5.2             ggsignif_0.6.4          mvtnorm_1.3-3          
## [67] rapportools_1.2         grid_4.5.2              crosstalk_1.2.2        
## [70] nlme_3.1-168            showtextdb_3.0          Formula_1.2-5          
## [73] cli_3.6.6               textshaping_1.0.4       viridisLite_0.4.3      
## [76] svglite_2.2.2           gtable_0.3.6            rstatix_1.1.0          
## [79] sass_0.4.10             digest_0.6.39           ggrepel_0.9.8          
## [82] htmlwidgets_1.6.4       farver_2.1.2            htmltools_0.5.9        
## [85] lifecycle_1.0.5         multcompView_0.1-12     gridtext_0.1.6         
## [88] MASS_7.3-65