Алгоритмы кластеризации — это мощные инструменты машинного обучения для группировки похожих точек данных. В этом исследовании мы рассмотрим четыре популярных алгоритма кластеризации: K-средних, иерархический, DBSCAN и Affinity Propagation.

  1. Кластеризация K-средних.
    K-средних — это итеративный алгоритм, который разбивает данные на K отдельных кластеров на основе близости точек данных к центроидам кластера. Он направлен на минимизацию суммы квадратов внутри кластера. K-средние вычислительно эффективны и хорошо работают, когда кластеры хорошо разделены и имеют одинаковый размер. Это требует указания количества кластеров заранее.

2. Иерархическая кластеризация.
Иерархическая кластеризация создает иерархию кластеров путем многократного слияния или разделения существующих кластеров на основе их сходства. Она может быть агломерационной (снизу вверх) или разделительной (сверху вниз). Иерархическая кластеризация не требует предварительного указания количества кластеров и предоставляет дендрограмму для визуализации иерархии кластеризации.

3. DBSCAN (пространственная кластеризация приложений с шумом на основе плотности):
DBSCAN — это алгоритм кластеризации на основе плотности, который группирует точки данных на основе их плотности. Он определяет кластеры как плотные области, разделенные более разреженными областями. DBSCAN может обнаруживать кластеры произвольной формы, обрабатывать зашумленные данные и не требует предварительного указания количества кластеров. Он классифицирует точки как ядро, границу или шум на основе плотности и связности.

4. Affinity Propagation:
Affinity Propagation — это итеративный алгоритм, не требующий указания количества кластеров. Он использует подход передачи сообщений для определения экземпляров, которые являются репрезентативными точками в данных. Каждая точка данных сообщает о своем желании быть образцом, и алгоритм обновляет эти сообщения до тех пор, пока не появится стабильный набор образцов. Affinity Propagation может потребовать значительных вычислительных ресурсов, но полезно, когда количество кластеров неизвестно.

Чтобы применить эти алгоритмы кластеризации, мы будем использовать данные сегментации клиентов торгового центра, доступные на Kaggle https://www.kaggle.com/datasets/vjchoudhary7/customer-segmentation-tutorial-in-python.

Набор данных

  1. CustomerID: идентификатор для каждого клиента.
  2. Пол: указывает пол клиента (мужской или женский).
  3. Возраст: представляет возраст клиента в годах.
  4. Годовой доход (k$): Обозначает годовой доход клиента в тысячах долларов.
  5. Spending Score (1–100): балл от 1 до 100, который дает количественную оценку потребительских привычек и предпочтений. Более высокий балл указывает на более высокую склонность к тратам.

Импорт связанных библиотек

import pandas as pd
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
import lightgbm as lgb
import warnings
from itertools import combinations
import plotly.express as px
import matplotlib.pyplot as plt
from matplotlib.pyplot import figure

Предварительная обработка

pd.set_option('max_columns',100)
pd.set_option('max_rows',900)
pd.set_option('max_colwidth',200)
df = pd.read_csv('/kaggle/input/customer-segmentation-tutorial-in-python/Mall_Customers.csv')
df.head()

df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 200 entries, 0 to 199
Data columns (total 5 columns):
 #   Column                  Non-Null Count  Dtype 
---  ------                  --------------  ----- 
 0   CustomerID              200 non-null    int64 
 1   Gender                  200 non-null    object
 2   Age                     200 non-null    int64 
 3   Annual Income (k$)      200 non-null    int64 
 4   Spending Score (1-100)  200 non-null    int64 
dtypes: int64(4), object(1)
memory usage: 7.9+ KB
df.describe()

#Numerical values distribution graphs
# get the numerical columns of the DataFrame
num_cols = df.select_dtypes(include=['float64','int64']).columns
# create a figure with size (10, 10) for each numerical column
for col in num_cols:
    plt.figure(figsize=(10,10))
    sns.histplot(data=df, x=col)
    plt.title(col)
    plt.show()

#Categorical values graphs
# get the categorical columns of the DataFrame
cat_cols = df.select_dtypes(include=['object','category']).columns
# create a countplot for each categorical column
for col in cat_cols:
    plt.figure(figsize=(10,10))
    ax = sns.countplot(data=df, x=col)
    plt.title(col)
    
    # add percentage labels on each bar
    for p in ax.containers[0].patches:
        percent = (p.get_height()/len(df))*100
        ax.text(p.get_x()+p.get_width()/2,
                p.get_height()+20,
                '{:1.2f}%'.format(percent),
                ha='center', fontsize=12)
    plt.show()

# create a countplot for each categorical column
for col in  df.select_dtypes(include=['float64','int64']).columns:
    plt.figure(figsize=(10,10))
    ax = sns.boxplot(data=df, y=col, x="Gender")
    plt.title(col)
   
    plt.show()

АНАЛИЗ ИССЛЕДОВАТЕЛЬСКИХ ДАННЫХ

df_train=df.drop('CustomerID',axis=1)
df_train.corr()

plt.figure(figsize=(10,10))
sns.heatmap(df_train.corr(), annot=True, cmap='coolwarm')
plt.show()

# create a scatterplot for each numeric column
plt.figure(figsize=(10,10))
sns.scatterplot(data=df_train, x="Age",y="Annual Income (k$)", hue="Gender")
plt.show()

# create a scatterplot for each numeric column
plt.figure(figsize=(10,10))
sns.scatterplot(data=df_train, x="Age",y="Spending Score (1-100)", hue="Gender")
plt.show()

# create a scatterplot for each numeric column
plt.figure(figsize=(10,10))
sns.scatterplot(data=df_train, x="Annual Income (k$)",y="Spending Score (1-100)", hue="Gender")
plt.show()

ПОСТРОЕНИЕ МОДЕЛИ

K означает

df_km=df_train.copy(deep=True)
df_train.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 200 entries, 0 to 199
Data columns (total 4 columns):
 #   Column                  Non-Null Count  Dtype 
---  ------                  --------------  ----- 
 0   Gender                  200 non-null    object
 1   Age                     200 non-null    int64 
 2   Annual Income (k$)      200 non-null    int64 
 3   Spending Score (1-100)  200 non-null    int64 
dtypes: int64(3), object(1)
memory usage: 6.4+ KB
from sklearn.preprocessing import LabelEncoder
# Create an instance of the LabelEncoder class
le = LabelEncoder()
# Get a list of categorical columns
categorical_cols = df_km.select_dtypes(include='object').columns
# Apply the label encoder to each categorical column
for col in categorical_cols:
    df_km[col] = le.fit_transform(df_km[col])
df_km.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 200 entries, 0 to 199
Data columns (total 4 columns):
 #   Column                  Non-Null Count  Dtype
---  ------                  --------------  -----
 0   Gender                  200 non-null    int64
 1   Age                     200 non-null    int64
 2   Annual Income (k$)      200 non-null    int64
 3   Spending Score (1-100)  200 non-null    int64
dtypes: int64(4)
memory usage: 6.4 KB
from sklearn.cluster import KMeans

# select the features
X = df_km
#Scaling Data
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X = scaler.fit_transform(X)
from yellowbrick.cluster import KElbowVisualizer
model = KMeans(random_state=1)
visualizer = KElbowVisualizer(model, k=(2,10))
visualizer.fit(X)
visualizer.show()
plt.show()

model = KMeans(random_state=1)
visualizer = KElbowVisualizer(model, k=(2,10), metric='silhouette')
visualizer.fit(X)
visualizer.show()
plt.show()

оптимальный k = 5 может быть хорошим выбором.

# create a k-means object with the optimal number of clusters
optimal_k = 5 # number of clusters where the elbow is
kmeans = KMeans(n_clusters=optimal_k, init='k-means++', max_iter=300, n_init=10, random_state=0)
# fit the k-means object to the data
kmeans.fit(X)
# predict the cluster for each data point
y_kmeans = kmeans.predict(X)
# add the cluster predictions to the dataframe
df_km['cluster'] = y_kmeans
# display the first 5 rows of the dataframe with the cluster predictions
print(df_km.head())

# create a scatter plot of the data with different colors for each cluster
sns.scatterplot(x='Annual Income (k$)', y='Spending Score (1-100)', hue='cluster', data=df_km, palette="deep")
# add a title and labels to the plot
plt.title('Clusters of Customers')
plt.xlabel('Annual Income (k$)')
plt.ylabel('Spending Score (1-100)')
# show the plot
plt.show()

# create a scatter plot of the data with different colors for each cluster
sns.scatterplot(x='Annual Income (k$)', y='Spending Score (1-100)', hue='cluster', data=df_km, palette="deep")
# add a title and labels to the plot
plt.title('Clusters of Customers')
plt.xlabel('Annual Income (k$)')
plt.ylabel('Gender')
# show the plot
plt.show()

# create a scatter plot of the data with different colors for each cluster
sns.scatterplot(x='Annual Income (k$)', y='Spending Score (1-100)', hue='cluster', data=df_km, palette="deep")
# add a title and labels to the plot
plt.title('Clusters of Customers')
plt.xlabel('Age)')
plt.ylabel('Gender')
# show the plot
plt.show()

from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure(figsize=(7, 7))
ax = Axes3D(fig, rect=[0, 0, .99, 1], elev=20, azim=210)
ax.scatter(df_km['Age'],
           df_km['Annual Income (k$)'],
           df_km['Spending Score (1-100)'],
           c=df_km['cluster'],
           s=35, edgecolor='k', cmap=plt.cm.Set1)
ax.w_xaxis.set_ticklabels([])
ax.w_yaxis.set_ticklabels([])
ax.w_zaxis.set_ticklabels([])
ax.set_xlabel('Age')
ax.set_ylabel('Annual Income (k$)')
ax.set_zlabel('Spending Score (1-100)')
ax.set_title('3D view of K-Means 5 clusters')
ax.dist = 12
plt.show()

from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure(figsize=(7, 7))
ax = Axes3D(fig, rect=[0, 0, .99, 1], elev=20, azim=210)
ax.scatter(df_km['Gender'],
           df_km['Annual Income (k$)'],
           df_km['Spending Score (1-100)'],
           c=df_km['cluster'],
           s=35, edgecolor='k', cmap=plt.cm.Set1)
ax.w_xaxis.set_ticklabels([])
ax.w_yaxis.set_ticklabels([])
ax.w_zaxis.set_ticklabels([])
ax.set_xlabel('Gender')
ax.set_ylabel('Annual Income (k$)')
ax.set_zlabel('Spending Score (1-100)')
ax.set_title('3D view of K-Means 5 clusters')
ax.dist = 12
plt.show()

df_km.groupby('cluster').describe()

Иерархическая кластеризация (агломеративная)

df_ag=df_train.copy(deep=True)
df_ag.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 200 entries, 0 to 199
Data columns (total 4 columns):
 #   Column                  Non-Null Count  Dtype 
---  ------                  --------------  ----- 
 0   Gender                  200 non-null    object
 1   Age                     200 non-null    int64 
 2   Annual Income (k$)      200 non-null    int64 
 3   Spending Score (1-100)  200 non-null    int64 
dtypes: int64(3), object(1)
memory usage: 6.4+ KB
from sklearn.preprocessing import LabelEncoder
# Create an instance of the LabelEncoder class
le = LabelEncoder()
# Get a list of categorical columns
categorical_cols = df_ag.select_dtypes(include='object').columns
# Apply the label encoder to each categorical column
for col in categorical_cols:
    df_ag[col] = le.fit_transform(df_ag[col])
df_ag.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 200 entries, 0 to 199
Data columns (total 4 columns):
 #   Column                  Non-Null Count  Dtype
---  ------                  --------------  -----
 0   Gender                  200 non-null    int64
 1   Age                     200 non-null    int64
 2   Annual Income (k$)      200 non-null    int64
 3   Spending Score (1-100)  200 non-null    int64
dtypes: int64(4)
memory usage: 6.4 KB
from sklearn.cluster import AgglomerativeClustering
from scipy.cluster.hierarchy import dendrogram
from scipy.cluster import hierarchy
model = AgglomerativeClustering(n_clusters=None,distance_threshold=0)
cluster_labels = model.fit_predict(df_ag)
cluster_labels

linkage_matrix = hierarchy.linkage(model.children_)
linkage_matrix[:][:5]

plt.figure(figsize=(30,10))
hierarchy.set_link_color_palette(['r','grey', 'b', 'grey', 'm', 'grey', 'g', 'grey', 'orange']) # set colors for the clusters
dn = hierarchy.dendrogram(linkage_matrix,truncate_mode='level',p=15, color_threshold=23) # color_threshold=23 sets clusters below y-axis value of 23 to be of the same color

model = AgglomerativeClustering(n_clusters=5)
df_ag['cluster']=model.fit_predict(df_ag) # predict the categories for each point.
# create a scatter plot of the data with different colors for each cluster
sns.scatterplot(x='Annual Income (k$)', y='Spending Score (1-100)', hue='cluster', data=df_ag, palette="deep")
# add a title and labels to the plot
plt.title('Clusters of Customers')
plt.xlabel('Annual Income (k$)')
plt.ylabel('Spending Score (1-100)')
# show the plot
plt.show()

# create a scatter plot of the data with different colors for each cluster
sns.scatterplot(x='Annual Income (k$)', y='Spending Score (1-100)', hue='cluster', data=df_ag, palette="deep")
# add a title and labels to the plot
plt.title('Clusters of Customers')
plt.xlabel('Age)')
plt.ylabel('Gender')
# show the plot
plt.show()

from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure(figsize=(7, 7))
ax = Axes3D(fig, rect=[0, 0, .99, 1], elev=20, azim=210)
ax.scatter(df_ag['Age'],
           df_ag['Annual Income (k$)'],
           df_ag['Spending Score (1-100)'],
           c=df_ag['cluster'],
           s=35, edgecolor='k', cmap=plt.cm.Set1)
ax.w_xaxis.set_ticklabels([])
ax.w_yaxis.set_ticklabels([])
ax.w_zaxis.set_ticklabels([])
ax.set_xlabel('Age')
ax.set_ylabel('Annual Income (k$)')
ax.set_zlabel('Spending Score (1-100)')
ax.set_title('3D view of K-Means 5 clusters')
ax.dist = 12
plt.show()

from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure(figsize=(7, 7))
ax = Axes3D(fig, rect=[0, 0, .99, 1], elev=20, azim=210)
ax.scatter(df_ag['Gender'],
           df_ag['Annual Income (k$)'],
           df_ag['Spending Score (1-100)'],
           c=df_ag['cluster'],
           s=35, edgecolor='k', cmap=plt.cm.Set1)
ax.w_xaxis.set_ticklabels([])
ax.w_yaxis.set_ticklabels([])
ax.w_zaxis.set_ticklabels([])
ax.set_xlabel('Gender')
ax.set_ylabel('Annual Income (k$)')
ax.set_zlabel('Spending Score (1-100)')
ax.set_title('3D view of K-Means 5 clusters')
ax.dist = 12
plt.show()

DBScan

df_db=df_train.copy(deep=True)
df_db.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 200 entries, 0 to 199
Data columns (total 4 columns):
 #   Column                  Non-Null Count  Dtype 
---  ------                  --------------  ----- 
 0   Gender                  200 non-null    object
 1   Age                     200 non-null    int64 
 2   Annual Income (k$)      200 non-null    int64 
 3   Spending Score (1-100)  200 non-null    int64 
dtypes: int64(3), object(1)
memory usage: 6.4+ KB
from sklearn.preprocessing import LabelEncoder
# Create an instance of the LabelEncoder class
le = LabelEncoder()
# Get a list of categorical columns
categorical_cols = df_db.select_dtypes(include='object').columns
# Apply the label encoder to each categorical column
for col in categorical_cols:
    df_db[col] = le.fit_transform(df_db[col])
from sklearn.cluster import DBSCAN
from itertools import product
eps_values = np.arange(8,12.75,0.25) # eps values to be investigated
min_samples = np.arange(3,10) # min_samples values to be investigated
DBSCAN_params = list(product(eps_values, min_samples))
from sklearn.metrics import silhouette_score
no_of_clusters = []
sil_score = []
for p in DBSCAN_params:
    DBS_clustering = DBSCAN(eps=p[0], min_samples=p[1]).fit(df_db)
    no_of_clusters.append(len(np.unique(DBS_clustering.labels_)))
    sil_score.append(silhouette_score(df_db, DBS_clustering.labels_))
tmp = pd.DataFrame.from_records(DBSCAN_params, columns =['Eps', 'Min_samples'])   
tmp['No_of_clusters'] = no_of_clusters
pivot_1 = pd.pivot_table(tmp, values='No_of_clusters', index='Min_samples', columns='Eps')
fig, ax = plt.subplots(figsize=(12,6))
sns.heatmap(pivot_1, annot=True,annot_kws={"size": 16}, cmap="YlGnBu", ax=ax)
ax.set_title('Number of clusters')
plt.show()

tmp = pd.DataFrame.from_records(DBSCAN_params, columns =['Eps', 'Min_samples'])   
tmp['Sil_score'] = sil_score
pivot_1 = pd.pivot_table(tmp, values='Sil_score', index='Min_samples', columns='Eps')
fig, ax = plt.subplots(figsize=(18,6))
sns.heatmap(pivot_1, annot=True, annot_kws={"size": 10}, cmap="YlGnBu", ax=ax)
plt.show()

DBS_clustering = DBSCAN(eps=12.5, min_samples=4).fit(df_db)
DBSCAN_clustered = df_db.copy()
DBSCAN_clustered.loc[:,'Cluster'] = DBS_clustering.labels_ # append labels to points
DBSCAN_clust_sizes = DBSCAN_clustered.groupby('Cluster').size().to_frame()
DBSCAN_clust_sizes.columns = ["DBSCAN_size"]
DBSCAN_clust_sizes

outliers = DBSCAN_clustered[DBSCAN_clustered['Cluster']==-1]
fig2, (axes) = plt.subplots(1,2,figsize=(12,5))

sns.scatterplot('Annual Income (k$)', 'Spending Score (1-100)',
                data=DBSCAN_clustered[DBSCAN_clustered['Cluster']!=-1],
                hue='Cluster', ax=axes[0], palette='Set1', legend='full', s=45)
sns.scatterplot('Age', 'Spending Score (1-100)',
                data=DBSCAN_clustered[DBSCAN_clustered['Cluster']!=-1],
                hue='Cluster', palette='Set1', ax=axes[1], legend='full', s=45)
axes[0].scatter(outliers['Annual Income (k$)'], outliers['Spending Score (1-100)'], s=5, label='outliers', c="k")
axes[1].scatter(outliers['Age'], outliers['Spending Score (1-100)'], s=5, label='outliers', c="k")
axes[0].legend()
axes[1].legend()
plt.setp(axes[0].get_legend().get_texts(), fontsize='10')
plt.setp(axes[1].get_legend().get_texts(), fontsize='10')
plt.show()

Модель точки доступа

df_ap=df_train.copy(deep=True)
df_ap.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 200 entries, 0 to 199
Data columns (total 4 columns):
 #   Column                  Non-Null Count  Dtype 
---  ------                  --------------  ----- 
 0   Gender                  200 non-null    object
 1   Age                     200 non-null    int64 
 2   Annual Income (k$)      200 non-null    int64 
 3   Spending Score (1-100)  200 non-null    int64 
dtypes: int64(3), object(1)
memory usage: 6.4+ KB
from sklearn.preprocessing import LabelEncoder
# Create an instance of the LabelEncoder class
le = LabelEncoder()
# Get a list of categorical columns
categorical_cols = df_ap.select_dtypes(include='object').columns
# Apply the label encoder to each categorical column
for col in categorical_cols:
    df_ap[col] = le.fit_transform(df_ap[col])
df_ap.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 200 entries, 0 to 199
Data columns (total 4 columns):
 #   Column                  Non-Null Count  Dtype
---  ------                  --------------  -----
 0   Gender                  200 non-null    int64
 1   Age                     200 non-null    int64
 2   Annual Income (k$)      200 non-null    int64
 3   Spending Score (1-100)  200 non-null    int64
dtypes: int64(4)
memory usage: 6.4 KB
from sklearn.cluster import AffinityPropagation
from sklearn.metrics import silhouette_score
no_of_clusters = []
preferences = range(-20000,-5000,100) # arbitraty chosen range
af_sil_score = [] # silouette scores
for p in preferences:
    AF = AffinityPropagation(preference=p, max_iter=200).fit(df_ap)
    no_of_clusters.append((len(np.unique(AF.labels_))))
    af_sil_score.append(silhouette_score(df_ap, AF.labels_))
    
af_results = pd.DataFrame([preferences, no_of_clusters, af_sil_score], index=['preference','clusters', 'sil_score']).T
af_results.sort_values(by='sil_score', ascending=False).head() # display only 5 best scores

fig, ax = plt.subplots(figsize=(12,5))
ax = sns.lineplot(preferences, af_sil_score, marker='o', ax=ax)
ax.set_title("Silhouette score method")
ax.set_xlabel("number of clusters")
ax.set_ylabel("Silhouette score")
ax.axvline(-11800, ls="--", c="red")
plt.grid()
plt.show()

AF = AffinityPropagation(preference=-11800).fit(df_ap)
AF_clustered = df_ap.copy()
AF_clustered.loc[:,'Cluster'] = AF.labels_ # append labels to points
AF_clust_sizes = AF_clustered.groupby('Cluster').size().to_frame()
AF_clust_sizes.columns = ["AF_size"]
AF_clust_sizes

fig3, (ax_af) = plt.subplots(1,2,figsize=(12,5))

scat_1 = sns.scatterplot('Annual Income (k$)', 'Spending Score (1-100)', data=AF_clustered,
                hue='Cluster', ax=ax_af[0], palette='Set1', legend='full')
sns.scatterplot('Age', 'Spending Score (1-100)', data=AF_clustered,
                hue='Cluster', palette='Set1', ax=ax_af[1], legend='full')
plt.setp(ax_af[0].get_legend().get_texts(), fontsize='10')
plt.setp(ax_af[1].get_legend().get_texts(), fontsize='10')
plt.show()

Заключение

Лично я думаю, что иерархическая кластеризация дает лучшие результаты в этом случае. Однако важно признать, что производительность моделей кластеризации может варьироваться в зависимости от конкретного набора данных и контекста. Поэтому крайне важно учитывать производительность и пригодность различных моделей в каждом конкретном случае.