Calibration Curves: When Your Model's Probabilities Are Lying to You
1. The Probability Trap: Why Your Model’s Confidence Is Fake
Imagine you are building a model to predict whether a loan applicant will default. Your model looks at a new application and says there is a 90% probability of successful repayment. You feel great. You approve the loan.
But here is the catch: what if, among all the people your model gave a “90% probability” to, only 60% actually paid back? Then your model isn’t just wrong. It is overconfident — claiming certainty it can’t back up.
Most of us focus on Accuracy (how often is the model right?) or AUC (how well does it rank people?). We often forget Calibration. Calibration is about honesty. If a weather forecaster says there is a 70% chance of rain every day for a month, it better rain on exactly 21 of those 30 days. If it only rains on 10, the forecaster is miscalibrated.
Let’s check whether a standard model is honest with us.
import numpy as np
import pandas as pd
from sklearn.datasets import make_classification
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
# Create a fake dataset
X, y = make_classification(n_samples=10000, n_features=20, n_informative=2, n_redundant=10, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# Train a Random Forest
model = RandomForestClassifier(random_state=42)
model.fit(X_train, y_train)
# Get probabilities for the positive class
probs = model.predict_proba(X_test)[:, 1]
# Look at cases where the model is very confident ( > 90%)
high_conf_mask = probs > 0.9
actual_rate = y_test[high_conf_mask].mean()
mean_predicted = probs[high_conf_mask].mean()
print(f"When the model says >90% probability, the actual success rate is: {actual_rate:.2%}")
print(f"Average predicted probability in that group: {mean_predicted:.2%}")
# Output: When the model says >90% probability, the actual success rate is: 96.97%
# Output: Average predicted probability in that group: 97.73%
Run it yourself and the story isn’t the dramatic one you might expect. The actual success rate among these confident predictions is 96.97%, barely three-quarters of a point below the 97.73% average probability the model assigned — well within noise for a group this size (1,088 predictions). This particular Random Forest came out close to honest on this data. That’s not a guarantee: Random Forests don’t have to be well calibrated (Section 4 explains why), and it’s easy to build one that skews a lot more confident than this. The rest of this article shows you how to check for yourself, rather than assume either way.
2. Calibration in One Sentence: What We’re Actually Measuring
Calibration is a reality check for your model. To measure it, we group predictions into buckets.
Everything the model predicted between 0% and 10% goes in Bucket A. Then 10% to 20% in Bucket B, and so on. For each bucket, we calculate the actual percentage of positive outcomes.
If the model is perfectly calibrated, the average predicted probability in the bucket should equal the actual fraction of positives. So let’s work through it manually.
# Create bins
bins = np.linspace(0, 1, 11)
df = pd.DataFrame({'pred': probs, 'actual': y_test})
df['bin'] = pd.cut(df['pred'], bins)
# Calculate the average prediction vs actual outcome per bin
calibration_data = df.groupby('bin', observed=True).agg({'pred': 'mean', 'actual': 'mean'})
print(calibration_data)
The output has a pred column (what the model thought) and an actual column (what really happened). If pred is 0.75 and actual is 0.60, the model is over-promising.
3. The Calibration Curve: Reading the Graph
Rather than scan raw tables, we plot a Calibration Curve (also called a Reliability Diagram).
- The Diagonal Line: The “Perfectly Calibrated” line. A model sitting here is 100% honest.
- Below the Diagonal: The model is overconfident. It says 80%, but the truth is 60%.
- Above the Diagonal: The model is underconfident. It says 20%, but the truth is 40%.
import matplotlib.pyplot as plt
from sklearn.calibration import calibration_curve
prob_true, prob_pred = calibration_curve(y_test, probs, n_bins=10)
plt.plot(prob_pred, prob_true, marker='o', label='Random Forest')
plt.plot([0, 1], [0, 1], linestyle='--', label='Perfectly Calibrated')
plt.xlabel('Mean Predicted Probability')
plt.ylabel('Fraction of Positives')
plt.legend()
plt.show()
The Random Forest curve usually takes an “S” shape. Too cautious near the middle, too extreme at the edges.
4. Why Do Models Lie? Common Causes of Miscalibration
Why doesn’t every model just tell the truth?
- Algorithm Design: Logistic Regression maximizes the likelihood of the data, which naturally produces well-calibrated probabilities. Random Forests take a different route — they average votes. If 90 out of 100 trees pick “Class A,” you get 0.9. But those trees are correlated, so that “90%” isn’t a true statistical probability.
- Overfitting: When a model memorizes noise in the training data, it grows overly “certain” about its decisions, pushing probabilities toward 0 or 1.
- Imbalanced Data: If only 1% of your data is the positive class, models struggle to find the right “baseline” for their confidence.
5. Fixing It: Calibration Methods That Actually Work
The good news: you don’t have to retrain your whole model. A post-processing step does the job.
Two main approaches fix a lying model:
- Platt Scaling: Train a tiny Logistic Regression on top of your model’s outputs. It learns to “stretch” or “squish” the probabilities back toward reality.
- Isotonic Regression: More flexible — it fits a non-decreasing line to the data. Works well with plenty of examples, but it can overfit on small samples.
from sklearn.calibration import CalibratedClassifierCV
from sklearn.frozen import FrozenEstimator
# Platt Scaling (method='sigmoid') — wrap the already-fitted model in
# FrozenEstimator so CalibratedClassifierCV only fits the calibration step
calibrated_platt = CalibratedClassifierCV(FrozenEstimator(model), method='sigmoid')
calibrated_platt.fit(X_test, y_test) # Usually you use a separate validation set here!
# Isotonic Regression
calibrated_iso = CalibratedClassifierCV(FrozenEstimator(model), method='isotonic')
calibrated_iso.fit(X_test, y_test)
# Let's see the improvement
probs_platt = calibrated_platt.predict_proba(X_test)[:, 1]
prob_true_platt, prob_pred_platt = calibration_curve(y_test, probs_platt, n_bins=10)
plt.plot(prob_pred_platt, prob_true_platt, marker='s', label='Platt Scaled')
plt.plot([0, 1], [0, 1], linestyle='--')
plt.legend()
plt.show()
6. When to Calibrate (and When Not To)
Do you always need to do this? No.
Calibration matters if:
- You’re making decisions based on a threshold (e.g., “Only call customers with a >15% churn risk”).
- You’re calculating expected value (e.g., “Probability of Sale * Price”).
- You’re combining multiple models.
Calibration doesn’t matter if:
- You only care about ranking. If you just need to know who is more likely to buy than someone else, the exact probability won’t change that order.
7. Putting It Together: A Real Workflow
In a real project, you’ll want three data splits: Train for building the model, Calibration for fixing the probabilities, and Test to see how you actually did.
# 1. Split data
X_temp, X_test, y_temp, y_test = train_test_split(X, y, test_size=0.2)
X_train, X_calib, y_train, y_calib = train_test_split(X_temp, y_temp, test_size=0.2)
# 2. Train model
rf = RandomForestClassifier().fit(X_train, y_train)
# 3. Calibrate on the calibration set
calibrator = CalibratedClassifierCV(FrozenEstimator(rf), method='isotonic')
calibrator.fit(X_calib, y_calib)
# 4. Evaluate on the final test set
final_probs = calibrator.predict_proba(X_test)[:, 1]
8. The Catch: Calibration Isn’t Magic
Here’s the hard truth: calibration does not make your model smarter.
A low-AUC model that can’t tell good loans from bad ones stays that way after calibration. What you get is an “honestly bad” model. Instead of confidently giving the wrong answer, it tells you, “I’m not sure, it’s about a 50/50 chance.”
Worth noting: calibrating on a tiny dataset can mean fitting the noise. Always check your calibration on fresh data.
9. Next Steps: Calibration in Production
Real-world data shifts. That’s Concept Drift. A model perfectly calibrated in January can turn overconfident by June as customer behavior changes.
To handle this:
- Monitor ECE: Track the Expected Calibration Error — the average gap between the curve and the diagonal.
- Recalibrate often: You don’t always need to retrain the heavy Random Forest. Sometimes updating just the Platt Scaling (the Logistic Regression on top) is enough to fix the drift.
Summary:
- Accuracy tells you whether the model is right; calibration tells you if it knows when it’s right.
- Use
calibration_curveto visualize how honest your model is. - Use
CalibratedClassifierCVto fix overconfident or underconfident models. - Calibration matters for risk management and financial decisions.
Check Your Understanding
Questions below move from simple recall up to open-ended design, roughly following Bloom’s Taxonomy.
Remember What does it mean for a model to be “overconfident” versus “underconfident” on a calibration curve, relative to the diagonal line?
Understand In your own words, explain why Random Forests tend to be less well-calibrated than Logistic Regression, using the article’s explanation of how each algorithm produces its probability.
Apply
Using the article’s bucket logic (average predicted probability vs. actual positive rate), if a bucket of predictions has a mean predicted probability of 0.75 but the actual positive rate in that bucket is 0.55, is the model overconfident or underconfident for that bucket, and by how many percentage points?
Analyze The article says calibration “does not make your model smarter” — a low-AUC model just becomes “honestly bad” instead of confidently wrong. Walk through why fixing calibration (a post-processing step on probabilities) can’t improve a model’s ability to rank good loans above bad ones, even though it changes the actual probability numbers.
Evaluate
The article’s Section 7 recommends a three-way split: Train, Calibration, and Test. Critique the code in Section 5, which instead calibrates and evaluates CalibratedClassifierCV on the same X_test/y_test. What specifically goes wrong with a calibration curve that’s plotted using the same data the calibrator was fit on?
Create Design a monitoring plan for calibration drift in production for a credit-risk model: what would you track (referencing the article’s “Expected Calibration Error” idea), how often would you check it, and what would trigger a recalibration versus a full model retrain?
Apply What You Learned is for Supporter and Insider subscribers.
Subscribe to unlock the exercises on this post.
See plansRelated articles
- Machine Learning Under review
Learning Curves: How to Read Your Model's Mind to Fix Overfitting and Underfitting
Learn to read learning curves to diagnose overfitting and underfitting, tell high bias from high variance, and pick the right fix to boost your model.
- Machine Learning Under review
Feature Scaling: Why Your Model Might Be Ignoring Half Your Data
Unscaled features silently skew your models: learn why KNN and SVM ignore small-range variables and how StandardScaler and MinMaxScaler fix it in Python.
- Machine Learning Under review
Nested Cross-Validation: How to Tune Your Model Without Lying to Yourself
Learn how nested cross-validation prevents optimistic bias when tuning hyperparameters, giving you an honest estimate of model performance before shipping to production.
- Machine Learning Under review
Data Leakage: Why Your 'Perfect' Model is Probably Lying to You
Learn how data leakage silently sabotages your machine learning models with 100% accuracy that fails in production, and discover three rules to leak-proof your workflow.
Looking for something else?
Search every article by title, summary or topic.