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-30

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 (1990) 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) . 

Parte ii: 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

Podemos considerar a los países socios comerciales que participan en el comercio del café para 2025, 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.3024562 -0.2631513
3    -0.3024587 -0.2631535
4    -0.3020024 -0.2629510
5    -0.2874785 -0.2536423
6    -0.2000989 -0.2056193
7    -0.2829380 -0.2473312

  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
89   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
87      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.

Parte iii: Algoritmo K-MEANS

El clustering por K-Means se define como un método de aprendizaje no supervisado utilizado para particionar un conjunto de observaciones multidimensionales en K subconjuntos mutuamente excluyentes, maximizando la homogeneidad interna de cada grupo (James et al., 2021)

A diferencia de los modelos econométricos tradicionales que asumen formas funcionales condicionales (como las regresiones lineales estructurales), K-Means permite la identificación de regímenes, perfiles de consumidores o tipologías de países directamente a partir de la estructura subyacente de los datos, reduciendo el sesgo de especificación del investigador (Kaufman & Rousseeuw, 1990)

Fundamento estadístico

Desde la perspectiva de la optimización numérica, el algoritmo busca minimizar la varianza residual interna, formalizada a través de la Suma de Cuadrados Intra-cluster (WCSS - Within-Cluster Sum of Squares). Se define en principio como:

\[ \arg\min_{S} \sum_{i=1}^{K} \sum_{x \in S_i} \left\| x - \mu_i \right\|^2 \]

Sujeto a que u_i es el vector de medias o centroide del cluster:

\[\mu_i = \frac{1}{|S_i|} \sum_{x \in S_i} x\]

Procedimiento general

Sintaxis en R

Los principales comandos y su sintaxis a emplear son:

Escalado (R Core Team, 2019b) :

scale(x, center = TRUE, scale = TRUE)

Argumentos de la función:

  • x: Pasar el vector numérico, matriz o data frame que se desea transformar.

  • center: Controlar el centrado de los datos.

    • TRUE: Restar la media de cada columna para obtener una nueva media igual a 0.

    • FALSE: Mantener la media original sin modificaciones.

  • scale: Determinar la división de los datos.

    • TRUE: Dividir las columnas centradas por su desviación estándar para lograr una varianza de 1.

    • FALSE: Evitar la división por la desviación estándar.

Estimación del número Optimo

La función fviz_nbclust() pertenece a la biblioteca factoextra en R y sirve para determinar y visualizar el número óptimo de clústeres en un conjunto de datos. Su propósito principal es automatizar el cálculo de métricas de agrupamiento y graficar los resultados de forma clara mediante ggplot2 (Kassambara, 2024):

library(factoextra)
fviz_nbclust(x, FUNcluster, method, ...)

Argumentos de la función:

  • x: Pasar la matriz o el marco de datos numérico (previamente estandarizado con scale()).

  • FUNcluster: Especificar la función de partición a evaluar (por ejemplo, kmeans, pam, clara o hcut).

  • method: Seleccionar el criterio geométrico o estadístico para encontrar los clústeres óptimos:

    • “wss”: Calcular la suma de cuadrados dentro del grupo (Within Cluster Sum of Squares). Se busca el “punto de codo” (elbow method).

    • “silhouette”: Medir el ancho de la silueta promedio. Se busca maximizar este valor.

    • “gap_stat”: Comparar la dispersión observada contra una distribución de referencia nula.

Estimación de las distancias mínimas (R Core Team, 2019a):

library (stats)
kmeans(x, centers, iter.max = 10, nstart = 1)

Argumentos de la función:

  • x: Pasar la matriz o el dataframe con datos numérico (previamente estandarizado mediante scale()).

  • centers: Especificar el número de clústeres ([K]) deseados o proveer las coordenadas de los centroides iniciales.1

  • iter.max: Establecer el número máximo de iteraciones permitidas para evitar bucles infinitos.

  • nstart: Definir la cantidad de particiones iniciales aleatorias a intentar, permitiendo a R elegir la configuración con la menor suma de cuadrados interna.

Caso práctico

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

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

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

  1. Carga de librerias:
library(ggplot2)
library(dplyr)
library(comtradr)
library(ggplot2)
library(factoextra) # Librería clave para optimización y gráficos de K-Means

# Configurar clave API
comtradr::set_primary_comtrade_key("0019ae66e6294f76aa6071daf2557a20")
secundary_comtrade_key <- "4e16602deffc43279e344f14b62bb1b4"

knitr::opts_chunk$set(echo = TRUE)
  1. Extracción de los datos desde COMTRADE para el comercio del café, con datos de 2025:
datos_comtrade2 <- 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
)

  1. Limpieza de datos.
# Las columnas clave devueltas ahora son: 'primary_value_usd' y 'net_wgt_kg'.
datos_limpios2 <- datos_comtrade2[
  !is.na(datos_comtrade2$primary_value) & 
    !is.na(datos_comtrade2$net_wgt) & 
    datos_comtrade2$primary_value > 0 & 
    datos_comtrade2$net_wgt > 0, 
]

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



# Escalar los datos para ecualizar las magnitudes monetarias y de peso
df_escalado2 <- scale(df_cluster2)
  1. Estimación y calibración.
###########SETEO DE PARAMETROS PARA EL ANALISIS DE KMEANS

set.seed(3141592) # Semilla pseudoaleatoria para reproducibilidad del inicio estocástico

minPts_elegido <- 4 # Numero K escogido

###########


df_escalado2 <- as.data.frame(df_escalado2)
# Análisis de la tasa de variación de la varianza residual
fviz_nbclust(df_escalado2, kmeans, method = "wss") +
  labs(
    title = "Optimización del Parámetro K: Método del Codo",
    subtitle = "Suma de Cuadrados Intra-cluster (WCSS) según el Número de Grupos",
    x = "Número de Clústeres (K)", y = "WCSS Global (Varianza No Explicada)"
  ) +
  theme_minimal()+
geom_vline(xintercept = minPts_elegido, linetype = "dashed", color = "red", size = 1) 

  1. Estimación de las sumatoria de las medias
modelo_kmeans <- kmeans(df_escalado2, centers = minPts_elegido)

# Guardar la clasificación en el dataset original
datos_limpios2$cluster_km <- as.factor(modelo_kmeans$cluster)
  1. Gráfica de clúster
grafica1 <- ggplot(datos_limpios2, aes(x = net_wgt, y = primary_value, color = cluster_km)) +
  geom_point(size = 3, alpha = 0.8) +
  scale_x_log10() + 
  scale_y_log10() + 
  scale_color_manual(
    values = c(
      "1" = "#2c3e50",  # Azul Oscuro / Corporativo o Base
      "2" = "#e74c3c",  # Rojo / Alerta o Potencias Principales
      "3" = "#27ae60",  # Verde / Sustentable, Premium o Agrícola
      "4" = "#f39c12",  # Naranja / Regímenes Intermedios
      "5" = "#8e44ad",  # Morado / Mercados de Especialidad o Nicho
      "6" = "#3498db",  # Azul Claro / Economías Emergentes
      "7" = "#16a085",  # Turquesa / Regiones en Transición
      "8" = "#34495e"   # Gris Pizarra / Economías de Menor Escala o Restantes
    ))+
  labs(
    title = "Segmentación K-Means de Exportadores Mundiales de Café",
    subtitle = "Uso de datos oficiales UN Comtrade y algoritmos de optimización estructural",
    x = "Peso Neto Exportado (Kg) - Escala Log",
    y = "Valor del Comercio Ganado (USD) - Escala Log",
    color = "Régimen Comercial"
  ) +
  theme_minimal()

Interpretación de datos

Caso práctico 2

Conglomerados en el Consumo Hídrico Residencial con datos de la OCDE

El agua es un recurso estratégico y un servicio básico indispensable cuyo acceso y gestión eficiente definen el desarrollo sostenible de las naciones. Para comprender las verdaderas dinámicas de aprovechamiento, es fundamental evaluar de manera conjunta la dimensión demográfica y el consumo de agua en los hogares (PUB_HH).

• Población Total (Población): Representa el número total de habitantes residentes en cada país evaluado. Esta variable actúa como el indicador de escala demográfica. Es un factor crítico en el modelo porque establece la magnitud del mercado interno y determina el techo de demanda potencial de los servicios públicos esenciales en cada nación.

• Consumo de Agua Residencial (Agua_Total / Código API: PUB_HH): Registra el volumen total de agua suministrada y utilizada exclusivamente por el sector de los hogares (hogares residenciales). Esta variable mide el comportamiento del consumo doméstico directo (cocina, higiene, riego doméstico y lavado), dejando fuera el uso agrícola o industrial a gran escala. Refleja la presión hídrica que la vida cotidiana de la población ejerce sobre los sistemas de distribución municipales.

  1. Carga de librerias
library(rsdmx)
library(ggplot2)
library(scales)
  1. Carga del Endpoint
#URL agua
#https://data-explorer.oecd.org/vis?pg=0&bp=true&snb=21&tm=freshwater&df[ds]=dsDisseminateFinalDMZ&df[id]=DSD_WATER_USE%40DF_WATER_USE&df[ag]=OECD.ENV.EPI&df[vs]=1.0&dq=AUS.A...PUBLIC&pd=2015,&to[TIME_PERIOD]=false

#URL poblacion
#https://data-explorer.oecd.org/vis?fs[0]=Topic,1%7CSociety%23SOC%23%7CDemography%23SOC_DEM%23&pg=0&fc=Topic&bp=true&snb=2&df[ds]=dsDisseminateFinalDMZ&df[id]=DSD_POPULATION%40DF_POP_HIST&df[ag]=OECD.ELS.SAE&df[vs]=1.0&dq=AUS..PS._T..&pd=2014,2024&to[TIME_PERIOD]=false

url_agua_oecd <- "https://sdmx.oecd.org/public/rest/data/OECD.ENV.EPI,DSD_WATER_USE@DF_WATER_USE,1.0/AUT+BEL+CRI+CZE+DNK+EST+FIN+GRC+HUN+ISR+ITA+KOR+LVA+LTU+LUX+MEX+NLD+NOR+POL+SVK+SVN+CHE+WXOECD+ARM+AZE+BLR+BGR+HRV+GEO+ROU+ZAF+THA+AUS.A.PUB+PUB_AGR+PUB_MINE+PUB_MANU+PUB_COOL_MANU+PUB_ELEC+PUB_COOL_ELEC+PUB_CONST+PUB_HH+PUB_FOOD+PUB_MET+PUB_TR+PUB_TEX+PUB_PAPER+PUB_CHEM+PUB_OTHER+PUB_SER+PUB_CONMAN..?startPeriod=2023&endPeriod=2023&dimensionAtObservation=AllDimensions" 

url_pob_oecd  <- "https://sdmx.oecd.org/public/rest/data/OECD.ELS.SAE,DSD_POPULATION@DF_POP_HIST,1.0/AUT+BEL+CAN+CHL+COL+CRI+CZE+DNK+EST+FIN+FRA+DEU+GRC+HUN+ISL+IRL+ISR+ITA+JPN+KOR+LVA+LTU+LUX+MEX+NLD+NZL+NOR+POL+PRT+SVK+SVN+ESP+SWE+CHE+TUR+GBR+USA+G20+EU27+OECD+ARG+BRA+BGR+CHN+HRV+CYP+IND+IDN+MLT+ROU+RUS+SAU+SGP+ZAF+W+AUS.POP.PS._T._T.?startPeriod=2023&endPeriod=2023&dimensionAtObservation=AllDimensions"

# =========================================================================
# A. EXTRAER Y CONFIGURAR DATOS DE CONSUMO DE AGUA DE USO DOMESTICO (PUB_HH)
# =========================================================================
sdmx_agua <- readSDMX(url_agua_oecd)
df_agua_raw <- as.data.frame(sdmx_agua)

df_agua_2023 <- df_agua_raw %>%
  # Filtrar estrictamente por el grupo residencial y el año 2023
  # Nota: Verifica si tu columna de año se llama TIME_PERIOD o TIME
  filter(MEASURE == "PUB_HH", TIME_PERIOD == "2023") %>%
  mutate(Agua_Hogares = as.numeric(obsValue)) %>%
  select(REF_AREA, Agua_Hogares)

# =========================================================================
# B. EXTRAER Y CONFIGURAR DATOS DE POBLACIÓN (2023)
# =========================================================================
sdmx_pob <- readSDMX(url_pob_oecd)
df_pob_raw <- as.data.frame(sdmx_pob)

# Convertimos el valor a número limpio
df_pob_raw$Poblacion <- as.numeric(df_pob_raw$obsValue)

# Sumamos la población por país para asegurar que cada país aparezca una sola vez
df_pob_2023 <- aggregate(Poblacion ~ REF_AREA, data = df_pob_raw, FUN = sum)


# =========================================================================
# C. UNIR LAS TABLAS DE AGUA Y POBLACIÓN
# =========================================================================
# Cruzamos ambas tablas usando la columna común (REF_AREA)
df_analisis <- merge(df_agua_2023, df_pob_2023, by = "REF_AREA")

# Calculamos el consumo por persona (Per Cápita)
df_analisis$Agua_Per_Capita <- df_analisis$Agua_Hogares / df_analisis$Poblacion


# =========================================================================
# D. PREPARAR IDENTIFICADORES Y SELECCIONAR VARIABLES
# =========================================================================
# Convertimos el código del país en el nombre de la fila
rownames(df_analisis) <- df_analisis$REF_AREA

# Dejamos solo las columnas numéricas que va a usar el algoritmo K-Means
df_cluster_input <- df_analisis[, c("Poblacion", "Agua_Hogares")]

  1. Escalado y calibración
# =========================================================================
# E. ESCALADO DE DATOS (CRUCIAL PARA K-MEANS)
# =========================================================================
df_escalado <- scale(df_cluster_input)
df_escalado_df <- as.data.frame(df_escalado)

# =========================================================================
# F. SET DE PARAMETROS KMEANS 
# =========================================================================

set.seed(3141592) # Semilla pseudoaleatoria para reproducibilidad del inicio estocástico

minPts_elegido <- 3 # Numero K escogido

# =========================================================================
# G. EVALUAR EL MÉTODO DEL CODO (ELBOW METHOD)
# =========================================================================
fviz_nbclust(df_escalado_df, kmeans, method = "wss") +
  labs(
    title = "Calibración de Parámetros: Método del Codo",
    subtitle = "Suma de Cuadrados Intra-cluster (WCSS) para Población vs Consumo del agua para fines domésticos(PUB_HH)",
    x = "Número de Clusters (K)", 
    y = "Varianza Total Intra-cluster"
  ) +
  theme_minimal()+
  geom_vline(xintercept = minPts_elegido, linetype = "dashed", color = "red", size = 1) 

  1. Algoritmo Kmeans y visualizacion de cluster.
# =========================================================================
# H. EJECUTAR EL ALGORITMO K-MEANS
# =========================================================================

modelo_kmeans <- kmeans(df_escalado_df, centers = minPts_elegido)

# =========================================================================
# I. VISUALIZACIÓN GRÁFICA DE LOS CLUSTERS
# =========================================================================
# fviz_cluster proyectará de forma automática tus dos variables en los ejes x e y
grafica8 <- fviz_cluster(
  modelo_kmeans, 
  data = df_escalado_df,
  geom = c("point", "text"), # Muestra el punto del país y su código (REF_AREA) como etiqueta
  palette = c(
    "1" = "#2c3e50",  # Azul Oscuro / Base o Medios
    "2" = "#e74c3c",  # Rojo / Macro-consumidores u Outliers
    "3" = "#27ae60",  # Verde / Eficientes o Pequeña Escala
    "4" = "#f39c12",  
    "5" = "#8e44ad",  
    "6" = "#3498db",  
    "7" = "#16a085",  
    "8" = "#34495e"   
  ), # Colores estéticos para diferenciar grupos
  ellipse.type = "convex",   # Dibuja un polígono convexo alrededor de cada grupo de países
  star.plot = TRUE,
  repel = TRUE,              # Evita de forma automática que los nombres de los países se encimen
  theme = theme_minimal(),
  main = "OECD Water Use & Population (2023)",
  submain = "K-Means Clustering Analysis (PUB_HH Group)"
  
)

# Combinamos los datos originales con la asignación de clusters del modelo
datos_finales_analisis <- df_analisis %>%
  mutate(Cluster_Asignado = as.factor(modelo_kmeans$cluster))

# =========================================================================
# 10. GRÁFICA DE SEGMENTACIÓN ESTILO GGPLOT2
# =========================================================================
grafica5 <- ggplot(datos_finales_analisis, aes(x = Poblacion, y = Agua_Hogares, color = Cluster_Asignado)) +
  geom_point(size = 3, alpha = 0.8) +
  # Mantenemos las escalas logarítmicas por si hay países gigantes (como EE.UU.) que distorsionen el gráfico
  scale_x_log10() + 
  scale_y_log10() + 
  # Tu paleta de colores personalizada (se adaptará automáticamente a los 3 clusters calculados)
  scale_color_manual(
    values = c(
      "1" = "#2c3e50",  # Azul Oscuro / Base o Medios
      "2" = "#e74c3c",  # Rojo / Macro-consumidores u Outliers
      "3" = "#27ae60",  # Verde / Eficientes o Pequeña Escala
      "4" = "#f39c12",  
      "5" = "#8e44ad",  
      "6" = "#3498db",  
      "7" = "#16a085",  
      "8" = "#34495e"   
    )
  ) +
  labs(
    title = "Segmentación K-Means de Consumo Hídrico Residencial (OECD)",
    subtitle = "Uso de datos oficiales y algoritmos de optimización estructural (2023)",
    x = "Población Total (Millones de personas) (Escala Log)",
    y = "Destino del agua pàra consumo Residencial PUB_HH (millones de metros cubicos) (Escala Log)",
    color = "Régimen de Consumo"
  ) +
  theme_minimal()

  # 1. PREPARACIÓN DE LA TABLA DE PAÍSES (PROPÓSITO: AGUA OECD)
  # =========================================================================
# Extraemos el código del país desde los rownames para usarlo como etiqueta visible
datos_finales_analisis$Pais_Desc <- rownames(datos_finales_analisis)

# Seleccionamos las columnas necesarias adaptadas al propósito hídrico
tabla_paises <- datos_finales_analisis[, c("Pais_Desc", "Cluster_Asignado", "Poblacion", "Agua_Hogares")]

# 2. Garantizar la codificación numérica para evitar errores de ordenamiento
tabla_paises$Agua_Hogares <- as.numeric(tabla_paises$Agua_Hogares)

# 3. Ordenar la tabla de MAYOR A MENOR consumo de agua usando arrange
tabla_paises_para_top <- tabla_paises %>% arrange(desc(Agua_Hogares))

# 4. EXTRAER EL TOP DE FORMA CORRECTA PARA CADA CLÚSTER
top_por_grupo <- do.call(rbind, lapply(split(tabla_paises_para_top, tabla_paises_para_top$Cluster_Asignado), head, minPts_elegido))

# 5. Ordenar secuencialmente el resultado por Clúster
top_por_grupo <- top_por_grupo %>% arrange(Cluster_Asignado)


# =========================================================================
# GRÁFICA 6: Barras horizontales facetadas (Top N de Consumidores por Clúster)
# =========================================================================
grafica6 <- ggplot(top_por_grupo, aes(x = reorder(Pais_Desc, Agua_Hogares), y = Agua_Hogares, fill = Cluster_Asignado)) +
  geom_bar(stat = "identity", color = "black") +
  
  # Voltear la gráfica para que las barras sean horizontales y legibles
  coord_flip() + 
  
  # Dividir la pantalla en paneles según el grupo original de K-Means
  facet_wrap(~ Cluster_Asignado, scales = "free_y", ncol = 1) + 
  
  # Escala logarítmica adaptada a formato numérico de volumen (Comas decimales/miles)
  scale_y_log10(labels = scales::label_comma()) +
  
  # Control manual de tu paleta de colores corporativa
  scale_fill_manual(
    values = c(
      "1" = "#2c3e50",  # Azul Oscuro / Consumo Base o Medio
      "2" = "#e74c3c",  # Rojo / Macro-consumidores u Outliers
      "3" = "#27ae60",  # Verde / Consumo Eficiente o Pequeña Escala
      "4" = "#f39c12",  
      "5" = "#8e44ad",  
      "6" = "#3498db",  
      "7" = "#16a085",  
      "8" = "#34495e"   
    )
  ) +
  labs(
    title = paste0("Top ", minPts_elegido, " Máximos Consumidores de Agua por Grupo"),
    subtitle = "Países líderes en volumen de uso residencial (PUB_HH) organizados por clúster",
    x = "Código de País (OECD)",
    y = "Volumen del Recurso Hídrico destinado a los Hogares (millones de metros cubicos)(Escala Log)"
  ) +
  theme_minimal() +
  theme(
    strip.text = element_text(face = "bold", size = 11), 
    legend.position = "none"
  )


# =========================================================================
# GRÁFICA 7: Distribución (Muestra cuántos países totales hay en cada grupo)
# =========================================================================
grafica7 <- ggplot(datos_finales_analisis, aes(x = Cluster_Asignado, fill = Cluster_Asignado)) +
  geom_bar(color = "black", alpha = 0.8) +
  geom_text(stat = 'count', aes(label = after_stat(count)), vjust = -0.5, fontface = "bold") +
  scale_fill_manual(
    values = c(
      "1" = "#2c3e50",  
      "2" = "#e74c3c",  
      "3" = "#27ae60",  
      "4" = "#f39c12",  
      "5" = "#8e44ad",  
      "6" = "#3498db",  
      "7" = "#16a085",  
      "8" = "#34495e"   
    )
  ) +
  labs(
    title = "Distribución Global de Países por Régimen de Consumo Hídrico",
    subtitle = "Frecuencia absoluta de naciones asignadas a cada clúster (K-Means)",
    x = "Régimen de Consumo (Clúster)",
    y = "Cantidad de Países",
    fill = "Clúster"
  ) +
  theme_minimal() +
  theme(legend.position = "none")

Resumen del código

{r}
#| eval: false
#| include: false

library(rsdmx)
library(ggplot2)
library(scales)

#CARGA DE DATOS
url <- "mi url"
sdmx_agua <- readSDMX(url)

#ESCALADO
df_escalado <- scale(df_cluster_input)
df_escalado_df <- as.data.frame(df_escalado)

#CALIBRACION
set.seed(3141592) 
minPts_elegido <- N
fviz_nbclust(df_escalado_df, kmeans, method = "wss")

#KMEANS
modelo_kmeans <- kmeans(df_escalado_df, centers = minPts_elegido)

fviz_cluster(modelo_kmeans)

Preguntas para pensar

Pregunta: ¿Cuál es el objetivo principal de usar el algoritmo K-Means? ¿Busca predecir un valor exacto o busca agrupar cosas?

Busca agrupar las observaciones según su nivel de homogeneidad.

Pregunta: ¿Cuál es el propósito de incluir set.seed en el seteo de variables previo al calculo de Nmeans?

Reducir las variaciones en cada calculo, asignando un numero de computo inicial , ya que NMEANS realiza su estimación de forma aleatoria, por lo que los grupos podrían ser identificados en diferente orden, aunque esto no afecte la cantidad de grupos detectados.

Pregunta: Al usar el Método del Codo evaluamos la Suma de Cuadrados Intra-cluster (WCSS o Within-Cluster Sum of Squares). Matemáticamente, si incrementamos el valor de K hasta que sea K=N, que fenómeno ocurriría?

El WCSS sería exactamente cero, porque cada país sería su propio clúster y la distancia a su centro sería nula.

Pregunta: ¿Como reacciona KMEANS ante la presencia de valores atípicos (outliers)?

El algoritmo calcula la “media” (el promedio de las coordenadas) para mover el centro de cada clúster en cada iteración. Al igual que el promedio matemático común, un solo valor extremadamente gigantesco “arrastrará” con mucha fuerza el centroide hacia él, distorsionando y descuadrando los límites de los países normales que comparten ese clúster.

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
James, G., Witten, D., Hastie, T., & Tibshirani, R. (2021). An introduction to statistical learning: With applications in r (2nd ed.). Springer. https://doi.org/10.1007/978-1-0716-1418-1
Kassambara, A. (2024). Fviz_nbclust: Determining and visualizing the optimal number of clusters. https://www.rdocumentation.org/packages/factoextra/versions/2.2.0/topics/fviz_nbclust
Kaufman, L., & Rousseeuw, P. J. (1990). Finding groups in data: An introduction to cluster analysis. John Wiley & Sons. https://doi.org/10.1002/9780470316801
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
R Core Team. (2019a). Kmeans: K-means clustering. R Foundation for Statistical Computing. https://rdocumentation.org
R Core Team. (2019b). Scale: Scaling and centering of matrix-like objects. R Foundation for Statistical Computing. https://www.rdocumentation.org/packages/base/versions/3.6.2/topics/scale
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