idaniloju 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- idaniloju-0.1.0/PKG-INFO +168 -0
- idaniloju-0.1.0/README.md +150 -0
- idaniloju-0.1.0/idaniloju/__init__.py +16 -0
- idaniloju-0.1.0/idaniloju/bike.py +69 -0
- idaniloju-0.1.0/idaniloju/conformal.py +104 -0
- idaniloju-0.1.0/idaniloju/models.py +30 -0
- idaniloju-0.1.0/idaniloju.egg-info/PKG-INFO +168 -0
- idaniloju-0.1.0/idaniloju.egg-info/SOURCES.txt +12 -0
- idaniloju-0.1.0/idaniloju.egg-info/dependency_links.txt +1 -0
- idaniloju-0.1.0/idaniloju.egg-info/requires.txt +1 -0
- idaniloju-0.1.0/idaniloju.egg-info/top_level.txt +1 -0
- idaniloju-0.1.0/pyproject.toml +29 -0
- idaniloju-0.1.0/setup.cfg +4 -0
- idaniloju-0.1.0/tests/test_idaniloju.py +97 -0
idaniloju-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: idaniloju
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Conformal prediction from scratch: the exact finite-sample guarantee, what drift does to it, and the online repair
|
|
5
|
+
Author-email: Kenny Obidele <obidelek19@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Kenny0bi/idaniloju
|
|
8
|
+
Project-URL: Issues, https://github.com/Kenny0bi/idaniloju/issues
|
|
9
|
+
Keywords: conformal-prediction,uncertainty-quantification,prediction-intervals,distribution-free,drift
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: numpy>=1.24
|
|
18
|
+
|
|
19
|
+
# idaniloju
|
|
20
|
+
|
|
21
|
+
Conformal prediction from scratch: the exact finite-sample guarantee,
|
|
22
|
+
proof by experiment, the deployment that breaks it, and the online
|
|
23
|
+
repair. The tenth and last project of this series, and it closes the
|
|
24
|
+
loop: the drift that kills the guarantee here is detected by
|
|
25
|
+
[iyipada](https://github.com/Kenny0bi/iyipada), my drift library,
|
|
26
|
+
installed from PyPI like any other user would.
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
pip install idaniloju
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
*Idaniloju* is Yoruba for assurance. Conformal prediction is the cleanest
|
|
33
|
+
promise in machine learning: wrap ANY point predictor, and its intervals
|
|
34
|
+
will contain the truth 90% of the time, guaranteed at finite sample size
|
|
35
|
+
with no distributional assumptions at all. The entire method is one
|
|
36
|
+
counting argument, and this repository walks it from the theorem to the
|
|
37
|
+
hurricane that broke it.
|
|
38
|
+
|
|
39
|
+
## The guarantee is a law, not a hope
|
|
40
|
+
|
|
41
|
+

|
|
42
|
+
|
|
43
|
+
600 fresh train/calibrate/test splits of the same data. Conditional
|
|
44
|
+
coverage is not just ">= 90% on average": it has a known exact
|
|
45
|
+
distribution, Beta(n+1-l, l) over the calibration draw (Vovk 2012). The
|
|
46
|
+
600-run empirical mean lands on the law's mean to the fourth digit
|
|
47
|
+
(0.9001 vs 0.9001), and the histogram sits inside the density like it
|
|
48
|
+
was poured there. Also visible: 47% of individual runs are below 0.90,
|
|
49
|
+
which is exactly what the law says should happen, and exactly the fine
|
|
50
|
+
print people miss.
|
|
51
|
+
|
|
52
|
+

|
|
53
|
+
|
|
54
|
+
The whole proof: an exchangeable point's score has uniform rank among
|
|
55
|
+
the n+1 scores, so it falls at or below the ceil((n+1)(1-alpha))-th
|
|
56
|
+
calibration score with probability at least 1-alpha. Counting, nothing
|
|
57
|
+
else. (Source: [assets/manim_ranks.py](assets/manim_ranks.py), video in
|
|
58
|
+
[assets/ranks.mp4](assets/ranks.mp4).)
|
|
59
|
+
|
|
60
|
+
## Then you deploy it, and the fine print detonates
|
|
61
|
+
|
|
62
|
+
Train on early 2011 of the Washington DC bike-sharing panel, calibrate
|
|
63
|
+
on late 2011, then serve every hour of 2012 in order:
|
|
64
|
+
|
|
65
|
+

|
|
66
|
+
|
|
67
|
+
2012 ridership grew about 65% over 2011. Exchangeability with the
|
|
68
|
+
calibration set is simply false, and the guarantee follows it down:
|
|
69
|
+
78% delivered against 90% promised for the year, and when Hurricane
|
|
70
|
+
Sandy closes the city on October 29 (22 rides all day against a normal
|
|
71
|
+
7,500), the rolling window bottoms out at 42%.
|
|
72
|
+
|
|
73
|
+
Three things about that figure that I care about:
|
|
74
|
+
|
|
75
|
+
- **iyipada alarms in the first week of 2012** (PSI 0.15 on the model's
|
|
76
|
+
conformity scores, p = 0.005), eleven months before Sandy. The drift
|
|
77
|
+
was structural, not meteorological.
|
|
78
|
+
- **The shuffled control behaves**: run the same machinery on a
|
|
79
|
+
shuffled (exchangeable) version of the stream and coverage holds at
|
|
80
|
+
90.3% with zero alarm weeks out of 51. The alarm is not trigger-happy;
|
|
81
|
+
the guarantee did not break by accident.
|
|
82
|
+
- **Adaptive conformal repairs it online.** ACI (Gibbs & Candes 2021)
|
|
83
|
+
moves the miscoverage dial with the observed errors,
|
|
84
|
+
alpha_t+1 = alpha_t + gamma(alpha - err_t), and ends the year at
|
|
85
|
+
89.97% against the 90% target, hurricane included, without ever
|
|
86
|
+
refitting the model.
|
|
87
|
+
|
|
88
|
+
## What the repair costs
|
|
89
|
+
|
|
90
|
+

|
|
91
|
+
|
|
92
|
+
Nothing is free: the adaptive intervals are about 5% wider in the median
|
|
93
|
+
hour, and for 36 hours around Sandy the dial demands more assurance than
|
|
94
|
+
4,395 calibration points can certify, so the honest interval is
|
|
95
|
+
unbounded. I consider that a feature: a system that says "this week I
|
|
96
|
+
can promise nothing" is telling the truth, and the fixed system that
|
|
97
|
+
kept quoting tidy intervals through a hurricane was not.
|
|
98
|
+
|
|
99
|
+

|
|
100
|
+
|
|
101
|
+
## Using it
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from idaniloju import Ridge, SplitConformal, AdaptiveConformal, coverage
|
|
105
|
+
|
|
106
|
+
sc = SplitConformal(Ridge(lam=5.0), alpha=0.1) # any model with fit/predict
|
|
107
|
+
sc.fit(X_train, y_train, X_cal, y_cal)
|
|
108
|
+
lo, hi = sc.predict(X_new)
|
|
109
|
+
|
|
110
|
+
aci = AdaptiveConformal(sc, gamma=0.01) # for streams
|
|
111
|
+
lo, hi, err = aci.step(x_t, y_t)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`SplitConformal` takes any object with fit/predict (a scale model makes
|
|
115
|
+
the intervals difficulty-aware without touching the guarantee), and
|
|
116
|
+
`conformal_quantile` is the exact order statistic the theorem is proved
|
|
117
|
+
for, not an interpolated quantile: at small n and small alpha it
|
|
118
|
+
returns an infinite interval instead of a fake finite one.
|
|
119
|
+
|
|
120
|
+
## Honest limits
|
|
121
|
+
|
|
122
|
+
- The guarantee is **marginal**: 90% over everything, not 90% for every
|
|
123
|
+
hour of the day or every kind of input. Group-conditional coverage
|
|
124
|
+
needs group-wise calibration this library does not implement.
|
|
125
|
+
- ACI trades a broken assumption for a feedback loop: it needs y_t
|
|
126
|
+
observed reasonably soon after predicting. With delayed labels the
|
|
127
|
+
dial lags exactly that delay.
|
|
128
|
+
- The base model here is a ridge regression on purpose (any model
|
|
129
|
+
works). Sharper models give narrower intervals; nothing about the
|
|
130
|
+
coverage story changes.
|
|
131
|
+
- The 65% growth and the hurricane make this dataset a vivid drift
|
|
132
|
+
story, which is why I chose it; datasets with gentler drift break more
|
|
133
|
+
slowly and less photogenically.
|
|
134
|
+
|
|
135
|
+
## Reproduce it
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
python -m venv .venv && .venv/bin/pip install numpy pytest iyipada
|
|
139
|
+
bash data/get_data.sh # or let the loader fetch it
|
|
140
|
+
|
|
141
|
+
.venv/bin/python -m pytest tests/ -q # 6 contracts
|
|
142
|
+
.venv/bin/python benchmarks/bench_guarantee.py # the Beta law, 600 refits
|
|
143
|
+
.venv/bin/python benchmarks/bench_stream.py # the 2012 deployment
|
|
144
|
+
.venv/bin/python assets/make_visuals.py # the four figures
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Layout
|
|
148
|
+
|
|
149
|
+
- [idaniloju/conformal.py](idaniloju/conformal.py) the method: exact
|
|
150
|
+
quantile, split conformal, difficulty scaling, ACI
|
|
151
|
+
- [idaniloju/models.py](idaniloju/models.py) a closed-form ridge, the
|
|
152
|
+
deliberately boring predictor
|
|
153
|
+
- [idaniloju/bike.py](idaniloju/bike.py) the panel, with the year column
|
|
154
|
+
deliberately withheld from the features (deployment does not get told
|
|
155
|
+
the world changed)
|
|
156
|
+
- [benchmarks/](benchmarks/) the law and the deployment, everything into
|
|
157
|
+
JSON the figures and tests read
|
|
158
|
+
|
|
159
|
+
## Papers
|
|
160
|
+
|
|
161
|
+
- Vovk, Gammerman, Shafer (2005), *Algorithmic Learning in a Random
|
|
162
|
+
World*. The theory.
|
|
163
|
+
- Lei et al. (2018), *Distribution-Free Predictive Inference for
|
|
164
|
+
Regression*, JASA. The split conformal recipe used here.
|
|
165
|
+
- Vovk (2012), *Conditional validity of inductive conformal predictors*.
|
|
166
|
+
The Beta law the first figure draws.
|
|
167
|
+
- Gibbs & Candes (2021), *Adaptive Conformal Inference Under
|
|
168
|
+
Distribution Shift*. The repair.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# idaniloju
|
|
2
|
+
|
|
3
|
+
Conformal prediction from scratch: the exact finite-sample guarantee,
|
|
4
|
+
proof by experiment, the deployment that breaks it, and the online
|
|
5
|
+
repair. The tenth and last project of this series, and it closes the
|
|
6
|
+
loop: the drift that kills the guarantee here is detected by
|
|
7
|
+
[iyipada](https://github.com/Kenny0bi/iyipada), my drift library,
|
|
8
|
+
installed from PyPI like any other user would.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
pip install idaniloju
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
*Idaniloju* is Yoruba for assurance. Conformal prediction is the cleanest
|
|
15
|
+
promise in machine learning: wrap ANY point predictor, and its intervals
|
|
16
|
+
will contain the truth 90% of the time, guaranteed at finite sample size
|
|
17
|
+
with no distributional assumptions at all. The entire method is one
|
|
18
|
+
counting argument, and this repository walks it from the theorem to the
|
|
19
|
+
hurricane that broke it.
|
|
20
|
+
|
|
21
|
+
## The guarantee is a law, not a hope
|
|
22
|
+
|
|
23
|
+

|
|
24
|
+
|
|
25
|
+
600 fresh train/calibrate/test splits of the same data. Conditional
|
|
26
|
+
coverage is not just ">= 90% on average": it has a known exact
|
|
27
|
+
distribution, Beta(n+1-l, l) over the calibration draw (Vovk 2012). The
|
|
28
|
+
600-run empirical mean lands on the law's mean to the fourth digit
|
|
29
|
+
(0.9001 vs 0.9001), and the histogram sits inside the density like it
|
|
30
|
+
was poured there. Also visible: 47% of individual runs are below 0.90,
|
|
31
|
+
which is exactly what the law says should happen, and exactly the fine
|
|
32
|
+
print people miss.
|
|
33
|
+
|
|
34
|
+

|
|
35
|
+
|
|
36
|
+
The whole proof: an exchangeable point's score has uniform rank among
|
|
37
|
+
the n+1 scores, so it falls at or below the ceil((n+1)(1-alpha))-th
|
|
38
|
+
calibration score with probability at least 1-alpha. Counting, nothing
|
|
39
|
+
else. (Source: [assets/manim_ranks.py](assets/manim_ranks.py), video in
|
|
40
|
+
[assets/ranks.mp4](assets/ranks.mp4).)
|
|
41
|
+
|
|
42
|
+
## Then you deploy it, and the fine print detonates
|
|
43
|
+
|
|
44
|
+
Train on early 2011 of the Washington DC bike-sharing panel, calibrate
|
|
45
|
+
on late 2011, then serve every hour of 2012 in order:
|
|
46
|
+
|
|
47
|
+

|
|
48
|
+
|
|
49
|
+
2012 ridership grew about 65% over 2011. Exchangeability with the
|
|
50
|
+
calibration set is simply false, and the guarantee follows it down:
|
|
51
|
+
78% delivered against 90% promised for the year, and when Hurricane
|
|
52
|
+
Sandy closes the city on October 29 (22 rides all day against a normal
|
|
53
|
+
7,500), the rolling window bottoms out at 42%.
|
|
54
|
+
|
|
55
|
+
Three things about that figure that I care about:
|
|
56
|
+
|
|
57
|
+
- **iyipada alarms in the first week of 2012** (PSI 0.15 on the model's
|
|
58
|
+
conformity scores, p = 0.005), eleven months before Sandy. The drift
|
|
59
|
+
was structural, not meteorological.
|
|
60
|
+
- **The shuffled control behaves**: run the same machinery on a
|
|
61
|
+
shuffled (exchangeable) version of the stream and coverage holds at
|
|
62
|
+
90.3% with zero alarm weeks out of 51. The alarm is not trigger-happy;
|
|
63
|
+
the guarantee did not break by accident.
|
|
64
|
+
- **Adaptive conformal repairs it online.** ACI (Gibbs & Candes 2021)
|
|
65
|
+
moves the miscoverage dial with the observed errors,
|
|
66
|
+
alpha_t+1 = alpha_t + gamma(alpha - err_t), and ends the year at
|
|
67
|
+
89.97% against the 90% target, hurricane included, without ever
|
|
68
|
+
refitting the model.
|
|
69
|
+
|
|
70
|
+
## What the repair costs
|
|
71
|
+
|
|
72
|
+

|
|
73
|
+
|
|
74
|
+
Nothing is free: the adaptive intervals are about 5% wider in the median
|
|
75
|
+
hour, and for 36 hours around Sandy the dial demands more assurance than
|
|
76
|
+
4,395 calibration points can certify, so the honest interval is
|
|
77
|
+
unbounded. I consider that a feature: a system that says "this week I
|
|
78
|
+
can promise nothing" is telling the truth, and the fixed system that
|
|
79
|
+
kept quoting tidy intervals through a hurricane was not.
|
|
80
|
+
|
|
81
|
+

|
|
82
|
+
|
|
83
|
+
## Using it
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from idaniloju import Ridge, SplitConformal, AdaptiveConformal, coverage
|
|
87
|
+
|
|
88
|
+
sc = SplitConformal(Ridge(lam=5.0), alpha=0.1) # any model with fit/predict
|
|
89
|
+
sc.fit(X_train, y_train, X_cal, y_cal)
|
|
90
|
+
lo, hi = sc.predict(X_new)
|
|
91
|
+
|
|
92
|
+
aci = AdaptiveConformal(sc, gamma=0.01) # for streams
|
|
93
|
+
lo, hi, err = aci.step(x_t, y_t)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`SplitConformal` takes any object with fit/predict (a scale model makes
|
|
97
|
+
the intervals difficulty-aware without touching the guarantee), and
|
|
98
|
+
`conformal_quantile` is the exact order statistic the theorem is proved
|
|
99
|
+
for, not an interpolated quantile: at small n and small alpha it
|
|
100
|
+
returns an infinite interval instead of a fake finite one.
|
|
101
|
+
|
|
102
|
+
## Honest limits
|
|
103
|
+
|
|
104
|
+
- The guarantee is **marginal**: 90% over everything, not 90% for every
|
|
105
|
+
hour of the day or every kind of input. Group-conditional coverage
|
|
106
|
+
needs group-wise calibration this library does not implement.
|
|
107
|
+
- ACI trades a broken assumption for a feedback loop: it needs y_t
|
|
108
|
+
observed reasonably soon after predicting. With delayed labels the
|
|
109
|
+
dial lags exactly that delay.
|
|
110
|
+
- The base model here is a ridge regression on purpose (any model
|
|
111
|
+
works). Sharper models give narrower intervals; nothing about the
|
|
112
|
+
coverage story changes.
|
|
113
|
+
- The 65% growth and the hurricane make this dataset a vivid drift
|
|
114
|
+
story, which is why I chose it; datasets with gentler drift break more
|
|
115
|
+
slowly and less photogenically.
|
|
116
|
+
|
|
117
|
+
## Reproduce it
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
python -m venv .venv && .venv/bin/pip install numpy pytest iyipada
|
|
121
|
+
bash data/get_data.sh # or let the loader fetch it
|
|
122
|
+
|
|
123
|
+
.venv/bin/python -m pytest tests/ -q # 6 contracts
|
|
124
|
+
.venv/bin/python benchmarks/bench_guarantee.py # the Beta law, 600 refits
|
|
125
|
+
.venv/bin/python benchmarks/bench_stream.py # the 2012 deployment
|
|
126
|
+
.venv/bin/python assets/make_visuals.py # the four figures
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Layout
|
|
130
|
+
|
|
131
|
+
- [idaniloju/conformal.py](idaniloju/conformal.py) the method: exact
|
|
132
|
+
quantile, split conformal, difficulty scaling, ACI
|
|
133
|
+
- [idaniloju/models.py](idaniloju/models.py) a closed-form ridge, the
|
|
134
|
+
deliberately boring predictor
|
|
135
|
+
- [idaniloju/bike.py](idaniloju/bike.py) the panel, with the year column
|
|
136
|
+
deliberately withheld from the features (deployment does not get told
|
|
137
|
+
the world changed)
|
|
138
|
+
- [benchmarks/](benchmarks/) the law and the deployment, everything into
|
|
139
|
+
JSON the figures and tests read
|
|
140
|
+
|
|
141
|
+
## Papers
|
|
142
|
+
|
|
143
|
+
- Vovk, Gammerman, Shafer (2005), *Algorithmic Learning in a Random
|
|
144
|
+
World*. The theory.
|
|
145
|
+
- Lei et al. (2018), *Distribution-Free Predictive Inference for
|
|
146
|
+
Regression*, JASA. The split conformal recipe used here.
|
|
147
|
+
- Vovk (2012), *Conditional validity of inductive conformal predictors*.
|
|
148
|
+
The Beta law the first figure draws.
|
|
149
|
+
- Gibbs & Candes (2021), *Adaptive Conformal Inference Under
|
|
150
|
+
Distribution Shift*. The repair.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""idaniloju: conformal prediction from scratch, and what drift does to it.
|
|
2
|
+
|
|
3
|
+
*Idaniloju* is Yoruba for assurance. Conformal prediction turns any point
|
|
4
|
+
predictor into interval predictions with a finite-sample coverage
|
|
5
|
+
guarantee that assumes nothing about the data's distribution, only that
|
|
6
|
+
calibration and future points are exchangeable. That one assumption is
|
|
7
|
+
also the method's weak joint: break it with drift and the guarantee
|
|
8
|
+
quietly dies. This library ships the estimator, the proof-by-experiment,
|
|
9
|
+
the failure, and the online repair.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .conformal import SplitConformal, AdaptiveConformal, coverage
|
|
13
|
+
from .models import Ridge
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.0"
|
|
16
|
+
__all__ = ["SplitConformal", "AdaptiveConformal", "Ridge", "coverage"]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""The Washington DC bike-sharing panel (UCI 275): 17,379 hours over
|
|
2
|
+
2011-2012, with weather. Chosen because its drift is real, not
|
|
3
|
+
simulated: 2012 ridership grew about 65% over 2011, and no model trained
|
|
4
|
+
on 2011 has seen that world.
|
|
5
|
+
|
|
6
|
+
The year indicator is deliberately excluded from the features: a
|
|
7
|
+
deployed model does not get told "the world has changed" as a column.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import csv
|
|
11
|
+
import io
|
|
12
|
+
import urllib.request
|
|
13
|
+
import zipfile
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
DATA = Path(__file__).resolve().parents[1] / "data" / "hour.csv"
|
|
19
|
+
URL = "https://archive.ics.uci.edu/static/public/275/bike+sharing+dataset.zip"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _fetch():
|
|
23
|
+
"""Installed from PyPI there is no data/ folder; fetch to a cache."""
|
|
24
|
+
cache = Path.home() / ".cache" / "idaniloju" / "hour.csv"
|
|
25
|
+
if not cache.exists():
|
|
26
|
+
cache.parent.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
with urllib.request.urlopen(URL, timeout=120) as r:
|
|
28
|
+
z = zipfile.ZipFile(io.BytesIO(r.read()))
|
|
29
|
+
cache.write_bytes(z.read("hour.csv"))
|
|
30
|
+
return cache
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load(path=None):
|
|
34
|
+
p = Path(path) if path else (DATA if DATA.exists() else _fetch())
|
|
35
|
+
rows = list(csv.DictReader(open(p, encoding="utf-8")))
|
|
36
|
+
n = len(rows)
|
|
37
|
+
|
|
38
|
+
def f(name):
|
|
39
|
+
return np.array([float(r[name]) for r in rows])
|
|
40
|
+
|
|
41
|
+
hr, wd = f("hr"), f("weekday")
|
|
42
|
+
season = f("season").astype(int)
|
|
43
|
+
weather = f("weathersit").astype(int)
|
|
44
|
+
feats = [
|
|
45
|
+
np.sin(2 * np.pi * wd / 7), np.cos(2 * np.pi * wd / 7),
|
|
46
|
+
f("temp"), f("atemp"), f("hum"), f("windspeed"),
|
|
47
|
+
f("workingday"), f("holiday"),
|
|
48
|
+
]
|
|
49
|
+
names = ["wd_sin", "wd_cos", "temp", "atemp", "hum", "windspeed",
|
|
50
|
+
"workingday", "holiday"]
|
|
51
|
+
hh = hr.astype(int)
|
|
52
|
+
for h in range(1, 24): # hour-of-day dummies: the ridership
|
|
53
|
+
feats.append((hh == h).astype(float)) # pattern is not sinusoidal
|
|
54
|
+
names.append(f"hr_{h}")
|
|
55
|
+
# working-day x hour interactions for the commute spikes
|
|
56
|
+
work = f("workingday")
|
|
57
|
+
for h in (7, 8, 9, 17, 18, 19):
|
|
58
|
+
feats.append(((hh == h) * work).astype(float))
|
|
59
|
+
names.append(f"work_hr_{h}")
|
|
60
|
+
for s in (2, 3, 4):
|
|
61
|
+
feats.append((season == s).astype(float))
|
|
62
|
+
names.append(f"season_{s}")
|
|
63
|
+
for w in (2, 3, 4):
|
|
64
|
+
feats.append((weather == w).astype(float))
|
|
65
|
+
names.append(f"weather_{w}")
|
|
66
|
+
X = np.column_stack(feats)
|
|
67
|
+
y = np.log1p(f("cnt"))
|
|
68
|
+
dates = np.array([r["dteday"] for r in rows])
|
|
69
|
+
return X, y, dates, names
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Split conformal prediction, and the online repair for when drift
|
|
2
|
+
breaks it.
|
|
3
|
+
|
|
4
|
+
The whole method is a counting argument. Hold out n calibration points,
|
|
5
|
+
score each by how wrong the model was (here |y - yhat|, optionally
|
|
6
|
+
scaled by a difficulty estimate), and let q be the
|
|
7
|
+
ceil((n+1)(1-alpha))-th smallest score. If a new point is exchangeable
|
|
8
|
+
with the calibration points, its score's rank among all n+1 is uniform,
|
|
9
|
+
so
|
|
10
|
+
|
|
11
|
+
P(score_new <= q) >= (1 - alpha)
|
|
12
|
+
|
|
13
|
+
exactly, for any model, any distribution, any finite n. No asymptotics,
|
|
14
|
+
no Gaussians. The price is the assumption of exchangeability, which
|
|
15
|
+
drift destroys; AdaptiveConformal (Gibbs & Candes 2021) repairs coverage
|
|
16
|
+
online by moving alpha with the observed errors.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def conformal_quantile(scores, alpha):
|
|
23
|
+
"""The ceil((n+1)(1-alpha))/n empirical quantile, the exact one the
|
|
24
|
+
guarantee is proved for (not numpy's interpolated quantiles)."""
|
|
25
|
+
s = np.sort(np.asarray(scores, float))
|
|
26
|
+
n = len(s)
|
|
27
|
+
k = int(np.ceil((n + 1) * (1 - alpha)))
|
|
28
|
+
if k > n:
|
|
29
|
+
return np.inf # not enough calibration data for this alpha
|
|
30
|
+
return float(s[k - 1])
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def coverage(y, lo, hi):
|
|
34
|
+
y = np.asarray(y, float)
|
|
35
|
+
return float(np.mean((y >= np.asarray(lo)) & (y <= np.asarray(hi))))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SplitConformal:
|
|
39
|
+
"""Intervals yhat +/- q, with q from a held-out calibration set.
|
|
40
|
+
|
|
41
|
+
scale_model, if given, is fit to |residual| on the calibration set's
|
|
42
|
+
training half and divides the scores, so hard inputs get wide
|
|
43
|
+
intervals and easy ones narrow (locally weighted conformal). The
|
|
44
|
+
guarantee is unchanged: it never depends on either model being good.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, model, alpha=0.1, scale_model=None):
|
|
48
|
+
self.model = model
|
|
49
|
+
self.alpha = alpha
|
|
50
|
+
self.scale_model = scale_model
|
|
51
|
+
|
|
52
|
+
def _sigma(self, X):
|
|
53
|
+
if self.scale_model is None:
|
|
54
|
+
return np.ones(len(X))
|
|
55
|
+
return np.maximum(self.scale_model.predict(X), 1e-6)
|
|
56
|
+
|
|
57
|
+
def fit(self, X_train, y_train, X_cal, y_cal):
|
|
58
|
+
self.model.fit(X_train, y_train)
|
|
59
|
+
if self.scale_model is not None:
|
|
60
|
+
resid = np.abs(y_train - self.model.predict(X_train))
|
|
61
|
+
self.scale_model.fit(X_train, resid)
|
|
62
|
+
scores = (np.abs(y_cal - self.model.predict(X_cal))
|
|
63
|
+
/ self._sigma(X_cal))
|
|
64
|
+
self.scores_ = np.sort(scores)
|
|
65
|
+
self.q_ = conformal_quantile(scores, self.alpha)
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
def predict(self, X, alpha=None):
|
|
69
|
+
q = (self.q_ if alpha is None
|
|
70
|
+
else conformal_quantile(self.scores_, alpha))
|
|
71
|
+
yhat = self.model.predict(X)
|
|
72
|
+
w = q * self._sigma(X)
|
|
73
|
+
return yhat - w, yhat + w
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class AdaptiveConformal:
|
|
77
|
+
"""Adaptive conformal inference on a stream (Gibbs & Candes 2021).
|
|
78
|
+
|
|
79
|
+
Keeps the fitted SplitConformal but retunes its miscoverage level
|
|
80
|
+
online:
|
|
81
|
+
|
|
82
|
+
alpha_{t+1} = alpha_t + gamma * (alpha_target - err_t)
|
|
83
|
+
|
|
84
|
+
with err_t = 1 when y_t misses the interval. Coverage errors above
|
|
85
|
+
target push alpha down (wider intervals), quiet stretches push it
|
|
86
|
+
back up. The long-run error rate converges to alpha_target for ANY
|
|
87
|
+
sequence, even adversarial: the guarantee moves from exchangeability
|
|
88
|
+
to online feedback.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(self, conformal, gamma=0.005):
|
|
92
|
+
self.c = conformal
|
|
93
|
+
self.gamma = gamma
|
|
94
|
+
self.alpha_t = conformal.alpha
|
|
95
|
+
self.history = []
|
|
96
|
+
|
|
97
|
+
def step(self, x, y):
|
|
98
|
+
a = float(np.clip(self.alpha_t, 1e-4, 0.999))
|
|
99
|
+
lo, hi = self.c.predict(x[None, :], alpha=a)
|
|
100
|
+
err = float(not (lo[0] <= y <= hi[0]))
|
|
101
|
+
self.alpha_t += self.gamma * (self.c.alpha - err)
|
|
102
|
+
self.history.append({"alpha": a, "lo": float(lo[0]),
|
|
103
|
+
"hi": float(hi[0]), "err": err})
|
|
104
|
+
return lo[0], hi[0], err
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""A plain ridge regression, closed form, numpy only.
|
|
2
|
+
|
|
3
|
+
Conformal prediction wraps any point predictor, so the predictor here is
|
|
4
|
+
deliberately boring: standardized features, an intercept, and one line of
|
|
5
|
+
linear algebra. The interesting guarantees live in conformal.py.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Ridge:
|
|
12
|
+
def __init__(self, lam=1.0):
|
|
13
|
+
self.lam = lam
|
|
14
|
+
|
|
15
|
+
def fit(self, X, y):
|
|
16
|
+
X = np.asarray(X, float)
|
|
17
|
+
y = np.asarray(y, float)
|
|
18
|
+
self.mu_ = X.mean(0)
|
|
19
|
+
self.sd_ = X.std(0)
|
|
20
|
+
self.sd_[self.sd_ == 0] = 1.0
|
|
21
|
+
Xs = (X - self.mu_) / self.sd_
|
|
22
|
+
n, d = Xs.shape
|
|
23
|
+
A = Xs.T @ Xs + self.lam * np.eye(d)
|
|
24
|
+
self.coef_ = np.linalg.solve(A, Xs.T @ (y - y.mean()))
|
|
25
|
+
self.intercept_ = float(y.mean())
|
|
26
|
+
return self
|
|
27
|
+
|
|
28
|
+
def predict(self, X):
|
|
29
|
+
Xs = (np.asarray(X, float) - self.mu_) / self.sd_
|
|
30
|
+
return Xs @ self.coef_ + self.intercept_
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: idaniloju
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Conformal prediction from scratch: the exact finite-sample guarantee, what drift does to it, and the online repair
|
|
5
|
+
Author-email: Kenny Obidele <obidelek19@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Kenny0bi/idaniloju
|
|
8
|
+
Project-URL: Issues, https://github.com/Kenny0bi/idaniloju/issues
|
|
9
|
+
Keywords: conformal-prediction,uncertainty-quantification,prediction-intervals,distribution-free,drift
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: numpy>=1.24
|
|
18
|
+
|
|
19
|
+
# idaniloju
|
|
20
|
+
|
|
21
|
+
Conformal prediction from scratch: the exact finite-sample guarantee,
|
|
22
|
+
proof by experiment, the deployment that breaks it, and the online
|
|
23
|
+
repair. The tenth and last project of this series, and it closes the
|
|
24
|
+
loop: the drift that kills the guarantee here is detected by
|
|
25
|
+
[iyipada](https://github.com/Kenny0bi/iyipada), my drift library,
|
|
26
|
+
installed from PyPI like any other user would.
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
pip install idaniloju
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
*Idaniloju* is Yoruba for assurance. Conformal prediction is the cleanest
|
|
33
|
+
promise in machine learning: wrap ANY point predictor, and its intervals
|
|
34
|
+
will contain the truth 90% of the time, guaranteed at finite sample size
|
|
35
|
+
with no distributional assumptions at all. The entire method is one
|
|
36
|
+
counting argument, and this repository walks it from the theorem to the
|
|
37
|
+
hurricane that broke it.
|
|
38
|
+
|
|
39
|
+
## The guarantee is a law, not a hope
|
|
40
|
+
|
|
41
|
+

|
|
42
|
+
|
|
43
|
+
600 fresh train/calibrate/test splits of the same data. Conditional
|
|
44
|
+
coverage is not just ">= 90% on average": it has a known exact
|
|
45
|
+
distribution, Beta(n+1-l, l) over the calibration draw (Vovk 2012). The
|
|
46
|
+
600-run empirical mean lands on the law's mean to the fourth digit
|
|
47
|
+
(0.9001 vs 0.9001), and the histogram sits inside the density like it
|
|
48
|
+
was poured there. Also visible: 47% of individual runs are below 0.90,
|
|
49
|
+
which is exactly what the law says should happen, and exactly the fine
|
|
50
|
+
print people miss.
|
|
51
|
+
|
|
52
|
+

|
|
53
|
+
|
|
54
|
+
The whole proof: an exchangeable point's score has uniform rank among
|
|
55
|
+
the n+1 scores, so it falls at or below the ceil((n+1)(1-alpha))-th
|
|
56
|
+
calibration score with probability at least 1-alpha. Counting, nothing
|
|
57
|
+
else. (Source: [assets/manim_ranks.py](assets/manim_ranks.py), video in
|
|
58
|
+
[assets/ranks.mp4](assets/ranks.mp4).)
|
|
59
|
+
|
|
60
|
+
## Then you deploy it, and the fine print detonates
|
|
61
|
+
|
|
62
|
+
Train on early 2011 of the Washington DC bike-sharing panel, calibrate
|
|
63
|
+
on late 2011, then serve every hour of 2012 in order:
|
|
64
|
+
|
|
65
|
+

|
|
66
|
+
|
|
67
|
+
2012 ridership grew about 65% over 2011. Exchangeability with the
|
|
68
|
+
calibration set is simply false, and the guarantee follows it down:
|
|
69
|
+
78% delivered against 90% promised for the year, and when Hurricane
|
|
70
|
+
Sandy closes the city on October 29 (22 rides all day against a normal
|
|
71
|
+
7,500), the rolling window bottoms out at 42%.
|
|
72
|
+
|
|
73
|
+
Three things about that figure that I care about:
|
|
74
|
+
|
|
75
|
+
- **iyipada alarms in the first week of 2012** (PSI 0.15 on the model's
|
|
76
|
+
conformity scores, p = 0.005), eleven months before Sandy. The drift
|
|
77
|
+
was structural, not meteorological.
|
|
78
|
+
- **The shuffled control behaves**: run the same machinery on a
|
|
79
|
+
shuffled (exchangeable) version of the stream and coverage holds at
|
|
80
|
+
90.3% with zero alarm weeks out of 51. The alarm is not trigger-happy;
|
|
81
|
+
the guarantee did not break by accident.
|
|
82
|
+
- **Adaptive conformal repairs it online.** ACI (Gibbs & Candes 2021)
|
|
83
|
+
moves the miscoverage dial with the observed errors,
|
|
84
|
+
alpha_t+1 = alpha_t + gamma(alpha - err_t), and ends the year at
|
|
85
|
+
89.97% against the 90% target, hurricane included, without ever
|
|
86
|
+
refitting the model.
|
|
87
|
+
|
|
88
|
+
## What the repair costs
|
|
89
|
+
|
|
90
|
+

|
|
91
|
+
|
|
92
|
+
Nothing is free: the adaptive intervals are about 5% wider in the median
|
|
93
|
+
hour, and for 36 hours around Sandy the dial demands more assurance than
|
|
94
|
+
4,395 calibration points can certify, so the honest interval is
|
|
95
|
+
unbounded. I consider that a feature: a system that says "this week I
|
|
96
|
+
can promise nothing" is telling the truth, and the fixed system that
|
|
97
|
+
kept quoting tidy intervals through a hurricane was not.
|
|
98
|
+
|
|
99
|
+

|
|
100
|
+
|
|
101
|
+
## Using it
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from idaniloju import Ridge, SplitConformal, AdaptiveConformal, coverage
|
|
105
|
+
|
|
106
|
+
sc = SplitConformal(Ridge(lam=5.0), alpha=0.1) # any model with fit/predict
|
|
107
|
+
sc.fit(X_train, y_train, X_cal, y_cal)
|
|
108
|
+
lo, hi = sc.predict(X_new)
|
|
109
|
+
|
|
110
|
+
aci = AdaptiveConformal(sc, gamma=0.01) # for streams
|
|
111
|
+
lo, hi, err = aci.step(x_t, y_t)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`SplitConformal` takes any object with fit/predict (a scale model makes
|
|
115
|
+
the intervals difficulty-aware without touching the guarantee), and
|
|
116
|
+
`conformal_quantile` is the exact order statistic the theorem is proved
|
|
117
|
+
for, not an interpolated quantile: at small n and small alpha it
|
|
118
|
+
returns an infinite interval instead of a fake finite one.
|
|
119
|
+
|
|
120
|
+
## Honest limits
|
|
121
|
+
|
|
122
|
+
- The guarantee is **marginal**: 90% over everything, not 90% for every
|
|
123
|
+
hour of the day or every kind of input. Group-conditional coverage
|
|
124
|
+
needs group-wise calibration this library does not implement.
|
|
125
|
+
- ACI trades a broken assumption for a feedback loop: it needs y_t
|
|
126
|
+
observed reasonably soon after predicting. With delayed labels the
|
|
127
|
+
dial lags exactly that delay.
|
|
128
|
+
- The base model here is a ridge regression on purpose (any model
|
|
129
|
+
works). Sharper models give narrower intervals; nothing about the
|
|
130
|
+
coverage story changes.
|
|
131
|
+
- The 65% growth and the hurricane make this dataset a vivid drift
|
|
132
|
+
story, which is why I chose it; datasets with gentler drift break more
|
|
133
|
+
slowly and less photogenically.
|
|
134
|
+
|
|
135
|
+
## Reproduce it
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
python -m venv .venv && .venv/bin/pip install numpy pytest iyipada
|
|
139
|
+
bash data/get_data.sh # or let the loader fetch it
|
|
140
|
+
|
|
141
|
+
.venv/bin/python -m pytest tests/ -q # 6 contracts
|
|
142
|
+
.venv/bin/python benchmarks/bench_guarantee.py # the Beta law, 600 refits
|
|
143
|
+
.venv/bin/python benchmarks/bench_stream.py # the 2012 deployment
|
|
144
|
+
.venv/bin/python assets/make_visuals.py # the four figures
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Layout
|
|
148
|
+
|
|
149
|
+
- [idaniloju/conformal.py](idaniloju/conformal.py) the method: exact
|
|
150
|
+
quantile, split conformal, difficulty scaling, ACI
|
|
151
|
+
- [idaniloju/models.py](idaniloju/models.py) a closed-form ridge, the
|
|
152
|
+
deliberately boring predictor
|
|
153
|
+
- [idaniloju/bike.py](idaniloju/bike.py) the panel, with the year column
|
|
154
|
+
deliberately withheld from the features (deployment does not get told
|
|
155
|
+
the world changed)
|
|
156
|
+
- [benchmarks/](benchmarks/) the law and the deployment, everything into
|
|
157
|
+
JSON the figures and tests read
|
|
158
|
+
|
|
159
|
+
## Papers
|
|
160
|
+
|
|
161
|
+
- Vovk, Gammerman, Shafer (2005), *Algorithmic Learning in a Random
|
|
162
|
+
World*. The theory.
|
|
163
|
+
- Lei et al. (2018), *Distribution-Free Predictive Inference for
|
|
164
|
+
Regression*, JASA. The split conformal recipe used here.
|
|
165
|
+
- Vovk (2012), *Conditional validity of inductive conformal predictors*.
|
|
166
|
+
The Beta law the first figure draws.
|
|
167
|
+
- Gibbs & Candes (2021), *Adaptive Conformal Inference Under
|
|
168
|
+
Distribution Shift*. The repair.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
idaniloju/__init__.py
|
|
4
|
+
idaniloju/bike.py
|
|
5
|
+
idaniloju/conformal.py
|
|
6
|
+
idaniloju/models.py
|
|
7
|
+
idaniloju.egg-info/PKG-INFO
|
|
8
|
+
idaniloju.egg-info/SOURCES.txt
|
|
9
|
+
idaniloju.egg-info/dependency_links.txt
|
|
10
|
+
idaniloju.egg-info/requires.txt
|
|
11
|
+
idaniloju.egg-info/top_level.txt
|
|
12
|
+
tests/test_idaniloju.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
numpy>=1.24
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
idaniloju
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "idaniloju"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Conformal prediction from scratch: the exact finite-sample guarantee, what drift does to it, and the online repair"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [{name = "Kenny Obidele", email = "obidelek19@gmail.com"}]
|
|
13
|
+
keywords = ["conformal-prediction", "uncertainty-quantification",
|
|
14
|
+
"prediction-intervals", "distribution-free", "drift"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
21
|
+
]
|
|
22
|
+
dependencies = ["numpy>=1.24"]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/Kenny0bi/idaniloju"
|
|
26
|
+
Issues = "https://github.com/Kenny0bi/idaniloju/issues"
|
|
27
|
+
|
|
28
|
+
[tool.setuptools]
|
|
29
|
+
packages = ["idaniloju"]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Contracts: the quantile the guarantee is proved for, the guarantee
|
|
2
|
+
itself on synthetic data, ACI's error convergence, and the benchmark
|
|
3
|
+
numbers once they exist."""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
13
|
+
sys.path.insert(0, str(ROOT))
|
|
14
|
+
|
|
15
|
+
from idaniloju import (AdaptiveConformal, Ridge, SplitConformal, # noqa
|
|
16
|
+
coverage)
|
|
17
|
+
from idaniloju.conformal import conformal_quantile # noqa
|
|
18
|
+
|
|
19
|
+
STREAM = ROOT / "benchmarks" / "stream.json"
|
|
20
|
+
GUAR = ROOT / "benchmarks" / "guarantee.json"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_conformal_quantile_is_the_exact_order_statistic():
|
|
24
|
+
scores = np.arange(1, 11, dtype=float) # 1..10, n = 10
|
|
25
|
+
# ceil(11 * 0.9) = 10 -> the 10th smallest = 10
|
|
26
|
+
assert conformal_quantile(scores, 0.1) == 10.0
|
|
27
|
+
# ceil(11 * 0.5) = 6 -> 6
|
|
28
|
+
assert conformal_quantile(scores, 0.5) == 6.0
|
|
29
|
+
# alpha so small that k > n: must refuse with an infinite interval
|
|
30
|
+
assert conformal_quantile(scores, 0.05) == np.inf
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_guarantee_holds_on_synthetic_exchangeable_data():
|
|
34
|
+
rng = np.random.default_rng(11)
|
|
35
|
+
covs = []
|
|
36
|
+
for _ in range(200):
|
|
37
|
+
X = rng.normal(0, 1, (700, 3))
|
|
38
|
+
y = X @ np.array([2.0, -1.0, 0.5]) + rng.standard_t(3, 700)
|
|
39
|
+
sc = SplitConformal(Ridge(lam=1.0), alpha=0.2)
|
|
40
|
+
sc.fit(X[:300], y[:300], X[300:500], y[300:500])
|
|
41
|
+
lo, hi = sc.predict(X[500:])
|
|
42
|
+
covs.append(coverage(y[500:], lo, hi))
|
|
43
|
+
# marginal coverage >= 1 - alpha in expectation; with 200 trials the
|
|
44
|
+
# mean should sit within a point of the target band
|
|
45
|
+
assert np.mean(covs) >= 0.79
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_aci_error_rate_converges_even_under_shift():
|
|
49
|
+
rng = np.random.default_rng(5)
|
|
50
|
+
X = rng.normal(0, 1, (3000, 2))
|
|
51
|
+
y = X @ np.array([1.0, 1.0]) + rng.normal(0, 1, 3000)
|
|
52
|
+
sc = SplitConformal(Ridge(lam=1.0), alpha=0.1)
|
|
53
|
+
sc.fit(X[:1000], y[:1000], X[1000:1500], y[1000:1500])
|
|
54
|
+
# the stream drifts hard: outputs shift by +3 halfway through
|
|
55
|
+
Xs, ys = X[1500:], y[1500:].copy()
|
|
56
|
+
ys[750:] += 3.0
|
|
57
|
+
aci = AdaptiveConformal(sc, gamma=0.02)
|
|
58
|
+
errs = [aci.step(Xs[i], ys[i])[2] for i in range(len(ys))]
|
|
59
|
+
# long-run error close to alpha despite the jump
|
|
60
|
+
assert abs(np.mean(errs) - 0.1) < 0.03
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_scale_model_widens_hard_inputs():
|
|
64
|
+
rng = np.random.default_rng(2)
|
|
65
|
+
X = rng.uniform(-1, 1, (2000, 1))
|
|
66
|
+
y = (X[:, 0] * 2 + rng.normal(0, 0.2 + 1.3 * (X[:, 0] > 0), 2000))
|
|
67
|
+
sc = SplitConformal(Ridge(0.1), alpha=0.1, scale_model=Ridge(0.1))
|
|
68
|
+
sc.fit(X[:800], y[:800], X[800:1400], y[800:1400])
|
|
69
|
+
lo, hi = sc.predict(X[1400:])
|
|
70
|
+
w = hi - lo
|
|
71
|
+
right = X[1400:, 0] > 0
|
|
72
|
+
assert w[right].mean() > 1.5 * w[~right].mean()
|
|
73
|
+
assert coverage(y[1400:], lo, hi) > 0.85
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@pytest.mark.skipif(not STREAM.exists(), reason="run bench_stream first")
|
|
77
|
+
def test_stream_results_hold():
|
|
78
|
+
s = json.loads(STREAM.read_text())
|
|
79
|
+
assert s["static_coverage"] < 0.80 # drift broke the guarantee
|
|
80
|
+
assert abs(s["aci_coverage"] - 0.9) < 0.01 # ACI repaired it
|
|
81
|
+
assert s["first_alarm"]["start"] == "2012-01-01" # iyipada saw it
|
|
82
|
+
ex = s["exchangeable_check"]
|
|
83
|
+
assert ex["weeks_alarmed"] == 0 # and does not cry wolf
|
|
84
|
+
assert ex["coverage"] > 0.89
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@pytest.mark.skipif(not GUAR.exists(), reason="run bench_guarantee first")
|
|
88
|
+
def test_coverage_distribution_matches_beta_law():
|
|
89
|
+
g = json.loads(GUAR.read_text())
|
|
90
|
+
covs = np.array(g["coverages"])
|
|
91
|
+
a, b = g["beta_a"], g["beta_b"]
|
|
92
|
+
beta_mean = a / (a + b)
|
|
93
|
+
beta_sd = np.sqrt(a * b / ((a + b) ** 2 * (a + b + 1)))
|
|
94
|
+
assert abs(covs.mean() - beta_mean) < 0.004
|
|
95
|
+
# spread of the same order as the law predicts (test-set noise adds a
|
|
96
|
+
# little on top of the Beta's own width)
|
|
97
|
+
assert beta_sd * 0.8 < covs.std() < beta_sd * 2.2
|