Repository navigation
fix: use the finite-sample conformal quantile for conformal intervals - #1246
Conversation
|
Hi Saul (@nasaul), a gentle ping on this PR when you get a chance. The CLA is signed, and I'm happy to change the approach if you'd prefer a different one. Thanks! |
|
Hi Saul (@nasaul), a gentle follow-up on this one (and its twin in neuralforecast, Nixtla/neuralforecast#1628). Happy to adjust the approach if you'd prefer something different. Thanks! |
|
Thanks for this, Nicoló Angileri (@nicoloangileri), and sorry for the late reply. The diagnosis is right and the fix is the right direction. One request on the motivation: the description mixes two separate problems, and this PR solves only one of them.
Leading with "a 90% interval covers about 64% with the default On the implementation: the statistical correction should be the one While extracting the helper, please check the lower tail. With linear interpolation, A rebase on |
…rder statistics Move the correction into utils._conformal_quantile, used by both ConformalIntervals methods and by ConformalSeasonalPool (replacing _oriented_index). It returns the exact floor((n + 1) q)-th or ceil((n + 1) q)-th smallest score instead of interpolating, which under-covered the lower tail.
|
Thanks a lot for the careful review, Saul (@nasaul). All points are addressed in a18c71d:
One side effect worth flagging: with absolute conformity scores, both |
|
Great job on the update, Nicoló Angileri (@nicoloangileri)! The reframed motivation reads well, the shared helper is exactly what we were after, and the exact order statistic fixes the CSP lower tail. Coverage now sits on the One last thing before merging:
At level 40, Could you make the tail explicit instead of inferring it from Thanks again, this is a really nice piece of work. |
conformal_error passes lv / 100 for an absolute-score margin, which is always an upper bound; inferring the tail from q took the floor rank for levels below 50 and under-covered. _conformal_rank and _conformal_quantile now take an explicit upper flag.
|
Good catch, thank you! Inferring the tail from
Same setup as your table (10 windows), coverage after the fix for both methods: 0.364 / 0.458 / 0.546 at 30 / 40 / 49, on the ceil-rank bound. The description is updated with these numbers. |
|
Looks good, just waiting on the CI to run |
Saul (nasaul)
left a comment
There was a problem hiding this comment.
LGTM, thanks for the contribution, happy forecasting
Share _conformal_quantile between both conformal methods. It returns the exact floor((n + 1) q)-th or ceil((n + 1) q)-th smallest score instead of interpolating, and takes the tail explicitly so absolute-error margins below 50% round up (mirrors Nixtla/statsforecast#1246).
Summary
ConformalIntervalstakes plain empirical quantiles of then = n_windowsconformity scores, so its intervals under-cover for any number of windows:conformal_errorusesnp.quantile(cs, lv / 100), a linear interpolation between scores.conformal_distributionuses thealpha / 2and1 - alpha / 2quantiles of the2npooled valuesmean -/+ cs.Split conformal prediction needs the finite-sample correction: the
ceil((n + 1) * q)-th smallest score for an upper quantile and thefloor((n + 1) * q)-th for a lower one.ConformalSeasonalPoolalready applies it (_oriented_index), the sharedConformalIntervalspath does not. With 10 windows, a 90% interval covered 82% before this change and covers 91% after it.Change
_conformal_quantileinutils.py, next toConformalIntervals. It returns the exact order statistic of_conformal_rank(n, q, upper):ceil((n + 1) q)for an upper bound andfloor((n + 1) q)for a lower one, clipped to[1, n]. The tail is passed explicitly rather than inferred fromq, because an absolute-score margin is an upper bound even at levels below 50. There is no interpolation, because interpolating atfloor((n + 1) q) / nlands slightly above the target score and under-covers the lower tail.conformal_errorandconformal_distributioncall it instead ofnp.quantile.conformal_erroralways passesupper=True.conformal_distributionkeeps its originalalpha / 2and1 - alpha / 2cuts on the pooled scores, withupper=Falsefor the lower cuts andupper=Truefor the upper ones.ConformalSeasonalPoolcalls the same helper in_intervals_from_samplesandpredict_in_sample, and_oriented_indexis removed. Its lower bounds now use the exact order statistic, and its upper bounds use the exactceil((n + 1) q)-th sample instead of a point slightly above it.With absolute conformity scores, both
ConformalIntervalsmethods now returnmean -/+the same order statistic; forconformal_distributionit is occasionally one rank higher, since the pooled2nvalues use a slightly more conservative rank.Known limitation, not addressed here
The default
n_windows=2cannot reach typical levels. With two windows no finite interval covers more than 2/3, so the 80/90/95 bounds all use the largest score. That is the correct behavior for two scores, but the default itself stays too small. It would be better handled separately, for example with a warning or a larger default.Coverage
Setup: 4000 i.i.d. N(0, 1) series of length 100,
HistoricAveragewithConformalIntervals(h=1, n_windows=k). Coverage is measured on the next observation, and the Monte Carlo standard error is about 0.005. Both methods give the same numbers after the change.Two-sided coverage,
conformal_distribution:n_windowsEach tail after the change (share of observations below
lo/ abovehi, target(1 - level) / 2):n_windowsLevels below 50 with 10 windows (both methods, after the change; the target is the ceil rank
ceil(11 * lv / 100) / 11):main(conformal_error)conformal_errorbefore the change, at 90%: 0.649 / 0.782 / 0.831 / 0.859 / 0.880 for 2 / 5 / 10 / 20 / 50 windows.Tests
tests/test_utils.py:_conformal_rankfor both tails and the clipping;_conformal_quantilereturns exact order statistics, handles sequences of levels and other axes, and rejects empty scores; Monte Carlo checks that each tail misses at most(1 - level) / 2for 10, 20 and 50 scores, and that an upper margin at 30, 40 and 49 covers at least its level (within 3 standard errors).tests/test_models.py: with 10 windows, both methods put the bounds exactly at-/+the 8th and 9th smallest scores for 70% and 80%, and at the 4th, 5th and 6th for 30%, 40% and 49%; the 2-window exact-value check is unchanged.tests/test_csp.py: the orientation tests now check the exact order statistics through the shared helper.tests/test_utils.py,tests/test_csp.pyandtests/test_models.py261 passed,tests/test_core.py60 passed.ruff check0.2.1 passes, andmypyreports no new errors.Merged
maininto the branch.