Introducción al análisis estádistico de conglomerados en R

Universidad de El Salvador · Facultad de Ciencias Económicas Ciclo 02 2026 · Métodos para el análisis económico · estudiante: Aarón Meléndez GM11008

2026-09-28

Parte 1: introducción teórica

Cluster Analysis

El análisis de conglomerados (Cluster Analysis) es una técnica estadística multivariante de clasificación que tiene como objetivo agrupar observaciones, individuos, objetos o unidades territoriales en conjuntos homogéneos (conglomerados o clústeres), de modo que los elementos pertenecientes a un mismo grupo sean más similares entre sí que respecto a los elementos de otros grupos.

Según Hair et al. (2019), el análisis de conglomerados es una técnica exploratoria cuyo propósito es identificar estructuras naturales dentro de los datos cuando no existe una clasificación previa establecida.

¿Cuál es el propósito del análisis de conglomerados?

El análisis de conglomerados, en términos técnicos, busca maximizar hasla lograr diferenciar los criterios:

La homogeneidad intragrupo, es decir, que los elementos dentro de cada conglomerado sean muy similares.

La heterogeneidad intergrupo, es decir, que los conglomerados sean claramente diferentes entre sí.

De acuerdo con Kaufman & Rousseeuw (2009) un buen agrupamiento produce grupos compactos internamente y bien separados externamente.

Características del análisis de conglomerados

  • Es una técnica de interdependencia, ya que no distingue entre variables dependientes e independientes.

  • Se utiliza fundamentalmente con fines exploratorios.

  • Permite identificar patrones ocultos en grandes conjuntos de datos.

  • Puede emplearse con variables cuantitativas, cualitativas o mixtas (según el método utilizado).

Ejemplo Ilustrativo.

En junio de 2024, el grupo de activistas hackers Ciberinteligencia SV, filtro y publico una base de datos en formato XLSX de aprox 104.8MB con la información de 974,428 cotizantes del ISSS, tanto del sector público como privado, En dicho archivo se detalla los nombres completos de los empleados, su DUI y NIT, la empresa a la que laboral, y el salario declarado al ISSS, y su numero telefónico.

Con el conocimiento de la realidad salvadoreña, podriamos considerar posibles escenarios de conglomerados, que podriamos descubrir con esta informaciòn, tal como:

  • Conglomerados de salarios relacionados a la edad del contribuyente.

  • Conglomerados de saslarios, relacionados a la ubicación geográfica de la empresa

  • Conglomerados de salarios, relacionados al domicilio del contribuyente.

  • Concentración salarial por municipio

  • Concentración salarial del sector publico.

Y con ello podríamos descubrir cuales son las mejores empresas para trabajar en base al salario. Cual seria nuestra expectativa salarial en base a la experiencia y edad, etc.

Metodologías para determinar conglomerados

Clustering jerárquico

Este enfoque busca construir una jerarquía de grupos representada visualmente a través de un árbol binario o dendrograma. No requiere especificar el número de clusters k de antemano; el árbol se puede “cortar” a diferentes alturas para obtener distintas soluciones. 

Existen dos estrategias para su construcción (Murtagh & Contreras, 2012) : 

Aglomerativo (Bottom-up): Comienza con cada dato como un cluster individual. En cada paso, calcula la matriz de distancias y fusiona de par en par los dos grupos más cercanos (usando criterios de enlace o linkage como el método de Ward, enlace completo o enlace promedio) hasta que todos forman un único grupo raíz (Hastie et al., 2009).

Divisivo (Top-down): Comienza con todos los datos en un único gran grupo y lo va dividiendo sucesivamente de forma binaria.

Clustering No Jerárquico (o basado en densidad)

A diferencia del jerárquico, estos métodos asignan cada dato a un grupo definitivo directamente, sin crear una estructura de árbol. Suelen ser mucho más eficientes computacionalmente para grandes volúmenes de datos. 

Particional (K-Means / K-Medoids): Divide el conjunto de datos en exactamente k grupos predefinidos por el usuario. Funciona de manera iterativa: asigna cada punto al centroide más cercano y luego recalcula la posición de los centroides basándose en la media de los puntos asignados, repitiendo el proceso hasta la convergencia (MacQueen, 1967). 

Basado en Densidad (DBSCAN / OPTICS): Define los clusters como regiones continuas donde la densidad de puntos es alta, separadas por regiones de baja densidad. Es ideal para identificar formas arbitrarias y gestionar eficazmente el ruido o valores atípicos (outliers), sin necesidad de predefinir el número de grupos (Ester et al., 1996) . 

El Algoritmo DBSCAN

DBSCAN fue propuesto por Ester et al. (1996) como una solución a la dificultad de encontrar conglomerados de forma arbitraria en conjuntos de datos con diferentes densidades y formas. 

No obstante, aunque el algoritmo DBSCAN puede encontrar formas no tradicionales en algunas ocasiones puede detectar los mismos clústeres que detectarían otros algoritmos como el k-means Por otro lado, cuando los individuos se encuentran relativamente concentrados es poco probable que DBSCAN pueda construir grupos, como sí lo hacía el algoritmo k-means (Una Introducción a Los Modelos de Clústering Empleando r, n.d.).

Tabla comparativa de los enfoques

Caso práctico: DBSCAN aplicado a UN COMTRADE

DBSCAN (Density-Based Spatial Clustering of Applications with Noise) funciona bajo una premisa intuitiva:

Un grupo (conglomerado) es una región en el espacio donde hay una alta concentración de puntos, separada de otros grupos por zonas vacías o con muy pocos puntos (Sharma, 2025)

DBSA algoritmo de clústering particionado basado en densidad. En general, este tipo de algoritmos basado en densidad se caracteriza por identificar regiones de alta densidad de individuos separadas por regiones de baja densidad en el espacio de datos (Una Introducción a Los Modelos de Clústering Empleando r, n.d.).

A diferencia de K-Means, que asume que todos los grupos son círculos perfectos y requiere que se le indique cuántos buscar, DBSCAN descubre la cantidad de grupos automáticamente y puede encontrar formas geométricas complejas (como lunas entrelazadas o anillos) (Sharma, 2025)

La Función DBSCAN y sus argumentos en R

La sintaxis básica para llamar al algoritmo es la siguiente(Hahsler et al., 2019):

library(dbscan)
resultado <- dbscan(x, eps, minPts = 5, borderPoints = TRUE)

En donde(Hahsler et al., 2019) :

x: La matriz o data frame con los datos numéricos. También puede recibir una matriz de distancias precalculada (dist).

eps (ε): El radio de la vecindad. En R, si las variables no están escaladas (con scale()), eps se medirá en las unidades originales de los datos, lo cual puede distorsionar las distancias.

minPts: El número mínimo de puntos requeridos en la vecindad para declarar un “Punto Núcleo” (por defecto es 5).

borderPoints: Un argumento lógico propio de R. Si es TRUE (por defecto), los puntos periféricos se asignan al grupo de su punto núcleo más cercano. Si se cambia a FALSE, R aplica una variante estricta llamada DBSCAN* donde los bordes se consideran directamente ruido.

Flujo de calibración en R

Ejemplo paso a paso

Agruparemos a los países socios comerciales que participan en el comercio del café para 2023, basándonos en dos variables:

Valor de la exportación (Trade Value en USD).

Peso neto de la mercancía (Net Weight en kg).

Dado que el comercio global tiene economías “gigantes” bien conocidas (como EE. UU. o China) y muchos países pequeños, un enfoque como K-Means fallaría al verse arrastrado por esos extremos (outliers). DBSCAN es ideal aquí porque agrupará a los países con dinámicas comerciales densas y similares, aislando a las superpotencias o anomalías de registro como ruido.

  1. Carga de librerias y seteo de nuestra key para acceder a UN COMTRADE

  2. Extracción de los datos desde COMTRADE para el comercio del café, con datos de 2022-2025:

datos_comtrade <- ct_get_data(
  reporter = "all_countries",          # Países que reportan
  partner = "World",         # Destinado al resto del mundo
  commodity_code = "0901",   # Código HS para Café
  flow_direction = "export", # Dirección del flujo comercial
  start_date = 2025,         # Año de inicio
  end_date = 2025            # Año de fin
)
head(datos_comtrade)
  type_code freq_code ref_period_id ref_year ref_month period reporter_code
1         C         A      20250101     2025        52   2025            20
2         C         A      20250101     2025        52   2025            24
3         C         A      20250101     2025        52   2025            31
4         C         A      20250101     2025        52   2025            32
5         C         A      20250101     2025        52   2025            36
6         C         A      20250101     2025        52   2025            40
  reporter_iso reporter_desc flow_code flow_desc partner_code partner_iso
1          AND       Andorra         X    Export            0         W00
2          AGO        Angola         X    Export            0         W00
3          AZE    Azerbaijan         X    Export            0         W00
4          ARG     Argentina         X    Export            0         W00
5          AUS     Australia         X    Export            0         W00
6          AUT       Austria         X    Export            0         W00
  partner_desc partner2code partner2iso partner2desc classification_code
1        World            0         W00        World                  H6
2        World            0         W00        World                  H6
3        World            0         W00        World                  H6
4        World            0         W00        World                  H6
5        World            0         W00        World                  H6
6        World            0         W00        World                  H6
  classification_search_code is_original_classification cmd_code
1                         HS                       TRUE     0901
2                         HS                       TRUE     0901
3                         HS                       TRUE     0901
4                         HS                       TRUE     0901
5                         HS                       TRUE     0901
6                         HS                       TRUE     0901
                                                                                                                  cmd_desc
1 Coffee, whether or not roasted or decaffeinated; husks and skins; coffee substitutes containing coffee in any proportion
2 Coffee, whether or not roasted or decaffeinated; husks and skins; coffee substitutes containing coffee in any proportion
3 Coffee, whether or not roasted or decaffeinated; husks and skins; coffee substitutes containing coffee in any proportion
4 Coffee, whether or not roasted or decaffeinated; husks and skins; coffee substitutes containing coffee in any proportion
5 Coffee, whether or not roasted or decaffeinated; husks and skins; coffee substitutes containing coffee in any proportion
6 Coffee, whether or not roasted or decaffeinated; husks and skins; coffee substitutes containing coffee in any proportion
  aggr_level is_leaf customs_code customs_desc mos_code mot_code  mot_desc
1          4   FALSE          C00    TOTAL CPC        0        0 TOTAL MOT
2          4   FALSE          C00    TOTAL CPC        0        0 TOTAL MOT
3          4   FALSE          C00    TOTAL CPC        0        0 TOTAL MOT
4          4   FALSE          C00    TOTAL CPC        0        0 TOTAL MOT
5          4   FALSE          C00    TOTAL CPC        0        0 TOTAL MOT
6          4   FALSE          C00    TOTAL CPC        0        0 TOTAL MOT
  qty_unit_code qty_unit_abbr         qty is_qty_estimated alt_qty_unit_code
1             8            kg      842.95            FALSE                 8
2            -1           N/A        0.00            FALSE                -1
3             8            kg      260.90            FALSE                 8
4             8            kg    53819.08            FALSE                 8
5             8            kg  2515803.19             TRUE                -1
6             8            kg 15216909.20             TRUE                -1
  alt_qty_unit_abbr  alt_qty is_alt_qty_estimated     net_wgt
1                kg   842.95                FALSE      842.95
2               N/A     0.00                FALSE          NA
3                kg   260.90                FALSE      260.90
4                kg 53819.08                FALSE    53819.08
5               N/A     0.00                FALSE  2515803.19
6               N/A     0.00                FALSE 15216909.20
  is_net_wgt_estimated gross_wgt is_gross_wgt_estimated cifvalue     fobvalue
1                FALSE     0.000                  FALSE       NA     12195.68
2                FALSE     0.000                  FALSE  9065070   9065019.79
3                FALSE     0.000                  FALSE       NA      7605.11
4                FALSE     0.000                  FALSE       NA    851339.76
5                 TRUE  2656.152                  FALSE       NA  27708234.07
6                 TRUE     0.000                  FALSE       NA 189286204.15
  primary_value legacy_estimation_flag is_reported is_aggregate
1      12195.68                      0       FALSE         TRUE
2    9065019.79                      0       FALSE         TRUE
3       7605.11                      0       FALSE         TRUE
4     851339.76                      0       FALSE         TRUE
5   27708234.07                      6       FALSE         TRUE
6  189286204.15                      6       FALSE         TRUE
  1. Limpieza de datos.
# Las columnas clave devueltas ahora son: 'primary_value_usd' y 'net_wgt_kg'.
datos_limpios <- datos_comtrade[
  !is.na(datos_comtrade$primary_value) & 
    !is.na(datos_comtrade$net_wgt) & 
    datos_comtrade$primary_value > 0 & 
    datos_comtrade$net_wgt > 0, 
]

# Seleccionamos las columnas para el agrupamiento
df_cluster <- datos_limpios[, c("primary_value", "net_wgt")]

head(df_cluster)
  primary_value     net_wgt
1      12195.68      842.95
3       7605.11      260.90
4     851339.76    53819.08
5   27708234.07  2515803.19
6  189286204.15 15216909.20
7   36104304.98  4184959.13
# Escalar los datos para ecualizar las magnitudes monetarias y de peso
df_escalado <- scale(df_cluster)

head(df_escalado)
  primary_value    net_wgt
1    -0.2960031 -0.2512729
3    -0.2960056 -0.2512751
4    -0.2955561 -0.2510737
5    -0.2812495 -0.2418124
6    -0.1951772 -0.1940346
7    -0.2767769 -0.2355335

  1. Determinación de parametros optimos:
# 4. DETERMINACIÓN DE PARÁMETROS (Método de la rodilla)
minPts_elegido <- 5
epsilon<- 1.6

kNNdistplot(df_escalado, k = minPts_elegido)
abline(h = epsilon, col = "red", lty = 2) # El valor exacto se ajusta según la gráfica resultante

  1. Implementación de DBSCAN
res_dbscan <- dbscan(df_escalado, eps= epsilon, minPts = minPts_elegido, borderPoints = FALSE)

# Guardar los clusters en el set de datos limpio (0 = Ruido)
datos_limpios$cluster <- as.factor(res_dbscan$cluster)
  1. Visualización gráfica
ggplot(datos_limpios, aes(x = net_wgt, y = primary_value, color = cluster)) +
  geom_point(size = 3, alpha = 0.8) +
  scale_x_log10() + 
  scale_y_log10() + 
  scale_color_manual(
    values = c("0" = "grey50", "1" = "#E41A1C", "2" = "#377EB8", "3" = "#4DAF4A"),
    labels = c("0" = "Ruido / Potencias Extremas", "1" = "Grupo 1", "2" = "Grupo 2", "3" = "Grupo 3")
  ) +
  labs(
    title = "Clustering DBSCAN sobre Datos de Exportación de Café (UN Comtrade)",
    subtitle = "Uso integrado de comtradr v1.0+ y dbscan",
    x = "Peso Neto (Kg) - Escala Log",
    y = "Valor del Comercio (USD) - Escala Log",
    color = "Asignación"
  ) +
  theme_minimal()

Interpretación de los datos

# Ver la lista de países atípicos aislados por DBSCAN
paises_ruido <- datos_limpios[datos_limpios$cluster == 0, c("reporter_desc", "primary_value", "net_wgt")]

paises_grupo1 <- datos_limpios[datos_limpios$cluster == 1, c("reporter_desc", "primary_value", "net_wgt")]

paises_ruido <- paises_ruido[order(paises_ruido$primary_value), ]
paises_ruido$ValorAgregadoUnitario <-paises_ruido$primary_value/paises_ruido$net_wgt

paises_grupo1 <- paises_grupo1[order(paises_grupo1$primary_value), ]
paises_grupo1$ValorAgregadoUnitario <-paises_grupo1$primary_value/paises_grupo1$net_wgt

head(paises_ruido)
   reporter_desc primary_value    net_wgt ValorAgregadoUnitario
88   Switzerland    4231831250  112246136             37.701353
34       Germany    5554082283  605293037              9.175857
20      Colombia    5964753544  752054025              7.931283
12        Brazil   14918395321 2274254323              6.559686
head(paises_grupo1)
      reporter_desc primary_value  net_wgt ValorAgregadoUnitario
13           Belize       201.750    5.128             39.342824
38           Guyana       602.470  104.490              5.765815
86      South Sudan      1500.000 3000.000              0.500000
32 French Polynesia      2985.109  173.833             17.172280
52       Kyrgyzstan      4239.000  151.000             28.072848
73         Paraguay      4335.620  215.000             20.165674

# Ordenar de mayor a menor valor agregado antes de graficar
paises_ruido <- paises_ruido[order(-paises_ruido$ValorAgregadoUnitario), ]

# Gráfica de barras horizontales para el Ruido
ggplot(paises_ruido, aes(x = reorder(reporter_desc, ValorAgregadoUnitario), y = ValorAgregadoUnitario)) +
  geom_col(fill = "grey40", width = 0.7) +
  coord_flip() + # Voltea el gráfico para leer los nombres de los países fácilmente
  labs(
    title = "Valor Agregado Unitario: Potencias Comerciales (Ruido DBSCAN)",
    subtitle = "Periodo multianual analizado",
    x = "País",
    y = "Valor Agregado por Kilo (USD / Kg)"
  ) +
  theme_minimal()

# 1. Ordenar de mayor a menor valor agregado
paises_grupo1 <- paises_grupo1[order(-paises_grupo1$ValorAgregadoUnitario), ]

# 2. Seleccionar únicamente los primeros 15 países (El Top 15)
top15_grupo1 <- head(paises_grupo1, 15)

# 3. Gráfica de barras horizontales para el Grupo 1
ggplot(top15_grupo1, aes(x = reorder(reporter_desc, ValorAgregadoUnitario), y = ValorAgregadoUnitario)) +
  geom_col(fill = "#E41A1C", width = 0.7) +
  coord_flip() + 
  labs(
    title = "Top 15 Países con Mayor Valor Agregado (Grupo 1)",
    subtitle = "Exportadores estándar con mayor precio promedio por kilogramo",
    x = "País",
    y = "Valor Agregado por Kilo (USD / Kg)"
  ) +
  theme_minimal()

Preguntas para pensar

  • Si estamos aplicando el algoritmo DBSCAN en R sobre datos de exportaciones y configuramos un radio de vecindad (eps) extremadamente pequeño, ¿qué impacto visual y matemático sufren los datos de los países clasificados? ¿Qué pasaría si el eps fuera demasiado grande?

  • Un eps muy pequeño fragmentará los grupos densos y enviará a la mayoría de los países al grupo 0 (ruido). Un eps muy grande fusionará todo el mapa de puntos en un único gran cluster masivo, perdiendo la capacidad de aislar outliers.

  • Al ejecutar la función kNNdistplot() en R para calibrar nuestro modelo de comercio, observamos una curva que tiene una llanura muy pegada a cero y luego un acantilado vertical. ¿Cómo se interpreta esa llanura plana desde la perspectiva de densidad de los países y en qué parte exacta debemos trazar la línea horizontal con abline()?

  • La llanura indica que la inmensa mayoría de los países están masificados a distancias muy cortas (alta densidad). La línea de abline(h = …) debe cruzarse justo en el punto de inflexión (“la rodilla”) donde la curva empieza a levantarse, que es el límite matemático de la densidad estable.

  • En nuestro script de R combinamos variables monetarias (USD) y de volumen físico (Kilogramos). Si elimináramos por completo la instrucción scale() del código antes de correr el modelo, ¿por qué fallaría el cálculo de las distancias euclidianas de DBSCAN?

  • Como las cifras de dólares y kilos difieren por millones de unidades, la variable con el rango numérico más grande dominaría de forma absoluta el cálculo de la distancia, dándonos una medición distorsionada a la vista.

  • Cuando revisamos los resultados de DBSCAN en R y vemos que algunos países tienen asignado el número 0, ¿qué nos está diciendo el algoritmo sobre el comportamiento de esos países? ¿Están agrupados o aislados

  • Significa que esos países son ruido o valores atípicos (outliers). No tienen suficientes vecinos cerca para formar un grupo, por lo que el algoritmo los dejó aislados

  • En el algoritmo DBSCAN configuramos el parámetro minPts = 5. ¿Qué significa este número en la vida real cuando el algoritmo dibuja un círculo alrededor de un país para decidir si puede formar un grupo

  • Significa que se necesitan como mínimo 5 países dentro de ese círculo (contando al del centro) para considerar que esa zona es lo suficientemente densa como para abrir o continuar un grupo comercial.

Referencias empleadas

Ester, M., Kriegel, H.-P., Sander, J., & Xu, X. (1996). A density-based algorithm for discovering clusters in large spatial databases with noise. Proceedings of the Second International Conference on Knowledge Discovery and Data Mining (KDD-96), 226–231.
Hahsler, M., Piekenbrock, M., & Doran, D. (2019). Dbscan: Fast density-based clustering with R. Journal of Statistical Software, 91(1), 1–30. https://doi.org/10.18637/jss.v091.i01
Hair, J. F., Black, W. C., Babin, B. J., & Anderson, R. E. (2019). Multivariate data analysis (8th ed.). Cengage Learning.
Hastie, T., Tibshirani, R., & Friedman, J. (2009). The elements of statistical learning: Data mining, inference, and prediction (2nd ed.). Springer. https://doi.org/10.1007/978-0-387-84858-7
Kaufman, L., & Rousseeuw, P. J. (2009). Finding groups in data: An introduction to cluster analysis (1st ed.). John Wiley & Sons.
MacQueen, J. (1967). Some methods for classification and analysis of multivariate observations. Proceedings of the Fifth Berkeley Symposium on Mathematical Statistics and Probability, 1, 281–297.
Murtagh, F., & Contreras, P. (2012). Algorithms for hierarchical clustering: An overview. Wiley Interdisciplinary Reviews: Data Mining and Knowledge Discovery, 2(1), 86–97. https://doi.org/10.1002/widm.53
Sharma, A. (2025). How to master the popular DBSCAN clustering algorithm for machine learning? https://www.analyticsvidhya.com/blog/2020/09/how-dbscan-clustering-works/.
Una introducción a los modelos de clústering empleando r (7. Modelo DBSCAN). (n.d.). Editorial Universidad Icesi. Retrieved September 28, 2026, from https://www.icesi.edu.co/editorial/intro-clustering-web/DBSCAN.html