Skip to content

Fix under-coverage in CovariateLabel's finite-sample correction - #1223

Open
lehendo wants to merge 3 commits into
sunlabuiuc:masterfrom
lehendo:covariateshiftfix
Open

Fix under-coverage in CovariateLabel's finite-sample correction#1223
lehendo wants to merge 3 commits into
sunlabuiuc:masterfrom
lehendo:covariateshiftfix

Conversation

@lehendo

@lehendo lehendo commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

test_forward_with_embeddings_uses_per_point_weight failed on CI (0.3241... == 0.3241...). Root cause: a test point's KDE-derived weight is always a small fraction of total calibration weight, so whether it shifts the selected quantile index is essentially a coin flip depending on random model init

_query_weighted_quantile added the test point's reserved weight only to
the normalizing denominator, never inserting it as an actual point in
the weighted empirical distribution. Verified against Corollary 1 of
Tibshirani, Barber, Candes, and Ramdas, "Conformal Prediction Under
Covariate Shift" (NeurIPS 2019, arXiv:1904.06019): before this fix, the
"corrected" quantile under-covered relative to target (0.81-0.89 vs a
0.90 target in Monte Carlo simulation) -- worse than no correction at
all. Fix: prepend the reserved weight to the cumulative sum before
dividing, matching the paper's construction exactly (confirmed via a
hand-computed example and against a from-scratch reference
implementation of the paper's formula).

Also fix a related gap: calibrate() computed a single threshold using
the *mean* calibration likelihood ratio as a stand-in for a test
point's own w(x), but Corollary 1 defines the threshold per test point
using that point's actual weight. forward() now accepts an optional
test_embeddings argument to compute the real per-point threshold
matching the paper exactly; omitting it keeps the old single-threshold
behavior as a documented, explicitly-warned approximation, since not
every wrapped model exposes an embedding extraction path.

18 tests pass (4 new), including a deterministic regression test
against a hand-computed Corollary 1 example and new coverage of the
forward() per-point/fallback paths.
@lehendo lehendo changed the title Fix Covariate Shift CP Fix Fix under-coverage in CovariateLabel's finite-sample correction Aug 31, 2026
CI failed on test_forward_with_embeddings_uses_per_point_weight
(AssertionError: 0.3241... == 0.3241...). Root-caused this to a real
design flaw, not just missing randomness control: a test point's
likelihood-ratio weight is drawn from the same KDE-derived distribution
as the calibration weights, so by construction it can never be more than
a small fraction of total calibration weight mass. Whether prepending it
shifts _query_weighted_quantile's selected order-statistic index depends
entirely on where the alpha-quantile boundary happens to fall relative to
the (data-dependent, effectively random given the model's unseeded init)
distribution of calibration weights along the sorted-score axis.

Confirmed this wasn't just 'rare bad luck': reproduced the exact failure
deterministically for multiple fixed seeds, and directly inspected the
internals (total_weight, cum_weights) showing the two test points'
weights (0.05 vs 10.0) simply didn't straddle a boundary for that data --
an inherent fragility in comparing *output values* for two randomly-KDE
-derived weights, not a bug in the underlying Corollary-1 implementation.

Replaced the flaky output-comparison with two more precise checks:
1. Spy on _query_weighted_quantile during a real forward() call and
   verify it's invoked once per test point with that exact point's own
   weight -- directly verifies the claim ('forward uses per-point
   weight') without depending on the resulting threshold *values*
   differing.
2. A new, fully hand-computed unit test proving test_weight does change
   _query_weighted_quantile's result in general (by forcing the
   documented -inf fallback with a deliberately large weight), isolated
   from any KDE/model randomness.

Also seeded _build_pointwise_setup()'s model init for reproducibility.
@lehendo lehendo changed the title Fix Fix under-coverage in CovariateLabel's finite-sample correction Fix under-coverage in CovariateLabel's finite-sample correction Aug 31, 2026
@fbonc

fbonc commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

The threshold should be calculated separately for each test patient, using that patient's embedding. However, Trainer.inference() only passes the data batch to the model, not test_embeddings. Since the example uses Trainer.inference(), it always falls back to one shared, approximate threshold for every patient.

I think put the embeddings in each batch or compute them inside CovariateLabel.forward(). Also raise an error when the number of embeddings does not match the batch size instead of ignoring extras.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants