Dominare la segmentazione dei clienti con LLM

Dominare la segmentazione dei clienti con LLM Strategie vincenti per il successo commerciale

Sblocca tecniche avanzate di segmentazione dei clienti utilizzando LLMs e migliorare i tuoi modelli di clustering con tecniche avanzate

Indice dei contenuti

· Introduzione· Dati· Metodo 1: K-means· Metodo 2: K-Prototypes· Metodo 3: LLM + K-means· Conclusione

Introduzione

Un progetto di segmentazione dei clienti può essere affrontato in diversi modi. In questo articolo ti insegnerò tecniche avanzate, non solo per definire i cluster, ma anche per analizzare i risultati. Questo post è destinato a quei data scientist che vogliono avere diverse opzioni per affrontare i problemi di clustering e avvicinarsi al ruolo di senior DS.

Cosa vedremo in questo articolo?

Vedremo 3 metodi per affrontare questo tipo di progetto:

  • K-means
  • K-Prototypes
  • LLM + K-means

Come anteprima, mostrerò il seguente confronto della rappresentazione 2D (PCA) dei diversi modelli creati:

Confronto grafico dei tre metodi (Immagine di Author).

<p.imparerai anche="" come:

  • PCA
  • t-SNE
  • MCA

<p.alcuni dei="" i="" p="" risultati="" seguenti:

Confronto grafico dei tre metodi di riduzione della dimensionalità (Immagine di Author).

Puoi trovare il progetto con i notebook qui. E puoi anche dare un’occhiata al mio github:

damiangilgonzalez1995 – Panoramica

Appassionato di dati, sono passato dalla fisica alla scienza dei dati. Ho lavorato presso Telefonica, HP e ora sono CTO presso…

github.com

Una precisazione molto importante è che questo non è un progetto completo. Questo perché abbiamo saltato una delle parti più importanti in questo tipo di progetto: La fase di analisi esplorativa dei dati (EDA) o la selezione delle variabili.

Dati

I dati originali utilizzati in questo progetto provengono da un pubblico Kaggle: Banking Dataset — Marketing Targets. Ogni riga di questo set di dati contiene informazioni sui clienti di un’azienda. Alcuni campi sono numerici e altri sono categorici, e vedremo che ciò espande le possibili modalità di approccio al problema.

<p.ci 8="" alle="" colonne.="" così:

Vediamo una breve descrizione delle colonne del nostro dataset:

  • età (numerico)
  • lavoro: tipo di lavoro (categorico: “admin.”, “sconosciuto”, “disoccupato”, “management”, “colf”, “imprenditore”, “studente”, “operaio”, “libero professionista”, “pensionato”, “tecnico”, “servizi”)
  • stato matrimoniale: stato civile (categorico: “sposato”, “divorziato”, “singolo”; nota: “divorziato” significa divorziato o vedovo)
  • istruzione (categorico: “sconosciuto”, “secondario”, “primario”, “terziario”)
  • mancanza credito: ha debiti in sospeso? (binario: “sì”, “no”)
  • saldo: saldo medio annuale, in euro (numerico)
  • alloggio: ha un prestito per l’alloggio? (binario: “sì”, “no”)
  • prestito: ha un prestito personale? (binario: “sì”, “no”)

Per il progetto, ho utilizzato il dataset di addestramento di Kaggle. Nel repository del progetto, è possibile trovare la cartella “data” in cui è archiviato un file compresso del dataset utilizzato nel progetto. Inoltre, troverete due file CSV all’interno del file compresso. Uno è il dataset di addestramento fornito da Kaggle (train.csv), l’altro è il dataset dopo aver eseguito un embedding (embedding_train.csv), che spiegheremo meglio in seguito.

Per chiarire ulteriormente come è strutturato il progetto, viene mostrato l’albero del progetto:

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

Metodo 1: Kmeans

Questo è il metodo più comune e quello che sicuramente conosci. Comunque, lo studieremo perché mostrerò tecniche di analisi avanzate in questi casi. Il notebook Jupyter in cui troverai la procedura completa si chiama kmeans.ipynb

Preelaborato

Viene effettuata una preelaborazione delle variabili:

  1. Consiste nella conversione delle variabili categoriche in numeriche. Possiamo applicare un Onehot Encoder (la soluzione più comune), ma in questo caso applicheremo un Ordinal Encoder.
  2. Cerchiamo di garantire che le variabili numeriche abbiano una distribuzione gaussiana. Per farlo applicheremo un PowerTransformer.

Vediamo come appare nel codice.

import pandas as pd # manipolazione dei dataframeimport numpy as np # algebra lineare# visualizzazione dei datiimport matplotlib.pyplot as pltimport matplotlib.cm as cmimport plotly.express as pximport plotly.graph_objects as goimport seaborn as snsimport shap# sklearn from sklearn.cluster import KMeansfrom sklearn.preprocessing import PowerTransformer, OrdinalEncoderfrom sklearn.pipeline import Pipelinefrom sklearn.manifold import TSNEfrom sklearn.metrics import silhouette_score, silhouette_samples, accuracy_score, classification_reportfrom pyod.models.ecod import ECODfrom yellowbrick.cluster import KElbowVisualizerimport lightgbm as lgbimport princedf = 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

Output:

Outliers

È fondamentale che ci siano il meno possibile outliers nei nostri dati poiché Kmeans è molto sensibile a questo. Possiamo applicare il metodo tipico di scelta degli outliers utilizzando lo z-score, ma in questo articolo ti mostrerò un metodo molto più avanzato e interessante.

Bene, qual è questo metodo? Beh, useremo la libreria di rilevamento degli outliers Python (PyOD). Questa libreria si concentra nel rilevare gli outliers per diversi casi. Più specificamente, utilizzeremo il metodo ECOD (“funzioni empiriche di distribuzione cumulativa per la rilevazione degli outliers”).

Questo metodo cerca di ottenere la distribuzione dei dati e quindi di sapere quali sono i valori in cui la densità di probabilità è più bassa (outliers). Dai un’occhiata al Github se vuoi.

from pyod.models.ecod import ECODclf = ECOD()clf.fit(data)outliers = clf.predict(data) data["outliers"] = outliers# Dati senza outliersdata_no_outliers = data[data["outliers"] == 0]data_no_outliers = data_no_outliers.drop(["outliers"], axis = 1)# Dati con outliersdata_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)

Modellazione

Uno svantaggio nell’utilizzare l’algoritmo Kmeans è che devi scegliere il numero di cluster che vuoi utilizzare. In questo caso, per ottenere tali dati, utilizzeremo il Metodo del Gomito. Consiste nel calcolare la distorsione che esiste tra i punti di un cluster e il suo centroide. L’obiettivo è chiaro, ottenere la minima distorsione possibile. In questo caso useremo il seguente codice:

from yellowbrick.cluster import KElbowVisualizer# Istanziare il modello di clustering e il visualizzatorekm = KMeans(init="k-means++", random_state=0, n_init="auto")visualizer = KElbowVisualizer(km, k=(2,10)) visualizer.fit(data_no_outliers)        # Adatta i dati al visualizzatorevisualizer.show()

Output:

Punteggio del Gomito per diversi numeri di cluster (Immagine dell'Autore).

Vediamo che da k=5, la distorsione non varia drasticamente. È vero che l’ideale sarebbe che il comportamento a partire da k= 5 sia quasi piatto. Questo raramente accade e possono essere applicati altri metodi per essere sicuri del numero di cluster più ottimale. Per essere sicuri, potremmo eseguire una visualizzazione Silhoutte. Il codice è il seguente:

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(        "Per n_clusters =", n_clusters,        "Il valore medio del coefficiente Silhouette è :", silhouette_avg,    )# Calcola i punteggi Silhouette per ogni campione    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"Plot Silhouette per n_cluster = {n_clusters}", fontsize=26)        plt.xlabel("Valori coefficiente Silhouette", fontsize=24)        plt.ylabel("Etichetta cluster", 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()OUTPUT:"""N. cluster: 2Per n_clusters = 2 Il valore medio del coefficiente Silhouette è : 0.1775761520337095N. cluster: 3Per n_clusters = 3 Il valore medio del coefficiente Silhouette è : 0.20772622268785523N. cluster: 4Per n_clusters = 4 Il valore medio del coefficiente Silhouette è : 0.2038116470937145N. cluster: 5Per n_clusters = 5 Il valore medio del coefficiente Silhouette è : 0.20142888327171368N. cluster: 6Per n_clusters = 6 Il valore medio del coefficiente Silhouette è : 0.20252892716996912N. cluster: 7Per n_clusters = 7 Il valore medio del coefficiente Silhouette è : 0.21185490763840265N. cluster: 8Per n_clusters = 8 Il valore medio del coefficiente Silhouette è : 0.20867816457291538N. cluster: 9Per n_clusters = 9 Il valore medio del coefficiente Silhouette è : 0.21154289421300868"""

Si può vedere che il punteggio di silhouette più alto si ottiene con n_cluster=9, ma è anche vero che la variazione nel punteggio è abbastanza piccola se confrontata con gli altri punteggi. Al momento il risultato precedente non ci fornisce molte informazioni. D’altra parte, il codice precedente crea la visualizzazione della Silhouette, che ci dà maggiori informazioni:

Rappresentazione grafica del metodo della silhouette per diversi numeri di cluster (Immagine dell'Autore).

Poiché capire bene queste rappresentazioni non è l’obiettivo di questo post, concluderò dicendo che sembra non ci sia una decisione molto chiara su quale numero sia il migliore. Dopo aver visualizzato le rappresentazioni precedenti, possiamo scegliere K=5 o K=6. Questo perché per i diversi cluster, il loro punteggio di Silhouette è al di sopra del valore medio e non c’è uno squilibrio nelle dimensioni dei cluster. Inoltre, in alcune situazioni, il dipartimento marketing potrebbe essere interessato ad avere il minor numero di cluster/tipi di clienti (Questo potrebbe essere o non essere il caso).

Infine possiamo creare il nostro modello Kmeans con 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])"""

Valutazione

Il modo di valutare i modelli kmeans è in qualche modo più aperto rispetto ad altri modelli. Possiamo usare

  • metriche
  • visualizzazioni
  • interpretazione (Qualcosa di molto importante per le aziende).

In relazione alle metriche di valutazione del modello, possiamo usare il seguente codice:

from sklearn.metrics import silhouette_scorefrom sklearn.metrics import calinski_harabasz_scorefrom sklearn.metrics import davies_bouldin_score"""L'indice di Davies Bouldin è definito come la misura di similarità media di ogni cluster con il cluster più simile, dove la similarità è il rapporto delle distanze all'interno dei cluster rispetto alle distanze tra i cluster.Il valore minimo dell'indice DB è 0, mentre un valore più piccolo (più vicino a 0) rappresenta un modello migliore che produce cluster migliori."""print(f"Punteggio di Davies bouldin: {davies_bouldin_score(data_no_outliers,clusters_predict)}")"""Indice di Calinski Harabaz -> Criterio del rapporto di varianza.L'indice di Calinski Harabaz è definito come il rapporto tra la somma della dispersione tra i cluster e la dispersione all'interno dei cluster.Più alto è l'indice, più separabili sono i cluster."""print(f"Punteggio di Calinsk: {calinski_harabasz_score(data_no_outliers,clusters_predict)}")"""Il punteggio di Silhouette è una metrica utilizzata per calcolare la bontà di adattamento di un algoritmo di clustering, ma può anche essere utilizzato come metodo per determinare un valore ottimale di k (vedere qui per maggiori informazioni).Il suo valore varia da -1 a 1.Un valore di 0 indica che i cluster si sovrappongono e che sia i dati che il valore di k sono incorretti. 1 è il valore ideale e indica che i cluster sono molto densi e ben separati."""print(f"Punteggio Silhouette: {silhouette_score(data_no_outliers,clusters_predict)}")OUTPUT:"""Punteggio di Davies bouldin: 1.5480952939773156Punteggio di Calinsk: 7646.959165727562Punteggio Silhouette: 0.2013600389183821"""

Per quanto possa essere mostrato, non abbiamo un modello eccessivamente buono. Il punteggio di Davies ci sta dicendo che la distanza tra i cluster è piuttosto piccola.

Questo può essere dovuto a diversi fattori, ma tenete presente che l’energia di un modello è rappresentata dai dati; se i dati non hanno un potere predittivo sufficiente, non ci si può aspettare risultati eccezionali.

Per le visualizzazioni, possiamo utilizzare il metodo per ridurre la dimensionalità, PCA. Per questo useremo la libreria Prince, focalizzata sull’analisi esplorativa e sulla riduzione della dimensionalità. Se preferite, potete utilizzare PCA di Sklearn, sono identici.

Prima calcoleremo i componenti principali in 3D, e poi faremo la rappresentazione. Queste sono le due funzioni eseguite dai passaggi precedenti:

import princeimport plotly.express as pxdef 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 = ["comp1", "comp2"]    df_pca_2d["cluster"] = predict    return pca_2d_object, df_pca_2ddef 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 = ["comp1", "comp2", "comp3"]    df_pca_3d["cluster"] = predict    return pca_3d_object, df_pca_3ddef plot_pca_3d(df, title = "Spazio PCA", opacity=0.8, width_line = 0.1):    df = df.astype({"cluster": "object"})    df = df.sort_values("cluster")    fig = px.scatter_3d(          df,           x='comp1',           y='comp2',           z='comp3',          color='cluster',          template="plotly",                    # symbol = "cluster",                    color_discrete_sequence=px.colors.qualitative.Vivid,          title=title).update_traces(              # mode = 'markers',              marker={                  "size": 4,                  "opacity": opacity,                  # "symbol" : "diamond",                  "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 = 'comp1', titlefont_color = 'black'),                              yaxis=dict(title = 'comp2', titlefont_color = 'black'),                              zaxis=dict(title = 'comp3', titlefont_color = 'black')),                  font = dict(family = "Gilroy", color  = 'black', size = 15))                fig.show()

Non preoccuparti troppo di queste funzioni, usale come segue:

pca_3d_object, df_pca_3d = pca_plot_3d(data_no_outliers, clusters_predict)plot_pca_3d(df_pca_3d, title = "Spazio PCA", opacity=1, width_line = 0.1)print("La variabilità è:", pca_3d_object.eigenvalues_summary)

Output:

Spazio PCA e i cluster creati dal modello (Immagine dell'Autore).

Si può vedere che i cluster hanno quasi nessuna separazione tra loro e non c’è una divisione chiara. Questo è in accordo con le informazioni fornite dalle metriche.

Qualcosa da tenere a mente e che poche persone tengono in considerazione è la PCA e la variabilità dei vettori di autovettori.

Diciamo che ogni campo contiene una certa quantità di informazioni, e questo aggiunge il suo contributo di informazioni. Se la soma accumulata dei 3 componenti principali arriva a circa il 80% di variabilità, possiamo dire che è accettabile, ottenendo buoni risultati nelle rappresentazioni. Se il valore è più basso, dobbiamo prendere le visualizzazioni con cautela poiché ci mancano molte informazioni contenute in altri autovettori.

La prossima domanda è ovvia: Qual è la variabilità della PCA eseguita?

La risposta è la seguente:

Come si può vedere, abbiamo una variabilità del 48,37% con i primi 3 componenti, qualcosa di insufficiente per trarre conclusioni informate.

Risulta che quando viene eseguita un’analisi PCA, la struttura spaziale non viene preservata. Per fortuna esiste un metodo meno conosciuto, chiamato t-SNE, che ci permette di ridurre la dimensionalità e mantenere anche la struttura spaziale. Questo può aiutarci nella visualizzazione, dato che con il metodo precedente non abbiamo avuto molto successo.

Se provate a farlo sui vostri computer, tenete presente che ha un costo computazionale più elevato. Per questo motivo, ho campionato il mio dataset originale e mi sono comunque voluti circa 5 minuti per ottenere il risultato. Il codice è il seguente:

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 = "Spazio PCA", opacity=1, width_line = 0.1)

Come risultato, ho ottenuto l’immagine seguente. Mostra una maggiore separazione tra i cluster e ci permette di trarre conclusioni in modo più chiaro.

Spazio t-SNE e i cluster creati dal modello (Immagine dell'autore).

In realtà, possiamo confrontare la riduzione effettuata da PCA e da t-SNE, in 2 dimensioni. Il miglioramento è evidente utilizzando il secondo metodo.

Risultati diversi per diversi metodi di riduzione della dimensionalità e cluster definiti dal modello (Immagine dell'autore).

Infine, esploriamo un po’ come funziona il modello, quali sono le caratteristiche più importanti e quali sono le principali caratteristiche dei cluster.

Per vedere l’importanza di ciascuna variabile, useremo un “trucco” tipico in questo tipo di situazione. Creeremo un modello di classificazione in cui “X” sono gli input del modello Kmeans e “y” sono i cluster predetti dal modello Kmeans.

Il modello scelto è un LGBMClassifier. Questo modello è molto potente e funziona bene con variabili categoriche e numeriche. Dopo aver addestrato il nuovo modello, utilizzando la libreria SHAP, possiamo ottenere l’importanza di ciascuna delle caratteristiche nella previsione. Il codice è il seguente:

import lightgbm as lgbimport shap# Creiamo il modello LGBMClassifier e lo addestriamoclf_km = lgb.LGBMClassifier(colsample_by_tree=0.8)clf_km.fit(X=data_no_outliers, y=clusters_predict)# Valori SHAPexplainer_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))

Output:

L'importanza delle variabili nel modello (Immagine dell'autore).

È possibile osservare che la caratteristica alloggio ha il più grande potere predittivo. È anche possibile osservare che il cluster numero 4 (verde) è principalmente differenziato dalla variabile prestito.

Infine, è necessario analizzare le caratteristiche dei cluster. Questa parte dello studio è ciò che è decisivo per il business. Per questo motivo otterremo il valore medio (per le variabili numeriche) e il valore più frequente (variabili categoriche) di ciascuna delle caratteristiche dell’insieme di dati per ciascuno dei cluster:

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()

Risultato:

<p+ occupazione=operaio non presentano una grande differenziazione tra le loro caratteristiche. Questo è qualcosa che non è desiderabile poiché è difficile differenziare i clienti di ciascuno dei cluster. Nel caso di occupazione=gestione, otteniamo una migliore differenziazione.

Dopo aver effettuato l’analisi in modi diversi, si giunge alla stessa conclusione: “Dobbiamo migliorare i risultati”.

Metodo 2: K-Prototipo

Se ricordiamo il nostro dataset originale, vediamo che abbiamo variabili categoriche e numeriche. Sfortunatamente, l’algoritmo K-Means fornito da Skelearn non accetta variabili categoriche, costringendo la modifica e l’alterazione drastica del dataset originale.

Fortunatamente, sei con me e il mio post. Ma soprattutto, grazie a ZHEXUE HUANG e al suo articolo Estensioni dell’algoritmo k-Means per il clustering di grandi insiemi di dati con valori categorici, esiste un algoritmo che accetta variabili categoriche per il clustering. Questo algoritmo è chiamato K-Prototipo. Il pacchetto che lo fornisce è Prince.

La procedura è la stessa del caso precedente. Per non rendere questo articolo eterno, andiamo alle parti più interessanti. Ma ricordate che potete accedere al Jupyter notebook qui.

Preelaborato

Perché abbiamo variabili numeriche, dobbiamo apportare determinate modifiche. È sempre consigliabile che tutte le variabili numeriche abbiano scale simili e distribuzioni il più possibile vicine a Gaussiane. Il dataset che useremo per creare i modelli viene creato nel seguente modo:

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()# Sostituisci le colonne di età e saldo con i valori preelaboratidf_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

Outlier

Perché il metodo che ho presentato per il rilevamento degli outlier (ECOD) accetta solo variabili numeriche, è necessario eseguire la stessa trasformazione del metodo K-Means. Applichiamo il modello di rilevamento degli outlier che ci fornirà quali righe eliminare, lasciando infine il dataset che utilizzeremo come input per il modello K-Prototipo:

Modellazione

Creamo il modello e per farlo abbiamo bisogno di ottenere prima il k ottimale. Per fare ciò utilizziamo il metodo del gomito e questo pezzo di codice:

# Scelta del K ottimale utilizzando il metodo del gomito
from kmodes.kprototypes import KPrototypes
from plotnine import *
import plotnine

cost = []
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('Inizializzazione del cluster: {}'.format(cluster))

# Conversione dei risultati in un dataframe e visualizzazione
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 = 'Numero ottimale di cluster con il metodo del gomito')+
    xlab('Numero di Cluster k')+
    ylab('Costo')+
    theme_minimal())

Output:

Punteggio del gomito per diversi numeri di cluster (Immagine dell'autore).

Possiamo vedere che la migliore opzione è K=5.

Attenzione, poiché questo algoritmo richiede un po’ più di tempo rispetto a quelli normalmente utilizzati. Per il grafico precedente sono stati necessari 86 minuti, qualcosa da tenere a mente.

Bene, ora abbiamo chiaro il numero di cluster, dobbiamo solo creare il modello:

# Otteniamo l'indice delle colonne categoriche
numerics = ['int16', 'int32', 'int64', 'float16', 'float32', 'float64']
categorical_columns = df_no_outliers_norm.select_dtypes(exclude=numerics).columns
print(categorical_columns)
categorical_columns_index = [df_no_outliers_norm.columns.get_loc(col) for col in categorical_columns]

# Creiamo il modello
cluster_num = 5
kprototype = 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)

Abbiamo già il nostro modello e le sue previsioni, dobbiamo solo valutarlo.

Valutazione

Come abbiamo visto in precedenza, possiamo applicare diverse visualizzazioni per ottenere un’idea intuitiva di quanto sia buono il nostro modello. Sfortunatamente, il metodo PCA e il t-SNE non ammettono variabili categoriche. Ma non preoccuparti, la libreria Prince contiene il metodo MCA (Multiple correspondence analysis) e accetta un dataset misto. Infatti, ti incoraggio a visitare il Github di questa libreria, ha diversi metodi molto utili per situazioni diverse, guarda l’immagine seguente:

I diversi metodi di riduzione della dimensionalità in base al tipo di caso (Immagine di Autore e Documentazione di Prince).

Bene, il piano è quello di applicare una MCA per ridurre la dimensionalità e poter fare rappresentazioni grafiche. Per questo utilizziamo il seguente codice:

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)

Ricorda che se vuoi seguire ogni passo al 100%, puoi dare un’occhiata al quaderno Jupyter.

Il dataset chiamato mca_3d_df contiene queste informazioni:

Creiamo un grafico utilizzando la riduzione fornita dal metodo MCA:

Spazio MCA e i cluster creati dal modello (Immagine di Autore)

Wow, non sembra molto buono… Non è possibile differenziare i cluster l’uno dall’altro. Possiamo dire quindi che il modello non è abbastanza buono, giusto?

Spero tu abbia detto qualcosa del genere:

“Ehi Damian, non andare così veloce!! Hai guardato la variabilità dei primi 3 componenti forniti dal MCA?”

In effetti, dobbiamo verificare se la variabilità dei primi 3 componenti è sufficiente per poter trarre delle conclusioni. Il metodo MCA ci permette di ottenere questi valori in modo molto semplice:

mca_3d.eigenvalues_summary

Aha, ecco qualcosa di interessante. A causa dei nostri dati otteniamo una variabilità essenzialmente pari a zero.

In altre parole, non possiamo trarre conclusioni chiare dal nostro modello con le informazioni fornite dalla riduzione della dimensionalità fornita da MCA.

Mostrando questi risultati cerco di dare un esempio di ciò che accade nei progetti di dati reali. Non si ottengono sempre buoni risultati, ma un bravo data scientist sa come riconoscere le cause.

Abbiamo un’ultima opzione per determinare visivamente se il modello creato dal metodo K-Prototype è adatto o meno. Questo percorso è semplice:

  1. Applicare la PCA al dataset su cui è stato eseguito il preprocessing per trasformare le variabili categoriche in numeriche.
  2. Ottenere i componenti del PCA
  3. Creare una rappresentazione utilizzando i componenti del PCA come gli assi e il colore dei punti per prevedere il modello K-Prototype.

Note che i componenti forniti dal PCA saranno gli stessi del metodo 1: Kmeans, poiché è lo stesso dataframe.

Vediamo cosa otteniamo…

Spazio PCA e i cluster creati dal modello (Immagine dell'Autore).

Non sembra male, infatti ha una certa somiglianza con quanto ottenuto in Kmeans.

Infine otteniamo il valore medio dei cluster e l’importanza di ciascuna delle variabili:

L'importanza delle variabili nel modello. La tabella rappresenta il valore più frequente di ciascun cluster (Immagine dell'Autore).

Le variabili con il peso maggiore sono quelle numeriche, e si può notare che la limitazione di queste due caratteristiche è quasi sufficiente a differenziare ciascun cluster.

In breve, si può dire che sono stati ottenuti risultati simili a quelli di Kmeans.

Metodo 3: LLM + Kmeans

Questa combinazione può essere piuttosto potente e migliorare i risultati ottenuti. Andiamo al sodo!

Gli LLM non possono comprendere il testo scritto direttamente, è necessario trasformare l’input di questo tipo di modelli. Per fare ciò, viene effettuato un Embedding di Frasi. Consiste nella trasformazione del testo in vettori numerici. L’immagine successiva può chiarire l’idea:

Concetto di embedding e similarità (Immagine dell'Autore).

Questo codice viene eseguito in modo intelligente, ovvero le frasi che contengono un significato simile avranno un vettore più simile. Guarda l’immagine successiva:

Concetto di embedding e similarità (Immagine dell'Autore).

L’embedding di frasi è realizzato tramite transform, algoritmi specializzati in questo tipo di codifica. Tipicamente è possibile scegliere la dimensione del vettore numerico generato da questa codifica. Ed ecco uno dei punti chiave:

Grazie alla grande dimensione del vettore creato dall’embedding, piccole variazioni nei dati possono essere osservate con maggiore precisione.

Pertanto, se forniamo input al nostro modello Kmeans ricco di informazioni, otterremo previsioni migliori. Questa è l’idea che stiamo perseguendo e questi sono i suoi passaggi:

  1. Trasformare il nostro dataset originale tramite l’embedding di frasi/li>
  2. Creare un modello Kmeans/li>
  3. Valutarlo/li>

Bene, il primo passo è codificare le informazioni tramite l’embedding di frasi. L’obiettivo è prendere le informazioni di ciascun cliente e unificarle in un testo che contenga tutte le sue caratteristiche. Questa parte richiede molto tempo di elaborazione. Ecco perché ho creato uno script che facesse questo lavoro, chiamato embedding_creation.py. Questo script raccoglie i valori contenuti nel dataset di addestramento e crea un nuovo dataset fornito dall’embedding. Ecco il codice dello script:

import pandas as pd # manipolazione dataframeimport numpy as np # algebra linearefrom sentence_transformers import SentenceTransformerdf = pd.read_csv("data/train.csv", sep = ";")# -------------------- Primo Passo --------------------def compile_text(x):    text =  f"""Età: {x['age']},                  carico alloggio: {x['housing']},                 Lavoro: {x['job']},                 Stato civile: {x['marital']},                 Educazione: {x['education']},                 Default: {x['default']},                 Bilancio: {x['balance']},                 Prestito personale: {x['loan']},                 contatto: {x['contact']}            """    return textsentences = df.apply(lambda x: compile_text(x), axis=1).tolist()# -------------------- Secondo Passo --------------------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

Poiché è piuttosto importante che questo passaggio venga compreso. Andiamo per punti:

  • Passaggio 1: Il testo viene creato per ogni riga, che contiene le informazioni complete del cliente/riga. Lo memorizziamo anche in una lista python per un utilizzo successivo. Vedere l’immagine seguente che lo esemplifica.
Descrizione grafica del primo passaggio (Immagine dell'autore).
  • Passaggio 2: Qui viene effettuata la chiamata al transformer. Per questo useremo il modello memorizzato in HuggingFace. Questo modello è specificamente addestrato per eseguire l’embedding a livello di frase, a differenza del modello di Bert, che è focalizzato sulla codifica a livello di token e parole. Per chiamare il modello è sufficiente fornire l’indirizzo del repository, che in questo caso è “sentence-transformers/paraphrase-MiniLM-L6-v2”. Il vettore numerico restituito per ogni testo sarà normalizzato, poiché il modello Kmeans è sensibile alle scale degli input. I vettori creati hanno una lunghezza di 384. Con questi creiamo quindi un dataframe con lo stesso numero di colonne. Vedi l’immagine seguente:
Descrizione grafica del secondo passaggio (Immagine dell'autore),

Infine, otteniamo il dataframe dall’embedding, che sarà l’input del nostro modello Kmeans.

Questo passaggio è stato uno dei più interessanti e importanti, poiché abbiamo creato l’input per il modello Kmeans che creeremo.

La procedura di creazione ed valutazione è simile a quella mostrata sopra. Per non rendere il post eccessivamente lungo, verranno mostrati solo i risultati di ogni punto. Non preoccuparti, tutto il codice è contenuto nel jupyter notebook chiamato embedding, quindi puoi riprodurre i risultati da solo.

Inoltre, il dataset risultante dalla applicazione dell’embedding di frase è stato salvato in un file csv. Questo file csv si chiama embedding_train.csv. Nel notebook Jupyter vedrai che accediamo a quel dataset e creiamo il nostro modello basato su di esso.

# Dataset normaledf = pd.read_csv("data/train.csv", sep = ";")df = df.iloc[:, 0:8]# Dataset di Embeddingdf_embedding = pd.read_csv("data/embedding_train.csv", sep = ",")

Preprocessato

Potremmo considerare l’embedding come preprocessing.

Outliers

Applichiamo il metodo già presentato per individuare gli outliers, ECOD. Creiamo un dataset che non contiene questo tipo di punti.

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

Modellazione

Prima dobbiamo scoprire quale è il numero ottimale di cluster. Per questo utilizziamo il metodo Elbow.

Punteggio Elbow per diversi numeri di cluster (Immagine dell'autore).

Dopo aver visualizzato il grafico, scegliamo k=5 come numero di cluster.

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)

Valutazione

La prossima cosa da fare è creare il nostro modello Kmeans con k=5. Successivamente possiamo ottenere alcune metriche come queste:

Punteggio di Davies bouldin: 1.8095386826791042Punteggio di Calinski: 6419.447089002081Punteggio di Silhouette: 0.20360442824114108

Vedendo quindi che i valori sono molto simili a quelli ottenuti nel caso precedente. Studiamo le rappresentazioni ottenute con l’analisi PCA:

Spazio PCA e i cluster creati dal modello (Immagine dell'autore).

Si può vedere che i cluster sono molto meglio differenziati rispetto al metodo tradizionale. Questa è una buona notizia. Ricordiamo che è importante prendere in considerazione la variabilità contenuta nei primi 3 componenti della nostra analisi PCA. Dall’esperienza posso dire che quando si aggira intorno al 50% (PCA 3D) si possono trarre conclusioni più o meno chiare.

Spazio PCA e i cluster creati dal modello. È mostrata anche la variabilità dei primi 3 componenti della PCA (Immagine dell'autore).

Vediamo quindi che è una variabilità cumulativa del 40,44% dei 3 componenti, è accettabile ma non ideale.

Un modo per vedere visivamente quanto sono compatti i cluster è modificare l’opacità dei punti nella rappresentazione 3D. Ciò significa che quando i punti sono agglomerati in uno spazio specifico, si può osservare una macchia nera. Per capire ciò che sto dicendo, mostro il seguente gif:

plot_pca_3d(df_pca_3d, title = "Spazio PCA", opacity=0.2, width_line = 0.1)
Spazio PCA e i cluster creati dal modello (Immagine dell'autore).

Come si può vedere, ci sono diversi punti nello spazio dove i punti dello stesso cluster si raggruppano insieme. Questo indica che sono ben differenziati dagli altri punti e che il modello sa riconoscerli abbastanza bene.

Tuttavia, si può notare che vari cluster non possono essere differenziati bene (Es: cluster 1 e 3). Per questo motivo, effettuiamo un’analisi t-SNE, che ricordiamo essere un metodo per ridurre la dimensionalità ma che mantiene anche la struttura spaziale.

Spazio t-SNE e i cluster creati dal modello (Immagine dell'autore).

Si nota un miglioramento evidente. I cluster non si sovrappongono e c’è una chiara differenziazione tra i punti. L’incremento ottenuto utilizzando il secondo metodo di riduzione della dimensionalità è notevole. Vediamo un confronto in 2D:

Risultati diversi per diversi metodi di riduzione della dimensionalità e cluster definiti dal modello (Immagine dell'autore).

Ancora una volta, si può vedere che i cluster nel t-SNE sono più separati e meglio differenziati rispetto alla PCA. Inoltre, la differenza tra i due metodi in termini di qualità è minore rispetto all’utilizzo del tradizionale metodo Kmeans.

Per capire su quali variabili si basa il nostro modello Kmeans, facciamo la stessa operazione di prima: creiamo un modello di classificazione (LGBMClassifier) e analizziamo l’importanza delle caratteristiche.

L'importanza delle variabili nel modello (Immagine dell'autore).

Vediamo quindi che questo modello si basa soprattutto sulle variabili “marital” e “job”. D’altra parte, vediamo che ci sono variabili che non forniscono molte informazioni. In un caso reale, dovrebbe essere creata una nuova versione del modello senza queste variabili con poche informazioni.

Il modello Kmeans + Embedding è più ottimale perché ha bisogno di meno variabili per poter fare buone previsioni. Buone notizie!

Concludiamo con la parte più rivelatrice e importante.

I manager e l’azienda non sono interessati a PCA, t-SNE o embedding. Ciò che vogliono è poter sapere quali sono le principali caratteristiche, in questo caso, dei loro clienti.

Per fare ciò, creiamo una tabella con informazioni sui profili dominanti che possiamo trovare in ciascuno dei cluster:

Accade qualcosa di molto curioso: i cluster in cui la posizione più frequente è quella di “management” sono 3. In essi troviamo un comportamento molto particolare in cui i manager single sono più giovani, quelli sposati sono più anziani e i divorziati sono quelli più anziani. D’altra parte, il bilancio si comporta in modo diverso, le persone single hanno un saldo medio più elevato rispetto alle persone divorziate e le persone sposate hanno un saldo medio più elevato. Quanto detto può essere riassunto nella seguente immagine:

Diversi profili di clienti definiti dal modello (Immagine dell'autore).

Questa rivelazione è in linea con la realtà e gli aspetti sociali. Rivela anche profili molto specifici dei clienti. Questa è la magia della scienza dei dati.

Conclusione

La conclusione è chiara:

(Immagine dell'autore)

È necessario avere strumenti diversi perché in un progetto reale, non tutte le strategie funzionano e è necessario avere risorse per aggiungere valore. È chiaramente visibile che il modello creato con l’aiuto degli LMM si distingue.