Probability Calibration

A probability model is calibrated when cases assigned probability 0.8 are positive 80% of the time in the population, and the same agreement holds at its other probability levels. A finite sample only estimates this agreement. Calibration differs from ranking: a model can rank cases well while systematically overstating their probabilities. The classification-metrics article uses probabilities to compare decision costs; here we examine whether those probabilities are reliable.

Reading a reliability curve

A reliability curve groups predictions into bins and plots each bin’s mean predicted probability on the horizontal axis and observed positive fraction on the vertical axis. Points above the diagonal indicate under-prediction, and points below indicate over-prediction. For example, a bin averaging 0.25 with 12 positives among 100 cases has an observed frequency of 0.12 and a gap of −0.13. Bin frequencies fluctuate with sample size; a point off the diagonal is an estimate, not proof of a population gap. Report bin counts when assessing how precise those estimates are.

import numpy as np
from sklearn.datasets import make_classification
from sklearn.naive_bayes import GaussianNB
from sklearn.calibration import calibration_curve
from sklearn.model_selection import train_test_split

rng = np.random.default_rng(0)
X0, y = make_classification(n_samples=30000, n_features=8, n_informative=5,
                            n_redundant=0, class_sep=0.8, random_state=0)
# 20 noisy copies violate naive Bayes conditional independence
X = np.hstack([X0, X0[:, [0]] + rng.normal(0, 0.35, (30000, 20))])
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.5, random_state=0)

p = GaussianNB().fit(Xtr, ytr).predict_proba(Xte)[:, 1]
observed, predicted = calibration_curve(yte, p, n_bins=10, strategy="uniform")
counts = np.bincount(np.searchsorted(np.linspace(0, 1, 11)[1:-1], p), minlength=10)
for count, o, q in zip(counts[counts > 0], observed, predicted):
    print(f"n {count:5d}   predicted {q:.4f}   observed {o:.4f}   gap {o - q:+.4f}")
# n  2840   predicted 0.0301   observed 0.0359   gap +0.0059
# n  1100   predicted 0.1472   observed 0.1082   gap -0.0390
# n   894   predicted 0.2499   observed 0.1230   gap -0.1269
# n   827   predicted 0.3493   observed 0.1995   gap -0.1498
# n   896   predicted 0.4515   observed 0.2980   gap -0.1535
# n  1054   predicted 0.5521   observed 0.4858   gap -0.0663
# n  1367   predicted 0.6540   observed 0.6452   gap -0.0088
# n  1962   predicted 0.7518   observed 0.8048   gap +0.0530
# n  2163   predicted 0.8517   observed 0.9011   gap +0.0493
# n  1897   predicted 0.9386   observed 0.9805   gap +0.0419

Gaussian naive Bayes models features as conditionally independent given the class. The 20 noisy copies are dependent on the original feature, so multiplying their likelihood contributions treats related measurements as separate evidence. They are not 21 identical likelihood factors, and this experiment does not isolate duplication as the sole cause of the curve. In the displayed bins, predicted probabilities around 0.15–0.55 exceed observed frequencies by as much as 0.154, while bins above 0.75 underpredict by about 0.04–0.05. A signed average gap could cancel opposing errors; an absolute-gap summary would not cancel them across bins.

Brier, log loss, and what they weigh

ScoreDefinitionBehavior
Brier\(\frac{1}{n}\sum (p_i-y_i)^2\)bounded in \([0,1]\); a confident mistake costs at most 1
Log loss\(-\frac{1}{n}\sum [y_i \log p_i + (1-y_i)\log(1-p_i)]\)unbounded; a confident mistake can cost arbitrarily much

Both are strictly proper scoring rules: their population expected loss is uniquely minimized by the true conditional probability. That property specifies a target; finite data, a restricted model, or regularization can still produce miscalibrated fitted probabilities. Brier and log loss assess probability quality, but neither isolates calibration from the model’s ability to distinguish cases. Use reliability diagnostics alongside them, and evaluate decision costs separately when those costs define the application.

Log loss penalizes extreme errors more heavily. For an actual positive, a report of 0.01 gives Brier loss 0.9801 and log loss about 4.605; at 0.000001 these become about 1 and 13.816. Their numerical scales differ, so ratios between the two metrics are not meaningful. In exercise 2, log loss increases more than ninefold between two calibration procedures, while Brier increases by about 15%. Neither percentage alone specifies an application’s cost.

Platt scaling and isotonic regression

A calibrator is another fitted model that maps a base score to a probability. Fit it on predictions for cases excluded from base-model fitting, then evaluate it on separate data. Sigmoid calibration fits \(r(s)=1/(1+\exp[-(as+b)])\); isotonic regression fits a non-decreasing mapping, allowing more flexibility. Sigmoid calibration is strictly increasing when \(a>0\); its fitted slope is not constrained to be positive in this implementation. A decreasing map would reverse the ranking. Isotonic fits pooled levels at observed scores; scikit-learn interpolates between fitted thresholds for new scores.

from sklearn.calibration import CalibratedClassifierCV
from sklearn.ensemble import RandomForestClassifier
from sklearn.frozen import FrozenEstimator          # sklearn >= 1.6; replaces cv="prefit"
from sklearn.metrics import brier_score_loss, log_loss, roc_auc_score

Xcal, Xte2, ycal, yte2 = train_test_split(Xte, yte, test_size=0.5, random_state=0)
rf = RandomForestClassifier(n_estimators=300, random_state=0, n_jobs=-1).fit(Xtr, ytr)

p_raw = rf.predict_proba(Xte2)[:, 1]
print(f"{'raw':9s} AUC {roc_auc_score(yte2, p_raw):.6f}"
      f"  Brier {brier_score_loss(yte2, p_raw):.4f}  logloss {log_loss(yte2, p_raw):.4f}")
for method in ("sigmoid", "isotonic"):
    cal = CalibratedClassifierCV(FrozenEstimator(rf), method=method).fit(Xcal, ycal)
    p = cal.predict_proba(Xte2)[:, 1]
    print(f"{method:9s} AUC {roc_auc_score(yte2, p):.6f}"
          f"  Brier {brier_score_loss(yte2, p):.4f}  logloss {log_loss(yte2, p):.4f}")
# raw       AUC 0.974098  Brier 0.0690  logloss 0.2500
# sigmoid   AUC 0.974098  Brier 0.0590  logloss 0.1996
# isotonic  AUC 0.973380  Brier 0.0587  logloss 0.2031

On the separate evaluation subset, sigmoid reduces log loss from 0.2500 to 0.1996, about 20%, and preserves the reported AUC. Its increasing fitted map preserves ranking mathematically, although numerical saturation can create ties. Isotonic has the slightly lower Brier score here, 0.0587 against 0.0590, but higher log loss, 0.2031 against 0.1996. Its AUC falls from 0.974098 to 0.973380 as scores are pooled into ties. Such ties can raise or lower AUC depending on which positive–negative pairs are merged; the decrease is an observation from this run.

The code requires scikit-learn 1.6 or later for FrozenEstimator, which keeps the already-fitted forest fixed while the calibrator is fitted. Here the original split supplies 15,000 base-training rows; the remaining rows are divided into 7,500 calibration and 7,500 evaluation rows. The examples reuse data for demonstration, so they are not a sequence of independent final tests. In an actual development workflow, choose the calibration method without repeatedly consulting the final test set. Passing an unfitted estimator with integer cv supports cross-validated calibration; with ensemble=True, it fits a base-model/calibrator pair on complementary portions in each fold and averages their predictions. The split must still respect groups and time.

ECE is not a single number

Expected calibration error (ECE) here is the sample-weighted mean absolute bin gap: \(\sum_b(n_b/n)|\bar y_b-\bar p_b|\). A bin containing 20% of the cases with a gap of 0.1 contributes 0.02. Uniform bins divide the probability range into equal widths; quantile bins aim for similar case counts. Repeated scores can make quantile edges coincide, leaving fewer nonempty bins than requested.

def ece(y, p, n_bins, strategy="uniform"):
    edges = (np.linspace(0, 1, n_bins + 1) if strategy == "uniform"
             else np.quantile(p, np.linspace(0, 1, n_bins + 1)))
    idx = np.clip(np.searchsorted(edges, p, side="right") - 1, 0, n_bins - 1)
    return sum((idx == b).mean() * abs(y[idx == b].mean() - p[idx == b].mean())
               for b in range(n_bins) if (idx == b).sum())

p_nb = GaussianNB().fit(Xtr, ytr).predict_proba(Xte)[:, 1]
for nb in (5, 10, 20, 50, 100, 500):
    print(f"bins {nb:4d}   ECE uniform {ece(yte, p_nb, nb):.4f}"
          f"   ECE quantile {ece(yte, p_nb, nb, 'q'):.4f}")
# bins    5   ECE uniform 0.0499   ECE quantile 0.0530
# bins   10   ECE uniform 0.0538   ECE quantile 0.0534
# bins   20   ECE uniform 0.0564   ECE quantile 0.0561
# bins   50   ECE uniform 0.0573   ECE quantile 0.0566
# bins  100   ECE uniform 0.0586   ECE quantile 0.0580
# bins  500   ECE uniform 0.0692   ECE quantile 0.0712

On these predictions, uniform-bin ECE rises from 0.0499 with five bins to 0.0692 with 500, so the five-bin figure is about 28% lower. This change does not identify a single cause. Coarse bins can hide variation within a bin, while fine bins have fewer observations and noisier positive fractions. Finite-sample variation and binning can bias ECE estimates; even calibrated probabilities can yield nonzero empirical ECE. The opposing gaps below 0.6 and above 0.75 are mostly in different coarse bins, so that pattern alone does not explain the increase. With bins that are not nested, changing their number can also change their boundaries; ECE need not rise monotonically in general.

Report the bin count, edge strategy, and evaluation sample size with ECE. Inspect the reliability curve and the number of observations supporting each region. Brier and log loss avoid binning, but they measure overall probability quality and therefore answer a different question. A useful evaluation can include both kinds of evidence.

Two models, one dataset, different calibration

The next example fits gradient boosting and a random forest on the same credit-default training data and uses identical probability-band boundaries. The models differ in both ranking and probability estimation, and their bands contain different accounts. The fairlearn dataset fetch needs internet access on its first call.

import numpy as np
from fairlearn.datasets import fetch_credit_card
from sklearn.model_selection import train_test_split
from sklearn.ensemble import HistGradientBoostingClassifier, RandomForestClassifier
from sklearn.metrics import roc_auc_score, brier_score_loss

cc = fetch_credit_card(as_frame=True)
X = cc.data.astype(float).to_numpy()
y = cc.target.astype(int).to_numpy()
Xtr, Xte, ytr, yte = train_test_split(
    X, y, test_size=0.3, random_state=0, stratify=y)

hgb = HistGradientBoostingClassifier(random_state=0).fit(Xtr, ytr).predict_proba(Xte)[:, 1]
rf = RandomForestClassifier(n_estimators=200, random_state=0,
                            n_jobs=-1).fit(Xtr, ytr).predict_proba(Xte)[:, 1]

for name, q in (("HistGradientBoosting", hgb), ("RandomForest", rf)):
    print(f"{name:22s} AUC {roc_auc_score(yte, q):.4f}  Brier {brier_score_loss(yte, q):.4f}")
    for lo, hi in [(0.0, 0.1), (0.1, 0.3), (0.3, 0.5), (0.5, 0.7), (0.7, 1.01)]:
        k = (q >= lo) & (q < hi)
        if k.sum() > 30:
            print(f"   band {lo:.1f}-{hi:.1f}  n {k.sum():5,}  predicted {q[k].mean():.4f}"
                  f"  observed {yte[k].mean():.4f}  gap {yte[k].mean() - q[k].mean():+.4f}")
# HistGradientBoosting   AUC 0.7765  Brier 0.1347
#    band 0.0-0.1  n 2,578  predicted 0.0656  observed 0.0671  gap +0.0015
#    band 0.1-0.3  n 4,431  predicted 0.1681  observed 0.1690  gap +0.0009
#    band 0.3-0.5  n   938  predicted 0.3775  observed 0.3785  gap +0.0009
#    band 0.5-0.7  n   587  predicted 0.6113  observed 0.6201  gap +0.0088
#    band 0.7-1.0  n   466  predicted 0.7588  observed 0.7511  gap -0.0078
# RandomForest           AUC 0.7627  Brier 0.1392
#    band 0.0-0.1  n 2,427  predicted 0.0572  observed 0.0746  gap +0.0174
#    band 0.1-0.3  n 4,314  predicted 0.1763  observed 0.1627  gap -0.0136
#    band 0.3-0.5  n 1,117  predicted 0.3861  observed 0.3491  gap -0.0370
#    band 0.5-0.7  n   707  predicted 0.5944  observed 0.5573  gap -0.0371
#    band 0.7-1.0  n   435  predicted 0.7907  observed 0.7448  gap -0.0459

The boosted model’s five observed band gaps are small, with maximum absolute gap 0.0088. This is evidence at the chosen bin resolution on one test sample, not calibration everywhere. Broad bands can hide within-band patterns or subgroup differences. Log-loss training targets conditional probabilities, but does not guarantee a calibrated fitted model.

The forest underpredicts in the lowest band by 0.0174 and overpredicts in the other four bands by 0.0136–0.0459. In its top band, the average report is 0.7907 and the observed default fraction is 0.7448. These are empirical default frequencies, not classification accuracies. Whether that band drives a decision depends on the policy and its costs.

The boosted model has better AUC and Brier scores in this run. Their gaps are on different scales and cannot be compared as measures of how much ranking or calibration deteriorated. Similar overall AUCs do not establish similar precision among the top few percent; evaluate that subset directly if it is the operating point. For expected monetary loss, case probabilities must be combined with the relevant loss amounts. Positive and negative probability errors may offset or accumulate depending on their signs and those amounts; these band averages do not determine the portfolio error.

Exercises

1. Which calibrator, and why. Compare Platt scaling and isotonic regression across calibration-set sizes, under two different kinds of miscalibration.

You should get: different average Brier rankings under the two distortions. Consider the calibration family, sample size, and information retained in the score.

Solution
import numpy as np
from sklearn.isotonic import IsotonicRegression
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import brier_score_loss

def logit(p):
    p = np.clip(p, 1e-6, 1 - 1e-6)
    return np.log(p / (1 - p))

def run(distort, label):
    print(f"\n{label}")
    print(f"{'n_cal':>7} {'raw':>8} {'sigmoid':>9} {'isotonic':>9}   winner")
    for n_cal in (200, 1000, 5000, 20000):
        s_, i_, r_ = [], [], []
        for rep in range(15):
            g = np.random.default_rng(rep)
            p_true = g.beta(2, 2, n_cal); y = (g.random(n_cal) < p_true).astype(int)
            score = distort(p_true)
            pt = g.beta(2, 2, 40000); yt = (g.random(40000) < pt).astype(int)
            st = distort(pt)
            r_.append(brier_score_loss(yt, np.clip(st, 1e-6, 1 - 1e-6)))
            lr = LogisticRegression().fit(logit(score).reshape(-1, 1), y)
            s_.append(brier_score_loss(yt, lr.predict_proba(logit(st).reshape(-1, 1))[:, 1]))
            iso = IsotonicRegression(out_of_bounds="clip").fit(score, y)
            i_.append(brier_score_loss(yt, iso.predict(st)))
        s, i, r = np.mean(s_), np.mean(i_), np.mean(r_)
        print(f"{n_cal:7d} {r:8.4f} {s:9.4f} {i:9.4f}   {'sigmoid' if s < i else 'isotonic'}")

run(lambda p: 1 / (1 + np.exp(-(2.5 * logit(p) + 0.8))), "A: sigmoidal distortion")
run(lambda p: np.clip(p + 0.18 * np.sin(3 * np.pi * p), 1e-6, 1 - 1e-6), "B: non-monotone distortion")
# A: sigmoidal distortion
#   n_cal      raw   sigmoid  isotonic   winner
#     200   0.2296    0.2009    0.2058   sigmoid
#    1000   0.2300    0.2001    0.2021   sigmoid
#    5000   0.2294    0.1996    0.2002   sigmoid
#   20000   0.2304    0.2001    0.2003   sigmoid
#
# B: non-monotone distortion
#   n_cal      raw   sigmoid  isotonic   winner
#     200   0.2159    0.2223    0.2131   isotonic
#    1000   0.2160    0.2223    0.2090   isotonic
#    5000   0.2159    0.2216    0.2075   isotonic
#   20000   0.2161    0.2219    0.2074   isotonic

In these averages over 15 repetitions, sigmoid has lower Brier at every tested size in A and isotonic at every tested size in B. This does not show that sample size is irrelevant or that either method always wins. In A the gap narrows from about 0.0049 to 0.0002 as the calibration sample grows; uncertainty in these average differences has not been quantified.

This exercise fits regularized logistic regression on the logit of the reported probability, \(\log(s/(1-s))\), whereas the earlier forest example passes its probability output directly to sigmoid calibration. The input scale changes what a two-parameter sigmoid can express. Ignoring clipping, A has \(\operatorname{logit}(s)=2.5\operatorname{logit}(p)+0.8\), so its inverse lies in the fitted family. That explains why the family is well suited to A; correct specification does not guarantee a finite-sample win.

B is not monotone: before clipping, the derivative of \(p+0.18\sin(3\pi p)\) is \(1+0.54\pi\cos(3\pi p)\), whose minimum is about −0.696. Some different true probabilities therefore receive the same score, and some orderings reverse. No function of that score alone can recover every original probability. A calibrator instead targets the positive fraction conditional on the score; isotonic additionally constrains that relationship to be non-decreasing. Its lower Brier here does not prove exact recovery or establish a result about monotone distortions.

Sigmoid has worse Brier than the raw probabilities in B at every tested size: at 200 calibration rows, 0.2223 against 0.2159. Its logistic fit optimizes log loss, while this table evaluates Brier, and its restricted shape may also be unsuitable. Inspect a reliability curve for possible patterns, but select and evaluate calibration methods on separate data using the intended score. A noisy curve is not enough to prescribe isotonic, and its ties do not always reduce AUC.

2. Calibrating on the training set. Fit a calibrator on the model's own training predictions and on held-out data, and compare both under Brier and log loss.

You should get: worse test scores after fitting calibration on training predictions, with a larger relative increase in log loss.

Solution
import numpy as np
from sklearn.datasets import make_classification
from sklearn.ensemble import RandomForestClassifier
from sklearn.calibration import CalibratedClassifierCV
from sklearn.frozen import FrozenEstimator
from sklearn.metrics import brier_score_loss, log_loss
from sklearn.model_selection import train_test_split

X, y = make_classification(n_samples=40000, n_features=12, n_informative=6,
                           class_sep=0.7, random_state=0)
Xtr, Xrest, ytr, yrest = train_test_split(X, y, test_size=0.6, random_state=0)
Xpool, Xte, ypool, yte = train_test_split(Xrest, yrest, test_size=0.5, random_state=0)
rf = RandomForestClassifier(n_estimators=300, random_state=0, n_jobs=-1).fit(Xtr, ytr)

for label, Xc, yc in (("training data", Xtr, ytr), ("held-out data", Xpool, ypool)):
    c = CalibratedClassifierCV(FrozenEstimator(rf), method="isotonic").fit(Xc, yc)
    p = c.predict_proba(Xte)[:, 1]
    print(f"calibrated on {label:14s} test Brier {brier_score_loss(yte, p):.4f}"
          f"  test logloss {log_loss(yte, p):.4f}")
# calibrated on training data  test Brier 0.0493  test logloss 1.4251
# calibrated on held-out data  test Brier 0.0429  test logloss 0.1551

Training predictions can have a much more optimistic relationship to outcomes than predictions on unseen cases. Fitting isotonic to that relationship can map ranges of scores to exactly 0 or 1. The raw forest probabilities need not all be 0 or 1 for this to happen. The learned certainty may fail on test cases.

Both metrics favor held-out calibration: Brier rises from 0.0429 to 0.0493 with training-set calibration, about 15%, and log loss rises from 0.1551 to 1.4251, more than ninefold. Log loss is especially sensitive to wrong predictions near 0 or 1. Exact certainty assigned to the wrong outcome has infinite mathematical log loss; scikit-learn clips probabilities at floating-point limits, producing finite reported values. The difference in sensitivity does not make the Brier degradation negligible.

The leakage article explains why calibration should use predictions made without fitting the base model on those cases. Use a dedicated calibration set or cross-validated predictions, and retain separate evaluation data. With an unfitted estimator and ensemble=True, CalibratedClassifierCV averages separately calibrated fold models.

The same principle applies to stacking: downstream training usually needs out-of-fold predictions from the complete base-model fitting procedure. Random-forest out-of-bag predictions can be another source, provided each case is predicted only by trees that excluded it and the sampling design matches the task.

3. Calibration without discrimination. Decompose the Brier score into reliability, resolution, and uncertainty, and compare a constant forecast, coarse probability estimates, and probabilities with a distorted scale.

You should get: a calibrated constant with no discrimination, and rescaled probabilities that rank well but have worse Brier than that constant. Distinguish original and binned forecasts.

Solution
import numpy as np
from sklearn.metrics import brier_score_loss, roc_auc_score

rng = np.random.default_rng(0)
n = 200_000
p_true = rng.beta(2, 5, n)
y = (rng.random(n) < p_true).astype(int)

def decompose(y, p, n_bins=20):
    edges = np.quantile(p, np.linspace(0, 1, n_bins + 1)); edges[-1] += 1e-9
    idx = np.clip(np.searchsorted(edges, p, side="right") - 1, 0, n_bins - 1)
    base = y.mean(); rel = res = 0.0
    grouped = np.empty_like(p)
    for b in range(n_bins):
        m = idx == b
        if not m.any(): continue
        w = m.mean()
        grouped[m] = p[m].mean()
        rel += w * (p[m].mean() - y[m].mean()) ** 2      # binned reliability
        res += w * (y[m].mean() - base) ** 2             # binned resolution
    unc = base * (1 - base)
    binned_brier = brier_score_loss(y, grouped)
    assert np.isclose(binned_brier, rel - res + unc)
    return rel, res, unc, binned_brier

models = {"true probabilities": p_true,
          "known base rate": np.full(n, 2 / 7),
          "coarse estimates": np.where(p_true > 0.28, 0.42, 0.20),
          "rescaled probabilities": np.clip(p_true * 2.4, 0, 1)}
print(f"{'model':28s} {'Brier':>7} {'binned':>7} {'AUC':>7} {'reliab':>8} {'resol':>8} {'uncert':>8}")
for name, p in models.items():
    rel, res, unc, binned = decompose(y, p)
    print(f"{name:28s} {brier_score_loss(y, p):7.4f} {binned:7.4f} {roc_auc_score(y, p):7.4f}"
          f" {rel:8.4f} {res:8.4f} {unc:8.4f}")
# model                          Brier  binned     AUC   reliab    resol   uncert
# true probabilities            0.1793  0.1795  0.7186   0.0000   0.0248   0.2044
# known base rate               0.2044  0.2044  0.5000   0.0000   0.0000   0.2044
# coarse estimates              0.1880  0.1880  0.6592   0.0007   0.0170   0.2044
# rescaled probabilities        0.3211  0.3210  0.7135   0.1402   0.0235   0.2044

The population Brier decomposition is reliability minus resolution plus uncertainty. Binning continuous forecasts changes which score the three estimated terms reconstruct. Reliability measures squared disagreement between reported probabilities and the true positive fractions conditional on those reports; resolution measures how much those fractions vary around prevalence. Writing prevalence as \(\pi=P(Y=1)\), uncertainty is \(\pi(1-\pi)\), the loss of the best constant forecast. It is not irreducible given informative features: resolution subtracts from it.

The table estimates these quantities by grouping predictions into up to 20 quantile bins. Within each bin, it uses the mean prediction and observed positive fraction. Before rounding the displayed columns, reliab - resol + uncert reconstructs the binned Brier, computed after replacing every prediction by its bin mean; the code checks that identity. It need not reconstruct the original Brier column. The estimates also depend on the bins and finite-sample variation, so a displayed reliability of 0.0000 is not proof of exact calibration.

The known base-rate forecast uses \(2/7\), the mean of the generating Beta distribution, rather than learning a constant from test labels. It is calibrated in the population and has AUC 0.5 because every score ties. It cannot prioritize cases, but it remains a useful baseline and supplies a population risk estimate. The coarse estimates row assigns 0.20 and 0.42 to two score regions; these are approximations, not their exact conditional positive rates, so this row is not perfectly calibrated.

Rescaling by 2.4 and clipping at 1 gives Brier 0.3211, worse than the constant's approximately 0.2044, despite AUC 0.7135 being close to the true-probability ranking's 0.7186. The multiplication preserves order, but clipping merges distinct scores into ties. An AUC near 0.72 is not near-perfect separation of realized labels: even true probabilities cannot identify which random outcomes will occur. Use the proper score to assess overall probability quality and the reliability curve to investigate calibration, with counts and uncertainty in the regions that matter.


Discover more from Insightful Data Lab

Subscribe to get the latest posts sent to your email.

Similar Posts

Questions, corrections, or additional insights?

This site uses Akismet to reduce spam. Learn how your comment data is processed.