library(paqueteMODELOS)
library(tidyverse)
library(FactoMineR)
library(factoextra)
library(cluster)
library(leaflet)
library(GGally)
library(knitr)
library(kableExtra)
library(psych)
library(scales)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.
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.
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. 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")| 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 denum_parqueaderos,num_baniosynum_habitaciones, no es razonable imputarla por grupo sin introducir un sesgo relevante (el piso de un inmueble no depende directamente detipo_viviendayestrato_socioeconomicode forma homogénea).
Para certificar la confiabilidad estadística, el tratamiento de la base de datos se rige por tres criterios.
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).
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")| 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")| Metrica | Valor |
|---|---|
| Registros Iniciales | 8322 |
| Registros Finales Retenidos | 8319 |
| Registros Eliminados | 3 |
| Porcentaje de Retencion | 99.96% |
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")| 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")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.
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")| 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.
# 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")| 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 |
# 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.
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.
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")| 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_millonesen función delsegmentoda 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.
# 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")| 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 |
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.
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")| 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 |
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")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)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")| 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")| 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 |
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")| 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.
# 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).
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")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.
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.
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")| 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 |
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")| 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 |
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:
## 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