Calibrate a self-signal against graded items
Source:R/core-signal-calibrate.R
hr_calibrate_signal.RdMeasures how well a signal predicts that an item came out right, on a
graded set, and fits a map from the signal to a probability. The result is
a calibration record: it says how good the signal is, and it is the only
thing hr_gate_threshold() and hr_apply_signal() accept as grounds for
letting a signal gate an action. A new record is always advisory.
Usage
hr_calibrate_signal(
signal,
graded,
key,
method = c("platt", "isotonic", "none"),
bins = 10L,
split = 0.5,
seed,
B = 2000L
)Arguments
- signal
The signal's name, as recorded by
hr_signal().- graded
The graded set, a data frame; see "The graded set".
- key
A named list with the fields the record is valid for:
served_model(the model the provider said answered, not the one asked for),version(the model version or snapshot),task,format,prompt_hash(of the prompt or template),effort,module(the reader or stage) andcontract_version. Each is one string;NAis allowed except forserved_model. A missingcontract_versionis filled with the running package's contract version.- method
"platt","isotonic"or"none".- bins
Number of equal-mass bins for the ECE and the decomposition, held between 5 and 10.
- split
Share of each cluster's items that goes to the fit half, strictly between 0 and 1.
- seed
Seed for the split and the bootstrap. Required, so the same graded set always gives the same record. The caller's random state is left as it was.
- B
Number of bootstrap resamples; 0 for no intervals.
Value
An hr_calibration: a list with signal, key, method,
n_items, n_errors, n_unanswerable, n_clusters, accuracy (on
the calibration half), metrics (a data frame of metric, estimate,
lower, upper), selective_risk, decomposition, map (the fitted
map), grid (the default thresholds for hr_gate_threshold(), from the
fit half), data (the graded items with their half and mapped p),
sample_design, bins (the number of ECE bins formed), B, seed, created_utc, created_by,
status ("advisory" or "gating"), reasons, notes (remarks that
do not stop a record from gating, such as a small cluster count behind
the intervals) and gate (NULL
until hr_gate_threshold() sets it). It has print() and as_json()
methods.
The graded set
One row per item, with these columns:
score(orvalue)The signal: a number, or a logical read as 1 and 0. Higher means more confident the item is right. The
scorecolumn ofhr_signals()is in this form.correctWhether the item came out right (logical, or 1 and 0). For an action such as "stop reading" or "accept", right means acting on the item was right.
answerableWhether the item has an answer in its source. The set must include unanswerable items: a signal calibrated only on answerable questions has never been asked about the case where saying "I can answer" is most often wrong.
clusterOptional: the document (or other unit) the item comes from. Without it each item is its own cluster.
item_id,signalOptional. When
signalis present only the rows for this signal are used.splitOptional, as
hr_assign_splits()andgr_read_labels()give it. When present only the rows in thecalibrationsplit are used: dev items were tuned on, and test items must stay unseen until they are scored. The rows left out are counted innotesand in a message, and a set with no calibration row stops withhr_bad_calibration.
Columns are matched by their exact names, so a column such as
cluster_size is never read as cluster.
How it is computed
Within each cluster, a share split of the items goes to the fit half and
the rest to the calibration half (a cluster's odd item goes to either side
at random). The map is fitted on the fit half. Every metric, and the
threshold search in hr_gate_threshold(), uses the calibration half, so
the map is never judged on the items it was fitted to.
Platt scaling (Platt 1999) fits
p = plogis(a + b * score)by logistic regression on the targets(N+ + 1) / (N+ + 2)for a right item and1 / (N- + 2)for a wrong one, which keeps the fit finite when the halves separate. It is the default and the only fitted map allowed below 1000 graded items.Isotonic regression (pool adjacent violators, weighted by the number of items at each score) needs at least 1000 graded items. The count is of the whole graded set before the split, so the map itself is fitted on about
splitof them (500 at the minimum with the defaultsplit). Between fitted scores it interpolates linearly; beyond them it holds the end values."none"takes the score as the probability, so it must lie in [0, 1]. It is the comparison that shows what fitting a map gained.
On the calibration half, with p the mapped probability and y 1 for a
right item. The Brier score, its decomposition and the ECE judge p;
AUROC2 and AURC judge the signal's own score, so they describe the signal
and not the map. A Platt slope is free to be negative, and a map fitted to
a signal that points the wrong way would otherwise rank well and hide it.
Brier score
mean((p - y)^2), with its decomposition over bins ofp(Murphy 1973; the two within-bin terms of Stephenson et al. 2008):brier = reliability - resolution + uncertainty + within_var - 2 * within_cov.reliabilityis the weighted mean of(mean p - mean y)^2per bin,resolutionthe weighted mean of(mean y in bin - mean y)^2,uncertaintyismean(y) * (1 - mean(y)), andwithin_varandwithin_covare the weighted within-bin variance ofpand covariance ofpandy. Whenptakes no more distinct values thanbins, each value is its own bin, both within-bin terms are 0, and this is Murphy's three-part decomposition exactly.AUROC2, the type-2 area under the ROC curve: the probability that a right item scores above a wrong one, ties counting half (the Mann-Whitney statistic), on the raw score. It measures ranking alone, and is below 0.5 for a signal that is higher when the item is wrong.
AURC, the area under the risk-coverage curve (Geifman and El-Yaniv 2017): items are accepted from the most to the least confident, and the error rate among the first
kis averaged overk = 1, ..., n, in the order of the raw score. Tied scores enter together, each position taking the expected error rate under a random order within the tie.selective_riskgives the same error rate at 50, 80 and 90 percent coverage. Lower is better.ECE, the debiased L2 calibration error over equal-mass bins (Kumar, Liang and Ma 2019):
sqrt(max(0, sum_k w_k * ((mean p - mean y)^2 - mean y * (1 - mean y) / (n_k - 1)))). The plug-in error is biased upward by the noise in each bin's mean, and the subtracted term removes that bias, so a perfectly calibrated signal scores near 0 rather than at the noise floor.binsis held between 5 and 10. A calibration half of fewer than 10 items is lowered to fewer bins so each bin has at least two items. The ECE and the decomposition use the same bins, and items with the samepalways share one: whenptakes no more distinct values thanbins, each value is its own bin, and otherwise an item's bin follows the average rank of itsp. So the ECE does not depend on the order of the rows, and a signal with two values (a logical one, or any map of it) is measured over 2 bins. The record'sbinsis the number of bins formed, andreasonssays so when it is below 5.
Intervals are 95 percent percentile intervals from a bootstrap that
resamples whole clusters of the calibration half, B times. With fewer
than two clusters in that half, or when every resample gives the same
value, there is nothing to resample and the bounds are NA. With fewer
than 20 clusters percentile intervals cover poorly, and notes says how
many clusters they rest on.
A record is advisory until hr_gate_threshold() finds a threshold that
passes. It also stays advisory when the graded set has fewer than 100
wrong items, when the fitted map does not increase with the signal (a
Platt slope of 0 or less, or a flat isotonic fit), or when the ECE used
fewer than 5 bins, as it does for a signal with fewer than 5 distinct
values, such as a logical one; reasons says why.
References
Geifman, Y. and El-Yaniv, R. (2017). Selective classification for deep neural networks. Advances in Neural Information Processing Systems 30.
Kumar, A., Liang, P. and Ma, T. (2019). Verified uncertainty calibration. Advances in Neural Information Processing Systems 32.
Murphy, A. H. (1973). A new vector partition of the probability score. Journal of Applied Meteorology 12(4), 595-600.
Platt, J. (1999). Probabilistic outputs for support vector machines and comparisons to regularized likelihood methods. In Advances in Large Margin Classifiers, 61-74.
Stephenson, D. B., Coelho, C. A. S. and Jolliffe, I. T. (2008). Two extra components in the Brier score decomposition. Weather and Forecasting 23(4), 752-757.
Examples
# A simulated graded set: 40 documents, 10 questions each, a quarter of
# them unanswerable, and a signal that is informative but overconfident.
set.seed(1)
n <- 400
answerable <- rep(c(TRUE, TRUE, TRUE, FALSE), length.out = n)
truth <- stats::runif(n, 0.2, 0.95) * answerable
graded <- data.frame(
cluster = rep(sprintf("doc%02d", 1:40), each = 10),
score = pmin(1, truth + 0.15),
correct = stats::runif(n) < truth,
answerable = answerable)
key <- list(served_model = "mock-model-2026-01-01", version = "2026-01-01",
task = "qa", format = "pdf", prompt_hash = "a1b2c3", effort = "low",
module = "iterative")
cal <- hr_calibrate_signal("gr.iterative.can_answer", graded, key, seed = 7, B = 200)
cal
#> <hr_calibration> gr.iterative.can_answer: advisory
#> 400 items (235 wrong, 100 unanswerable) in 40 clusters; map: platt
#> key: served_model=mock-model-2026-01-01, version=2026-01-01, task=qa, format=pdf, prompt_hash=a1b2c3, effort=low, module=iterative, contract_version=0.1
#> calibration half: 200 items, accuracy 0.410
#> brier 0.1598 [0.1305, 0.1920]
#> auroc2 0.8393 [0.7791, 0.8918]
#> aurc 0.3514 [0.2717, 0.4426]
#> ece 0.0552 [0.0000, 0.1964]
#> advisory: no gate threshold has been set
# The same split with no map shows what Platt scaling bought.
raw <- hr_calibrate_signal("gr.iterative.can_answer", graded, key, method = "none",
seed = 7, B = 0)
raw$metrics[raw$metrics$metric %in% c("brier", "ece"), ]
#> metric estimate lower upper
#> 1 brier 0.1843548 NA NA
#> 4 ece 0.1690354 NA NA