Автор статьи: Рустем Галиев

IBM Senior DevOps Engineer & Integration Architect. Официальный DevOps ментор и коуч в IBM

Привет Хабр!

Иерархическая кластеризация является мощным методом анализа данных, позволяющим группировать схожие объекты в кластеры. В этой статье мы рассмотрим процесс разработки и интерпретации иерархической кластеризации, погружаясь в методы создания кластеров и анализа результатов. Мы изучим этот подход, который визуализирует данные в виде дендрограммы, что позволяет наглядно оценить структуру полученных кластеров. Разберем основные шаги этого метода, включая выбор метрик расстояния, выполнение кластеризации и интерпретацию результатов. Давайте вместе углубимся в этот увлекательный мир анализа данных с использованием иерархической кластеризации.

Чтобы загрузить и импортировать транзакционный набор данных для этого упражнения, выполним:

import pandas as pd
import numpy as np
from matplotlib import pyplot as plt

df = pd.read_csv("https://ucarecdn.com/8d8cd2ee-47d4-474f-b3a7-66eb9a20b43e/retail_data_clean.csv")
df.head()

Наша цель - идентифицировать оптовых клиентов. Как мы можем это сделать? Мы предполагаем, что оптовые клиенты заказывают большие объемы и в среднем имеют более высокие продажи, чем потребители.

Давайте суммируем наш набор данных и рассчитаем общие объемы и средний доход на одного клиента.

order_volume_df = df.groupby(['CustomerID']).agg({
    'Revenue': "mean",
    'Quantity': "sum"
})
order_volume_df.columns = ['AvgRevenue', 'TotalQuantity']
order_volume_df

Давайте посмотрим на распределение AvgRevenue (средний доход) относительно TotalQuantity (общее количество).

(order_volume_df
.plot
.scatter(x = 'AvgRevenue', y='TotalQuantity')
.get_figure()
.savefig('scatter.png'))
plt.clf()

Прежде чем применять алгоритм кластеризации, важно убедиться, что все переменные, которые мы используем для кластеризации, имеют одинаковый масштаб. Этот процесс также называется нормализацией. В настоящее время наши значения для AvgRevenue находятся в диапазоне от 2 до примерно 4000. Значения TotalQuantity варьируются от 2 до почти 70 000.

Различные масштабы могут затруднить или иногда сделать невозможным для нашего алгоритма кластеризации нахождение связанных точек.

Чтобы нормализовать наш набор данных, мы применим функцию масштабирования из библиотеки Scikit-learn.

StandardScaler нормализует серию x с помощью вычисления:

z = (x - u) / std

где u - это среднее значение x, а std - стандартное отклонение x.

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

from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X = order_volume_df
scaled_df = pd.DataFrame(scaler.fit_transform(X), columns = X.columns)
scaled_df.describe()

Мы не знаем, сколько кластеров мы ищем. Пока можем отличить потенциальных оптовых клиентов от остальных, любое количество кластеров подойдет.

Хорошим инструментом для решения этой проблемы являются дендрограммы кластеров.

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

Для построения дендрограммы мы используем функцию plot_dendogram, которая поставляется с документацией scikit-learn и использует некоторые функции другого статистического пакета, называемого scipy.

Подробное объяснение функции выходит за рамки данного курса. Вы можете найти ссылку на справку в конце этой лабораторной работы.

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

Давайте попробуем эту функцию и применим дендрограмму к нашим данным.

from scipy.cluster.hierarchy import dendrogram
from sklearn.cluster import AgglomerativeClustering

def plot_dendrogram(model, **kwargs):
  # Create linkage matrix and then plot the dendrogram
  # create the counts of samples under each node
  counts = np.zeros(model.children_.shape[0])
  n_samples = len(model.labels_)
  for (i, merge) in enumerate(model.children_):
    current_count = 0
    for child_idx in merge:
      if child_idx < n_samples:
        current_count += 1  # leaf node
      else:
        current_count += counts[child_idx - n_samples]
    counts[i] = current_count
  linkage_matrix = np.column_stack([model.children_,
      model.distances_, counts]).astype(float)
  # Plot the corresponding dendrogram
  dendrogram(linkage_matrix, **kwargs)

X = scaled_df

# setting distance_threshold=0 ensures we compute the full tree.
model = AgglomerativeClustering(distance_threshold=0, n_clusters=None)

fitted = model.fit(X)
plt.title("Hierarchical Clustering Dendrogram")
# plot the top three levels of the dendrogram
plot_dendrogram(fitted, truncate_mode="level", p=3)
plt.xlabel("Number of points in node (or index of point if no parentheses).")
plt.savefig('dendogram.png')
plt.clf()

Откройте файл dendogram.png

Как интерпретировать дендрограмму? Читайте ее сверху вниз. Каждая вертикальная линия представляет собой кластер. На верхнем уровне, при индексе около 40, будет только одна линия, что означает - все данные принадлежат одному большому кластеру.

Если мы разрежем дерево на высоте около 38, у нас будут два кластера: вертикальная линия слева и вертикальная линия справа. Если опустимся чуть ниже, на высоту ниже 36 и выше 20, у нас будет три кластера, один слева и два справа.

Высота между каждым столбцом указывает на то, насколько далеко друг от друга находятся кластеры. Длинная вертикальная линия представляет большое расстояние, а короткая вертикальная линия - небольшое расстояние.

Если бы мы построили всё дерево до конца, то нашли бы столько вертикальных линий, сколько есть точек данных в наборе данных - в нашем случае это 1000 (количество клиентов).

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

Что делаем с этой дендрограммой?

Если мы хотим максимизировать расстояние между кластерами, имеет смысл разделить данные на три кластера (три синие линии).

Однако, поскольку крайние правые линии зеленого кластера содержат только 12 точек данных (1x индекс 721 + 5 + 2 + 4), я предпочел бы разделить их на отдельный кластер.

Это приведет к четырем кластерам, поэтому разрез будет на высоте 18.

Для получения определенного количества кластеров с помощью метода AgglomerativeClustering мы можем указать желаемое количество кластеров n_clusters или distance_threshold для отсечения на определенной высоте.

Давайте укажем количество кластеров напрямую.

fitted = AgglomerativeClustering(n_clusters=4 ).fit(scaled_df)

Мы можем использовать метод labels_ для получения метки кластера для каждой точки данных.

fitted.labels_[:10]

Как видите, все эти начальные точки данных находятся в одном кластере.

Давайте посмотрим, сколько точек данных содержится в каждом кластере.

np.unique(fitted.labels_, return_counts = True)

Сравните эти значения с дендрограммой. Вы увидите, что это точно соответствует разбиению на 984 точки данных в одном большом кластере и 12 + 1 + 3 в других трех.

Мы приближаемся к определению наших оптовых покупателей!

Чтобы добавить метки кластеров обратно в наш исходный фрейм данных, выполните следующий код:

order_volume_df['Cluster'] = fitted.labels_.astype(str)

Давайте снова создадим диаграмму рассеяния.

import seaborn as sns
(sns.lmplot(x = 'AvgRevenue', y='TotalQuantity', data=order_volume_df, hue='Cluster', fit_reg=False)
.savefig('scatterplot-clusters.png'))

Теперь мы хотим сопоставить сегменты с нашими идентификаторами клиентов. Мы предполагаем, что клиенты в самом большом кластере скорее всего являются конечными пользователями, а все остальные - оптовиками.

largest_cluster = (order_volume_df                  
.groupby("Cluster")
.count()
.sort_values("AvgRevenue", ascending = False)
.reset_index()
.loc[0,'Cluster'])
largest_cluster

Давайте определим короткую функцию, которая назначает имя сегмента в зависимости от номера кластера.

def rename_clusters(x):
  if x == largest_cluster:
    return("Consumer")
  else:
    return("Wholesale")

order_volume_df['segment'] = order_volume_df.apply(lambda x: rename_clusters(x['Cluster']), axis = 1)
order_volume_df.head()

Давайте получим всех клиентов в сегменте оптовой торговли:

order_volume_df[order_volume_df['segment'] =="Wholesale"]

Отлично! Вот наши 16 (предполагаемых) оптовых клиентов. У них либо необычно высокий объем, либо высокая средняя выручка. Заметьте, как мы смогли идентифицировать этих клиентов, не делая предположений о фактическом пороге для объема заказов и продаж?

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

Наконец, экспортируем идентификаторы клиентов вместе с их сегментами в файл CSV, чтобы мы могли использовать эту информацию в следующей лабораторной работе.

Для создания segments_df мы просто используем подмножество order_volume_df с CustomerID и segment и удаляем все дублирующиеся значения. Таким образом, мы получаем новый фрейм данных, где каждая строка представляет собой идентификатор клиента с соответствующим описанием сегмента.

segments_df = order_volume_df.reset_index()[['CustomerID', 'segment']].drop_duplicates()
segments_df

Экспортируйте этот файл в CSV с помощью следующего кода:

segments_df.to_csv('customerid_segments.csv', index = False)

В завершение хочу порекомендовать вам бесплатный урок, на котором вы познакомитесь с основными терминами теории графов: вершина, ребро, граф; петля, псевдограф; кратные рёбра, мультиграф; смежность, инцидентность; степень вершины, изолированная вершина; путь, цикл, цикл Эйлера и Гамильтона; дерево, лес, мост; полный граф, подграф, взвешенный граф; дуга, ориентированный граф, компоненты сильной/слабой связности. А также подробно ознакомитесь с программой курса «Алгоритмы и структуры данных».

Комментарии (0)