ek.qe.calibrate#
Calibration: make a raw score mean a probability (the non-optional stage).
A confidence of 0.9 should mean “correct 90% of the time.” Modern models violate
this badly – they are systematically overconfident (Guo et al. 2017), and RLHF
makes LLMs worse. So **calibration is non-optional: never gate a raw posterior or a
raw logprob** (misc/docs/ek_03 §2, Hard Rule 1). A Calibrator
is fit on a labelled holdout of (raw_score, field_correct?) pairs and maps any
later raw score to a calibrated probability.
Three methods, by what input you have:
PlattCalibrator– logistic fit on any scalar score (no logits needed): the default for aggregated OCR confidence or aggregated logprobs.IsotonicCalibrator– non-parametric monotonic fit; more flexible, needs more data, can overfit small sets.TemperatureCalibrator– a single scalarTon logits; use only when you have logits (it does not change the argmax).
All three are pure-Python (stdlib only) so the calibration stage works with zero
extra dependencies; sklearn_calibrator() / netcal_calibrator() offer the
library-backed equivalents behind the ek[calibration] extra. Measure calibration
with expected_calibration_error() (+ a reliability curve). For per-field-type
validity, wrap per group with GroupCalibrator (Mondrian / class-conditional)
– distribution-free conditional coverage is otherwise impossible (Hard Rule 2).
When the raw signals are heterogeneous (a judge score next to a logprob next to a
ROVER agreement), calibrate each signal family on its own with
SignalFamilyCalibrator and fuse the calibrated probabilities with a
documented combiner (mean_probability() by default, min_probability()
for a conservative gate), rather than calibrating the mean of incomparable scores.
Calibrate at the granularity of the decision (gate on fields -> calibrate a
“field-correct?” target), and persist the fit (save_calibrator()); calibration
is dataset-specific and decays, so re-fit on drift.
Example
>>> # An overconfident raw signal, calibrated against ground truth.
>>> raw = [0.95, 0.93, 0.92, 0.90, 0.55, 0.52, 0.51, 0.50]
>>> correct = [True, True, False, False, True, False, False, False]
>>> cal = PlattCalibrator().fit(raw, correct)
>>> cal(0.95) < 0.95 # overconfidence pulled down
True
>>> 0.0 <= cal(0.5) <= 1.0
True
Module Attributes
Default number of bins for |
Functions
|
Expected Calibration Error: weighted mean gap between confidence and accuracy. |
|
Reconstruct a persisted calibrator by name (dispatched on its |
|
Fuse calibrated per-family probabilities by their mean (the default combiner). |
|
Fuse calibrated per-family probabilities by their minimum (conservative). |
|
ECE via |
|
Per-bin |
|
Persist a fitted calibrator's parameters to the |
|
A calibrator backed by scikit-learn ( |
Classes
|
Per-group (Mondrian) calibration: one calibrator per |
|
Isotonic (monotonic non-decreasing) calibration via pool-adjacent-violators. |
|
Platt scaling: |
|
One calibrator per signal family, fused by a documented combination rule. |
|
Temperature scaling: |
- ek.qe.calibrate.DEFAULT_N_BINS = 10#
Default number of bins for
expected_calibration_error()/ reliability curves.
- class ek.qe.calibrate.GroupCalibrator(factory=<class 'ek.qe.calibrate.PlattCalibrator'>, by_group=<factory>, pooled=None, kind='group')[source]#
Bases:
objectPer-group (Mondrian) calibration: one calibrator per
NodeType/FieldSpec.Distribution-free conditional (per-field-type) coverage is impossible in general (Barber et al. 2019); calibrating separately per group restores it approximately.
fittakes a parallelgroupssequence;__call__routes by group key, falling back to a pooled calibrator for unseen groups.- Parameters:
factory (
Callable[[],Any]) – Zero-arg callable producing a fresh per-group calibrator (defaultPlattCalibrator).
- factory#
alias of
PlattCalibrator
- class ek.qe.calibrate.IsotonicCalibrator(x=<factory>, y=<factory>, kind='isotonic')[source]#
Bases:
objectIsotonic (monotonic non-decreasing) calibration via pool-adjacent-violators.
More flexible than Platt; needs more calibration data and can overfit small sets. Predicts by linear interpolation between fitted points, clipped at the ends.
- class ek.qe.calibrate.PlattCalibrator(a=1.0, b=0.0, max_iter=100, kind='platt')[source]#
Bases:
objectPlatt scaling:
sigmoid(a * score + b), fit by Newton/IRLS on labels.The default calibrator: works on any scalar (aggregated confidence/logprob), no logits required. Uses Platt’s target smoothing so it does not overfit small calibration sets.
- Parameters:
max_iter (
int) – Newton iterations (converges in a handful for a 2-parameter model).
- class ek.qe.calibrate.SignalFamilyCalibrator(calibrators=<factory>, combine=<function mean_probability>, factory=<class 'ek.qe.calibrate.PlattCalibrator'>, fusion=None, kind='signal_family')[source]#
Bases:
objectOne calibrator per signal family, fused by a documented combination rule.
Averaging heterogeneous raw signals (an LLM-judge score, a token logprob, a ROVER agreement) and calibrating the mean discards exactly what a calibrator needs: each family has its own scale, reliability and direction of miscalibration. This calibrates every signal by name with its own calibrator, then fuses the calibrated probabilities with
combine.Pass it (or simply a
Mapping[str, Calibrator]) asestimate_quality(..., calibrator=...).The fused value must itself be calibrated before it is gated. Fusing calibrated probabilities (by mean, min, …) does not yield a calibrated probability: agreeing families make the mean under-confident, a weak family drags it toward the base rate. So
fit()also learns a second-stagefusioncalibrator oncombine’s output, and__call__applies it. Without afusioncalibrator (e.g. a hand-built mapping),ek.estimate_quality()refuses to gate a value fused from two or more families unless toldassume_calibrated=True(Hard Rule 1). A value from a single family is that family’s calibrated probability and gates as is.A field’s own upstream
confidenceis just one more family here, named"confidence": include it in the training rows under that name.- Parameters:
calibrators (
dict) –signal name -> Calibrator. A"*"entry is the fallback for any signal without its own entry. A signal with neither is refused (ValueError) rather than silently dropped or passed through raw (Hard Rule 1).combine (
Callable[[Mapping[str,float]],float]) –Mapping[str, float] -> floatfusing the calibrated values;mean_probability()(default) ormin_probability(), or any callable of that shape (e.g. a learned stacker).factory (
Callable[[],Any]) – Zero-arg callable producing a fresh calibrator, used byfit().fusion (
Any) – Second-stage calibrator applied tocombine’s output (learned byfit());Noneleaves the fused value uncalibrated.
Example
>>> rows = [{"judge": j, "logprob": l} for j, l in ... [(0.9, -0.1), (0.8, -0.2), (0.85, -2.0), (0.3, -0.1), (0.2, -2.5), (0.7, -1.8)]] >>> correct = [True, True, False, True, False, False] >>> cal = SignalFamilyCalibrator().fit(rows, correct) >>> sorted(cal.calibrators) ['judge', 'logprob'] >>> 0.0 <= cal({"judge": 0.9, "logprob": -0.1}) <= 1.0 True
- calibrate_signals(raw_signals, *, group=None)[source]#
{name: calibrated probability}for each raw signal (before fusion).- Return type:
- combine()#
Fuse calibrated per-family probabilities by their mean (the default combiner).
Each family’s calibrated value estimates the same event (“this field is correct”), so the mean is a defensible, assumption-light pooling rule.
- Return type:
>>> mean_probability({"judge": 0.9, "logprob": 0.7}) 0.8
- factory#
alias of
PlattCalibrator
- fit(signals, correct)[source]#
Fit one calibrator per signal name, then the
fusioncalibrator.Each family is fit over the rows where it appears with a finite value. A family with fewer than two such rows, or only one label class, cannot be calibrated and is refused (
ValueError) rather than turned into a constant that would then dilute the fusion.- Return type:
- class ek.qe.calibrate.TemperatureCalibrator(T=1.0, kind='temperature', t_min=0.05, t_max=10.0, max_iter=60)[source]#
Bases:
objectTemperature scaling:
sigmoid(logit / T)with oneTfit on a holdout.Use only when you have logits:
__call__expects a logit, not a probability.T > 1softens overconfidence; the argmax is unchanged.
- ek.qe.calibrate.expected_calibration_error(probs, correct, *, n_bins=10)[source]#
Expected Calibration Error: weighted mean gap between confidence and accuracy.
Bins predictions by confidence into
n_binsequal-width bins and averages|mean_confidence - accuracy|weighted by bin population.0is perfect. Non-finite probs are skipped; out-of-range probs are clamped into[0, 1].- Return type:
- ek.qe.calibrate.load_calibrator(name, *, rootdir=None)[source]#
Reconstruct a persisted calibrator by name (dispatched on its
kind).Validates the stored record so a malformed or unknown
kindfails with an actionable error rather than a rawKeyErroror a load-then-crash-later.- Return type:
- ek.qe.calibrate.mean_probability(probs)[source]#
Fuse calibrated per-family probabilities by their mean (the default combiner).
Each family’s calibrated value estimates the same event (“this field is correct”), so the mean is a defensible, assumption-light pooling rule.
- Return type:
>>> mean_probability({"judge": 0.9, "logprob": 0.7}) 0.8
- ek.qe.calibrate.min_probability(probs)[source]#
Fuse calibrated per-family probabilities by their minimum (conservative).
Use when any one family flagging a problem should be enough to lower the gate.
- Return type:
>>> min_probability({"judge": 0.9, "logprob": 0.7}) 0.7
- ek.qe.calibrate.netcal_ece(probs, correct, *, bins=10)[source]#
ECE via
netcal(behindek[calibration]); D-ECE for localized outputs lives there too.- Return type:
- ek.qe.calibrate.reliability_curve(probs, correct, *, n_bins=10)[source]#
Per-bin
{confidence, accuracy, count}for a reliability diagram.
- ek.qe.calibrate.save_calibrator(calibrator, name, *, rootdir=None)[source]#
Persist a fitted calibrator’s parameters to the
calibratorsstore.- Return type:
- ek.qe.calibrate.sklearn_calibrator(method='sigmoid')[source]#
A calibrator backed by scikit-learn (
method='sigmoid'Platt or'isotonic').Behind
ek[calibration]. Returns an object satisfying theCalibratorprotocol that wraps sklearn’s calibration. The pure-PythonPlattCalibrator/IsotonicCalibratorare the dependency-free defaults; use this for parity with an sklearn-centric stack.