WeightedSplit keeps the whole split-conformal construction and changes
exactly one thing: each calibration score gets a weight saying how
likely its features are under the target distribution rather than the
source one. Nothing about the forecaster changes, and no target labels
are needed.
What it does
Writex for the features of a predicted step. The importance weight is
the density ratio
s_1 ... s_n are then normalized including the test
point’s own weight, and the interval takes the smallest score whose
cumulative normalized weight reaches 1 - alpha:
[y_hat - q, y_hat + q], one q per
(series, sample, horizon) cell — the quantile now moves with the test
point, which the unweighted method’s does not. Set every w to 1 and
the w(x_test) term becomes the familiar +1 of
k = ceil((1 - alpha)(n + 1)), recovering split conformal exactly. That
is not a metaphor: before adapt() binds a weight function,
WeightedSplit is bit-identical to SplitConformal, and a test asserts
it.
The ratio itself is estimated, not known. DensityRatio labels source
rows 0 and target rows 1, fits any binary classifier you hand it,
and reads the ratio off the odds p / (1 - p). Calibration weights are
computed out of fold, so a calibration point’s weight never comes
from a model that trained on it.
Worked example
AirPassengers has one property that makes it a clean covariate-shift test: its error scale grows with its level. Solog of the last twelve
months’ mean is a real driver of how large the next residual will be — a
genuine covariate, not a label in disguise.
The shift is built by splitting one-step-ahead windows on that feature:
each window goes to calibration or holdout with a probability that rises
with its level. Both halves keep full support — high-level windows
appear in calibration too, just rarely — which is the regime weighted
conformal is built for, and the regime it needs. A source that never saw
the target’s features cannot be reweighted into it.
Calibrate, then adapt
calibrate(forecaster, histories, truths) is the ordinary source-domain
verb: it fits the conformal quantile on source forecasts and source
truths. adapt(weighting=..., target_X=...) is the transfer verb,
called on the fitted object calibrate returned: it hands a weight
function the features of the target domain — no target truths — and
returns a new fitted object with the quantile rebuilt, leaving the
source fit untouched. See Transfer and
adaptation for the full set of verbs.

Clipping, and how it can cost you coverage
An estimated density ratio is unbounded. One calibration point whose classifier odds come out at 300 can carry more weight than the rest of the panel put together, and the weighted quantile then rests on a single residual.DensityRatio guards against that by clipping weights at
their clip_quantile, default 0.99.
The guard is not free. Under a strong shift the typical target weight
is already large, so a clip calibrated on the source weights binds on
most of the test points — and every clipped point is one whose weighting
signal was thrown away. The sweep below is the synthetic covariate-shift
benchmark this package tests against: feature-dependent noise,
calibration drawn at loc=0 and test at loc=2.5, 90% target.
clip_quantile=0.99 the same weights deliver
only 0.8213, because under this much shift the median test weight
saturates at the clip value: the guard binds on 59% of the points and
discards most of the signal it was meant to protect. Loosening it to
0.999 recovers part of the gap, at 0.8692.
Tuneclip_quantileagainst measured coverageclip_quantile=0.99is a safe default for a mild shift and a poor one for a strong shift. If your weights are large across the board, the clip is not trimming outliers — it is flattening the whole weighting. Check the adapted object’scalibration_.diagnostics["weight_summary"]: when the median sits at the maximum, the clip is binding everywhere.
When not to use it
- The shift is in
P(y|x), notP(x). Weighting reweights calibration points by how likely their features are in the target domain. It assumes the conditional error distribution given those features is unchanged. If the relationship itself moved — a new pricing rule, a changed measurement process, a forecaster that has gone stale — no reweighting of the old residuals describes the new ones. That is a job for recalibration on target labels if you have them, or an online method if you do not. - The density ratio is badly estimated. The weights are the output of a classifier, and a bad classifier gives confidently wrong weights. Too flexible and it memorises which rows are source and which are target, sending the ratio to zero and infinity; too rigid and it returns a constant, which is uniform weighting under another name. Clipping bounds the damage without removing it — and as the sweep above shows, the clip has its own failure mode.
- The supports do not overlap. If no calibration window looks
anything like the target,
d_source(x)is near zero where the target lives, and the ratio is an extrapolation dressed up as a weight. Weighting redistributes evidence; it cannot manufacture it. - You have target labels. If you can observe target
(prediction, truth)pairs, calibrating on them directly is strictly better than reweighting a proxy. Userecalibrate(). - You have no features at all. With
X=Nonethe weight function cannot fit andWeightedSplitis plain split conformal. Reach for NexCP instead, which down-weights by recency rather than by features and needs no covariates.
References
- R. J. Tibshirani, R. F. Barber, E. J. Candes, and A. Ramdas, “Conformal prediction under covariate shift,” Advances in Neural Information Processing Systems 32, 2019. https://arxiv.org/abs/1904.06019
Next
- Transfer and adaptation —
calibrate,recalibrateandadapt, and which one your target data allows. - Split conformal — the unweighted method this reduces to.
- Diagnostics — measuring coverage and width together.

