Scoprire i segmenti più insoliti nei dati

Identificare i segmenti più insoliti nei dati

Come trovare i segmenti su cui focalizzarsi utilizzando il senso comune e il machine learning

Foto di Klara Kulikova su Unsplash

Gli analisti spesso hanno il compito di trovare i segmenti “interessanti” – i segmenti su cui potremmo concentrare i nostri sforzi per ottenere il massimo impatto potenziale. Ad esempio, potrebbe essere interessante determinare quali segmenti di clienti hanno l’effetto più significativo sulla churn. Oppure potresti cercare di capire quali tipi di ordini influenzano il carico di lavoro del supporto clienti e il fatturato dell’azienda.

Certo, potremmo guardare i grafici per trovare queste caratteristiche eccezionali. Ma potrebbe richiedere molto tempo perché di solito monitoriamo decine o addirittura centinaia di caratteristiche dei clienti. Inoltre, dobbiamo osservare le combinazioni di diversi fattori che possono portare a una vera e propria esplosione combinatoria. Con compiti del genere, un framework sarebbe davvero utile perché potrebbe risparmiarti ore di analisi.

In questo articolo, vorrei condividere con te due approcci per trovare le fette di dati più eccezionali:

  • basato sul senso comune e sulla matematica di base,
  • basato sul machine learning – il nostro team di data science di Wise ha reso open source una libreria Wise Pizza che ti fornisce risposte in tre righe di codice.

Esempio: Churn per i clienti delle banche

Puoi trovare il codice completo per questo esempio su GitHub.

Useremo i dati sulla churn dei clienti delle banche come esempio. Questo dataset può essere trovato su Kaggle con licenza CC0: Public Domain.

Cercheremo di trovare i segmenti con l’impatto più significativo sulla churn utilizzando approcci diversi: grafici, senso comune e machine learning. Ma iniziamo con la pre-elaborazione dei dati.

Il dataset elenca i clienti e le loro caratteristiche: punteggio di credito, paese di residenza, età e genere, quanti soldi i clienti hanno in bilancio, ecc. Inoltre, per ogni cliente, sappiamo se ha churned o no – il parametro exited.

Il nostro obiettivo principale è trovare i segmenti di clienti con il maggior impatto sul numero di clienti churned. Dopo di che, potremmo cercare di capire i problemi specifici di questi gruppi di utenti. Se ci concentriamo sulla risoluzione dei problemi per questi segmenti, avremo l’effetto più significativo sul numero di clienti churned.

Per semplificare i calcoli e le interpretazioni, definiremo i segmenti come insiemi di filtri, ad esempio, genere = Maschio o genere = Maschio, paese = Regno Unito.

Lavoreremo con caratteristiche discrete, quindi dobbiamo trasformare le metriche continue, come età o bilancio. Per fare ciò, possiamo guardare le distribuzioni e definire intervalli appropriati. Ad esempio, guardiamo l’età.

Grafico dell'autore

Esempio di codice per raggruppare una caratteristica continua

def get_age_group(a):    if a < 25:        return '18 - 25'    if a < 35:        return '25 - 34'    if a < 45:        return '35 - 44'    if a < 55:        return '45 - 54'    if a < 65:        return '55 - 64'    return '65+'raw_df['age_group'] = raw_df.age.map(get_age_group)

Il modo più semplice per trovare segmenti interessanti nei dati è guardare le visualizzazioni. Possiamo esaminare i tassi di churn suddivisi per una o due dimensioni utilizzando grafici a barre o mappe di calore.

Guardiamo la correlazione tra età e churn. I tassi di churn sono bassi per i clienti sotto i 35 anni – meno del 10%. Mentre per i clienti tra i 45 ei 64 anni, la retention è la peggiore – quasi la metà dei clienti ha churned.

Grafico dell'autore

Aggiungiamo un altro parametro (sesso) per cercare relazioni più complesse. Il grafico a barre non sarà in grado di mostrarci le relazioni bidimensionali, quindi passiamo a una mappa di calore.

Le percentuali di churn per le donne sono più alte per tutti i gruppi di età, quindi il sesso è un fattore influente.

Grafico dell'autore

Tali visualizzazioni possono essere molto informative, ma ci sono un paio di problemi con questo approccio:

  • non teniamo conto delle dimensioni dei segmenti,
  • potrebbe richiedere molto tempo guardare tutte le possibili combinazioni di caratteristiche che hai,
  • è difficile visualizzare più di due dimensioni in un solo grafico.

Quindi passiamo a approcci più strutturati che ci aiuteranno a ottenere un elenco prioritario di segmenti interessanti con effetti stimati.

Approccio del buon senso

Ipotesi

Come potremmo calcolare l’impatto potenziale della risoluzione dei problemi per un segmento specifico? Possiamo confrontarlo con uno scenario “ideale” con una percentuale di churn più bassa.

Potresti chiederti come potremmo stimare il benchmark per la percentuale di churn. Ci sono diversi modi per farlo:

  • benchmark dal mercato: puoi cercare le tipiche percentuali di churn per i prodotti nel tuo settore,
  • segmenti ad alte prestazioni nel tuo prodotto: di solito, hai segmenti leggermente migliori (ad esempio, puoi suddividere per paese o piattaforma) e puoi usarli come benchmark,
  • valore medio: l’approccio più conservativo consiste nel guardare il valore medio globale e stimare l’effetto potenziale di raggiungere le percentuali di churn medie per tutti i segmenti.

Giocando in sicurezza, useremo la percentuale di churn media dal nostro dataset come benchmark – 20,37%.

Elenco di tutti i possibili segmenti

Il passo successivo è costruire tutti i possibili segmenti. Il nostro dataset ha dieci dimensioni con 3-6 valori unici per ciascuna. Il numero totale di combinazioni è di circa 1,2 milioni. Sembrerebbe computazionalmente costoso anche se abbiamo solo poche dimensioni e valori diversi per esse. In compiti reali, di solito hai decine di caratteristiche e valori unici.

Sicuramente dobbiamo pensare a ottimizzazioni delle prestazioni. Altrimenti, potremmo dover passare ore ad aspettare i risultati. Ecco alcuni suggerimenti per ridurre i calcoli:

  • Prima di tutto, non è necessario costruire tutte le possibili combinazioni. Sarà ragionevole limitare la profondità a 4-6. La possibilità che il tuo team di prodotto debba concentrarsi su un segmento utente definito da 42 filtri diversi è piuttosto bassa.
  • In secondo luogo, possiamo definire la dimensione dell’effetto che ci interessa. Diciamo che vorremmo aumentare la percentuale di retention di almeno 1 punto percentuale. Significa che non siamo interessati a segmenti con una dimensione inferiore all’1% di tutti gli utenti. Quindi possiamo smettere di suddividere ulteriormente un segmento se la sua dimensione è al di sotto di questa soglia, riducendo il numero di operazioni.
  • Ultimo ma non meno importante, puoi ridurre significativamente la dimensione dei dati e le risorse utilizzate nei calcoli nei dataset della vita reale. Per fare ciò, puoi raggruppare tutte le piccole caratteristiche per ogni dimensione in un gruppo altro. Ad esempio, ci sono centinaia di paesi e la quota degli utenti di ciascun paese segue di solito la legge di Zipf come molte altre relazioni di dati reali. Quindi avrai molti paesi con una dimensione inferiore all’1% di tutti gli utenti. Come abbiamo discusso in precedenza, non siamo interessati a tali piccoli gruppi di utenti e possiamo raggrupparli tutti in un unico segmento paese = altro per semplificare i calcoli.
Grafico dell'autore

Utilizzeremo la ricorsione per costruire tutte le combinazioni di filtri fino a max_depth. Mi piace questo concetto di informatica perché, in molti casi, consente di risolvere problemi complessi in modo elegante. Purtroppo, gli analisti di dati raramente si trovano nella necessità di scrivere codice ricorsivo: posso ricordare solo tre compiti in 10 anni di esperienza nell’analisi dei dati.

L’idea della ricorsione è piuttosto semplice: è quando una funzione si chiama da sola durante l’esecuzione. È utile quando si lavora con gerarchie o grafi. Se desideri saperne di più sulla ricorsione in Python, leggi questo articolo.

Il concetto di alto livello nel nostro caso è il seguente:

  • Iniziamo con l’intero set di dati e nessun filtro.
  • Poi cerchiamo di aggiungere un altro filtro (se la dimensione del segmento è abbastanza grande e non abbiamo raggiunto la profondità massima) e applichiamo la nostra funzione ad esso.
  • Ripeti il passaggio precedente finché le condizioni sono valide.
num_metric = 'uscito'denom_metric = 'totale'max_depth = 4def convert_filters_to_str(f):    lst = []    for k in sorted(f.keys()):        lst.append(str(k) + ' = ' + str(f[k]))            if len(lst) != 0:        return ', '.join(lst)    return ''def raw_deep_dive_segments(tmp_df, filters):    # restituisci il segmento    yield {        'filtri': filters,        'numeratore': tmp_df[num_metric].sum(),        'denominatore': tmp_df[denom_metric].sum()    }        # se non abbiamo raggiunto max_depth allora possiamo andare più in profondità    if len(filters) < max_depth:        for dim in dimensions:            # controlla se questa dimensione è già stata utilizzata            if dim in filters:                continua            # deduplicazione delle possibili combinazioni            if (filters != {}) and (dim < max(filters.keys())):                continua                  for val in tmp_df[dim].unique():                next_tmp_df = tmp_df[tmp_df[dim] == val]                # controlla se la dimensione del segmento è abbastanza grande                if next_tmp_df[denom_metric].sum() < min_segment_size:                    continua                next_filters = filters.copy()                next_filters[dim] = val                                 # esegui la funzione per il segmento successivo                for rec in raw_deep_dive_segments(next_tmp_df, next_filters):                    yield rec# aggregazione di tutti i segmenti per dataframesegments_df = pd.DataFrame(list(raw_deep_dive_segments(df, {})))

Come risultato, otteniamo circa 10K segmenti. Ora possiamo calcolare gli effetti stimati per ciascuno, filtrare i segmenti con effetti negativi e osservare i gruppi di utenti con il maggior impatto potenziale.

churn_base = 0.2037segments_df['quota_abbandono'] = segments_df.abbandono / segments_df.totalsegments_df['riduzione_stima_abbandono'] = (segments_df.quota_abbandono - churn_base) * segments_df.totalsegments_df['riduzione_stima_abbandono'] = segments_df['riduzione_stima_abbandono']\  .map(lambda x: int(round(x)))filt_segments_df = segments_df[segments_df.riduzione_stima_abbandono > 0]\    .sort_values('riduzione_stima_abbandono', ascending = False).set_index('segmento')

Dovrebbe essere un Graal sacro che fornisce tutte le risposte. Ma aspetta, ci sono troppi duplicati e segmenti successivi l’uno all’altro. Potremmo ridurre la duplicazione e mantenere solo i gruppi di utenti più informativi?

Grooming

Diamo un’occhiata a un paio di esempi.

Il tasso di abbandono per il segmento figlio gruppo_eta = 45-54, genere = Maschio è inferiore a gruppo_eta = 45-54. Aggiungere un filtro genere = Maschio non ci avvicina al problema specifico. Quindi possiamo eliminare tali casi.

L’esempio qui sotto mostra la situazione opposta: il tasso di abbandono per il segmento figlio è significativamente più alto e, in più, il segmento figlio include l’80% dei clienti che hanno abbandonato dal nodo genitore. In questo caso, è ragionevole eliminare un segmento gruppo_punteggio_credito = scarso, gruppo_durata = 8+ perché il problema principale è all’interno di un gruppo is_membro_attivo = 0.

Filtriamo tutti quei segmenti non così interessanti.

import statsmodels.stats.proportion# ottenere tutte le coppie genitore-figlio definiti in modo ricorsivodef get_all_ancestors_recursive(filt):    if len(filt) > 1:        for dim in filt:            cfilt = filt.copy()            cfilt.pop(dim)            yield cfilt            for f in get_all_ancestors_recursive(cfilt):                yield f                def get_all_ancestors(filt):    tmp_data = []    for f in get_all_ancestors_recursive(filt):        tmp_data.append(convert_filters_to_str(f))    return list(set(tmp_data))tmp_data = []for f in tqdm.tqdm(filt_segments_df['filters']):    parent_segment = convert_filters_to_str(f)    for af in get_all_ancestors(f):        tmp_data.append(            {                'parent_segment': af,                'ancestor_segment': parent_segment            }        )        full_ancestors_df = pd.DataFrame(tmp_data)# filtrare i nodi figlio in cui il tasso di diserzione è inferiore filt_child_segments = []for parent_segment in tqdm.tqdm(filt_segments_df.index):    for child_segment in full_ancestors_df[full_ancestors_df.parent_segment == parent_segment].ancestor_segment:        if child_segment in filt_child_segments:            continue                churn_diff_ci = statsmodels.stats.proportion.confint_proportions_2indep(            filt_segments_df.loc[parent_segment][num_metric],            filt_segments_df.loc[parent_segment][denom_metric],            filt_segments_df.loc[child_segment][num_metric],            filt_segments_df.loc[child_segment][denom_metric]        )                if churn_diff_ci[0] > -0.00:            filt_child_segments.append(                {                    'parent_segment': parent_segment,                    'child_segment': child_segment                }            )            filt_child_segments_df = pd.DataFrame(filt_child_segments)filt_segments_df = filt_segments_df[~filt_segments_df.index.isin(filt_child_segments_df.child_segment.values)]# filtrare i nodi genitore in cui il tasso di diserzione è inferiore filt_parent_segments = []for child_segment in tqdm.tqdm(filt_segments_df.index):    for parent_segment in full_ancestors_df[full_ancestors_df.ancestor_segment == child_segment].parent_segment:        if parent_segment not in filt_segments_df.index:            continue                    churn_diff_ci = statsmodels.stats.proportion.confint_proportions_2indep(            filt_segments_df.loc[parent_segment][num_metric],            filt_segments_df.loc[parent_segment][denom_metric],            filt_segments_df.loc[child_segment][num_metric],            filt_segments_df.loc[child_segment][denom_metric]        )        child_coverage = filt_segments_df.loc[child_segment][num_metric]/filt_segments_df.loc[parent_segment][num_metric]        if (churn_diff_ci[1] < 0.00) and (child_coverage >= 0.8):            filt_parent_segments.append(                {                    'parent_segment': parent_segment,                    'child_segment': child_segment                }            )              filt_parent_segments_df = pd.DataFrame(filt_parent_segments)filt_segments_df = filt_segments_df[~filt_segments_df.index.isin(filt_parent_segments_df.parent_segment.values)]

Ora abbiamo circa 4.000 segmenti interessanti. Con questo dataset di prova, vediamo poche differenze dopo questa selezione per i primi segmenti. Tuttavia, con dati reali, questi sforzi spesso danno i loro frutti.

Cause principali

L’ultima cosa che possiamo fare per ottenere solo le segmentazioni più significative è mantenere solo i nodi radice dei nostri segmenti. Questi segmenti sono le cause principali e gli altri sono inclusi in essi. Se desideri approfondire una delle cause principali, guarda i nodi figlio.

Per ottenere solo le cause principali, dobbiamo eliminare tutti i segmenti per i quali abbiamo un nodo genitore nella nostra lista finale dei segmenti interessanti.

root_segments_df = filt_segments_df[~filt_segments_df.index.isin(    full_ancestors_df[full_ancestors_df.parent_segment.isin(        filt_segments_df.index)].ancestor_segment    )]

Ecco qua, ora abbiamo una lista di gruppi di utenti su cui concentrarci. Abbiamo solo segmenti unidimensionali in cima poiché ci sono poche relazioni complesse nei dati in cui un paio di caratteristiche spiegano l’effetto completo.

È fondamentale discutere come potremmo interpretare i risultati. Abbiamo ottenuto un elenco di segmenti di clienti con l’impatto stimato. La nostra stima si basa sull’ipotesi che potremmo ridurre il tasso di churn per l’intero segmento per raggiungere il livello di riferimento (nel nostro esempio – il valore medio). Quindi abbiamo stimato l’impatto della risoluzione dei problemi per ogni gruppo di utenti.

Devi tenere presente che questo approccio ti offre solo una visione generale di quali gruppi di utenti concentrarti. Non tiene conto se sia possibile risolvere completamente questi problemi o meno.

Abbiamo scritto parecchio codice per ottenere i risultati. Forse c’è un altro approccio per risolvere questo compito utilizzando data science e machine learning che non richiede tanto sforzo.

È ora di pizza

In realtà, c’è un’altra strada. Il nostro team di data science di Wise ha sviluppato una libreria Wise Pizza che potrebbe trovare i segmenti più intriganti in un battibaleno. È open source con licenza Apache 2.0, quindi puoi usarlo anche per i tuoi compiti.

Se sei interessato a saperne di più sulla libreria Wise Pizza, non perdere la presentazione di Egor al Data Science Festival.

Applicazione di Wise Pizza

La libreria è facile da usare. Devi scrivere solo un paio di righe e specificare le dimensioni e il numero di segmenti desiderati come risultato.

# pip install wise_pizza - per l'installazioneimport wise_pizza# costruzione di un modello sf = wise_pizza.explain_levels(    df=df,    dims=dimensions,    total_name="exited",    size_name="total",    max_depth=4,    min_segments=15,    solver="lasso")# creazione di un graficosf.plot(width=700, height=100, plot_is_static=False)
Grafico dell'autore

Come risultato, abbiamo ottenuto anche un elenco dei segmenti più interessanti e del loro impatto potenziale sul churn del nostro prodotto. I segmenti sono simili a quelli ottenuti utilizzando l’approccio precedente. Tuttavia, le stime di impatto differiscono molto. Per interpretare correttamente i risultati di Wise Pizza e comprendere le differenze, dobbiamo discutere di come funziona in modo più dettagliato.

Come funziona

La libreria si basa su Lasso e LP solver. Se semplifichiamo, la libreria fa qualcosa di simile all’one-hot-encoding, aggiungendo flag per i segmenti (gli stessi che abbiamo calcolato in precedenza) e quindi utilizza la regressione Lasso con il tasso di churn come variabile target.

Come potresti ricordare dal machine learning, la regressione Lasso tende ad avere molti coefficienti nulli, selezionando pochi fattori significativi. Wise Pizza trova il coefficiente alpha appropriato per la regressione Lasso in modo che tu ottenga un numero specificato di segmenti come risultato.

Per rivedere le regolarizzazioni Lasso (L1) e Ridge (L2), puoi consultare l’articolo.

Come interpretare i risultati

Il valore dell’impatto è stimato come risultato della moltiplicazione tra il coefficiente e la dimensione del segmento.

Come puoi vedere, è completamente diverso da quanto stimato in precedenza. L’approccio del buon senso stima l’impatto della risoluzione completa dei problemi per i gruppi di utenti, mentre l’impatto di Wise Pizza mostra effetti incrementali su altri segmenti selezionati.

Il vantaggio di questo approccio è che puoi sommare diversi effetti. Tuttavia, devi essere accurato durante l’interpretazione dei risultati perché l’impatto per ogni segmento dipende dagli altri segmenti selezionati, poiché potrebbero essere correlati. Ad esempio, nel nostro caso, abbiamo tre segmenti correlati:

  • age_group = 45-54
  • num_of_products = 1, age_group = 44–54
  • is_active_member = 1, age_group = 44–54.

L’impatto per age_group = 45–54 comprende gli effetti potenziali per l’intero gruppo di età, mentre altri stimano un impatto aggiuntivo da sotto-gruppi specifici. Tali dipendenze possono portare a differenze significative nei risultati a seconda del parametro min_segments, poiché avrai diversi insiemi di segmenti finali e correlazioni tra di loro.

È fondamentale prestare attenzione all’intero quadro e interpretare correttamente i risultati di Wise Pizza. Altrimenti, potresti trarre conclusioni errate.

Apprezzo questa libreria come uno strumento prezioso per ottenere rapidamente informazioni dai dati e i primi candidati per i segmenti da approfondire. Tuttavia, se devo fare una stima delle opportunità e un’analisi più robusta per condividere l’impatto potenziale del nostro focus con il mio team di prodotto, continuo a utilizzare un approccio basato sul buon senso con un riferimento ragionevole perché è molto più facile da interpretare.

TL;DR

  1. Trovare segmenti interessanti nei tuoi dati è un compito comune per gli analisti (soprattutto nella fase di scoperta). Fortunatamente, non è necessario creare dozzine di grafici per risolvere tali domande. Ci sono framework più completi e facili da usare.
  2. Puoi utilizzare la libreria di intelligenza artificiale Wise Pizza per ottenere rapidamente informazioni sui segmenti con l’impatto medio più significativo (ti consente anche di confrontare due set di dati). Di solito la uso per ottenere la prima lista di dimensioni e segmenti significativi.
  3. L’approccio di intelligenza artificiale può darti una panoramica e una prioritizzazione in un battito di ciglia. Tuttavia, ti consiglio di prestare attenzione all’interpretazione dei risultati e assicurarti che tu e i tuoi stakeholder li comprendiate appieno. Tuttavia, se hai bisogno di fare una stima robusta dell’effetto potenziale sui KPI della risoluzione dei problemi per l’intero gruppo di utenti, vale la pena utilizzare un buon vecchio approccio basato sull’aritmetica.

Ti ringrazio molto per aver letto questo articolo. Spero che sia stato illuminante per te. Se hai domande o commenti, per favore non esitare a lasciarli nella sezione dei commenti.