3. Measuring in Money#
A cost matrix is not yet a number. A strategy decides how to turn it into one, and different strategies answer different questions. This page walks through all three on the same predictions, so the differences are concrete.
3.1. Setup#
import pandas as pd
from empulse.datasets import fetch_iranian_churn
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
dataset = fetch_iranian_churn(backend=pd)
X, y = dataset.data, dataset.target
clv = dataset.instance_costs['clv']
X_train, X_test, y_train, y_test, clv_train, clv_test = train_test_split(
X, y, clv, test_size=0.3, random_state=42, stratify=y
)
baseline = Pipeline([
('scaler', StandardScaler()),
('model', LogisticRegression(max_iter=1000)),
]).fit(X_train, y_train)
y_score = baseline.predict_proba(X_test)[:, 1]
3.2. Expected cost: what does this model cost me?#
The Cost strategy reports the expected cost per customer, in currency.
Lower is better, and negative means profit.
from empulse.metrics import Cost, Metric
expected_cost = Metric(dataset.cost_matrix, Cost())
print(f'{expected_cost(y_test, y_score, clv=clv_test):.2f}')
About 7.20 per customer. This is the most directly interpretable number: multiply by your customer base and you have the campaign’s bottom line.
Because the business parameters are symbolic, you can ask what-if questions without retraining anything:
generous = expected_cost(
y_test, y_score, clv=clv_test, incentive_fraction=0.15, accept_rate=0.6
)
print(f'{generous:.2f}')
A more generous offer that more people accept changes the economics — and the optimal customers to target change with it.
3.3. Savings: how good is that, really?#
7.20 is hard to judge in isolation. The Savings strategy divides by a
naive baseline, giving a scale-free ratio comparable across datasets and campaigns — much as
\(R^2\) does for regression.
from empulse.metrics import Savings
savings = Metric(dataset.cost_matrix, Savings())
print(f'{savings(y_test, y_score, clv=clv_test):.4f}')
About 0.63: the model captures 63% of the achievable saving relative to the naive rule of treating everyone the same way. Higher is better, 1.0 is perfect, and 0 means no better than the naive baseline.
3.4. Maximum profit: what if the threshold is not fixed?#
Both metrics above ask “how much does this model cost, given how it decides?” But the decision threshold is a choice, and 0.5 is rarely the right one.
The MaxProfit strategy asks a different question: at the best possible
cut-off, how much can this model make? It needs only a ranking, not calibrated probabilities.
from empulse.metrics import MaxProfit
max_profit = Metric(dataset.cost_matrix, MaxProfit())
print(f'{max_profit(y_test, y_score, clv=clv_test):.2f}')
3.5. Handling uncertainty in the business parameters#
So far accept_rate has been exactly 0.3. Nobody knows it that precisely — it is an estimate,
and the profit you report inherits its uncertainty.
The maximum-profit framework handles this by letting a parameter be a random variable rather
than a number. Instead of committing to 0.3, model the acceptance rate as a Beta distribution and
integrate over it. That is the Expected Maximum Profit for Customer Churn, and Empulse ships it
prebuilt as empc_score.
from empulse.metrics import empc_score
print(f'{empc_score(y_test, y_score, clv=clv_test):.2f}')
About 18.00 expected profit per customer, averaged over plausible acceptance rates rather than
betting on a single guess. Its defaults are alpha=6, beta=14, a Beta distribution with mean
\(6/(6+14) = 0.3\) — the same 0.3 as before, but now with honest uncertainty around it.
Note
The profit numbers (18.00) and the cost numbers (7.20) are not comparable: they answer different questions. Cost measures the model as it decides today; maximum profit measures it at its best possible cut-off. Comparing models is only meaningful within one strategy.
3.6. Which should you use?#
3.7. Using them as scikit-learn scorers#
Any metric can be wrapped with make_scorer and used anywhere scikit-learn
accepts a scorer:
from sklearn.metrics import make_scorer
savings_scorer = make_scorer(
savings, response_method='predict_proba', greater_is_better=True
)
Validating the Whole Thing shows how to route the per-customer clv through cross-validation so the
scorer sees the right values for each fold.
3.8. Next#
Training on the Cost Matrix stops merely measuring the cost and starts optimising it.