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
| Model | How the probability is produced |
|---|---|
| Logistic regression | A sigmoid applied to the log-odds: \(\sigma(z) = 1/(1 + e^{-z})\) |
| Neural network, multi-class | Softmax over the logits: \(P(y = k \mid x) = e^{z_k} / \sum_j e^{z_j}\) |
| Naive Bayes | Bayes’ rule applied to the per-feature likelihoods, under the independence assumption |
| Random forest | The mean of the per-tree class estimates, which for fully grown trees is the share of trees voting for the class |
| Gradient boosting | The 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
| Use | What it actually needs |
|---|---|
| Review the top 5 percent by risk | Ordering only. An uncalibrated score is fine |
| Block when the probability exceeds 0.9 | Ordering, plus a cutoff tuned on the scale actually in use |
| Multiply by a loss amount to get expected cost | Calibrated probabilities. Nothing less will do |
| Show the number to a clinician or a customer | Calibrated probabilities, since the reader will interpret 0.7 literally |
| Combine with another model’s output | A 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
- scikit-learn User Guide, Probability calibration
- scikit-learn API reference, RandomForestClassifier
- Chuan Guo, Geoff Pleiss, Yu Sun and Kilian Q. Weinberger, “On Calibration of Modern Neural Networks”, ICML 2017, arXiv:1706.04599
Discover more from Insightful Data Lab
Subscribe to get the latest posts sent to your email.
