Este documento presenta un análisis de clustering utilizando el algoritmo UMAP (Uniform Manifold Approximation and Projection) para reducción de dimensionalidad, seguido de técnicas de clustering no supervisado para identificar al menos 3 grupos distintos en los datos.
Fuente seleccionada: Dataset ChocolateSales de
Kaggle
https://www.kaggle.com/datasets/atharvasoundankar/chocolate-sales
¿Cómo varía la preferencia de producto entre los diferentes países registrados en el dataset y qué relación existe entre el tipo de producto y el país de origen de la venta?
Variables: Producto (Nominal), País (Nominal), Canal de venta (Nominal)
¿Cómo se distribuyen los canales de venta utilizados en cada país para la comercialización de los distintos tipos de productos de chocolate?
Variables: Canal de venta (Nominal), País (Nominal), Producto (Nominal)
¿Qué relación existe entre el país de venta, el tipo de producto y el tipo de cliente registrado en el dataset?
Variables: País (Nominal), Producto (Nominal), Tipo de cliente (Nominal)
import re
import numpy as np
import pandas as pd
import umap
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
import seaborn as sns
import matplotlib.pyplot as plt
sns.set(style="whitegrid", context="notebook")
df = pd.read_csv("C:/UIABD/Chocolate_Sales.csv")
print(f"Dimensiones: {df.shape[0]} filas x {df.shape[1]} columnas")
## Dimensiones: 1094 filas x 6 columnas
print("\nPrimeras filas del dataset:")
##
## Primeras filas del dataset:
print(df.head())
## Sales Person Country ... Amount Boxes Shipped
## 0 Jehu Rudeforth UK ... $5,320 180
## 1 Van Tuxwell India ... $7,896 94
## 2 Gigi Bohling India ... $4,501 91
## 3 Jan Morforth Australia ... $12,726 342
## 4 Jehu Rudeforth UK ... $13,685 184
##
## [5 rows x 6 columns]
print("\nInformación del dataset:")
##
## Información del dataset:
print(df.info())
## <class 'pandas.core.frame.DataFrame'>
## RangeIndex: 1094 entries, 0 to 1093
## Data columns (total 6 columns):
## # Column Non-Null Count Dtype
## --- ------ -------------- -----
## 0 Sales Person 1094 non-null object
## 1 Country 1094 non-null object
## 2 Product 1094 non-null object
## 3 Date 1094 non-null object
## 4 Amount 1094 non-null object
## 5 Boxes Shipped 1094 non-null int64
## dtypes: int64(1), object(5)
## memory usage: 51.4+ KB
## None
for col in df.columns:
if df[col].dtype == "object":
cleaned = df[col].astype(str).str.replace(r"[^0-9.\-]", "", regex=True)
numeric = pd.to_numeric(cleaned, errors="coerce")
if numeric.notna().mean() > 0.8:
df[col] = numeric
num_cols = df.select_dtypes(include=[np.number]).columns.tolist()
df_num = df[num_cols].copy()
print("\nColumnas numéricas usadas para clustering:")
##
## Columnas numéricas usadas para clustering:
print(num_cols)
## ['Amount', 'Boxes Shipped']
df_num = df_num.dropna()
print("\nFilas después de eliminar NAs:", df_num.shape[0])
##
## Filas después de eliminar NAs: 1094
scaler = StandardScaler()
X_scaled = scaler.fit_transform(df_num.values)
umap_model = umap.UMAP(
n_neighbors=15,
n_components=2,
min_dist=0.1,
metric="euclidean",
random_state=42
)
umap_data = umap_model.fit_transform(X_scaled)
## C:\Users\steve\AppData\Local\Programs\Python\PYTHON~2\Lib\site-packages\umap\umap_.py:1952: UserWarning: n_jobs value 1 overridden to 1 by setting random_state. Use no seed for parallelism.
## warn(
df_umap = pd.DataFrame(umap_data, columns=["UMAP1", "UMAP2"])
df_umap.index = df_num.index
df_umap.head()
## UMAP1 UMAP2
## 0 4.004960 5.885582
## 1 3.418832 1.994409
## 2 8.090756 4.450562
## 3 -1.446002 5.767755
## 4 -1.242552 4.031403
k = 3
kmedias = KMeans(
n_clusters=k,
max_iter=2000,
n_init=150,
random_state=42
)
kmedias.fit(df_umap)
KMeans(max_iter=2000, n_clusters=3, n_init=150, random_state=42)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
| n_clusters | 3 | |
| init | 'k-means++' | |
| n_init | 150 | |
| max_iter | 2000 | |
| tol | 0.0001 | |
| verbose | 0 | |
| random_state | 42 | |
| copy_x | True | |
| algorithm | 'lloyd' |
grupos = kmedias.predict(df_umap)
df_umap["cluster"] = grupos
df_num["cluster"] = grupos
df_umap
## UMAP1 UMAP2 cluster
## 0 4.004960 5.885582 2
## 1 3.418832 1.994409 1
## 2 8.090756 4.450562 0
## 3 -1.446002 5.767755 2
## 4 -1.242552 4.031403 1
## ... ... ... ...
## 1089 0.849156 8.682555 2
## 1090 3.637950 3.283180 1
## 1091 5.326190 10.134583 2
## 1092 0.083328 9.981619 2
## 1093 0.448169 8.586951 2
##
## [1094 rows x 3 columns]
print("\nDistribución de clústeres:")
##
## Distribución de clústeres:
print(df_num["cluster"].value_counts().sort_index())
## cluster
## 0 379
## 1 393
## 2 322
## Name: count, dtype: int64
sil_prom = silhouette_score(df_umap[["UMAP1", "UMAP2"]], grupos)
print("\nCoeficiente de silueta promedio:", round(sil_prom, 4))
##
## Coeficiente de silueta promedio: 0.4775
plt.figure(figsize=(8, 6))
sns.scatterplot(
data=df_umap, x="UMAP1", y="UMAP2",
hue="cluster", palette="Set2", s=40, alpha=0.8
)
plt.title(f"Clústeres con UMAP + KMeans (k = {k})")
plt.legend(title="Cluster")
plt.tight_layout()
plt.show()
cluster_means = df_num.groupby("cluster").mean()
plt.figure(figsize=(len(cluster_means.columns) * 0.6 + 3, 4))
sns.heatmap(
cluster_means,
cmap="RdBu_r",
center=0,
annot=False,
cbar_kws={"label": "Valor medio (escalado o numérico)"}
)
plt.title("Mapa de Calor: Características por Clúster")
plt.xlabel("Variable")
plt.ylabel("Cluster")
plt.tight_layout()
plt.show()
cluster_means_norm = cluster_means.copy()
for col in cluster_means_norm.columns:
col_min = cluster_means_norm[col].min()
col_max = cluster_means_norm[col].max()
rango = col_max - col_min
if rango > 0:
cluster_means_norm[col] = (cluster_means_norm[col] - col_min) / rango * 100
else:
cluster_means_norm[col] = 50
max_vars = 12
vars_radar = cluster_means_norm.columns[:max_vars]
data_radar = cluster_means_norm[vars_radar].to_numpy()
labels_clusters = [f"Cluster {i}" for i in cluster_means_norm.index]
num_vars = len(vars_radar)
angles = np.linspace(0, 2 * np.pi, num_vars, endpoint=False).tolist()
angles += angles[:1]
plt.figure(figsize=(8, 6))
ax = plt.subplot(111, polar=True)
for i, (row, label) in enumerate(zip(data_radar, labels_clusters)):
values = row.tolist()
values += values[:1]
ax.plot(angles, values, label=label, linewidth=2)
ax.fill(angles, values, alpha=0.15)
ax.set_xticks(angles[:-1])
ax.set_xticklabels(vars_radar, fontsize=9)
ax.set_yticks([0, 25, 50, 75, 100])
ax.set_yticklabels(["0%", "25%", "50%", "75%", "100%"])
ax.set_title("Perfil de Características por Clúster (Radar)", fontsize=13, pad=20)
ax.legend(loc="upper right", bbox_to_anchor=(1.25, 1.1))
plt.tight_layout()
plt.show()
print("RESUMEN DEL ANÁLISIS DE CLÚSTERES")
## RESUMEN DEL ANÁLISIS DE CLÚSTERES
print("==================================\n")
## ==================================
print("Algoritmo UMAP: Reducción de dimensionalidad")
## Algoritmo UMAP: Reducción de dimensionalidad
print("Algoritmo K-Means: Clustering")
## Algoritmo K-Means: Clustering
print(f"Número de clústeres: {k}")
## Número de clústeres: 3
print(f"Coeficiente de silueta promedio: {round(sil_prom, 4)}")
## Coeficiente de silueta promedio: 0.4775
print("\nDistribución de observaciones:")
##
## Distribución de observaciones:
for clust in sorted(df_num["cluster"].unique()):
n_obs = (df_num["cluster"] == clust).sum()
porcentaje = round(n_obs / len(df_num) * 100, 2)
print(f" Cluster {clust}: {n_obs} observaciones ({porcentaje}%)")
## Cluster 0: 379 observaciones (34.64%)
## Cluster 1: 393 observaciones (35.92%)
## Cluster 2: 322 observaciones (29.43%)