Освоение сегментации клиентов с LLM

Мастерство сегментации клиентов с LLM

Разблокируйте передовые техники сегментации клиентов с помощью LLM и улучшите ваши модели кластеризации с передовыми техниками

Содержание

· Введение· Данные· Метод 1: Kmeans· Метод 2: K-Prototype· Метод 3: LLM + Kmeans· Вывод

Введение

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

Что мы увидим в этой статье?

Давайте рассмотрим 3 метода для работы с таким типом проектов:

  • Kmeans
  • K-Prototype
  • LLM + Kmeans

В качестве небольшого превью я покажу следующее сравнение двухмерного представления (PCA) различных созданных моделей:

Графическое сравнение трех методов (изображение от автора).

Вы также узнаете такие техники снижения размерности, как:

  • PCA
  • t-SNE
  • MCA

Некоторые из результатов включают в себя:

Графическое сравнение трех методов снижения размерности (изображение от автора).

Проект с заметками можно найти здесь. И также вы можете заглянуть на мой GitHub:

damiangilgonzalez1995 – Обзор

Leidenschaftlich für Daten wechselte ich von der Physik zur Datenwissenschaft. Beim Telefonica, HP und jetzt CTO bei…

github.com

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

Данные

Оригинальные данные, использованные в этом проекте, взяты из открытого Kaggle: Банковский набор данных – Маркетинговые цели. Каждая строка в этом наборе данных содержит информацию о клиентах компаний. Некоторые поля являются числовыми, а другие – категориальными, мы увидим, что это расширяет возможные способы решения проблем.

Мы оставим только первые 8 столбцов. Наш набор данных выглядит следующим образом:

Давайте рассмотрим краткое описание столбцов нашего набора данных:

  • возраст (числовой)
  • работа: тип работы (категориальный: “админ.”, “неизвестно”, “безработный”, “управление”, “домохозяйка”, “предприниматель”, “студент”, “физический трудник”, “самозанятый”, “пенсионер”, “техник”, “услуги”)
  • семейное положение: семейное положение (категориальный: “женат/замужем”, “разведен/вдовец”, “холост”; примечание: “разведен” означает разведенный или вдовец)
  • образование (категориальный: “неизвестно”, “среднее”, “начальное”, “высшее”)
  • дефолт: есть ли кредит в дефолте? (бинарный: “да”, “нет”)
  • баланс: средний годовой баланс, в евро (числовой)
  • недвижимость: есть ли ипотечный кредит? (бинарный: “да”, “нет”)
  • займ: есть ли личный займ? (бинарный: “да”, “нет”)

В проекте я использовал набор данных для обучения от Kaggle. В репозитории проекта вы можете найти папку “data”, где хранится сжатый файл набора данных, использованного в проекте. Кроме того, там вы найдете два CSV-файла внутри сжатого файла. Один – это тренировочный набор данных, предоставленный Kaggle (train.csv), а другой – набор данных после выполнения эмбеддинга (embedding_train.csv), о котором мы позже более подробно расскажем.

Чтобы более ясно объяснить структуру проекта, показано дерево проекта:

clustering_llm├─ data│  ├─ data.rar├─ img├─ embedding.ipynb├─ embedding_creation.py├─ kmeans.ipynb├─ kprototypes.ipynb├─ README.md└─ requirements.txt

Метод 1: Kmeans

Это самый распространенный метод, и, наверное, вы его знаете. В любом случае, мы изучим его, потому что я покажу продвинутые техники анализа в этих случаях. Комплексную процедуру вы найдете в Jupyter-ноутбуке с названием kmeans.ipynb

Предобработка

Выполняется предварительная обработка переменных:

  1. Заключается в преобразовании категориальных переменных в числовые. Мы можем применить кодировщик Onehot (обычное действие), но в данном случае мы применяем кодировщик Ordinal.
  2. Мы стараемся обеспечить нормальное распределение числовых переменных. Для этого мы применяем преобразование PowerTransformer.

Давайте посмотрим на код.

import pandas as pd # манипуляции с данными в виде таблиц import numpy as np # линейная алгебра # визуализация данных import matplotlib.pyplot as plt import matplotlib.cm as cm import plotly.express as px import plotly.graph_objects as go import seaborn as sns import shap # sklearn from sklearn.cluster import KMeans from sklearn.preprocessing import PowerTransformer, OrdinalEncoder from sklearn.pipeline import Pipeline from sklearn.manifold import TSNE from sklearn.metrics import silhouette_score, silhouette_samples, accuracy_score, classification_report from pyod.models.ecod import ECOD from yellowbrick.cluster import KElbowVisualizer import lightgbm as lgb import prince df = pd.read_csv("train.csv", sep=";") df=df.iloc[:, 0:8] pipe = Pipeline([('ordinal', OrdinalEncoder()), ('scaler', PowerTransformer())]) pipe_fit = pipe.fit(df) data = pd.DataFrame(pipe_fit.transform(df), columns=df.columns) data

Результат:

Ошибки

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

Что ж, в чем заключается этот метод? Мы будем использовать библиотеку Python Outlier Detection (PyOD). Эта библиотека фокусируется на выявлении ошибок для разных случаев. Более конкретно мы будем использовать метод ECOD (“эмпирические функции кумулятивного распределения для выявления ошибок”).

Этот метод стремится получить распределение данных и, таким образом, узнать, какие значения имеют более низкую плотность вероятности (ошибки). Посмотрите на Github, если хотите.

from pyod.models.ecod import ECODclf = ECOD()clf.fit(data)outliers = clf.predict(data) data["outliers"] = outliers# Данные без ошибокdata_no_outliers = data[data["outliers"] == 0]data_no_outliers = data_no_outliers.drop(["outliers"], axis = 1)# Данные с ошибкамиdata_with_outliers = data.copy()data_with_outliers = data_with_outliers.drop(["outliers"], axis = 1)print(data_no_outliers.shape) -> (40691, 8)print(data_with_outliers.shape) -> (45211, 8)

Моделирование

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

from yellowbrick.cluster import KElbowVisualizer# Создание модели кластеризации и визуализаторакм = KMeans(init="k-means++", random_state=0, n_init="auto")visualizer = KElbowVisualizer(km, k=(2,10)) visualizer.fit(data_no_outliers)        # Применение данных визуализаторуvisualizer.show()    

Вывод:

Оценка Elbow для различного числа кластеров (изображение автора).

Мы видим, что начиная с k=5, искажение не меняется резко. Хотя идеальным является то, что поведение начиная с k=5 было бы почти плоским. Это редко бывает, и можно применить другие методы, чтобы быть уверенным в наиболее оптимальном количестве кластеров. Чтобы быть уверенными, мы могли бы выполнить визуализацию Silhoutte. Код следующий:

from sklearn.metrics import davies_bouldin_score, silhouette_score, silhouette_samplesimport matplotlib.cm as cmdef make_Silhouette_plot(X, n_clusters):    plt.xlim([-0.1, 1])    plt.ylim([0, len(X) + (n_clusters + 1) * 10])    clusterer = KMeans(n_clusters=n_clusters, max_iter = 1000, n_init = 10, init = 'k-means++', random_state=10)    cluster_labels = clusterer.fit_predict(X)    silhouette_avg = silhouette_score(X, cluster_labels)    print(        "Для n_clusters =", n_clusters,        "Среднее значение silhouette_score :", silhouette_avg,    )# Вычисление коэффициентов silhouette_score для каждого образца    sample_silhouette_values = silhouette_samples(X, cluster_labels)    y_lower = 10    for i in range(n_clusters):        ith_cluster_silhouette_values = sample_silhouette_values[cluster_labels == i]        ith_cluster_silhouette_values.sort()        size_cluster_i = ith_cluster_silhouette_values.shape[0]        y_upper = y_lower + size_cluster_i        color = cm.nipy_spectral(float(i) / n_clusters)        plt.fill_betweenx(            np.arange(y_lower, y_upper),            0,            ith_cluster_silhouette_values,            facecolor=color,            edgecolor=color,            alpha=0.7,        )        plt.text(-0.05, y_lower + 0.5 * size_cluster_i, str(i))        y_lower = y_upper + 10        plt.title(f"График Silhouette для n_clusters = {n_clusters}", fontsize=26)        plt.xlabel("Значения коэффициента silhouette", fontsize=24)        plt.ylabel("Метки кластера", fontsize=24)        plt.axvline(x=silhouette_avg, color="red", linestyle="--")        plt.yticks([])          plt.xticks([-0.1, 0, 0.2, 0.4, 0.6, 0.8, 1])    range_n_clusters = list(range(2,10))for n_clusters in range_n_clusters:    print(f"N cluster: {n_clusters}")    make_Silhouette_plot(data_no_outliers, n_clusters)       plt.savefig('Silhouette_plot_{}.png'.format(n_clusters))    plt.close()ВЫВОД:"""N cluster: 2Для n_clusters = 2 Среднее значение silhouette_score : 0.1775761520337095N cluster: 3Для n_clusters = 3 Среднее значение silhouette_score : 0.20772622268785523N cluster: 4Для n_clusters = 4 Среднее значение silhouette_score : 0.2038116470937145N cluster: 5Для n_clusters = 5 Среднее значение silhouette_score : 0.20142888327171368N cluster: 6Для n_clusters = 6 Среднее значение silhouette_score : 0.20252892716996912N cluster: 7Для n_clusters = 7 Среднее значение silhouette_score : 0.21185490763840265N cluster: 8Для n_clusters = 8 Среднее значение silhouette_score : 0.20867816457291538N cluster: 9Для n_clusters = 9 Среднее значение silhouette_score : 0.21154289421300868"""

Можно видеть, что наивысший показатель силуэта получается при n_cluster=9, но также верно, что изменение показателя довольно небольшое по сравнению с другими показателями. На данный момент предыдущий результат не предоставляет нам много информации. С другой стороны, предыдущий код создает визуализацию Силуэта, которая дает нам больше информации:

Графическое представление метода силуэта для различного числа кластеров (Изображение от Автора).

Поскольку целью этого сообщения не является хорошее понимание этих представлений, я заключу, что здесь нет очень четкого решения о том, какое число лучше. После просмотра предыдущих представлений мы можем выбрать K=5 или K=6. Это потому, что для разных кластеров их показатель Силуэта выше среднего значения, и нет дисбаланса по размеру кластеров. Кроме того, в некоторых ситуациях отдел маркетинга может быть заинтересован в наименьшем числе кластеров/типов клиентов (это может быть или не быть так).

Наконец, мы можем создать нашу модель Kmeans с K=5.

km = KMeans(n_clusters=5,            init='k-means++',             n_init=10,            max_iter=100,             random_state=42)clusters_predict = km.fit_predict(data_no_outliers)"""clusters_predict -> array([4, 2, 0, ..., 3, 4, 3])np.unique(clusters_predict) -> array([0, 1, 2, 3, 4])"""

Оценка

Способ оценки моделей kmeans немного более открытый, чем для других моделей. Мы можем использовать

  • метрики
  • визуализации
  • интерпретация (Что очень важно для компаний).

В отношении метрик оценки модели, мы можем использовать следующий код:

from sklearn.metrics import silhouette_scorefrom sklearn.metrics import calinski_harabasz_scorefrom sklearn.metrics import davies_bouldin_score"""Индекс Дэвиса-Болдина определяется как средняя мера сходства каждого кластера с его наиболее похожим кластером, где сходство - это отношение внутрикластерного расстояния к межкластерному расстоянию.Минимальное значение индекса DB составляет 0, тогда как меньшее значение (ближе к 0) представляет собой лучшую модель, которая создает лучшие кластеры."""print(f"Оценка Дэвиса-Болдина: {davies_bouldin_score(data_no_outliers,clusters_predict)}")"""Индекс Calinski-Харабаза -> Критерий отношения дисперсий.Индекс Calinski-Харабаза определяется как отношение суммы межкластерной дисперсии к внутрикластерной дисперсии.Чем выше индекс, тем более отделимыми являются кластеры."""print(f"Оценка Calinski: {calinski_harabasz_score(data_no_outliers,clusters_predict)}")"""Показатель силуэта - это метрика, используемая для оценки соответствия алгоритма кластеризации, но также может использоваться как метод для определения оптимального значения k (см. здесь для более подробной информации). Его значение варьируется от -1 до 1. Значение 0 указывает на перекрывание кластеров и неправильность данных или значения к.1 - идеальное значение и указывает на то, что кластеры очень плотные и хорошо разделены."""print(f"Оценка силуэта: {silhouette_score(data_no_outliers,clusters_predict)}")Вывод:"""Оценка Дэвиса-Болдина: 1.5480952939773156Оценка Calinski: 7646.959165727562Оценка силуэта: 0.2013600389183821"""

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

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

Для визуализаций мы можем использовать метод снижения размерности, PCA. Для этого мы собираемся использовать библиотеку Prince, ориентированную на исследовательский анализ и снижение размерности. Если хотите, вы можете использовать PCA из Sklearn, они идентичны.

Сначала мы рассчитаем главные компоненты в 3D, а затем сделаем их представление. Это две функции, выполняемые предыдущими шагами:

import prince
import plotly.express as px

def get_pca_2d(df, predict):
    pca_2d_object = prince.PCA(
    n_components=2,
    n_iter=3,
    rescale_with_mean=True,
    rescale_with_std=True,
    copy=True,
    check_input=True,
    engine='sklearn',
    random_state=42
    )
    pca_2d_object.fit(df)
    df_pca_2d = pca_2d_object.transform(df)
    df_pca_2d.columns = ["Комп1", "Комп2"]
    df_pca_2d["кластер"] = predict
    return pca_2d_object, df_pca_2d

def get_pca_3d(df, predict):
    pca_3d_object = prince.PCA(
    n_components=3,
    n_iter=3,
    rescale_with_mean=True,
    rescale_with_std=True,
    copy=True,
    check_input=True,
    engine='sklearn',
    random_state=42
    )
    pca_3d_object.fit(df)
    df_pca_3d = pca_3d_object.transform(df)
    df_pca_3d.columns = ["Комп1", "Комп2", "Комп3"]
    df_pca_3d["кластер"] = predict
    return pca_3d_object, df_pca_3d

def plot_pca_3d(df, title = "Пространство PCA", opacity=0.8, width_line = 0.1):
    df = df.astype({"кластер": "object"})
    df = df.sort_values("кластер")
    fig = px.scatter_3d(          
          df,           
          x='Комп1',           
          y='Комп2',           
          z='Комп3',          
          color='кластер',         
          template="plotly",                    
          color_discrete_sequence=px.colors.qualitative.Vivid,          
          title=title
    ).update_traces(              
          marker={
              "size": 4,
              "opacity": opacity,
              "line": {
                  "width": width_line,
                  "color": "black",
              }
          }
    ).update_layout(                  
          width = 800,                   
          height = 800,                   
          autosize = True,                   
          showlegend = True,                  
          legend=dict(title_font_family="Times New Roman",
                      font=dict(size= 20)),
          scene = dict(xaxis=dict(title = 'Комп1', titlefont_color = 'black'),
                      yaxis=dict(title = 'Комп2', titlefont_color = 'black'),
                      zaxis=dict(title = 'Комп3', titlefont_color = 'black')),
          font = dict(family = "Gilroy", color  = 'black', size = 15)
    )
    
    fig.show()

Не беспокойтесь слишком много об этих функциях, используйте их следующим образом:

pca_3d_object, df_pca_3d = pca_plot_3d(data_no_outliers, clusters_predict)
plot_pca_3d(df_pca_3d, title = "Пространство PCA", opacity=1, width_line = 0.1)
print("Вариабельность:", pca_3d_object.eigenvalues_summary)

Вывод:

Пространство PCA и кластеры, созданные моделью (Изображение автора).

Видно, что кластеры практически не разделены между собой и нет четкого разделения. Это соответствует информации, предоставленной метриками.

Что важно и что очень мало кто учитывает, это PCA и вариабельность собственных векторов.

Допустим, что каждое поле содержит определенное количество информации, и это добавляет свою долю информации. Если накопленная сумма 3 основных компонентов составляет около 80% вариабельности, мы можем сказать, что это приемлемо, получая хорошие результаты в представлениях. Если значение ниже, то мы должны относиться к визуализациям с некоторой осторожностью, так как мы упускаем много информации, содержащейся в других собственных векторах.

Следующий вопрос очевиден: Какова вариабельность выполненной PCA?

Ответ следующий:

Как видно, у нас есть 48.37% вариабельности с первыми 3 компонентами, что недостаточно для делания информированных выводов.

Оказывается, когда выполняется анализ главных компонент (PCA), пространственная структура не сохраняется. К счастью, есть менее известный метод, называемый t-SNE, который позволяет уменьшить размерность и также сохраняет пространственную структуру. Это может помочь нам в визуализации, так как с предыдущим методом у нас не было большого успеха.

Если вы попробуете это на своих компьютерах, имейте в виду, что это требует больше вычислительных ресурсов. По этой причине я сократил свой исходный набор данных и все равно мне понадобилось около 5 минут, чтобы получить результат. Код выглядит следующим образом:

from sklearn.manifold import TSNEsampling_data = data_no_outliers.sample(frac=0.5, replace=True, random_state=1)sampling_clusters = pd.DataFrame(clusters_predict).sample(frac=0.5, replace=True, random_state=1)[0].valuesdf_tsne_3d = TSNE(                  n_components=3,                   learning_rate=500,                   init='random',                   perplexity=200,                   n_iter = 5000).fit_transform(sampling_data)df_tsne_3d = pd.DataFrame(df_tsne_3d, columns=["comp1", "comp2",'comp3'])df_tsne_3d["cluster"] = sampling_clustersplot_pca_3d(df_tsne_3d, title = "PCA Space", opacity=1, width_line = 0.1)

В результате я получил следующее изображение. Оно показывает большую разделенность между кластерами и позволяет делать выводы более ясным способом.

Пространство t-SNE и кластеры, созданные моделью (Изображение автора).

Фактически, мы можем сравнить уменьшение, выполненное с помощью PCA и t-SNE, в 2 измерениях. Улучшение вторым методом явно заметно.

Различные результаты для разных методов уменьшения размерности и определенных моделью кластеров (Изображение автора).

Наконец, давайте немного исследуем, как работает модель, какие характеристики наиболее важные и каковы основные характеристики кластеров.

Чтобы увидеть важность каждой переменной, мы воспользуемся типичным “трюком” в такой ситуации. Мы создадим модель классификации, где “X” является входными данными модели Kmeans, а “y” – кластерами, предсказанными моделью Kmeans.

Выбранная модель – LGBMClassifier. Эта модель довольно мощная и хорошо работает с категориальными и числовыми переменными. После обучения новой модели мы можем использовать библиотеку SHAP для получения значимости каждой из функций в прогнозе. Код выглядит следующим образом:

import lightgbm as lgbimport shap# We create the LGBMClassifier model and train itclf_km = lgb.LGBMClassifier(colsample_by_tree=0.8)clf_km.fit(X=data_no_outliers, y=clusters_predict)#SHAP valuesexplainer_km = shap.TreeExplainer(clf_km)shap_values_km = explainer_km.shap_values(data_no_outliers)shap.summary_plot(shap_values_km, data_no_outliers, plot_type="bar", plot_size=(15, 10))

Вывод:

Важность переменных в модели (Изображение автора).

Видно, что наибольшую предсказательную силу имеет признак ‘housing’. Также видно, что кластер номер 4 (зеленый) в основном отличается по переменной ‘loan’.

Наконец, мы должны проанализировать характеристики кластеров. Эта часть исследования является решающей для бизнеса. Мы получим средние значения (для числовых переменных) и наиболее частое значение (для категориальных переменных) каждой из характеристик набора данных для каждого из кластеров:

df_no_outliers = df[df.outliers == 0]df_no_outliers["cluster"] = clusters_predictdf_no_outliers.groupby('cluster').agg(    {        'job': lambda x: x.value_counts().index[0],        'marital': lambda x: x.value_counts().index[0],        'education': lambda x: x.value_counts().index[0],        'housing': lambda x: x.value_counts().index[0],        'loan': lambda x: x.value_counts().index[0],        'contact': lambda x: x.value_counts().index[0],        'age':'mean',        'balance': 'mean',        'default': lambda x: x.value_counts().index[0],            }).reset_index()

Вывод:

Мы видим, что кластеры с ‘job=blue-collar’ не имеют большой разницы в своих характеристиках. Это не желательно, так как сложно отличить клиентов каждого из кластеров. В случае ‘job=management’ мы получаем лучшую дифференциацию.

После проведения анализа различными способами они сходятся к одному и тому же выводу: “Нам нужно улучшить результаты”.

Метод 2: К-прототип

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

К счастью, вы остались со мной и моим постом. Но, прежде всего, благодаря ZHEXUE HUANG и его статье Расширения алгоритма k-средних для кластеризации больших наборов данных с категориальными значениями, существует алгоритм, который принимает категориальные переменные для кластеризации. Этот алгоритм называется К-прототип. Книжная лавка, предоставляющая его, называется Prince.

Процедура такая же, как и в предыдущем случае. Чтобы не сделать эту статью вечной, перейдем к самому интересному. Но помните, что вы можете получить доступ к Jupyter-ноутбуку здесь.

Обработка данных

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

pipe = Pipeline([('scaler', PowerTransformer())])df_aux = pd.DataFrame(pipe_fit.fit_transform(df_no_outliers[["age", "balance"]] ), columns = ["age", "balance"])df_no_outliers_norm = df_no_outliers.copy()# Заменить столбцы age и balance на обработанные значенияdf_no_outliers_norm = df_no_outliers_norm.drop(["age", "balance"], axis = 1)df_no_outliers_norm["age"] = df_aux["age"].valuesdf_no_outliers_norm["balance"] = df_aux["balance"].valuesdf_no_outliers_norm

Выбросы

Поскольку метод обнаружения выбросов, который я представил (ECOD), принимает только числовые переменные, необходимо выполнить ту же самую трансформацию, что и для метода kmeans. Мы применяем модель обнаружения выбросов, которая даст нам строки для удаления, и, наконец, оставим набор данных, который мы будем использовать в качестве входных данных для модели К-прототипа:

Моделирование

Мы создаем модель, и для этого нам сначала нужно получить оптимальное значение k. Для этого мы используем Метод локтя и следующий код:

  # Выбор оптимального K с помощью метода Elbow from kmodes.kprototypes import KPrototypesfrom plotnine import *import plotninecost = []range_ = range(2, 15)for cluster in range_: kprototype = KPrototypes(n_jobs = -1, n_clusters = cluster, init = 'Huang', random_state = 0) kprototype.fit_predict(df_no_outliers, categorical = categorical_columns_index) cost.append(kprototype.cost_) print('Cluster initiation: {}'.format(cluster)) # Преобразование результатов в фрейм данных и их построение df_cost = pd.DataFrame({'Cluster':range_, 'Cost':cost}) # Визуализация данных plotnine.options.figure_size = (8, 4.8)( ggplot(data = df_cost)+ geom_line(aes(x = 'Cluster', y = 'Cost'))+ geom_point(aes(x = 'Cluster', y = 'Cost'))+ geom_label(aes(x = 'Cluster', y = 'Cost', label = 'Cluster'), size = 10, nudge_y = 1000) + labs(title = 'Оптимальное число кластеров с помощью метода локтя')+ xlab('Количество кластеров k')+ ylab('Cost')+ theme_minimal()) 

Вывод:

Очковый показатель для различных чисел кластеров (изображение автора).

Мы видим, что лучшая опция – K=5 .

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

Итак, мы теперь ясно представляем количество кластеров, нам просто нужно создать модель:

  # Получаем индекс категориальных столбцов numerics = ['int16', 'int32', 'int64', 'float16', 'float32', 'float64']categorical_columns = df_no_outliers_norm.select_dtypes(exclude=numerics).columnsprint(categorical_columns)categorical_columns_index = [df_no_outliers_norm.columns.get_loc(col) for col in categorical_columns]# Создание модели cluster_num = 5kprototype = KPrototypes(n_jobs = -1, n_clusters = cluster_num, init = 'Huang', random_state = 0)kprototype.fit(df_no_outliers_norm, categorical = categorical_columns_index)clusters = kprototype.predict(df_no_outliers , categorical = categorical_columns_index)print(clusters) " -> array([3, 1, 1, ..., 1, 1, 2], dtype=uint16)" 

У нас уже есть наша модель и её прогнозы, нам просто нужно их оценить.

Оценка

Как мы видели ранее, мы можем применять несколько визуализаций, чтобы получить интуитивное представление о том, насколько хороша наша модель. К сожалению, метод PCA и t-SNE не принимают категориальные переменные. Но не волнуйтесь, так как библиотека Prince содержит метод MCA (Множественный корреспонденционный анализ) , и он принимает смешанный набор данных. Фактически, я призываю вас посетить Github этой библиотеки, она имеет несколько полезных методов для различных ситуаций, смотрите следующее изображение:

Различные методы снижения размерности в зависимости от типа случая (Изображение автора и документации Prince).

Что ж, план состоит в применении MCA для снижения размерности и возможности создания графических представлений. Для этого мы используем следующий код:

from prince import MCAdef get_MCA_3d(df, predict):    mca = MCA(n_components=3, n_iter=100, random_state=101)    mca_3d_df = mca.fit_transform(df)    mca_3d_df.columns = ["comp1", "comp2", "comp3"]    mca_3d_df["cluster"] = predict    return mca, mca_3d_dfdef get_MCA_2d(df, predict):    mca = MCA(n_components=2, n_iter=100, random_state=101)    mca_2d_df = mca.fit_transform(df)    mca_2d_df.columns = ["comp1", "comp2"]    mca_2d_df["cluster"] = predict    return mca, mca_2d_df"-------------------------------------------------------------------"mca_3d, mca_3d_df = get_MCA_3d(df_no_outliers_norm, clusters)

Не забудьте, что если вы хотите следовать каждому шагу на 100%, вы можете ознакомиться с Jupyter notebook.

Набор данных с именем mca_3d_df содержит эту информацию:

Давайте построим график, используя снижение размерности, предоставленное методом MCA:

Пространство MCA и кластеры, созданные моделью (Изображение автора)

Ну, это выглядит не очень хорошо… Невозможно отличить кластеры друг от друга. Можно сказать, что модель недостаточно хорошая, верно?

Надеюсь, вы сказали что-то вроде:

«Эй, Дэмьян, не так быстро!! Ты посмотрел на изменчивость первых 3 компонентов, полученных MCA?»

Действительно, мы должны увидеть, достаточна ли изменчивость первых 3 компонентов, чтобы делать выводы. Метод MCA позволяет нам получить эти значения очень простым способом:

mca_3d.eigenvalues_summary

Ага, вот что-то интересное. Из-за наших данных мы получаем в основном нулевую изменчивость.

Другими словами, мы не можем делать четкие выводы из нашей модели при использовании информации, предоставленной снижением размерности, предоставленным MCA.

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

У нас есть последний вариант визуально определить, является ли созданная модель с помощью метода K-Prototype подходящей или нет. Этот путь прост:

  1. Применить PCA к набору данных, где произведена предобработка для преобразования категориальных переменных в числовые.
  2. Получить компоненты PCA
  3. Сделать представление, используя компоненты PCA, такие как оси и цвет точек, чтобы предсказать модель K-Prototype.

Обратите внимание, что компоненты, предоставляемые PCA, будут такими же, как для метода 1: Kmeans, поскольку это та же таблица данных.

Давайте посмотрим, что у нас получилось…

Пространство PCA и созданные моделью кластеры (картинка от автора).

Не выглядит плохо, на самом деле есть определенное сходство с тем, что было получено в Kmeans.

Наконец, получим среднее значение кластеров и важность каждой из переменных:

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

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

В конце концов, можно сказать, что были получены результаты, похожие на результаты Kmeans.

Метод 3: LLM + Kmeans

Это сочетание может быть очень мощным и улучшить полученные результаты. Давайте перейдем к делу!

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

Концепция представления и сходства (картинка от автора).

Этот кодировщик выполняет интеллектуальное преобразование, то есть фразы, содержащие схожее значение, будут иметь более похожий вектор. См. следующее изображение:

Концепция представления и сходства (картинка от автора).

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

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

Таким образом, если мы предоставим входные данные нашей информационно насыщенной модели Kmeans, мы получим лучшие прогнозы. Это идея, которую мы преследуем, и вот ее шаги:

  1. Преобразовать наш исходный набор данных с помощью представления предложений
  2. Создать модель Kmeans
  3. Оценить ее

Ну, первый шаг – закодировать информацию с помощью представления предложений. Цель состоит в том, чтобы взять информацию каждого клиента и объединить ее в текст, который содержит все его характеристики. Эта часть занимает много времени. Вот почему я создал скрипт, который выполняет эту задачу, назовем его embedding_creation.py. В этом скрипте собираются значения, содержащиеся в обучающем наборе данных, и создается новый набор данных, предоставленный представлением. Вот код скрипта:

import pandas as pd # манипуляции с таблицей данныхimport numpy as np # линейная алгебраfrom sentence_transformers import SentenceTransformerdf = pd.read_csv("data/train.csv", sep = ";")# -------------------- Первый шаг --------------------def compile_text(x):    text =  f"""Возраст: {x['age']},                  Нагрузка на жилье: {x['housing']},                 Работа: {x['job']},                 Семейное положение: {x['marital']},                 Образование: {x['education']},                 Дефолт: {x['default']},                 Баланс: {x['balance']},                 Личный заем: {x['loan']},                 Контакт: {x['contact']}            """    return textsentences = df.apply(lambda x: compile_text(x), axis=1).tolist()# -------------------- Второй шаг --------------------model = SentenceTransformer(r"sentence-transformers/paraphrase-MiniLM-L6-v2")output = model.encode(sentences=sentences,         show_progress_bar=True,         normalize_embeddings=True)df_embedding = pd.DataFrame(output)df_embedding

Поскольку этот этап весьма важен, его следует понять. Пойдем по пунктам:

  • Шаг 1: Текст создается для каждой строки, которая содержит полную информацию о клиенте/строке. Мы также сохраняем его в списке Python для последующего использования. Ниже приведено изображение, иллюстрирующее это.
Графическое описание первого шага (Изображение автора).
  • Шаг 2: Здесь происходит вызов трансформера. Для этого мы будем использовать модель, сохраненную в HuggingFace. Эта модель специально обучена выполнять вложение на уровне предложений, в отличие от модели Bert, которая сфокусирована на кодировании токенов и слов. Чтобы вызвать модель, просто необходимо указать адрес репозитория, который в данном случае является «sentence-transformers/paraphrase-MiniLM-L6-v2». Числовой вектор, который будет возвращаться нам для каждого текста, будет нормализован, так как модель Kmeans чувствительна к масштабам входных данных. Созданные векторы имеют длину 384. С их помощью мы создаем фрейм данных с тем же количеством столбцов. См. следующее изображение:
Графическое описание второго шага (Изображение автора),

Наконец, мы получаем фрейм данных из вложения, который будет входным данными для нашей модели Kmeans.

Этот шаг был одним из наиболее интересных и важных, так как мы создали входные данные для модели Kmeans, которую мы создадим.

Процедура создания и оценки аналогична описанной выше. Чтобы не делать статью излишне длинной, показаны только результаты каждой точки. Не волнуйтесь, весь код содержится в jupyter-блокноте с именем embedding, поэтому вы можете воспроизвести результаты самостоятельно.

Кроме того, результат применения вложения предложений был сохранен в CSV-файле. Этот CSV-файл называется embedding_train.csv. В блокноте Jupyter вы сможете увидеть, что мы обращаемся к этому набору данных и создаем нашу модель на его основе.

# Нормальный набор данных
df = pd.read_csv("data/train.csv", sep = ";")
df = df.iloc[:, 0:8]
# Набор данных с вложением
df_embedding = pd.read_csv("data/embedding_train.csv", sep = ",")

Предобработка данных

Можем считать вложение как предварительную обработку.

Выбросы

Мы применяем уже представленный метод обнаружения выбросов, ECOD. Мы создаем набор данных, который не содержит этого типа точек.

df_embedding_no_out.shape  -> (40690, 384)
df_embedding_with_out.shape -> (45211, 384)

Моделирование

Сначала мы должны выяснить, какое оптимальное количество кластеров. Для этого мы используем Метод локтя.

Оценка методом локтя для разных чисел кластеров (Изображение автора).

Просмотрев график, мы выбираем k=5 в качестве количества кластеров.

n_clusters = 5clusters = KMeans(n_clusters=n_clusters, init = "k-means++").fit(df_embedding_no_out)print(clusters.inertia_)clusters_predict = clusters.predict(df_embedding_no_out)

Оценка

Следующим шагом является создание нашей модели Kmeans с k=5. Затем мы можем получить некоторые метрики, подобные этим:

Оценка Дэвида-Боулдина: 1.8095386826791042Оценка Калински: 6419.447089002081Оценка Силуэта: 0.20360442824114108

Учитывая, что значения действительно похожи на полученные в предыдущем случае, можно увидеть, что. Давайте изучим полученные результаты с помощью анализа PCA:

Пространство PCA и кластеры, созданные моделью (изображение от автора).

Можно заметить, что кластеры гораздо лучше отличимы, чем с помощью традиционного метода. Это хорошая новость. Необходимо помнить, что важно учесть изменчивость, содержащуюся в первых 3 компонентах нашего анализа PCA. Из опыта могу сказать, что когда она составляет около 50% (3D PCA), можно сделать достаточно ясные выводы.

Пространство PCA и кластеры, созданные моделью. Показана также изменчивость первых 3 компонент PCA (изображение от автора).

Следовательно, видно, что суммарная изменчивость первых 3 компонент составляет 40,44%, что является приемлемым, но не идеальным.

Для визуальной оценки компактности кластеров я изменяю непрозрачность точек в трехмерном представлении. При этом, когда точки сгруппированы в определенном пространстве, можно наблюдать черное пятно. Чтобы понять, о чем я говорю, я показываю следующую gif-анимацию:

plot_pca_3d(df_pca_3d, title = "Пространство PCA", opacity=0.2, width_line = 0.1)
Пространство PCA и кластеры, созданные моделью (изображение от автора).

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

Тем не менее, можно заметить, что различные кластеры не могут быть хорошо различимы (например, кластеры 1 и 3). По этой причине мы проводим анализ t-SNE, который, как мы помним, является методом снижения размерности, но также сохраняет пространственную структуру.

Пространство t-SNE и кластеры, созданные моделью (изображение от автора).

Заметно улучшение. Кластеры не перекрываются, и между точками есть четкое различие. Улучшение, полученное при использовании второго метода снижения размерности, заметно. Давайте посмотрим на сравнение в 2D:

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

Опять же, можно видеть, что кластеры в t-SNE разделены и более отличаются друг от друга, чем в случае с PCA. Кроме того, разница между этими двумя методами по качеству меньше, чем при использовании традиционного метода Kmeans.

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

Важность переменных в модели (изображение автора).

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

Модель Kmeans + Embedding является более оптимальной, так как она требует меньше переменных для хороших прогнозов. Хорошие новости!

Мы заканчиваем самой информативной и важной частью.

Менеджеры и бизнес не интересуются PCA, t-SNE или Embedding. Они хотят знать, каковы основные характеристики их клиентов.

Для этого мы создаем таблицу с информацией о преобладающих профилях, которые мы можем найти в каждом из кластеров:

Происходит интересное: кластеры, где самая частая должность – “управление”, имеют номер 3. В них мы находим очень особое поведение, где холостые менеджеры моложе, женатые старше, а разведенные – еще старше. С другой стороны, баланс поведения отличается: у холостых людей средний баланс выше, чем у разведенных, а у женатых он еще выше. Все сказанное можно описать следующим образом:

Различные профили клиентов, определенные моделью (изображение автора).

Это откровение соответствует действительности и социальным аспектам. Оно также открывает очень конкретные профили клиентов. В этом вся магия науки о данных.

Заключение

Вывод очевиден:

(Изображение автора)

Вам необходимо иметь различные инструменты, потому что в реальном проекте не все стратегии работают и необходимо иметь ресурсы для добавления ценности. Четко видно, что модель, созданная при помощи LLM, выделяется.