Classifier probabilities are easiest to trust when they line up with observed event rates. In scikit-learn, calibration fits a post-processing model around a classifier so predict_proba() values can be checked as probabilities rather than only as ranking scores.
CalibratedClassifierCV trains classifier and calibrator pairs with cross-validation. The base estimator still learns the decision pattern, while the calibrator maps its score output onto probabilities using a method such as sigmoid calibration.
Use data that was not used to fit the base classifier or the calibrator when judging the result. Brier loss and log loss check probability quality, while ROC AUC can stay similar because it measures ranking rather than probability scale.
import numpy as np import sklearn from sklearn.calibration import CalibratedClassifierCV, calibration_curve from sklearn.datasets import make_classification from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import brier_score_loss, log_loss, roc_auc_score from sklearn.model_selection import train_test_split X, y = make_classification( n_samples=4_000, n_features=20, n_informative=7, n_redundant=5, weights=[0.78, 0.22], class_sep=0.65, flip_y=0.08, random_state=42, ) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.25, stratify=y, random_state=42, ) base_classifier = RandomForestClassifier( n_estimators=200, min_samples_leaf=8, random_state=42, ) base_classifier.fit(X_train, y_train) base_probability = base_classifier.predict_proba(X_test)[:, 1] calibrated_classifier = CalibratedClassifierCV( estimator=RandomForestClassifier( n_estimators=200, min_samples_leaf=8, random_state=42, ), method="sigmoid", cv=5, ) calibrated_classifier.fit(X_train, y_train) calibrated_probability = calibrated_classifier.predict_proba(X_test)[:, 1] def print_scores(name, probability): print(f"{name} Brier loss: {brier_score_loss(y_test, probability):.4f}") print(f"{name} log loss: {log_loss(y_test, probability):.4f}") print(f"{name} ROC AUC: {roc_auc_score(y_test, probability):.4f}") print(f"scikit-learn {sklearn.__version__}") print(f"training rows: {X_train.shape[0]}") print(f"test rows: {X_test.shape[0]}") print(f"calibration folds: {len(calibrated_classifier.calibrated_classifiers_)}") print() print_scores("uncalibrated", base_probability) print_scores("calibrated", calibrated_probability) print() print("calibrated probability bins:") fraction_positive, mean_predicted = calibration_curve( y_test, calibrated_probability, n_bins=5, strategy="quantile", ) for bin_number, (predicted, observed) in enumerate( zip(mean_predicted, fraction_positive), start=1, ): print( f"bin {bin_number}: " f"mean predicted={predicted:.3f}, " f"observed positive rate={observed:.3f}" ) print() print("first five calibrated probabilities:") print(np.array2string(calibrated_probability[:5], precision=3, floatmode="fixed"))
CalibratedClassifierCV uses stratified 5-fold cross-validation by default for binary and multiclass labels when cv is an integer or left unset. The explicit cv=5 keeps that split count visible in the output.
$ python3 calibrate_classifier.py scikit-learn 1.9.0 training rows: 3000 test rows: 1000 calibration folds: 5 uncalibrated Brier loss: 0.1076 uncalibrated log loss: 0.3655 uncalibrated ROC AUC: 0.8831 calibrated Brier loss: 0.0949 calibrated log loss: 0.3281 calibrated ROC AUC: 0.8782 calibrated probability bins: ##### snipped ##### first five calibrated probabilities: [0.122 0.083 0.057 0.198 0.079]
The calibrated run lowers Brier loss from 0.1076 to 0.0949 and log loss from 0.3655 to 0.3281 on held-out rows. ROC AUC moves from 0.8831 to 0.8782 because calibration can change probability scale without improving class ranking.
calibrated probability bins: bin 1: mean predicted=0.040, observed positive rate=0.050 bin 2: mean predicted=0.072, observed positive rate=0.065 bin 3: mean predicted=0.124, observed positive rate=0.090 bin 4: mean predicted=0.250, observed positive rate=0.210 bin 5: mean predicted=0.785, observed positive rate=0.815
Closer predicted and observed rates indicate better calibration for that bin. Increase n_bins only when the held-out set is large enough to leave enough rows in each bin.
first five calibrated probabilities: [0.122 0.083 0.057 0.198 0.079]
Use calibrated_classifier.predict_proba(new_rows)[:, 1] for downstream probability thresholds after held-out Brier loss and log loss support the calibrated model.
Related: How to tune a classifier decision threshold in scikit-learn