Classification Probability

A classification probability is a model’s estimate of how likely each class is for a given input — \(P(Y = k \mid X = x)\) for \(k = 1, \dots, K\) — rather than a single predicted label. A spam filter that reports 0.85 for spam and 0.15 for not spam has said more than one that reports “spam”, and the extra information is what thresholds, rankings and expected-value calculations are built on.

Where the numbers come from

ModelHow the probability is produced
Logistic regressionA sigmoid applied to the log-odds: \(\sigma(z) = 1/(1 + e^{-z})\)
Neural network, multi-classSoftmax over the logits: \(P(y = k \mid x) = e^{z_k} / \sum_j e^{z_j}\)
Naive BayesBayes’ rule applied to the per-feature likelihoods, under the independence assumption
Random forestThe mean of the per-tree class estimates, which for fully grown trees is the share of trees voting for the class
Gradient boostingThe summed leaf values on the logit scale, passed through the link function

All five produce numbers in [0, 1] that sum to 1 across the classes. That is a property of the arithmetic, not evidence about the world.

Looking like a probability is not being one

A softmax normalizes whatever it is given. A vote share is a fraction by construction. Neither operation consults an outcome, so neither can establish that cases assigned 0.7 occur 70 percent of the time — which is the only claim that makes the number usable as a probability.

The random forest makes the gap visible without any calibration analysis at all.

import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression

rng = np.random.default_rng(3)
x = rng.normal(size=(20000, 4))
y = (rng.uniform(size=20000) < 1 / (1 + np.exp(-x.sum(axis=1)))).astype(int)
fit, test = slice(0, 10000), slice(10000, None)

forest = RandomForestClassifier(n_estimators=100, random_state=0).fit(x[fit], y[fit])
logistic = LogisticRegression().fit(x[fit], y[fit])
for name, model in (("forest", forest), ("logistic", logistic)):
    p = model.predict_proba(x[test])[:, 1]
    certain = (p == 0) | (p == 1)
    wrong = certain & (p != y[test])
    print(f"{name:9} distinct {len(np.unique(p)):5d}  min {p.min():.4f}  max {p.max():.4f}  "
          f"certain {certain.sum():4d}  certain and wrong {wrong.sum():3d}")
shares = forest.predict_proba(x[test])[:, 1] * forest.n_estimators
lattice = bool(np.allclose(shares, np.round(shares)))
print(f"forest probabilities are vote shares out of {forest.n_estimators}: {lattice}")
# forest    distinct   101  min 0.0000  max 1.0000  certain  197  certain and wrong   5
# logistic  distinct 10000  min 0.0006  max 0.9997  certain    0  certain and wrong   0
# forest probabilities are vote shares out of 100: True

The forest’s outputs occupy 101 distinct values on 10,000 test cases, because 100 trees can only vote 101 ways. It returns exactly 0 or exactly 1 on 197 of them, and is wrong on 5 of those. An output of exactly 1 is a claim of certainty, and log loss charges an infinite penalty when such a claim is wrong — scikit-learn only avoids reporting infinity by clipping. Nothing about 100 trees agreeing justifies certainty; it is what a small ensemble does at the edges of its training distribution.

Logistic regression, by contrast, gives 10,000 distinct values that never reach the endpoints, because the sigmoid is asymptotic. That is a better-shaped output, and it still says nothing about whether the values are correct — Guo and colleagues found that modern neural networks, whose softmax outputs have the same smooth shape, are poorly calibrated.

What the probability has to support

UseWhat it actually needs
Review the top 5 percent by riskOrdering only. An uncalibrated score is fine
Block when the probability exceeds 0.9Ordering, plus a cutoff tuned on the scale actually in use
Multiply by a loss amount to get expected costCalibrated probabilities. Nothing less will do
Show the number to a clinician or a customerCalibrated probabilities, since the reader will interpret 0.7 literally
Combine with another model’s outputA justified combination rule, including dependence assumptions when probabilities are combined directly; learned score combinations need separate training and validation

The first two rows are where most classifiers live, and they are why an uncalibrated model can run in production for years without anyone noticing. The trouble starts when someone takes the number at face value.

Checking and fixing

Accuracy cannot evaluate probabilities: it only sees the label that comes out after a cutoff, so it is blind to the difference between 0.51 and 0.99. Use a strictly proper scoring rule instead — Brier score or log loss — and read a reliability curve alongside it, since a single number will not say where the scale goes wrong. A forecast of 70 percent rain should be followed by rain about 70 percent of the time, and that is checkable.

Where the check fails, recalibration fits a correction on held-out data. A strictly increasing calibrator leaves the ranking alone and changes only what the numbers mean, which is exactly the problem to be solved; isotonic regression can additionally tie cases together, and any decision taken at a fixed cutoff has to be re-derived on the new scale.

References


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.