Pricing ibrido B2B con Python: simulare churn, GTV e profitto con Monte Carlo

Cerca:

Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors
Pricing ibrido B2B + Monte Carlo

Quanto dovrebbe pagare un cliente B2B per utilizzare una piattaforma?

La domanda sembra semplice, ma diventa rapidamente complessa quando il modello di business combina canone fisso, commissione sulle transazioni e comportamento dinamico del cliente.

Un cliente che aumenta il proprio volume transato genera più ricavi per la piattaforma. Ma proprio l’aumento del costo sostenuto può spingerlo a ridurre le transazioni, cercare alternative o abbandonare il servizio.

Il pricing genera quindi un possibile effetto circolare:

[math]\displaystyle Pricing \rightarrow Costo \rightarrow Churn \rightarrow GTV \rightarrow Revenue \rightarrow Profitto[/math]

Per studiare questo problema possiamo partire da un modello estremamente semplice:

[math]\displaystyle \boxed{C=F+r\cdot GTV}[/math]

dove:

  • [math]F[/math] è il canone fisso;
  • [math]r[/math] è il Take Rate;
  • [math]GTV[/math] è il Gross Transaction Volume;
  • [math]C[/math] è il costo sostenuto dal cliente.

Ma questa formula, da sola, non ci dice quale pricing sia migliore.

Per arrivare a una decisione quantitativa dobbiamo modellare anche ciò che accade dopo che il prezzo viene applicato.

In questo articolo costruiremo progressivamente un modello Python che integra:

  • pricing ibrido;
  • Tipping Point;
  • probabilità di churn;
  • evoluzione del GTV;
  • disintermediazione;
  • valore percepito dinamico;
  • simulazione Monte Carlo;
  • NPV;
  • confronto tra piani;
  • analisi di rischio;
  • sensitivity analysis;
  • ricerca del pricing più robusto.

L’obiettivo non è trovare un prezzo “magico”.

L’obiettivo è rispondere a una domanda più interessante:

quale strategia di pricing produce il miglior compromesso tra valore economico, retention e rischio, date le nostre ipotesi?

Pubblicità

1. Dal pricing proporzionale al pricing ibrido

Consideriamo tre piani:

Piano Canone mensile Take Rate
Starter €0 3,0%
Professional €300 1,5%
Enterprise €1.000 0,5%

I valori sono ipotetici e servono esclusivamente per costruire il modello.

Il costo mensile del cliente è:

[math]C=F+rGTV[/math]

Per lo Starter:

[math]C_S=0+0,03GTV[/math]

Per il Professional:

[math]C_P=300+0,015GTV[/math]

Per l’Enterprise:

[math]C_E=1000+0,005GTV[/math]

A questo punto possiamo chiederci:

a quale GTV il Professional diventa più conveniente dello Starter?


2. Il Tipping Point

Poniamo:

[math]C_S=C_P[/math]

quindi:

[math]0,03GTV = 300+0,015GTV[/math]

Da cui:

[math]0,015GTV=300[/math]

e quindi:

[math]\boxed{GTV^*=20.000€}[/math]

Il punto di pareggio tra Starter e Professional è quindi 20.000 € di GTV mensile.

Facciamo lo stesso confronto tra Professional ed Enterprise:

[math]300+0,015GTV = 1000+0,005GTV[/math]

da cui:

[math]0,01GTV=700[/math]

e quindi:

[math]\boxed{GTV^*=70.000€}[/math]

Abbiamo così:

0 €                 20.000 €                  70.000 €
│------------------------│--------------------------│---->
       Starter             Professional             Enterprise
         3%                     1,5%                    0,5%

Output atteso

GTV mensile Piano economicamente più conveniente
< €20.000 Starter
€20.000–€70.000 Professional
> €70.000 Enterprise

Questo è già un risultato utile.

Ma è ancora un modello statico.


3. Il problema: il GTV non è indipendente dal prezzo

Il calcolo precedente assume implicitamente che il GTV rimanga invariato quando cambia il prezzo.

È un’assunzione forte.

Immaginiamo un cliente con:

[math]GTV=25.000€[/math]

Con lo Starter pagherebbe:

[math]0,03\cdot25.000=750€[/math]

Con il Professional:

[math]300+0,015\cdot25.000 = 675€[/math]

Dal solo punto di vista del costo, il Professional è quindi più conveniente.

Ma nella realtà il cliente non è necessariamente un agente passivo.

Un pricing percepito come troppo elevato potrebbe provocare:

  • riduzione del volume transato;
  • spostamento delle transazioni fuori dalla piattaforma;
  • minore engagement;
  • aumento della probabilità di churn.

Il problema diventa quindi dinamico.


4. Dal prezzo al comportamento del cliente

Introduciamo due concetti:

Churn

La probabilità che il cliente abbandoni la piattaforma.

Disintermediazione

La quota di attività che viene spostata fuori dalla piattaforma, riducendo il GTV.

Possiamo rappresentare schematicamente il sistema:

                Pricing
                   │
                   ↓
             Costo percepito
              /           \
             ↓             ↓
          Churn      Disintermediazione
             │             │
             ↓             ↓
         Retention         GTV
             \             /
              \           /
               ↓         ↓
                 Revenue
                    │
                    ↓
                  Profitto

Questa struttura è molto più interessante di una semplice formula di pricing.


5. Un primo modello di churn

Possiamo utilizzare una funzione logistica:

[math]P(Churn_t)=\sigma(z_t)[/math]

dove:

[math]\sigma(z)=\frac{1}{1+e^{-z}}[/math]

e definiamo:

[math]\displaystyle z_t= \beta_0+ \beta_1PriceRatio_t -\beta_2Engagement_t -\beta_3Tenure_t[/math]

Il rapporto prezzo/valore è:

[math]PriceRatio_t= \frac{Price_t}{Value_t}[/math]

dove:

[math]Price_t=F+rGTV_t[/math]

Il significato è intuitivo:

  • se il costo aumenta rispetto al valore percepito, il churn tende ad aumentare;
  • maggiore engagement riduce il churn;
  • maggiore anzianità può ridurre il churn.

I coefficienti utilizzati nel modello Python saranno parametri simulati, non stime empiriche.


6. Implementiamo la probabilità di churn in Python

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

La funzione logistica:

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

e il modello di churn:

def churn_probability(
    monthly_fee,
    take_rate,
    gtv,
    perceived_value,
    engagement,
    tenure
):
    price = (
        monthly_fee
        + take_rate * gtv
    )

    price_ratio = (
        price / perceived_value
    )

    z = (
        -2.5
        + 5.0 * price_ratio
        - 1.5 * engagement
        - 1.0 * tenure
    )

    return sigmoid(z)

Possiamo fare un primo test:

p = churn_probability(
    monthly_fee=300,
    take_rate=0.015,
    gtv=25_000,
    perceived_value=900,
    engagement=0.70,
    tenure=0.65
)

print(f"Probabilità mensile di churn: {p:.2%}")

Output atteso

L’output sarà una percentuale compresa tra 0% e 100%.

Es:

Probabilità mensile di churn: 38.94%

Il valore preciso dipende dai coefficienti del modello, ma il punto importante è il meccanismo:

Prezzo ↑
   ↓
Price / Value ↑
   ↓
Probabilità di churn ↑

Non stiamo ancora dicendo che questa relazione descriva un mercato reale.

Stiamo costruendo un modello simulativo che possiamo successivamente calibrare su dati reali.


7. Il valore percepito non deve essere statico

Una delle semplificazioni più forti sarebbe mantenere:

[math]Value_t=Value_0[/math]

per tutta la vita del cliente.

Nella realtà il valore percepito può cambiare.

Un cliente che utilizza la piattaforma da tempo potrebbe:

  • aver integrato i propri processi;
  • avere maggiore familiarità con il servizio;
  • aver accumulato dati;
  • beneficiare di effetti di rete;
  • aver costruito workflow difficili da sostituire.

Possiamo rappresentare questa crescita con:

[math]V_t= V_0 \left[1+\gamma(1-e^{-kt})\right][/math]

Il valore cresce rapidamente all’inizio e poi tende a stabilizzarsi.

In Python:

def update_perceived_value(
    base_value,
    tenure,
    learning_rate=0.20,
    decay=0.10
):
    return base_value * (
        1
        + learning_rate
        * (1 - np.exp(-decay * tenure))
    )

Possiamo visualizzarlo:

tenures = np.linspace(0, 10, 100)

values = [
    update_perceived_value(
        base_value=900,
        tenure=t
    )
    for t in tenures
]

plt.figure(figsize=(9, 5))

plt.plot(
    tenures,
    values
)

plt.xlabel("Anzianità (anni)")
plt.ylabel("Valore percepito (€)")
plt.title("Evoluzione simulata del valore percepito")

plt.grid(alpha=0.3)
plt.show()

Come leggere il grafico

Ci aspettiamo una curva crescente e concava:

Il messaggio non è che il valore percepito debba necessariamente seguire questa formula.

La formula rappresenta una scelta modellistica.


8. Anche il GTV deve diventare dinamico

Una prima simulazione potrebbe essere:

[math]GTV_{t+1}= GTV_t(1+g+\epsilon_t)[/math]

dove:

[math]\epsilon_t\sim N(0,\sigma)[/math]

È una buona baseline, ma non rappresenta direttamente la disintermediazione.

Per introdurla possiamo modificare la crescita:

[math]\displaystyle GTV_{t+1}= GTV_t \left[1+g -\alpha PriceRatio_t +\epsilon_t\right][/math]

dove [math]\alpha[/math] misura la sensibilità del GTV alla pressione economica.

In Python:

def simulate_gtv(
    gtv,
    growth_rate,
    volatility,
    price_ratio,
    disintermediation_sensitivity,
    rng
):

    shock = rng.normal(
        0,
        volatility
    )

    price_effect = (
        disintermediation_sensitivity
        * price_ratio
    )

    growth = (
        growth_rate
        - price_effect
        + shock
    )

    new_gtv = gtv * (1 + growth)

    return max(new_gtv, 0)

Questa è una semplificazione importante.

Non stiamo costruendo un vero modello econometrico della disintermediazione.

Stiamo introducendo un feedback strutturale:

[math]Pricing \rightarrow Price/Value \rightarrow GTV[/math]

che mancava nella random walk iniziale.


9. La simulazione di un singolo cliente

Ora possiamo combinare:

  • pricing;
  • churn;
  • valore percepito;
  • GTV;
  • disintermediazione;
  • margine;
  • attualizzazione.

La funzione deve restituire i dati e non stampare nulla.

Questo è importante perché la stessa funzione verrà utilizzata migliaia di volte nella Monte Carlo Simulation.

def simulate_customer(
    initial_gtv,
    monthly_fee,
    take_rate,
    perceived_value,
    engagement,
    initial_tenure,
    months=36,
    growth_rate=0.01,
    gtv_volatility=0.05,
    discount_rate=0.01,
    disintermediation_sensitivity=0.10,
    variable_cost_rate=0.002,
    seed=None
):

    rng = np.random.default_rng(seed)

    gtv = initial_gtv
    tenure = initial_tenure

    records = []

    for month in range(1, months + 1):

        # Valore percepito dinamico
        current_value = update_perceived_value(
            perceived_value,
            tenure
        )

        # Prezzo
        price = (
            monthly_fee
            + take_rate * gtv
        )

        # Rapporto prezzo / valore
        price_ratio = (
            price / current_value
        )

        # Probabilità di churn
        p_churn = churn_probability(
            monthly_fee,
            take_rate,
            gtv,
            current_value,
            engagement,
            tenure
        )

        # Evento casuale
        churn = (
            rng.random() < p_churn
        )

        # Costi variabili
        variable_cost = (
            variable_cost_rate * gtv
        )

        # Margine
        margin = (
            price
            - variable_cost
        )

        # Discounting
        discount_factor = (
            1
            / (1 + discount_rate) ** month
        )

        discounted_margin = (
            margin
            * discount_factor
        )

        records.append({
            "month": month,
            "gtv": gtv,
            "perceived_value": current_value,
            "price": price,
            "price_ratio": price_ratio,
            "p_churn": p_churn,
            "churn": churn,
            "variable_cost": variable_cost,
            "margin": margin,
            "discounted_margin": discounted_margin
        })

        # Churn → fine della relazione
        if churn:
            break

        # Evoluzione del GTV
        gtv = simulate_gtv(
            gtv=gtv,
            growth_rate=growth_rate,
            volatility=gtv_volatility,
            price_ratio=price_ratio,
            disintermediation_sensitivity=(
                disintermediation_sensitivity
            ),
            rng=rng
        )

        # Un mese di anzianità
        tenure += 1 / 12

    return records

10. Prima di fare Monte Carlo: guardiamo una singola traiettoria

Eseguiamo una simulazione:

records = simulate_customer(
    initial_gtv=25_000,
    monthly_fee=300,
    take_rate=0.015,
    perceived_value=900,
    engagement=0.70,
    initial_tenure=0.65,
    months=36,
    seed=42
)

df = pd.DataFrame(records)

df.head()

Output atteso

Il DataFrame avrà colonne simili a:

Forse potrebbe interessarti anche:  Algoritmo di Huffman: Come Funziona la Compressione Dati (Esempi e Python)
month gtv perceived_value price price_ratio p_churn churn margin
1 25.000 911 675 0,74 False
2 False
3

I valori esatti dipendono dal seed.

Questa è una caratteristica importante della simulazione: la traiettoria non è deterministica.


11. Calcoliamo il valore economico della traiettoria

Il margine cumulato è:

[math]Margin= \sum_t Margin_t[/math]

mentre il valore attuale netto è:

[math]NPV= \sum_t \frac{Margin_t}{(1+d)^t}[/math]

In Python:

npv = df["discounted_margin"].sum()

total_margin = df["margin"].sum()

print(f"Margine cumulato: €{total_margin:,.2f}")
print(f"NPV: €{npv:,.2f}")

Output atteso

Margine cumulato: €...
NPV: €...

Qui il valore preciso è volutamente lasciato al notebook: non è un dato osservato, ma il risultato di una particolare traiettoria simulata.


12. Visualizzare una traiettoria

Possiamo osservare contemporaneamente GTV e valore percepito:

fig, ax = plt.subplots(
    2,
    1,
    figsize=(11, 8),
    sharex=True
)

ax[0].plot(
    df["month"],
    df["gtv"],
    marker="o"
)

ax[0].set_ylabel("GTV (€)")
ax[0].set_title(
    "Evoluzione simulata del GTV"
)

ax[1].plot(
    df["month"],
    df["perceived_value"],
    color="green",
    marker="o"
)

ax[1].set_ylabel("Valore percepito (€)")
ax[1].set_xlabel("Mese")
ax[1].set_title(
    "Evoluzione simulata del valore percepito"
)

plt.tight_layout()
plt.show()

Questo grafico permette di vedere una delle idee centrali del modello:

il cliente non è statico.

GTV, valore percepito, prezzo e rischio di churn evolvono nel tempo.


13. Probabilità di churn e rapporto prezzo/valore

Un secondo grafico è ancora più interessante:

fig, ax1 = plt.subplots(
    figsize=(11, 5)
)

ax1.plot(
    df["month"],
    df["price_ratio"],
    color="blue",
    label="Price / Value"
)

ax1.set_ylabel(
    "Price / Value",
    color="blue"
)

ax2 = ax1.twinx()

ax2.plot(
    df["month"],
    df["p_churn"] * 100,
    color="red",
    label="P(Churn)"
)

ax2.set_ylabel(
    "Probabilità churn (%)",
    color="red"
)

ax1.set_xlabel("Mese")

plt.title(
    "Pressione di pricing e rischio di churn"
)

plt.tight_layout()
plt.show()

Interpretazione

Se il modello è coerente, ci aspettiamo che una maggiore pressione economica sia associata a una maggiore probabilità di churn.

Ma attenzione:

questa è una relazione imposta dal modello, non una scoperta empirica.

Per trasformarla in una relazione empirica servirebbero dati reali.


14. Perché una singola simulazione non basta

Una traiettoria può essere particolarmente favorevole.

Un’altra può essere particolarmente negativa.

Quindi il risultato:

[math]NPV=8.000€[/math]

di una singola simulazione non significa che il cliente “vale 8.000 €”.

Significa soltanto:

una delle possibili traiettorie generate dal modello ha prodotto un NPV di 8.000 €.

Per studiare l’incertezza dobbiamo ripetere l’esperimento.

Ed è qui che entra Monte Carlo.


15. Monte Carlo Simulation

Eseguiamo, per esempio, 5.000 simulazioni.

Per ogni simulazione conserviamo soltanto gli indicatori necessari:

def monte_carlo_customer(
    n_simulations=5000,
    **kwargs
):

    npvs = []
    lifetimes = []

    for sim in range(n_simulations):

        records = simulate_customer(
            seed=sim,
            **kwargs
        )

        df = pd.DataFrame(records)

        npvs.append(
            df["discounted_margin"].sum()
        )

        lifetimes.append(
            len(df)
        )

    return {
        "npv": np.array(npvs),
        "lifetime": np.array(lifetimes)
    }

Notiamo una differenza fondamentale rispetto alla versione precedente:

nessuna stampa dentro il ciclo.

Con 5.000 simulazioni, l’output deve essere aggregato, non visualizzato individualmente.


16. Distribuzione dell’NPV

Eseguiamo:

results = monte_carlo_customer(
    n_simulations=5000,
    initial_gtv=25_000,
    monthly_fee=300,
    take_rate=0.015,
    perceived_value=900,
    engagement=0.70,
    initial_tenure=0.65,
    months=36
)

npv = results["npv"]

Calcoliamo alcuni indicatori:

print(
    f"NPV medio: "
    f"€{np.mean(npv):,.2f}"
)

print(
    f"NPV mediano: "
    f"€{np.median(npv):,.2f}"
)

print(
    f"5° percentile: "
    f"€{np.percentile(npv, 5):,.2f}"
)

print(
    f"95° percentile: "
    f"€{np.percentile(npv, 95):,.2f}"
)

Output atteso

NPV medio:       €...
NPV mediano:     €...
5° percentile:   €...
95° percentile:  €...

Es:

NPV medio: €2,272.70
NPV mediano: €1,218.75
5° percentile: €618.81
95° percentile: €8,097.66

I valori dipendono dai parametri e dal seed.

Ma la struttura dell’output ci permette già di distinguere:

[math]E[NPV][/math]

dalla mediana e dalle code della distribuzione.


17. Visualizziamo la distribuzione

plt.figure(figsize=(11, 6))

plt.hist(
    npv,
    bins=50,
    color="steelblue",
    alpha=0.75
)

plt.axvline(
    np.mean(npv),
    color="red",
    linestyle="--",
    label="NPV medio"
)

plt.axvline(
    np.median(npv),
    color="black",
    linestyle=":",
    label="Mediana"
)

plt.xlabel("NPV (€)")
plt.ylabel("Frequenza")
plt.title(
    "Distribuzione Monte Carlo dell'NPV"
)

plt.legend()
plt.tight_layout()
plt.show()

La figura non rappresenta “5.000 previsioni del futuro”.

Rappresenta 5.000 possibili traiettorie generate dalle assunzioni del modello.

È una distinzione metodologica fondamentale.


18. Probabilità di NPV negativo

Possiamo calcolare:

[math]P(NPV<0)[/math]

con:

prob_negative = np.mean(
    npv < 0
)

print(
    f"P(NPV < 0): "
    f"{prob_negative:.2%}"
)

Interpretazione

Se, per esempio, il notebook restituisse:

P(NPV < 0): 4,8%

la lettura corretta sarebbe:

nelle condizioni simulate dal modello, circa il 4,8% delle traiettorie produce un NPV negativo.

Non significa che “il 4,8% dei clienti perderà denaro”.


19. Lifetime del cliente

Abbiamo già salvato la durata di ogni traiettoria.

lifetime = results["lifetime"]

print(
    f"Lifetime medio: "
    f"{np.mean(lifetime):.1f} mesi"
)

print(
    f"Lifetime mediano: "
    f"{np.median(lifetime):.1f} mesi"
)

Es:

Lifetime medio: 4.5 mesi
Lifetime mediano: 2.0 mesi

Possiamo visualizzarla:

plt.figure(figsize=(10, 5))

plt.hist(
    lifetime,
    bins=np.arange(
        1,
        38
    ) - 0.5,
    color="darkorange",
    alpha=0.75
)

plt.xlabel("Mesi di permanenza")
plt.ylabel("Frequenza")
plt.title(
    "Distribuzione della lifetime simulata"
)

plt.tight_layout()
plt.show()

Questo ci permette di collegare direttamente pricing e retention.


20. Confrontiamo i tre piani

Ora possiamo confrontare:

plans = {
    "Starter": {
        "monthly_fee": 0,
        "take_rate": 0.03
    },

    "Professional": {
        "monthly_fee": 300,
        "take_rate": 0.015
    },

    "Enterprise": {
        "monthly_fee": 1000,
        "take_rate": 0.005
    }
}

Costruiamo una funzione:

def evaluate_plan(
    plan,
    n_simulations=5000,
    **customer_params
):

    npvs = []
    lifetimes = []

    for sim in range(n_simulations):

        records = simulate_customer(
            monthly_fee=plan["monthly_fee"],
            take_rate=plan["take_rate"],
            seed=sim,
            **customer_params
        )

        df = pd.DataFrame(records)

        npvs.append(
            df["discounted_margin"].sum()
        )

        lifetimes.append(
            len(df)
        )

    npvs = np.array(npvs)
    lifetimes = np.array(lifetimes)

    var_95 = np.percentile(
        npvs,
        5
    )

    cvar_95 = np.mean(
        npvs[npvs <= var_95]
    )

    return {
        "npv": npvs,
        "expected_npv": np.mean(npvs),
        "median_npv": np.median(npvs),
        "p_negative": np.mean(npvs < 0),
        "lifetime": lifetimes,
        "mean_lifetime": np.mean(lifetimes),
        "var_95": var_95,
        "cvar_95": cvar_95
    }

21. Un piccolo errore da evitare nel codice

Nel calcolo del CVaR dobbiamo utilizzare l’array locale del piano:

cvar_95 = np.mean(
    npvs[npvs <= var_95]
)

e non una variabile npv appartenente a una simulazione precedente.

Questa è una piccola ma importante lezione di Data Science:

un modello quantitativo deve essere verificato anche a livello di implementazione, non soltanto di formule.


22. Eseguiamo il confronto

plan_results = {}

for name, plan in plans.items():

    plan_results[name] = evaluate_plan(
        plan,
        n_simulations=5000,
        initial_gtv=25_000,
        perceived_value=900,
        engagement=0.70,
        initial_tenure=0.65,
        months=36
    )

Creiamo una tabella:

comparison = pd.DataFrame({
    name: {
        "NPV medio":
            result["expected_npv"],

        "NPV mediano":
            result["median_npv"],

        "P(NPV < 0)":
            result["p_negative"],

        "Lifetime media":
            result["mean_lifetime"],

        "VaR 5%":
            result["var_95"],

        "CVaR 5%":
            result["cvar_95"]
    }
    for name, result
    in plan_results.items()
}).T

comparison

Output atteso

La tabella avrà questa struttura:

Piano NPV medio NPV mediano P(NPV < 0) Lifetime media VaR 5% CVaR 5%
Starter
Professional
Enterprise

 


23. Perché la tabella è più interessante del solo NPV

Supponiamo, a titolo puramente illustrativo, che il notebook produca:

Piano NPV medio P(NPV < 0)
Starter €7.900 3%
Professional €8.700 5%
Enterprise €8.900 14%

La conclusione non sarebbe semplicemente:

“Enterprise è il migliore.”

Enterprise ha infatti il valore medio più alto, ma anche un rischio significativamente maggiore.

Potremmo preferire Professional se l’obiettivo è una maggiore robustezza.

Questa è la differenza tra massimizzare il rendimento atteso e ottimizzare una decisione sotto incertezza.


24. Confrontiamo direttamente le distribuzioni

plt.figure(figsize=(12, 6))

for name, result in plan_results.items():

    plt.hist(
        result["npv"],
        bins=50,
        alpha=0.40,
        label=name
    )

plt.axvline(
    0,
    color="black",
    linestyle="--"
)

plt.xlabel("NPV (€)")
plt.ylabel("Frequenza")

plt.title(
    "Confronto Monte Carlo dei piani di pricing"
)

plt.legend()
plt.tight_layout()
plt.show()

Cosa dobbiamo osservare?

Non soltanto quale distribuzione è più spostata verso destra.

Dobbiamo osservare:

  • posizione della distribuzione;
  • dispersione;
  • coda sinistra;
  • eventuale sovrapposizione;
  • probabilità di NPV negativo.
Forse potrebbe interessarti anche:  Guida Pratica a SQLite3 in Python: da Zero a Pandas (Esempi e Best Practice)

Il pricing migliore non è necessariamente quello con la media più alta.


25. VaR e CVaR

Per descrivere la coda negativa possiamo utilizzare il Value at Risk.

Il VaR al 5% è:

[math]VaR_{5\%}= Q_{0.05}(NPV)[/math]

In Python:

var_95 = np.percentile(
    npv,
    5
)

Il CVaR considera invece la media degli scenari peggiori:

[math]CVaR_{5\%}= E[NPV\mid NPV\leq VaR_{5\%}][/math]

cvar_95 = np.mean(
    npv[npv <= var_95]
)

Perché è utile?

Perché due pricing possono avere:

[math]E[NPV_A] \approx E[NPV_B][/math]

ma:

[math]CVaR_A \gg CVaR_B[/math]

In altre parole, il rendimento medio può essere simile mentre il rischio estremo è molto diverso.


26. La heatmap del pricing

A questo punto possiamo fare un passo ulteriore.

Invece di confrontare soltanto tre piani, esploriamo lo spazio:

[math](F,r)[/math]

dove:

  • [math]F[/math] = canone;
  • [math]r[/math] = Take Rate.
fees = np.arange(
    0,
    1001,
    100
)

rates = np.arange(
    0.005,
    0.035,
    0.0025
)

Per ogni combinazione eseguiamo una simulazione.

Per velocità, nella fase esplorativa possiamo utilizzare 1.000 simulazioni:

heatmap = np.zeros(
    (len(fees), len(rates))
)

for i, fee in enumerate(fees):

    for j, rate in enumerate(rates):

        result = evaluate_plan(
            {
                "monthly_fee": fee,
                "take_rate": rate
            },
            n_simulations=1000,
            initial_gtv=25_000,
            perceived_value=900,
            engagement=0.70,
            initial_tenure=0.65,
            months=36
        )

        heatmap[i, j] = (
            result["expected_npv"]
        )

Visualizziamo:

plt.figure(figsize=(12, 7))

sns.heatmap(
    heatmap,
    xticklabels=[
        f"{r:.2%}"
        for r in rates
    ],
    yticklabels=[
        f"€{f:,.0f}"
        for f in fees
    ],
    cmap="RdYlGn"
)

plt.xlabel("Take Rate")
plt.ylabel("Canone mensile")

plt.title(
    "NPV atteso al variare del pricing"
)

plt.tight_layout()
plt.show()


27. Come leggere la heatmap

La heatmap trasforma il problema in una superficie:

[math]NPV(F,r)[/math]

Le zone verdi rappresentano valori più elevati.

Le zone rosse valori più bassi.

Il punto di massimo può essere identificato con:

best_index = np.unravel_index(
    np.argmax(heatmap),
    heatmap.shape
)

best_fee = fees[best_index[0]]
best_rate = rates[best_index[1]]

print(
    f"Canone migliore: €{best_fee:,.0f}"
)

print(
    f"Take Rate migliore: {best_rate:.2%}"
)

Output atteso

Canone migliore: €...
Take Rate migliore: ...%

Anche in questo caso il risultato dipende dalle assunzioni del modello.

Es:

Canone migliore: €200
Take Rate migliore: 0.50%

Non è un “prezzo ottimale del mercato”.

È il miglior punto all’interno del modello simulato.


28. Il problema della precisione apparente

Supponiamo che il modello restituisca:

[math]F^*=327,41€[/math]

e:

[math]r^*=1,37%[/math]

Non dobbiamo concludere che il prezzo corretto sia davvero 327,41 €.

Quella precisione è spesso artificiale.

Se i coefficienti del churn sono incerti, se il valore percepito è simulato e se la distribuzione del GTV è soltanto ipotizzata, il risultato non può essere interpretato con la stessa precisione dei calcoli matematici che lo hanno prodotto.

È quindi più corretto parlare di:

regione di pricing robusto

piuttosto che di un unico prezzo perfetto.


29. Definiamo una regione di pricing robusto

Se:

[math]NPV_{max}[/math]

è il valore massimo osservato, possiamo definire:

[math]\Omega= \left\{ (F,r): NPV(F,r) \geq 0,98NPV_{max} \right\}[/math]

In pratica cerchiamo tutte le combinazioni che producono almeno il 98% del massimo.

max_npv = np.max(heatmap)

robust_mask = (
    heatmap >= 0.98 * max_npv
)

Questa idea è importante dal punto di vista manageriale.

Se dieci combinazioni producono risultati quasi identici, il management può scegliere quella:

  • più semplice da comunicare;
  • più coerente con il posizionamento;
  • più facile da implementare;
  • più competitiva;
  • più adatta alla segmentazione commerciale.

La matematica non deve necessariamente produrre una singola risposta.

Può identificare un territorio decisionale.


30. Sensitivity Analysis

Ora dobbiamo verificare quanto il risultato dipenda dalle nostre assunzioni.

Consideriamo il pricing candidato:

[math]F=300€[/math]

[math]r=1,5%[/math]

e modifichiamo una variabile alla volta.

Scenario 1

GTV iniziale:

[math]-20%[/math]

Scenario 2

GTV iniziale:

[math]+20%[/math]

Scenario 3

Valore percepito:

[math]-15%[/math]

Scenario 4

Costi variabili:

[math]+30%[/math]

Scenario 5

Volatilità GTV:

[math]\times2[/math]

Possiamo costruire:

scenarios = {
    "Base": {
        "initial_gtv": 25_000,
        "perceived_value": 900,
        "gtv_volatility": 0.05
    },

    "GTV -20%": {
        "initial_gtv": 20_000,
        "perceived_value": 900,
        "gtv_volatility": 0.05
    },

    "GTV +20%": {
        "initial_gtv": 30_000,
        "perceived_value": 900,
        "gtv_volatility": 0.05
    },

    "Value -15%": {
        "initial_gtv": 25_000,
        "perceived_value": 765,
        "gtv_volatility": 0.05
    },

    "Volatilità ×2": {
        "initial_gtv": 25_000,
        "perceived_value": 900,
        "gtv_volatility": 0.10
    }
}

31. Scenario Analysis vs Monte Carlo

È importante non confondere i due strumenti.

Scenario Analysis

Risponde:

“Cosa succede se il GTV diminuisce del 20%?”

È una variazione deliberata di un parametro.

Monte Carlo

Risponde:

“Cosa succede se il GTV è incerto e può seguire molte traiettorie?”

È una rappresentazione della distribuzione dell’incertezza.

Possiamo quindi combinarli:

Scenario Base
     ↓
Monte Carlo
     ↓
Distribuzione NPV

Scenario GTV -20%
     ↓
Monte Carlo
     ↓
Distribuzione NPV

Scenario GTV +20%
     ↓
Monte Carlo
     ↓
Distribuzione NPV

Questa combinazione è molto più informativa di una semplice previsione puntuale.


32. Un modello simulativo non è automaticamente un modello causale

Questo punto merita particolare attenzione.

Supponiamo che il modello produca:

[math]Professional \rightarrow Churn più basso[/math]

Non possiamo concludere automaticamente che il piano Professional causi il churn più basso.

Potrebbe accadere che i clienti più fedeli siano semplicemente quelli che scelgono il Professional.

Quindi dobbiamo distinguere:

[math]P(Churn|Pricing,X)[/math]

da:

[math]P(Churn|do(Pricing))[/math]

La prima è una relazione predittiva.

La seconda riguarda un effetto causale.

Per stimare il secondo servirebbero, a seconda del contesto:

  • A/B test;
  • esperimenti controllati;
  • propensity score;
  • causal inference;
  • modelli longitudinali;
  • quasi-experiments.

Il modello Monte Carlo è quindi uno strumento di simulazione e decision support, non una prova automatica di causalità.


33. Dal modello simulato al modello reale

Finora abbiamo utilizzato parametri artificiali.

In un progetto reale dovremmo sostituirli progressivamente con stime ottenute dai dati.

Churn

Stimare:

[math]P(Churn|X)[/math]

utilizzando variabili come:

  • GTV;
  • Take Rate;
  • canone;
  • engagement;
  • tenure;
  • frequenza delle transazioni;
  • numero di utenti;
  • utilizzo delle funzionalità;
  • ticket di assistenza.

GTV

Stimare:

[math]GTV_{t+1}= f(GTV_t,Pricing_t,X_t)[/math]

Costi

Separare:

[math]Cost= FixedCost+VariableCost(GTV)+CostToServe[/math]

Valore percepito

Non deve necessariamente essere osservato direttamente.

Può essere stimato tramite proxy come:

  • utilizzo;
  • retention;
  • frequenza;
  • feature adoption;
  • survey;
  • NPS;
  • comportamento di upgrade/downgrade.

34. La pipeline reale

A questo punto la pipeline diventa:

                 DATI STORICI
                      │
                      ↓
             ┌──────────────────┐
             │ Modello di churn │
             └────────┬─────────┘
                      │
                      ↓
             ┌──────────────────┐
             │ Modello GTV     │
             └────────┬─────────┘
                      │
                      ↓
             ┌──────────────────┐
             │ Modello valore  │
             └────────┬─────────┘
                      │
                      ↓
                 SIMULAZIONE
                      │
                      ↓
               DISTRIBUZIONE
                  DI NPV
                      │
                      ↓
               OTTIMIZZAZIONE
                      │
                      ↓
                PRICING TEST
                      │
                      ↓
                NUOVI DATI
                      │
                      └──────────→ MODELLO

Questo è il punto in cui il pricing diventa realmente un problema di Data Science applicata alla decisione.


35. Un possibile ciclo sperimentale

Supponiamo di avere due pricing candidati:

Piano A

[math]300€+1,5%[/math]

Piano B

[math]400€+1,0%[/math]

Il modello simulativo può suggerire che B abbia un NPV atteso superiore.

Non dovremmo però saltare direttamente alla conclusione:

“Implementiamo B per tutti.”

Una strategia più rigorosa sarebbe:

Simulazione
     ↓
Pricing candidato
     ↓
Esperimento controllato
     ↓
Osservazione
     ↓
Aggiornamento dei parametri
     ↓
Nuova simulazione

La simulazione diventa quindi parte di un ciclo di apprendimento.


36. Un modello di Decision Intelligence

Possiamo ora rappresentare l’intero problema in un’unica struttura:

                         PRICING
                       F + r × GTV
                            │
                            ↓
                    Price / Value Ratio
                     ┌──────┴──────┐
                     ↓             ↓
                  CHURN      DISINTERMEDIAZIONE
                     │             │
                     ↓             ↓
                 RETENTION         GTV
                     │             │
                     └──────┬──────┘
                            ↓
                    REVENUE / COST
                            │
                            ↓
                           NPV
                            │
                    ┌───────┴───────┐
                    ↓               ↓
                RENDIMENTO        RISCHIO
                    │               │
                    └───────┬───────┘
                            ↓
                    DECISIONE PRICING

La Monte Carlo Simulation permette di propagare l’incertezza lungo tutta questa catena.


37. Cosa ci dice realmente il modello?

Il risultato finale non dovrebbe essere formulato come:

“Il pricing ottimale è 327,41 €.”

Una formulazione più corretta è:

“Date le distribuzioni e le relazioni assunte nel modello, questa regione di pricing produce il miglior compromesso simulato tra NPV atteso, retention e rischio.”

È una differenza sostanziale.

Il modello non scopre una verità universale.

Forse potrebbe interessarti anche:  Padroneggiare le Serie Temporali con Pandas: Convertire Datetime, Aggregare Dati con Tabelle Pivot e Altro

Esplora le conseguenze delle nostre ipotesi quantitative.


38. Le quattro domande che il modello permette di porre

Un buon modello di pricing dovrebbe consentire di rispondere almeno a quattro domande.

1. Quanto guadagniamo?

[math]E[NPV][/math]

2. Quanto è rischiosa la strategia?

[math]P(NPV<0),\quad VaR,\quad CVaR[/math]

3. Cosa succede alla retention?

[math]P(Churn),\quad Lifetime[/math]

4. Quanto è robusta la decisione?

Attraverso:

  • sensitivity analysis;
  • scenario analysis;
  • confronto tra distribuzioni;
  • regione di pricing robusto.

Queste quattro dimensioni sono molto più informative di una singola metrica come il margine mensile.

Pubblicità

39. I limiti del modello e il passaggio all’operatività

Un modello simulato ha valore didattico solo se accompagnato da una dichiarazione onesta dei suoi confini. Ecco cosa questo codice non sta facendo.

GTV. La dinamica

[math]\displaystyle GTV_{t+1} = GTV_t [1+g-\alpha PriceRatio_t + \epsilon_t][/math]

è una semplificazione utile, non un modello completo di disintermediazione. Mancano: elasticità prezzo stimata, stagionalità, trend di mercato, shock esterni, concentrazione del cliente e reazione dei concorrenti.

Valore percepito. La curva logaritmica

[math]\displaystyle V_t = V_0 [1+\gamma(1-e^{-kt})][/math]

è una rappresentazione concettuale. In produzione va calibrata su dati comportamentali, survey o proxy di adozione delle feature.

Churn. La funzione logistica utilizzata ha coefficienti simulati. Non è stata stimata su un dataset storico di clienti reali.

Parametri. Tutti i numeri — dal tasso di crescita del GTV alla volatilità, dal costo variabile all’engagement — sono ipotesi di lavoro. Devono essere sostituiti da stime empiriche prima di qualsiasi decisione commerciale.

Questo ci porta a una distinzione fondamentale.

Il codice dell’articolo può essere usato in due modalità.

Modalità illustrativa. Serve a comprendere la struttura del problema:

[math]\displaystyle Pricing \rightarrow Behavior \rightarrow Economics[/math]

I parametri sono simulati e l’obiettivo è costruire intuizione.

Modalità operativa. La stessa architettura viene calibrata su dati reali:

Dataset → Feature engineering → Modello churn → Modello GTV → Stima parametri → Monte Carlo → Ottimizzazione → Validazione

Solo questa seconda pipeline può produrre un pricing utilizzabile in produzione.

La vera lezione, però, è metodologica. Il punto più interessante non è l’algoritmo Monte Carlo in sé, ma la struttura del problema che esso permette di esplorare. Il pricing modifica il comportamento del cliente; il comportamento modifica il GTV; il GTV modifica il ricavo; il churn modifica la durata della relazione; la durata modifica il valore economico. L’incertezza attraversa ogni anello della catena.

Per questo una previsione puntuale:

[math]NPV=8.500€[/math]

è molto meno informativa della distribuzione:

[math]P(NPV \mid Pricing)[/math]

La simulazione non scopre un prezzo magico. Esplora le conseguenze quantitative delle nostre ipotesi e ci permette di chiedere: quanto guadagniamo, quanto è rischiosa la strategia, cosa succede alla retention, e quanto è robusta la decisione? Queste quattro domande trasformano il pricing da decisione commerciale in problema di Data Science, Simulation e Decision Intelligence.


40. Dal prezzo alla distribuzione degli esiti

Possiamo riassumere il modello con:

[math]\displaystyle \boxed{ Pricing \rightarrow Price/Value \rightarrow \begin{cases} Churn\\ Disintermediation \end{cases} \rightarrow GTV \rightarrow Revenue \rightarrow Margin \rightarrow NPV }[/math]

La Monte Carlo Simulation ripete questo processo migliaia di volte:

[math]\theta \rightarrow Simulation_1[/math]

[math]\theta \rightarrow Simulation_2[/math]

[math]\vdots[/math]

[math]\theta \rightarrow Simulation_M[/math]

ottenendo:

[math]\boxed{ P(NPV|\theta) }[/math]

dove:

[math]\theta=(F,r,\alpha,\beta,\sigma,\ldots)[/math]

è il vettore dei parametri del modello.


41. E infine l’ottimizzazione

A questo punto possiamo definire una funzione obiettivo:

[math]Utility(\theta)= E[NPV|\theta] – \lambda Risk(\theta)[/math]

e cercare:

[math]\boxed{ \theta^* = \arg\max_\theta Utility(\theta) }[/math]

In Python, una prima esplorazione può essere effettuata anche semplicemente tramite grid search.

Per un modello con due parametri:

best = None

for fee in fees:

    for rate in rates:

        # risultato Monte Carlo
        expected_npv = ...

        if (
            best is None
            or expected_npv > best["npv"]
        ):
            best = {
                "fee": fee,
                "rate": rate,
                "npv": expected_npv
            }

print(best)

Per problemi più complessi possiamo utilizzare scipy.optimize, Bayesian Optimization o altri metodi di ottimizzazione simulation-based.

Ma prima di scegliere un algoritmo sofisticato dobbiamo verificare che il modello economico sottostante sia sensato.

Un optimizer non può correggere un modello sbagliato.


42. Conclusioni

Il pricing B2B può iniziare con una formula estremamente semplice:

[math]C=F+rGTV[/math]

Il Tipping Point permette di individuare le soglie economiche tra diversi piani.

Ma il cliente non è statico.

Il prezzo influenza il rapporto tra costo e valore percepito:

[math]Price/Value[/math]

che può modificare:

  • probabilità di churn;
  • disintermediazione;
  • GTV;
  • lifetime;
  • profitto.

Introducendo queste dinamiche in Python possiamo costruire una simulazione:

[math]\displaystyle \boxed{ Pricing \rightarrow Churn \rightarrow GTV \rightarrow Cash Flow \rightarrow NPV }[/math]

La Monte Carlo Simulation permette poi di trasformare una singola previsione in una distribuzione:

[math]\boxed{ P(NPV|Pricing) }[/math]

e quindi di confrontare non soltanto il rendimento medio dei diversi piani, ma anche:

  • mediana;
  • probabilità di perdita;
  • lifetime;
  • VaR;
  • CVaR;
  • sensibilità alle assunzioni.

Infine, esplorando lo spazio:

[math](F,r)[/math]

possiamo costruire una heatmap e identificare non necessariamente un singolo prezzo, ma una regione di pricing robusto.

Il risultato più interessante, però, è metodologico.

Un sistema di pricing data-driven non dovrebbe limitarsi a chiedere:

“Quanto dobbiamo far pagare?”

Dovrebbe chiedere:

“Come cambia il comportamento del cliente se modifichiamo il prezzo, quali conseguenze economiche produce questa modifica e quanto è robusta la decisione rispetto all’incertezza?”

La pipeline diventa quindi:

[math]\displaystyle \boxed{ \text{Dati} \rightarrow \text{Modello} \rightarrow \text{Simulazione} \rightarrow \text{Scenario Analysis} \rightarrow \text{Ottimizzazione} \rightarrow \text{Esperimento} \rightarrow \text{Apprendimento} }[/math]

Ed è qui che il pricing smette di essere soltanto una decisione commerciale e diventa un problema di Data Science, Simulation e Decision Intelligence.

Nota metodologica

Tutti i parametri numerici utilizzati negli esempi Python sono ipotetici e hanno finalità esclusivamente didattiche. Gli output prodotti da Monte Carlo dipendono dalle distribuzioni, dai coefficienti e dal seed utilizzati. In un caso reale, i parametri di churn, dinamica del GTV, disintermediazione, valore percepito e costi dovrebbero essere stimati o calibrati su dati osservazionali e, quando possibile, validati mediante esperimenti controllati.

Il codice mostrato nell’articolo non deve quindi essere interpretato come un modello predittivo già validato, ma come un framework riproducibile da calibrare sui dati reali dell’azienda.

📚 CLV, SaaS Analytics e Decisioni Strategiche Data-Driven

Valutare un business digitale richiede di collegare il valore del cliente, l’efficacia dei canali di acquisizione e la sostenibilità economica della crescita. Queste guide approfondiscono modelli quantitativi e strumenti di analytics per stimare il Customer Lifetime Value, attribuire correttamente le conversioni e valutare le strategie SaaS in condizioni di incertezza.

👉Comprendere il modello Pareto/NBD per la valutazione della Customer Lifetime Value (CLV)

👉Attribuzione Multi-Touch nel SaaS B2B: Catene di Markov, Shapley Value e calcolo del CAC

👉Attribuzione Multi-Touch nel SaaS B2B – Parte 2: guida completa a Markov, Shapley e calcolo del CAC

👉Rule of 40 nel SaaS: la formula che gli investitori usano per valutare un business

👉First Mover vs Fast Follower: come misurare il rischio strategico con la simulazione Monte Carlo

Pubblicità