Algoritmos de clustering con Scikit-Learn en Python

Sebastian Urdanegui
14 min readDec 9, 2022

El comercio no trata de mercancías, trata sobre información. Las mercancías se sientan en el almacén hasta que la información las mueve. (C. J. Cherryh)

La gran cantidad de datos que se generan con cada transacción realizada por los agentes económicos y el auge de la tecnología, específicamente, el crecimiento del aprendizaje automático que es una ramificación de la inteligencia artificial (IA) permite analizar el comportamiento del conjunto de individuos que forman parte de mercados específicos con el objetivo de tomar óptimas decisiones empresariales. Una de las herramientas de máxima relevancia es el clustering. Tal herramienta nos permite darle un sentido a la información recopilada, según la Universidad Europea.

El clúster es un grupo geográficamente próximo de compañías interconectadas y asociadas a instituciones en un campo particular, vinculadas por algo en común y por la complementariedad entre ellas, según la definición de Michael Porter. Entonces, el clustering es el proceso de aprendizaje automático no supervisado que consiste en agrupar a los agentes partícipes del mercado en función de características similares que se conocen como clústeres.

¿Cómo el clustering puede mejorar la toma de decisiones en el área de marketing? Básicamente, mediante la segmentación del mercado. El clustering permite definir estrategias de marketing en función de los features que caracterizan a cada clúster o grupo.

La importancia del presente artículo radica en la presentación de un caso práctico que permita al lector conocer el procedimiento base para la aplicación de algoritmos de clustering. Existen diversos tipos de algoritmos de clustering, pero en este caso utilizaré tres de ellos: K-means, Agglomerative Hierarchical Clustering (AHC) y Density-Based Spatial Clustering of Applications with Noise (DBSCAN). Adicionalmente, mediante la métrica del Silhouette Score evaluaré la calidad del agrupamiento obtenido con cada algoritmo de clustering.

Para lograr el objetivo utilizaré el dataset de la fuente de datos de Kaggle llamado “Credit Card Dataset for Clustering”. El reto del dataset propuesto es realizar la segmentación del consumidor para definir una estrategia de marketing. El dataset cuenta con el comportamiento de 9,000 clientes de tarjetas de créditos en los últimos 6 meses y posee 18 features o características que los definen.

En esta ocasión, el propósito del artículo no corresponde al procesamiento, limpieza y análisis de los datos (EDA), no obstante, adjunto el link del notebook en Deepnote, ya que, al dar click podrán visualizar el know-how del procesamiento, limpieza y exploración de los datos.

Notebook — Credit Card Dataset for Clustering:

Teniendo en cuenta que el dataset ha sido procesado para conocer la presencia de valores nulos, duplicados y outliers se mostrará a continuación el tratamiento que se debe realizar antes de correr el algoritmo de clustering.

En primer lugar, se debe efectuar un escalamiento de los datos, para ello, se utilizará el módulo StandardScaler() de la librería de Sklearn. En segundo lugar, se aplicará el método PCA (Principal Components Analysis) con el objetivo de captar la mayor varianza de la información con un menor número de features o características, puesto que, algunos algoritmos de clustering como el caso de K-means es sensible ante la dimensionalidad que puede presentar el conjunto de datos. Por último, el gráfico de varianza acumulada permitirá tomar la decisión sobre el número de features que se utilizarán para realizar el análisis.

¡Es hora de tirar código! 👨🏻‍💻💚📊

# Escalamiento de los datos
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
# Se elimina la columna "CUST_ID" por no ser relevante para el análisis
data_scaled = scaler.fit_transform(data.drop("CUST_ID", axis = 1))
data_scaled

# Conversión de la variable "data_scaled" a un DataFrame()
data_scaled = pd.DataFrame(data_scaled, columns = data.drop("CUST_ID",
axis = 1).columns)
data_scaled
# Principal Components Analysis (PCA)
from sklearn.decomposition import PCA
pca = PCA()
pca.fit_transform(data_scaled)
data_scaled_pca = pca.transform(data_scaled)
data_scaled_pca

# Determinar la varianza del dataset
var = pca.explained_variance_ratio_

# Determinar la varianza acumulada del dataset
cum_var = np.cumsum(np.round(var, decimals=4)*100)

# Gráfica de la varianza acumulada
fig, ax = plt.subplots(figsize=(8,6), dpi = 80)
plt.title("Cumulative Variation Graph")
plt.plot(cum_var, "b-*")
plt.axvline(x=8, color = "r")
for i in ['bottom', 'left']:
ax.spines[i].set_color('black')
ax.spines[i].set_linewidth(1.5)
right_side = ax.spines["right"]
right_side.set_visible(False)
top_side = ax.spines["top"]
top_side.set_visible(False)
ax.set_axisbelow(True)
ax.grid(color='gray', linewidth=1, axis='y', alpha=0.4)
plt.show()
Imagen 1. Fuente: Elaboración propia.
# Eliminar las variables que se encuentran en el rango de 9:16 porque
# se tomará en cuentra las primeras columnas por obtener la mayor varianza
data_scaled_pca_st = pd.DataFrame(data_scaled_pca)
data_scaled_pca_st.drop([9,10,11,12,13,14,15,16], axis = 1, inplace = True)
data_scaled_pca_st

En la Imagen 1 se observa la varianza acumulada en función del número de features que se toman en cuenta. Nuestro objetivo es seleccionar el número de features que capte la mayor varianza posible. En este caso, opté por tomar los primeros 8 features porque captan el 90% de la información, aproximadamente.

A continuación, se presenta las librerías de Python que se deben importar para aplicar los algoritmos de clustering.

# Librerías que se deben importar para el clustering
from sklearn.cluster import KMeans, AgglomerativeClustering, DBSCAN
from sklearn.metrics import silhouette_score
from scipy.cluster.hierarchy import dendrogram, linkage

Primer algoritmo de clustering: K-means

El algoritmo de K-means necesita que se le indique la cantidad de clústeres, luego, el propio algoritmo ubicará los centroides aleatoriamente para que cada punto sea asociado al centroide más cercano. Este proceso iterará n-veces hasta que los centroides no se muevan en el espacio.

Como bien expliqué, una de las desventajas del algoritmo de K-means es que el “k” (número de clústeres) debe ser elegido manualmente. Por lo tanto, se utilizará el método del codo para determinar el número óptimo de clústeres.

# Encontrar la cantidad óptima de clústeres, mediante el método del codo
# y la gráfica de silueta
from sklearn.metrics.cluster import silhouette_samples
sum_of_squared_distances = []
silhouette_scores = []
K = range(2,15)
for k in K:
km = KMeans(n_clusters = k)
y_predict = km.fit_predict(data_scaled_pca_st)
sum_of_squared_distances.append(km.inertia_)
silhouette_scores.append(silhouette_score(data_scaled_pca_st, y_predict))

# Gráfica del método del codo
fig, ax = plt.subplots(figsize = (8,8), dpi = 70)
plt.plot(K, sum_of_squared_distances, "r-*")
plt.xlabel("K")
plt.ylabel("Inertia")
plt.title("Elbow Method")
plt.axvline(x = 4, color = "blue")
for i in ['bottom', 'left']:
ax.spines[i].set_color('black')
ax.spines[i].set_linewidth(1.5)
right_side = ax.spines["right"]
right_side.set_visible(False)
top_side = ax.spines["top"]
top_side.set_visible(False)
ax.set_axisbelow(True)
ax.grid(color='gray', linewidth=1, axis='y', alpha=0.4)
plt.show()
Imagen 2. Fuente: Elaboración propia.

En la Imagen 2 se aprecia el gráfico del método del codo, cabe resaltar que el número de clústeres que se debe elegir es aquel que se encuentre en el punto de inflexión de la curva convexa. Según mi interpretación, elegí el número de clústeres igual a 4 porque se encuentra en el punto de inflexión.

Sin embargo, mediante el gráfico de silueta, analizaré si el número de clústeres que obtiene el mayor coeficiente de silueta es coherente con el valor arrojado por el método del codo.

# Gráfica de silueta
fig, ax = plt.subplots(figsize = (8,8), dpi = 70)
plt.plot(K, silhouette_scores, "b-*")
plt.xlabel("K")
plt.ylabel("Silhouette scores")
plt.title("Silhouette Scores")
plt.axvline(x = 3, color = "r")
for i in ['bottom', 'left']:
ax.spines[i].set_color('black')
ax.spines[i].set_linewidth(1.5)
right_side = ax.spines["right"]
right_side.set_visible(False)
top_side = ax.spines["top"]
top_side.set_visible(False)
ax.set_axisbelow(True)
ax.grid(color='gray', linewidth=1, axis='y', alpha=0.4)
plt.show()
Imagen 3. Fuente: Elaboración propia.

En la Imagen 3 se observa que el coeficiente de silueta se maximiza cuando el número de clústeres es 3, mientras que, cuando es 4 se reduce drásticamente. Por ello, elegiré el número de clústeres igual a 3.

# Análisis del coeficiente de silueta para el algoritmo K-means
km = KMeans(n_clusters=3)
y_predict = km.fit_predict(data_scaled_pca_st)
print(f'Silhouette Score to KMeans with PCA : {round(silhouette_score(data_scaled_pca_st, y_predict),4)}')
# Añadir una columna en el dataset con respecto a la clasificación realizada por K-means.
data["K_Means_PCA"] = y_predict

El coeficiente de silueta para el algoritmo de K-means con un k = 3 en el dataset de “Credit Card Dataset for Clustering” es 0.2632.

Segundo algoritmo de clustering: Agglomerative Hierarchical Clustering

Cabe resaltar que existen 2 tipos de clustering jerárquico: aglomerativo y divisivo. La diferencia entre ambos radica en la forma en cómo inician el proceso de clustering. El método aglomerativo toma un punto y paso a paso crea grupos, es decir, va de lo particular a lo general. Mientras que, el método divisivo o también denominado disociativo es la inversa del método aglomerativo. Para el caso del presente estudio, tomaré el método jerárquico aglomerativo por ser el más utilizado.

Por otro lado, los linkage son los métodos que se utilizan para que los clústeres hagan match entre ellos. Ejemplos de linkage: simple linkage, complete linkage, average linkage y ward. Tomaré el método de linkage ward por ser el que tiene un mejor performance frente a los demás. El proceso que realiza es tomar un datapoint y conducirlo hacia otro clúster para calcular su varianza frente a los datapoints de los otros clústeres y se queda con el que presenta la varianza mínima.

Una de las ventajas del algoritmo de Agglomerative Clustering es que no se necesita el número de clústeres porque tal valor se obtiene al analizar el dendrograma. Por otra parte, una de las desventajas es que le afectan drásticamente los outliers y no presenta un buen performance con datasets extensos.

Recomiendo utilizar el algoritmo de Agglomerative Clustering cuando se tenga datasets pequeños, se desconozca la cantidad de clústeres por completo y se desee resultados rápidos.

# Encontrar el número óptimo de clústeres en función del dendrograma
fig, ax = plt.subplots(figsize = (10,10))
dendrogram(linkage(data_scaled_pca_st, method = "ward"))
plt.title("Dendogram Credit Card")
plt.xlabel("Clusters")
plt.ylabel("Euclidean distances")
for i in ['bottom', 'left']:
ax.spines[i].set_color('black')
ax.spines[i].set_linewidth(1.5)
right_side = ax.spines["right"]
right_side.set_visible(False)
top_side = ax.spines["top"]
top_side.set_visible(False)
ax.set_axisbelow(True)
ax.grid(color='gray', linewidth=1, axis='y', alpha=0.4)
plt.show()
Imagen 4. Fuente: Elaboración propia.

El dendrograma es un diagrama de árbol que muestra los grupos que se forman al crear conglomerados de observaciones en cada paso y sus niveles de similitud. El nivel de similitud se mide en el eje vertical (o nivel de distancia) y las diferentes observaciones se muestran en el eje horizontal.

¿Cómo interpretar el dendrograma? Según la información de la página web de Minitab Statistical Software, para ver los niveles de similitud, coloque el puntero del ratón sobre una línea horizontal del dendrograma. El patrón de cómo los valores de similitud o de distancia cambian de un paso a otro puede ayudar a elegir la agrupación final para los datos. El paso donde los valores cambian de manera abrupta podría identificar un punto adecuado para definir la agrupación final.

Entonces, para obtener el número de conglomerados se debe cortar horizontalmente el dendrograma. En este caso, consideré tener una mayor distancia entre ellos, pero con un menor número de conglomerados. Por lo tanto, el número de conglomerados es 2.

# Evaluación del coeficiente de silueta para el algoritmo Agglomerative
# Hierarchical Clustering
hc = AgglomerativeClustering(n_clusters = 2, affinity = "euclidean", linkage = "ward")
y_hc = hc.fit_predict(data_scaled_pca_st)
print(silhouette_score(data_scaled_pca_st, y_hc))
# Añadir una columna en el dataset con respecto a la clasificación realizada
# por Agglomerative Clustering
data["HC_PCA"] = y_hc

El coeficiente de silueta para el algoritmo de Agglomerative Hierarchical Clustering con un k = 2 en el dataset de “Credit Card Dataset for Clustering” es 0.3763.

Tercer algoritmo de clustering: Density-Based Spatial Clustering of Applications with Noise (DBSCAN)

El tercer algoritmo está basado en densidad. Su principio es que un clúster tiene alta densidad, mientras que la región que los separa no la tiene. Por ello, para que cada punto en el clúster de acuerdo a un radio dado debe tener un número mínimo de vecinos o puntos cercanos para encontrar densidad.

El algoritmo DBSCAN necesita de dos parámetros: épsilon (eps) y el mínimo de puntos (min_sample). El épsilon hace referencia al radio de la vecindad. Mientras que el min_sample es el número mínimo de muestras para consolidarse como una vecindad.

Cabe resaltar que se debe prestar atención al momento de elegir los hiperparámetros porque los valores pequeños provocan que el modelo tarde en ejecutarse y los valores altos pueden generar que nunca converjan.

El algoritmo de DBSCAN es capaz de detectar outliers, puede encontrar clústeres en formas y tamaños arbitrarios. No obstante, una de las desventajas es que los hiperparámetros son muy determinantes para el algoritmo y que los puntos fronterizos a los que se puede acceder desde más de un clúster pueden formar parte de otro clúster.

Dado que no conocemos el número de clústeres, utilizaré el método de NearestNeighbors() que permite clasificar los vecinos más cercanos. Se puede apreciar el número de clústeres mediante el gráfico del método de la rodilla. Al igual que en el método del codo, se busca que el número de clústeres elegido sea aquel que pertenezca al punto de inflexión de la curva.

# Método de la rodilla para encontrar el número de clústeres óptimo
from sklearn.neighbors import NearestNeighbors
neighbors = NearestNeighbors(n_neighbors = 3)
neighbors_fit = neighbors.fit(data_scaled_pca_st)
distances, indices = neighbors_fit.kneighbors(data_scaled_pca_st)

distances = np.sort(distances, axis = 0)
distances = distances[:,1]

# Gráfica del método de la rodilla
fig, ax = plt.subplots(figsize = (6,6))
plt.plot(distances, "r-*")
plt.axhline(y = 2, color = "blue")
plt.title("Knee Method")
for i in ['bottom', 'left']:
ax.spines[i].set_color('black')
ax.spines[i].set_linewidth(1.5)
right_side = ax.spines["right"]
right_side.set_visible(False)
top_side = ax.spines["top"]
top_side.set_visible(False)
ax.set_axisbelow(True)
ax.grid(color='gray', linewidth=1, axis='y', alpha=0.4)
plt.show()
Imagen 5. Fuente: Elaboración propia.

En la Imagen 5 tenemos que aquel número de clústeres óptimo (eje vertical) que se encuentra en el punto de inflexión de la curva está en el rango de 0.5 y 3. Por ello, realizaré una combinatoria cruzada entre los valores de épsilon y el número de muestras que tiene un rango de 4 y 12 porque el número de features que acumula la mayor varianza de la información se encuentra entre tales valores.

# Rango posible de clústeres en el que se encuentra la rodilla
eps_values = np.arange(0.5,3,0.20)

# Rango en el que se encuentra el número de features que obtienen la mayor varianza acumulada
min_samples = np.arange(4,12)

from itertools import product
dbscan_paramns = list(product(eps_values, min_samples)) # Lista de combinatoria cruzada
n_of_clusters = []
sil_score = []
for p in dbscan_paramns:
y_dbscan = DBSCAN(eps = p[0], min_samples = p[1]).fit_predict(data_scaled_pca_st)
try:
sil_score.append(silhouette_score(data_scaled_pca_st, y_dbscan))
except:
sil_score.append(0)
n_of_clusters.append(len(np.unique(y_dbscan)))

data_paramns_tunning = pd.DataFrame.from_records(dbscan_paramns, columns = ["Eps", "Min_samples"])
data_paramns_tunning["sil_score"] = sil_score
data_paramns_tunning["n_clusters"] = n_of_clusters

Luego de realizar las operaciones correspondientes, procederé a evaluar gráficamente aquella combinación óptima entre el número de muestras y el épsilon que maximiza el coeficiente de silueta.

# Heatmap de la combinación óptima entre el número de muestras y el epsilon
# que maximiza el coeficiente de silueta
pivot_1 = pd.pivot_table(data_paramns_tunning, values = "sil_score", columns = "Eps", index = "Min_samples")
fig, ax = plt.subplots(figsize = (18,6))
sns.heatmap(pivot_1, annot = True, annot_kws = {"size":10}, cmap = "coolwarm", ax = ax)
plt.show()
Imagen 6. Fuente: Elaboración propia.
# Heatmap de la combinación óptima entre el número de muestras y el epsilon
# en función del número de clústeres
pivot_2 = pd.pivot_table(data_paramns_tunning, values = "n_clusters", columns = "Eps", index = "Min_samples")
fig, ax = plt.subplots(figsize = (18,6))
sns.heatmap(pivot_2, annot = True, annot_kws = {"size":10}, cmap = "coolwarm", ax = ax)
plt.show()
Imagen 7. Fuente: Elaboración propia.

Es importante mencionar que para tomar una decisión, tanto la Imagen 6 como la Imagen 7 son relevantes y complementarias. No se puede elegir aquella combinación óptima entre el épsilon (2.89) y el número mínimo de muestras (4) que maximiza el coeficiente de silueta (0.69) (Imagen 6) si en aquella combinación le corresponde un número de clústeres igual a 2 porque en realidad un clúster le corresponde al ruido de los datos y sólo se tendría un sólo clúster con la información y lo que se busca es segmentar los datos. Por ello, elegiré un épsilon de 2.09 y un mínimo de muestras de 4 resultándome en un coeficiente silueta de 0.59.

# Evaluación del coeficiente de silueta para el algoritmo DBSCAN
dbscan_train = DBSCAN(eps = 2.099, min_samples = 4)
y_dbscan = dbscan_train.fit_predict(data_scaled_pca_st)
print(silhouette_score(data_scaled_pca_st, y_dbscan))
# Añadir una columna en el dataset con respecto a la clasificación realizada
# por DBSCAN.
data["DBSCAN_PCA"] = y_dbscan

El coeficiente de silueta para el algoritmo de Density-Based Spatial Clustering of Applications with Noise (DBSCAN) con un k = 3 en el dataset de “Credit Card Dataset for Clustering” es 0.5931.

Evaluación de los algoritmos implementados

fig, ax = plt.subplots(figsize = (6,4))
sns.scatterplot(data = data, x = data["BALANCE"], y = data["PURCHASES"], hue = "K_Means_PCA", palette = "coolwarm")
plt.title("Clustering between BALANCE and PURCHASES\nthrough K-means")
for i in ['bottom', 'left']:
ax.spines[i].set_color('black')
ax.spines[i].set_linewidth(1.5)
right_side = ax.spines["right"]
right_side.set_visible(False)
top_side = ax.spines["top"]
top_side.set_visible(False)
ax.set_axisbelow(True)
ax.grid(color='gray', linewidth=1, axis='y', alpha=0.4)
plt.show()
Imagen 8. Fuente: Elaboración propia.
fig, ax = plt.subplots(figsize = (6,4))
sns.scatterplot(data = data, x = data["BALANCE"], y = data["PURCHASES"], hue = "HC_PCA", palette = "coolwarm")
plt.title("Clustering between BALANCE and PURCHASES\nthrough Agglomerative Hierarchical Clustering")
for i in ['bottom', 'left']:
ax.spines[i].set_color('black')
ax.spines[i].set_linewidth(1.5)
right_side = ax.spines["right"]
right_side.set_visible(False)
top_side = ax.spines["top"]
top_side.set_visible(False)
ax.set_axisbelow(True)
ax.grid(color='gray', linewidth=1, axis='y', alpha=0.4)
plt.show()
Imagen 9. Fuente: Elaboración propia.
fig, ax = plt.subplots(figsize = (6,4))
sns.scatterplot(data = data, x = data["BALANCE"], y = data["PURCHASES"], hue = "DBSCAN_PCA", palette = "coolwarm")
plt.title("Clustering between BALANCE and PURCHASES\nthrough DBSCAN")
for i in ['bottom', 'left']:
ax.spines[i].set_color('black')
ax.spines[i].set_linewidth(1.5)
right_side = ax.spines["right"]
right_side.set_visible(False)
top_side = ax.spines["top"]
top_side.set_visible(False)
ax.set_axisbelow(True)
ax.grid(color='gray', linewidth=1, axis='y', alpha=0.4)
plt.show()
Imagen 10. Fuente: Elaboración propia.

Conclusiones

  • El mejor algoritmo de clustering para el presente dataset es DBSCAN con un silhouette score de 0.5931.
  • El algoritmo de K-means asocia nuestro conjunto de datos en 3 clústeres (0, 1 y 2) (Imagen 8). En este caso, sólo comparé la características de “Purchases” y “Balance” con respecto a los clústeres. Se puede realizar este procedimiento con todos los features. En líneas generales, existe un grupo 0 que posee un rango de monto de compra entre 0 y 5,000 unidades monetarias y un monto de saldo en la tarjeta de crédito en un rango de 0 y 2,500 unidades monetarias. Por otro lado, el grupo 1 está conformado por aquellos consumidores que poseen un rango de compre entre 0 y 5,000 unidades monetarias, pero que presentan un monto de saldo en la tarjeta de crédito en un rango de 2,500 y 16,000 unidades monetarias, aproximadamente. Por último, el grupo 2 está conformado por aquellos que tienen un mayor poder adquisitivo porque realizan compras superiores a las 5,000 unidades monetarias y el rango de saldo de la tarjeta de crédito es más extenso porque existen consumidores que poseen entre 0 y 17,500 unidades monetarias. Es un buen análisis gráfico porque se puede establecer estrategias de marketing para cada clúster, sin embargo, el coeficiente de silueta para el algoritmo de K-means arroja un valor de 0.26, un resultado bajo para conocer el rendimiento que tuvo el algoritmo al momento de clasificar los datos. Es posible que el rendimiento no haya sido tan bueno porque existen outliers que afectan la performance del modelo.
  • El algoritmo de DBSCAN tuvo una mejor performance con respecto al coeficiente de silueta con un valor de 0.56, tal valor está por encima del algoritmo de K-means y Agglomerative Hierarchical Clustering. No obstante, se observa la presencia de ruido (clúster -1) (Imagen 10) y se reconoce sólo dos grupos (0 y 1). El grupo 0 conformado por aquellos consumidores que poseen un rango de compra entre 0 y 10,000 unidades monetarias y un rango de saldo en la tarjeta de crédito entre 0 y 16,000 unidades monetarias, aproximadamente. Es un grupo más amplio. El grupo 1 es un grupo más centrado porque está representado por consumidores que tienen un monto de compra alrededor de las 5,000 unidades monetarias y un saldo en la tarjeta de crédito de 5,000 unidades monetarias.

Bibliografía

--

--

Sebastian Urdanegui

Top #4 Exprésate Perú Con Datos 2023 🏅| Data Analyst 📊 | Programador de Python |