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?
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:
| 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.
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.
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.
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







