2026-09-30
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.
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.
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).
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.
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.
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) .
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.).
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 sintaxis básica para llamar al algoritmo es la siguiente(Hahsler et al., 2019):
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.
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.
Carga de librerias y seteo de nuestra key para acceder a UN COMTRADE
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
# 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
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
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()# 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
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()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.
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)
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\]
Los principales comandos y su sintaxis a emplear son:
Escalado (R Core Team, 2019b) :
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):
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):
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.
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)
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)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
)# 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)###########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) 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()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.
#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")]# =========================================================================
# 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) # =========================================================================
# 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"){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)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.