Analisi della Sopravvivenza e LTV: Dalla Curva di Kaplan-Meier al Modello di Cox

Cerca:

Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors


Come Stimare il Customer Lifetime Value con la Survival Analysis in Python: oltre la formula LTV = ARPU / Churn

Chiunque gestisca un business basato su abbonamenti, entrate ricorrenti o modelli SaaS, prima o poi si scontra con la solita equazione:

[math]LTV \approx \frac{ARPU \times \text{Gross Margin}}{\text{Churn}}[/math]

È la formula che domina ogni dashboard analitica e che viene presentata a ogni riunione di budget per calcolare velocemente quanto possiamo permetterci di spendere nell’acquisizione di un nuovo cliente (CAC).

Il problema è che questa metrica assume un’ipotesi silenziosa, fortissima e quasi sempre falsa: che la probabilità di abbandono di un cliente rimanga perfettamente costante dal primo giorno fino alla fine dei tempi.

Nella realtà dei fatti, il comportamento d’acquisto non funziona affatto in questo modo. Un utente SaaS che supera i delicati primi tre mesi di onboarding ha un rischio di churn nettamente inferiore rispetto a un nuovo iscritto. Chi rimane per un anno, spesso non se ne va più. Sostituire questa complessa dinamica temporale con un unico tasso medio significa commettere due errori strategici opposti: sottostimare pesantemente il valore dei clienti più fedeli e sovrastimare la tenuta dei nuovi arrivati. Se il marketing alloca budget basandosi su un LTV distorto, il risultato è un’emorragia di cassa mascherata da crescita.

In questo articolo vediamo come superare la scorciatoia della formula classica applicando la Survival Analysis in Python con la libreria lifelines. Analizzeremo come gestire rigorosamente i clienti ancora attivi, stimare la curva di retention, valutare l’impatto delle covariate (come il piano di abbonamento) e, infine, estrarre una misura di Customer Lifetime Value fondata sui dati reali.

Pubblicità

1. Perché il singolo tasso di Churn è pericoloso

Supponiamo di avere un ARPU mensile di 50 €, un margine lordo dell’80% (quindi 40 € netti al mese) e un tasso di churn medio del 5%.
La formula tradizionale produce:

[math]LTV \approx \frac{40}{0{,}05} = 800 \, \text{€}[/math]

Questo calcolo implica implicitamente che la vita media del cliente sia [math]\frac{1}{0{,}05} = 20[/math] mesi. Ma cosa succede se, osservando i dati grezzi, il tasso di abbandono reale è del 15% nel primo mese, del 10% nel secondo, e scende al 2% dal quarto mese in poi?

La media “schiaccia” la varianza temporale. Un manager che guarda solo il 5% di churn non sa se sta perdendo nuovi clienti (problema di onboarding o marketing fuorviante) o clienti storici (problema di obsolescenza del prodotto). La domanda corretta da porsi analiticamente non è “qual è il tasso di churn?”, ma:

“Qual è la probabilità che il cliente sia ancora attivo dopo [math]t[/math] mesi?”

Questo è l’esatto dominio della Survival Analysis.


2. Survival Analysis e il concetto di Censura a Destra

La Survival Analysis è una branca della statistica originariamente sviluppata per l’ambito medico (per calcolare i tempi di sopravvivenza a una patologia), ma i cui concetti si sposano perfettamente con le metriche di retention aziendale.

Ogni riga del nostro dataset deve contenere due informazioni cruciali:

  1. Tenure ([math]T[/math]): Il tempo (in mesi o giorni) durante il quale il cliente è stato osservato.
  2. Evento (Event): Una variabile binaria che indica se il “decesso” (il churn) è stato effettivamente osservato (1) oppure no (0).

L’assenza dell’evento (0) prende il nome di Right Censoring (Censura a destra).

Attenzione: Censura [math]\neq[/math] Dato Mancante

Questo è l’errore concettuale più diffuso. Se osserviamo un cliente per 24 mesi e ad oggi è ancora iscritto, il suo dato non è “mancante”. Noi possediamo un’informazione preziosissima: sappiamo con certezza che [math]T > 24[/math].

Se calcolassimo la vita media basandoci solo sui clienti che hanno disdetto (ignorando i censurati), sottostimeremmo la retention. Se trattassimo i clienti censurati come se avessero una vita esatta di 24 mesi, staremmo alterando la realtà perché potrebbero rimanere attivi per altri 5 anni. Il metodo di Kaplan-Meier utilizza i clienti censurati per calcolare la probabilità di sopravvivenza fino al momento in cui vengono censurati, neutralizzando le distorsioni.


3. Introduzione a lifelines e Setup dei Dati

La libreria lifelines è lo standard in Python per la Survival Analysis. Offre un’API pulita, simile a scikit-learn, che si integra nativamente con i DataFrame di pandas.

Costruiamo un dataset di 14 clienti per comprendere i calcoli esatti:

import pandas as pd
import numpy as np
from lifelines import KaplanMeierFitter

# Dataset di 14 clienti
df = pd.DataFrame({
    "customer_id": range(1, 15),
    "tenure": [2, 4, 6, 7, 8, 10, 12, 13, 15, 18, 20, 24, 30, 36],
    "event":  [1, 1, 1, 1, 1, 1,  1,  0,  0,  1,  0,  0,  0,  0]
})

In questo set abbiamo 8 abbandoni osservati e 6 clienti ancora attivi.


4. Kaplan-Meier: Stimare la probabilità di sopravvivenza

Il modello di partenza è lo stimatore non parametrico di Kaplan-Meier. La funzione di sopravvivenza [math]S(t) = P(T > t)[/math] stima empiricamente la probabilità di superare il tempo [math]t[/math].

kmf = KaplanMeierFitter()
kmf.fit(durations=df["tenure"], event_observed=df["event"])

# Visualizziamo le stime a intervalli specifici
for months in [6, 12, 18, 24]:
    retention = float(kmf.predict(months))
    print(f"Retention a {months} mesi: {retention:.2%}")

Analisi del risultato matematico:

  • A 12 mesi si sono verificati 7 churn su 14 clienti: la probabilità di sopravvivenza scende esattamente al 50,00%.
  • Tra il mese 13 e il 15 abbiamo due clienti censurati. Uscendo dal perimetro di osservazione senza fare churn, riducono la base dei “clienti a rischio” da 7 a 5, ma non abbassano la curva di sopravvivenza, che resta al 50%.
  • Al 18° mese, l’ultimo churn su un pool di 5 clienti a rischio fa scendere la curva di un ulteriore 20% relativo (da 0,50 a 0,40).
  • Dal mese 18 al mese 36 osserviamo solo censura. La curva genera un plateau al 40,00%.

Questo plateau riflette una verità manageriale: il business ha uno “zoccolo duro” del 40% di clienti altamente fidelizzati che smettono di fare churn.

La mediana di sopravvivenza, ovvero il punto in cui [math]S(t)[/math] scende al 50%, è ottenibile tramite:

print(kmf.median_survival_time_)
# Output: 18.0

Figura 1: La curva di sopravvivenza scende a gradini (eventi di churn). I marcatori rossi a forma di “x” indicano i clienti censurati: sappiamo che sono sopravvissuti fino a quel mese, dopo il quale l’osservazione si interrompe. La linea tratteggiata mostra che a 18 mesi la probabilità di permanenza scende al 50%.


5. Dalla curva di sopravvivenza alla RMST

L’area sotto la curva di sopravvivenza rappresenta teoricamente la vita media attesa del cliente, misurabile come l’integrale da zero a infinito: [math]\int_0^\infty S(t) \, dt[/math].
Ma non avendo osservato i dati fino a infinito, calcolare l’area totale è un azzardo matematico.

Entra in gioco la Restricted Mean Survival Time (RMST), che limita l’integrale a un orizzonte di business preciso [math]\tau[/math] (es. 24 mesi):

[math]RMST(\tau) = \int_0^\tau S(t) \, dt[/math]

from lifelines.utils import restricted_mean_survival_time

rmst_24 = restricted_mean_survival_time(kmf, t=24)
print(f"RMST a 24 mesi: {rmst_24:.2f} mesi")
# Output: RMST a 24 mesi: 14.90 mesi

Il calcolo esatto dell’area sotto i gradini della curva restituisce 14,90 mesi. Questo è un KPI manageriale potentissimo: ci dice che nei primi 2 anni, ci aspettiamo in media quasi 15 mesi di permanenza effettiva pagata per cliente. Non stiamo sovrastimando ignorando i churn iniziali, e non stiamo sottostimando trattando i clienti censurati come se fossero disdette.

Forse potrebbe interessarti anche:  Algoritmi Greedy: Una Scelta Ottimale (a Volte)

Figura 2: Evoluzione temporale dell’LTV calcolata come integrale continuo della probabilità di sopravvivenza per il margine mensile [math]LTV(\tau) = M \int_0^\tau S(t) \, dt[/math]. Osservando l’appiattimento progressivo della pendenza, il management può visualizzare il momento esatto in cui i ritorni marginali decrescono e definire con precisione chirurgica il target per il costo di acquisizione (CAC).

 


6. Il Modello Economico del LTV a Orizzonte Definito

Fino a qui abbiamo gestito le tempistiche (Survival). Ora dobbiamo tramutarle in euro (Economics).
Se il cliente garantisce un margine lordo costante [math]M[/math], l’LTV all’orizzonte [math]\tau[/math] non è nient’altro che il margine moltiplicato per l’area sotto la curva:

[math]LTV(\tau) = M \times RMST(\tau)[/math]

Assumendo un margine mensile di 40 €:

monthly_margin = 40
ltv_24 = rmst_24 * monthly_margin
print(f"LTV a 24 mesi: {ltv_24:.2f} €")
# Output: LTV a 24 mesi: 596.00 €

Il dato parla chiaro: 596 € contro gli 800 € calcolati all’inizio dalla media naive. Aver utilizzato il rischio dinamico e l’orizzonte corretto ha salvato l’azienda dal sovrastimare il budget di acquisizione di oltre 200 € per utente.

Integrare l’Attualizzazione (Discounting)

Il denaro oggi vale più del denaro domani.

Se il margine è funzione del tempo [math]M(t)[/math] (a causa di upsell) e aggiungiamo un tasso di sconto continuo [math]r[/math], l’integrale diventa il nostro modello definitivo:

[math]LTV(\tau) = \int_0^\tau S(t) M(t) e^{-rt} \, dt[/math]

Questo approccio permette simulazioni finanziare estremamente raffinate, trasformando l’LTV da un “numero magico” a un modello di capital budgeting.


Poiché la funzione di sopravvivenza [math]S(t)[/math] stimata da Kaplan-Meier è una curva a gradini (discreta) e il margine [math]M(t)[/math] può essere una qualsiasi funzione complessa non lineare, la soluzione più robusta ed elegante in Python è utilizzare l’integrazione numerica (regola dei trapezi).

Questo ci permette di valutare il valore esatto dell’integrale senza dover risolvere equazioni analitiche complesse.

Il Modello in Python

Di seguito lo snippet di codice. Ho strutturato la funzione in modo che accetti un margine costante o una funzione dinamica personalizzata (per simulare, ad esempio, l’upselling).

import numpy as np
import pandas as pd
from lifelines import KaplanMeierFitter

def calculate_discounted_ltv(
    kmf: KaplanMeierFitter, 
    horizon: float, 
    margin_func, 
    annual_discount_rate: float = 0.10,
    num_points: int = 1000
) -> float:
    """
    Calcola l'LTV attualizzato integrando S(t) * M(t) * e^(-rt) dt.
    
    Parameters
    ----------
    kmf : KaplanMeierFitter
        Modello lifelines già addestrato (is_fitted=True).
    horizon : float
        Orizzonte temporale in mesi (tau).
    margin_func : float o callable
        Se float, il margine mensile è costante.
        Se callable, è una funzione che accetta il tempo t e restituisce il margine M(t).
    annual_discount_rate : float
        Tasso di sconto annuale (es. WACC aziendale). Default 10% (0.10).
    num_points : int
        Risoluzione della griglia temporale per l'integrazione numerica.
    """
    
    # 1. Calcolo del tasso di sconto continuo mensile (r)
    # Partiamo dal tasso annuale (es. 10%) e lo convertiamo in continuo mensile
    r_monthly = np.log(1 + annual_discount_rate) / 12
    
    # 2. Creazione della griglia temporale (da 0 a tau)
    # Usiamo una griglia fitta per un'integrazione numerica ad alta precisione
    t_grid = np.linspace(0, horizon, num_points)
    
    # 3. Valutazione di S(t)
    # kmf.predict gestisce automaticamente la natura a gradini della curva
    S_t = kmf.predict(t_grid).values
    
    # 4. Valutazione di M(t)
    if callable(margin_func):
        M_t = margin_func(t_grid)
    else:
        M_t = np.full_like(t_grid, margin_func, dtype=float)
        
    # 5. Valutazione del fattore di attualizzazione e^(-rt)
    D_t = np.exp(-r_monthly * t_grid)
    
    # 6. Costruzione della funzione integranda
    integrand = S_t * M_t * D_t
    
    # 7. Integrazione numerica tramite la regola dei trapezi
    ltv = np.trapezoid(integrand, t_grid)
    
    return float(ltv)

Esempio Applicativo: Upselling nel Tempo

In un’azienda SaaS, è raro che un cliente paghi sempre la stessa cifra. Spesso si inizia con un piano base (es. 40 €/mese) e, man mano che il cliente utilizza il software, acquista add-on o aumenta i consumi.

Possiamo definire una funzione [math]M(t)[/math] che modella questa espansione dei ricavi (Net Revenue Retention > 100%).

# Simuliamo un dataset e fittiamo il modello (come nelle sezioni precedenti)
df = pd.DataFrame({
    "tenure": [2, 4, 6, 7, 8, 10, 12, 13, 15, 18, 20, 24, 30, 36],
    "event":  [1, 1, 1, 1, 1, 1,  1,  0,  0,  1,  0,  0,  0,  0]
})

kmf = KaplanMeierFitter()
kmf.fit(df["tenure"], df["event"])

# Definiamo un margine dinamico M(t): 
# Il cliente parte da 40€ al mese e aumenta il suo valore di 1.50€ ogni mese
def dynamic_expansion_margin(t):
    base_margin = 40.0
    monthly_growth = 1.50
    return base_margin + (monthly_growth * t)

# Calcoliamo 3 scenari a 24 mesi:
# 1. Baseline: Margine fisso (40€) senza attualizzazione (LTV classico = RMST * M)
ltv_base = calculate_discounted_ltv(kmf, 24, margin_func=40, annual_discount_rate=0.0)

# 2. Con Upselling: Margine dinamico ma senza attualizzazione
ltv_upsell = calculate_discounted_ltv(kmf, 24, margin_func=dynamic_expansion_margin, annual_discount_rate=0.0)

# 3. Scenario Reale (Capital Budgeting): Margine dinamico + Costo del capitale (10% WACC)
ltv_real = calculate_discounted_ltv(kmf, 24, margin_func=dynamic_expansion_margin, annual_discount_rate=0.10)

print(f"1. LTV (Margine Fisso, No Sconto):      € {ltv_base:.2f}")
print(f"2. LTV (Margine Dinamico, No Sconto):   € {ltv_upsell:.2f}")
print(f"3. LTV (Margine Dinamico + Sconto 10%): € {ltv_real:.2f}")

Output atteso:

1. LTV (Margine Fisso, No Sconto):      € 596.02
2. LTV (Margine Dinamico, No Sconto):   € 815.25
3. LTV (Margine Dinamico + Sconto 10%): € 747.66

Perché questo modello fa la differenza per il management

Questo script chiude definitivamente il cerchio tra Data Science e Finanza Aziendale:

  • [math]\displaystyle \frac{\log(1 + \text{annual_discount_rate})}{12}[/math]: Traduce correttamente il WACC (Weighted Average Cost of Capital) annuale imposto dal CFO in un tasso continuo mensile per le nostre equazioni differenziali.
  • [math]\displaystyle \text{np.trapezoid}[/math]: Gestisce il calcolo dell’area sotto la curva anche quando la combinazione tra probabilità di sopravvivenza, crescita dei ricavi e svalutazione del denaro crea andamenti non lineari impossibili da calcolare a mente.

L’impatto sul budget: Lo Scenario 3 mostra che, pur con un forte upselling (Scenario 2 a 815.25 €), il valore temporale del denaro riporta il valore reale a 747.66 €. Acquisire un cliente oggi ipotizzando incassi futuri ignorando l’attualizzazione è la causa principale di crisi di liquidità (cash flow negativi) nelle aziende ad alta crescita.


6.1 Integrare l’Attualizzazione e i Margini Dinamici

Finora abbiamo considerato un margine mensile fisso. In un business reale (specialmente SaaS), è raro che un cliente paghi sempre la stessa cifra. Spesso si inizia con un piano base e, man mano che il cliente utilizza il software, acquista add-on o aumenta i consumi (generando una Net Revenue Retention espansiva). Il margine diventa quindi una funzione del tempo: [math]M(t)[/math].

Inoltre, il denaro incassato oggi ha un valore superiore a quello incassato tra due anni. Se aggiungiamo un tasso di sconto continuo [math]r[/math] (che rappresenta il costo del capitale o WACC dell’azienda), l’LTV si trasforma da un semplice indicatore commerciale a un rigoroso modello di capital budgeting. L’equazione definitiva diventa:

[math]\displaystyle \begin{aligned} LTV(\tau) = \int_0^\tau S(t) \, M(t) \, e^{-rt} \, dt \end{aligned}[/math]

6.2 Il problema dell’integrazione numerica sulle curve di sopravvivenza

Per tradurre questo integrale in codice, potremmo essere tentati di valutare la curva [math]S(t)[/math] su una griglia temporale fitta (es. tramite np.linspace) e applicare una regola di integrazione numerica standard, come quella dei trapezi.

Forse potrebbe interessarti anche:  Distribuzione Esponenziale: 5 Esercizi Svolti (con Python) per Scenari Aziendali

Questa scorciatoia nasconde però un’insidia matematica importante. La curva di Kaplan-Meier è una funzione a gradini (costante a tratti e discontinua in corrispondenza degli eventi di churn). Interpolarla su una griglia fissa significa approssimare i gradini verticali con delle “rampe” (transizioni lineari), alterando matematicamente l’area reale sottesa alla curva e producendo valori sfasati rispetto al calcolo esatto della RMST.

Per garantire la massima precisione quantitativa, dobbiamo adottare un’integrazione esatta a tratti. Poiché [math]S(t)[/math] rimane perfettamente costante al valore [math]S_i[/math] su ogni intervallo [math][t_i, t_{i+1})[/math], l’integrale complessivo sull’orizzonte [math]\tau[/math] si scompone nella somma degli integrali della sola componente economica, pesati per la probabilità di sopravvivenza di quel tratto:

[math]\displaystyle \begin{aligned} LTV(\tau) = \sum_{i=0}^{k-1} S(t_i) \int_{t_i}^{t_{i+1}} M(t) \, e^{-rt} \, dt \end{aligned}[/math]

6.3 L’implementazione in Python: Integrazione esatta a tratti

In Python, possiamo estrarre i punti di discontinuità (i tempi in cui avvengono i churn o le censure) direttamente dall’oggetto KaplanMeierFitter e utilizzare scipy.integrate.quad per calcolare l’integrale continuo di [math]M(t) e^{-rt}[/math] esclusivamente all’interno di questi gradini costanti.

import numpy as np
import pandas as pd
from lifelines import KaplanMeierFitter
from scipy.integrate import quad

def calculate_discounted_ltv_exact(
    kmf: KaplanMeierFitter, 
    horizon: float, 
    margin_func, 
    annual_discount_rate: float = 0.10
) -> float:
    """
    Calcola l'LTV attualizzato in modo matematicamente esatto,
    sfruttando la struttura a gradini della curva di Kaplan-Meier.
    """
    # 1. Conversione del tasso annuale in tasso continuo mensile
    r_monthly = np.log(1 + annual_discount_rate) / 12.0 if annual_discount_rate > 0 else 0.0
    
    # 2. Normalizzazione della funzione di margine M(t)
    if not callable(margin_func):
        m_val = float(margin_func)
        M_t = lambda t: m_val
    else:
        M_t = margin_func

    # 3. Estrazione dei nodi di integrazione dai dati osservati
    sf_times = kmf.survival_function_.index.values
    
    # Costruiamo gli intervalli aggiungendo t=0 e l'orizzonte di business (tau)
    nodes = [0.0] + [t for t in sf_times if 0.0 < t < horizon] + [horizon]
    nodes = sorted(list(set(nodes)))
    
    ltv_total = 0.0
    
    # 4. Integrazione analitica intervallo per intervallo
    for i in range(len(nodes) - 1):
        t_start = nodes[i]
        t_end = nodes[i+1]
        
        # Nel tratto [t_start, t_end), S(t) è un gradino piatto
        s_val = float(kmf.predict(t_start))
        
        # Integriamo solo il fattore economico M(t) * e^(-rt)
        integrand = lambda t: M_t(t) * np.exp(-r_monthly * t)
        interval_integral, _ = quad(integrand, t_start, t_end)
        
        # Sommiamo l'area pesata per la probabilità di sopravvivenza
        ltv_total += s_val * interval_integral
        
    return float(ltv_total)

6.4 Analisi degli Scenari e Impatto sul Business

Utilizziamo il dataset di 14 clienti già fittato per simulare tre scenari reali. Supponiamo che il modello di pricing preveda un canone di partenza di 40 €, ma l’upselling porti il cliente a spendere in media 1,50 € in più per ogni mese di permanenza.

# Modello di Upselling: M(t) = 40 + 1.5t
def dynamic_margin(t):
    return 40.0 + 1.5 * t

# Scenario 1: Margine Fisso 40€, No Sconto (LTV base)
ltv_base = calculate_discounted_ltv_exact(
    kmf, horizon=24, margin_func=40.0, annual_discount_rate=0.0
)

# Scenario 2: Upselling, Nessuna attualizzazione del denaro
ltv_upsell = calculate_discounted_ltv_exact(
    kmf, horizon=24, margin_func=dynamic_margin, annual_discount_rate=0.0
)

# Scenario 3: Upselling + Tasso di Sconto (WACC 10%)
ltv_real = calculate_discounted_ltv_exact(
    kmf, horizon=24, margin_func=dynamic_margin, annual_discount_rate=0.10
)

print(f"1. LTV (Margine Fisso, r=0%):   € {ltv_base:.2f}")
print(f"2. LTV (Upselling, r=0%):       € {ltv_upsell:.2f}")
print(f"3. LTV (Upselling, r=10%):      € {ltv_real:.2f}")

L’output del nostro script sarà:

1. LTV (Margine Fisso, r=0%):   € 596.00
2. LTV (Upselling, r=0%):       € 815.23
3. LTV (Upselling, r=10%):      € 746.04

Questi tre numeri raccontano un’evoluzione fondamentale per il management:

  • Coerenza Matematica: Il primo scenario restituisce esattamente 596,00 €, che coincide al centesimo con la moltiplicazione statica che avevamo calcolato inizialmente ([math]14{,}90 \text{ mesi di RMST} \times 40 \, \text{€}[/math]). Il modello d’integrazione a tratti è perfettamente calibrato.
  • L’Illusione dell’Upselling Lineare: Il secondo scenario mostra che, trattenendo i clienti e facendoli spendere di più, l’LTV potenziale schizza a oltre 815 €. Un team commerciale potrebbe usare questo numero per chiedere budget di acquisizione sconsiderati.
  • La Realtà Finanziaria: Lo scenario 3 applica il correttivo del costo del capitale. Gli euro extra guadagnati al 23° mese valgono meno degli euro spesi oggi per acquisire l’utente. L’LTV reale scende a 746,04 €. Acquisire utenti basandosi sul calcolo dello scenario 2 ignora l’impatto sul flusso di cassa ed è storicamente una delle cause principali di crisi di liquidità (burn rate incontrollato) nelle aziende in fase di scaling.

Pubblicità

7. Oltre l’osservabile: Modelli Parametrici (Weibull)

Se il marketing preme per sapere “cosa succederà al mese 48” pur avendo dati storici fermi a 36 mesi, Kaplan-Meier non basta (è non-parametrico e si ferma all’ultimo dato osservato).

In questi casi occorre fittare un modello parametrico, come la distribuzione di Weibull, in cui la funzione di sopravvivenza assume la forma:

[math]S(t) = e^{-(t/\lambda)^\rho}[/math]

Il parametro [math]\rho[/math] è il cuore del business: se [math]\rho < 1[/math], il rischio di abbandono decresce man mano che il cliente matura (tipico dei SaaS). Lifelines permette di addestrare un WeibullFitter esattamente come il KaplanMeierFitter, garantendo la possibilità di estrapolare la curva verso il futuro. Ma attenzione: estrapolare nel vuoto richiede forti assunzioni; i risultati non sono certezze, ma scenari probabilistici.


8. Segmentare l’LTV: La Regressione di Cox

Finora abbiamo analizzato l’intera base utenti.

Ma cosa succede se vogliamo isolare l’impatto del “Piano di abbonamento” (Basic vs Pro) sul rischio di churn?
Utilizziamo il Modello a Rischi Proporzionali di Cox (Cox Proportional Hazards), in cui l’hazard rate dipendente dalle covariate [math]x[/math] è definito come:

[math]h(t\vert{}x) = h_0(t) \exp(\beta_1 x_1 + \dots + \beta_k x_k)[/math]

Se arricchiamo il dataset aggiungendo la covariata is_pro_plan (0 = Basic, 1 = Pro), possiamo isolare l’effetto:

from lifelines import CoxPHFitter

# Aggiungiamo una feature fittizia al dataframe esistente
df["is_pro_plan"] = [1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1]

cph = CoxPHFitter()
# Il dataframe passato al fit non deve contenere colonne superflue (come customer_id)
# per non alterare la matrice di regressione
cph.fit(df[["tenure", "event", "is_pro_plan"]], duration_col="tenure", event_col="event")
cph.print_summary()

 


Interpretazione del Modello a Rischi Proporzionali di Cox

L’output del modello fornisce i coefficienti di regressione per la covariata is_pro_plan. Più che un risultato definitivo, in questa fase didattica l’output va letto come la dimostrazione della meccanica del modello.

1. Parametri della Covariata (is_pro_plan)

  • Hazard Ratio (exp(coef) = 0.21): Nel piccolo dataset simulato, il modello stima un rischio istantaneo di abbandono per gli utenti Pro pari al 21% rispetto a quello degli utenti base (una stima puntuale di riduzione del rischio del 79%).
  • Incertezza e Limiti del Campione (p = 0.04, CI = [0.05, 0.92]): Sebbene il [math]p[/math]-value sia formalmente inferiore a [math]0{,}05[/math] e l’intervallo di confidenza al 95% non includa il valore neutro di [math]1{,}0[/math], questi parametri non devono essere utilizzati come dimostrazione sostanziale. Il risultato è matematicamente compatibile con un hazard inferiore per i clienti Pro, ma il modello poggia su sole 14 osservazioni e 8 eventi totali. Una numerosità così estremamente ridotta e l’ampiezza dell’intervallo di confidenza impediscono di considerare questo effetto come generalizzabile o di prendere il 79% come un target misurabile.

2. Valutazione e Bontà di Adattamento

  • Concordance Index (0.67): Corrisponde a un’estensione dell’area sotto la curva ROC per la Survival Analysis. Misura la capacità del modello di ordinare correttamente i tempi di sopravvivenza. Un valore di 0,67 indica una capacità discriminativa discreta, ma su un campione toy rimane un puro esercizio algebrico.
  • Log-likelihood ratio test (4.46 on 1 df): Il test segnala un miglioramento dell’adattamento rispetto a un modello nullo (senza covariate), suggerendo che l’informazione sul piano tariffario è effettivamente legata al tempo di permanenza nel campione specifico.
Forse potrebbe interessarti anche:  Tutorial Numpy: Array & Broadcasting

3. Dall’Inferenza Statistica all’Impatto sul Business (In Scenari Reali)

Cosa accadrebbe se ottenessimo questi stessi coefficienti validati su un database aziendale con decine di migliaia di transazioni reali? Le implicazioni strategiche sarebbero radicali:

  • Traslazione della Curva di Retention: Un Hazard Ratio di [math]0{,}21[/math] sposterebbe la curva di sopravvivenza [math]S(t)[/math] dei clienti Pro nettamente verso l’alto rispetto alla base utenti standard.
  • Aumento della RMST: La maggiore probabilità di sopravvivenza estenderebbe l’area sotto la curva, incrementando matematicamente i mesi di permanenza attesa ([math]\tau[/math]) per chi sottoscrive il piano premium.
  • Effetto Composto sull’LTV: Poiché il piano Pro garantisce per definizione un margine economico superiore ([math]M_{pro} > M_{base}[/math]), l’impatto sul Customer Lifetime Value diventa moltiplicativo:[math display=”block”]LTV_{Pro}(\tau) = M_{pro} \times RMST_{Pro}(\tau) \gg M_{base} \times RMST_{base}(\tau)[/math]È esattamente questo tipo di segmentazione, quando supportata dalla significatività di grandi numeri, a giustificare a livello finanziario l’allocazione di budget di acquisizione (CAC) molto più elevati per specifici canali di traffico o forti investimenti nelle campagne di upselling.

9. Una Classe Python Robusta per l’Astrazione del Modello

Per rendere scalabile l’analisi, racchiudiamo il lavoro in una classe orientata agli oggetti. È stata inserita una gestione rigorosa degli input (dropna() e sanity checks) per evitare i classici errori di run-time sui DataFrame, come metodi invalidi su oggetti Index o array non conformi.

import pandas as pd
from lifelines import KaplanMeierFitter
from lifelines.utils import restricted_mean_survival_time

class LTVEstimator:
    """
    Stima del Customer Lifetime Value basata su Kaplan-Meier Survival Analysis.
    """
    def __init__(self, monthly_margin: float):
        if monthly_margin <= 0:
            raise ValueError("Il margine mensile deve essere positivo.")
        self.monthly_margin = monthly_margin
        self.kmf = KaplanMeierFitter()
        self.is_fitted = False

    def fit(self, df: pd.DataFrame, tenure_col: str = "tenure", event_col: str = "event"):
        # Controlli di sicurezza per un parsing robusto dei dati
        if not {tenure_col, event_col}.issubset(df.columns):
            raise ValueError(f"Colonne richieste non trovate nel DataFrame.")

        # Copia profonda per evitare SettingWithCopyWarning
        data = df[[tenure_col, event_col]].dropna().copy()

        if (data[tenure_col] <= 0).any(): raise ValueError("La variabile tenure deve essere > 0.")
        
        if not set(data[event_col].unique()).issubset({0, 1}):
            raise ValueError("La variabile event deve contenere esclusivamente 0 e 1.")

        self.kmf.fit(durations=data[tenure_col], event_observed=data[event_col])
        self.is_fitted = True
        return self

    def rmst(self, horizon: float) -> float:
        self._check_fitted()
        return restricted_mean_survival_time(self.kmf, t=horizon)

    def ltv(self, horizon: float) -> float:
        return self.rmst(horizon) * self.monthly_margin

    def summary(self, horizon: float = 24) -> dict:
        self._check_fitted()
        return {
            "horizon_months": horizon,
            "rmst_months": round(self.rmst(horizon), 2),
            "median_lifetime_months": round(self.kmf.median_survival_time_, 2),
            "retention_at_horizon": round(float(self.kmf.predict(horizon)), 2),
            "ltv_expected": round(self.ltv(horizon), 2)
        }

    def _check_fitted(self):
        if not self.is_fitted:
            raise RuntimeError("Modello non inizializzato. Eseguire fit() sul dataset.")

Testiamo la classe sul nostro dataset di partenza:

estimator = LTVEstimator(monthly_margin=40)
estimator.fit(df)
print(estimator.summary(horizon=24))

L’output, pulito e immediatamente intellegibile per il management, confermerà i nostri calcoli analitici:

{
    "horizon_months": 24,
    "rmst_months": 14.9,
    "median_lifetime_months": 18.0,
    "retention_at_horizon": 0.4,
    "ltv_expected": 596.0
}


Dal KPI al Modello: Un Quadro di Sintesi

Per chi si occupa di Data Science e Business Analytics, la scelta dello strumento dipende dal livello di maturità delle domande a cui l’azienda deve rispondere. Passare dai KPI aggregati di base alla modellazione avanzata della sopravvivenza non significa solo complicare l’analisi, ma passare da una misurazione retrospettiva e distorta a una capacità previsionale e finanziariamente rigorosa.

La tabella seguente riassume la progressione concettuale trattata nell’articolo, collegando ciascun approccio analitico alla specifica domanda di business che permette di risolvere:

Approccio / Metrica Domanda di Business Ruolo Analitico e Decisionale
Churn Medio (Semplice) Quanto churn abbiamo mediamente? Fornisce una metrica aggregata statica; ignora la dimensione temporale, soffre di distorsione per via delle censure e non guida l’allocazione delle risorse.
Kaplan-Meier Qual è la probabilità di sopravvivere fino al tempo [math]t[/math]? Stima in modo non parametrico la curva di retention [math]S(t)[/math], gestendo correttamente i clienti ancora attivi (dati censurati).
RMST ([math]\tau[/math]) Quanti mesi di vita attesa accumula il cliente entro l’orizzonte [math]\tau[/math]? Calcola la durata media attesa entro un orizzonte definito (area sotto la curva [math]S(t)[/math]), trasformando la curva di sopravvivenza in un KPI scalare.
LTV ([math]\tau[/math]) Quanto margine economico netto genera il cliente entro l’orizzonte [math]\tau[/math]? Integra la probabilità di sopravvivenza con la funzione di margine [math]M(t)[/math] e il tasso di sconto [math]r[/math], definendo il tetto massimo per il costo di acquisizione (CAC).
Modelli Parametrici (es. Weibull) Cosa possiamo modellare e proiettare oltre l’orizzonte osservato? Assumono una distribuzione teorica per l’hazard, consentendo di estrapolare la sopravvivenza e l’LTV anche oltre i tempi coperti dai dati storici.
Regressione di Cox Quali covariate o segmenti sono associati al rischio di churn? Quantifica l’impatto delle caratteristiche del cliente (es. piano Pro vs Base, comportamento d’uso) sul tasso istantaneo di abbandono tramite gli Hazard Ratio.

Questa struttura ad albero guida il passaggio da una pura descrizione del fenomeno alla comprensione dei fattori causali e alla quantificazione del valore economico, fornendo al decision maker sia la visione d’insieme che i correttivi quantitativi per guidare la crescita sostenibile del business.

Conclusioni

L’equazione [math]LTV = \frac{ARPU}{\text{Churn}}[/math] resta un’ottima approssimazione a spanne, utile durante una conversazione in ascensore, ma per prendere decisioni di allocazione del budget di marketing o per pianificare la tenuta di cassa di un business basato sugli abbonamenti serve una struttura analitica infinitamente più solida.

La Survival Analysis colma esattamente questa lacuna metodologica. Trattare in modo rigoroso i dati parziali (censura), rinunciare alle medie statiche e calcolare la Restricted Mean Survival Time evita di proiettare falsi tassi di retention nell’infinito, restituendo al contrario misurazioni concrete, legate a specifici orizzonti temporali.

Sfruttando un framework in Python con lifelines, si passa dall’estrapolazione di un singolo KPI a un vero e proprio modello stocastico. E quando la probabilità di sopravvivenza viene unita a tassi di sconto finanziari e margini non lineari, il calcolo del Lifetime Value smette di essere un esercizio teorico e si trasforma nel più potente strumento di simulazione per le strategie di crescita aziendale.

📚 Customer Economics, Churn, Retention e Predictive Analytics

Capire il valore di un cliente significa andare oltre il semplice costo di acquisizione. LTV, retention, NRR e churn descrivono la sostenibilità economica della relazione con il cliente, mentre Machine Learning, dati sintetici, catene di Markov e uplift modeling permettono di prevedere i comportamenti e misurare l’impatto reale delle strategie di marketing.

👉Customer Economics nel SaaS: perché LTV, Retention e NRR contano più del costo di acquisizione (CAC)

👉Analisi avanzata Churn Rate e CLV: modello Python ed Excel per ottimizzare la retention clienti

👉Come prevedere il churn con Python: guida completa al Machine Learning e SHAP

👉Synthetic Data: come creare clienti artificiali con Python, AI e modelli comportamentali

👉Catene di Markov in Python: modellare funnel, retention e churn oltre le classiche dashboard

👉Uplift Modeling: cos’è e come misurare l’impatto reale del marketing con un esempio Python

.

Pubblicità