conforme 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.
- conforme-0.1.0/LICENSE +21 -0
- conforme-0.1.0/PKG-INFO +216 -0
- conforme-0.1.0/README.md +181 -0
- conforme-0.1.0/pyproject.toml +97 -0
- conforme-0.1.0/pyproject.toml.orig +84 -0
- conforme-0.1.0/src/conforme/__init__.py +81 -0
- conforme-0.1.0/src/conforme/backtest/__init__.py +6 -0
- conforme-0.1.0/src/conforme/backtest/forecasts.py +142 -0
- conforme-0.1.0/src/conforme/backtest/replay.py +96 -0
- conforme-0.1.0/src/conforme/conformal/__init__.py +42 -0
- conforme-0.1.0/src/conforme/conformal/calibrators/__init__.py +25 -0
- conforme-0.1.0/src/conforme/conformal/calibrators/aci.py +42 -0
- conforme-0.1.0/src/conforme/conformal/calibrators/base.py +97 -0
- conforme-0.1.0/src/conforme/conformal/calibrators/ranks.py +92 -0
- conforme-0.1.0/src/conforme/conformal/calibrators/risk.py +60 -0
- conforme-0.1.0/src/conforme/conformal/calibrators/split.py +120 -0
- conforme-0.1.0/src/conforme/conformal/calibrators/tracker.py +33 -0
- conforme-0.1.0/src/conforme/conformal/losses.py +31 -0
- conforme-0.1.0/src/conforme/conformal/scores.py +44 -0
- conforme-0.1.0/src/conforme/conformal/targets.py +47 -0
- conforme-0.1.0/src/conforme/data/__init__.py +6 -0
- conforme-0.1.0/src/conforme/data/hierarchy.py +103 -0
- conforme-0.1.0/src/conforme/data/panel.py +56 -0
- conforme-0.1.0/src/conforme/decision/__init__.py +6 -0
- conforme-0.1.0/src/conforme/decision/lost_sales.py +43 -0
- conforme-0.1.0/src/conforme/decision/policy.py +27 -0
- conforme-0.1.0/src/conforme/forecast/__init__.py +16 -0
- conforme-0.1.0/src/conforme/forecast/models/__init__.py +10 -0
- conforme-0.1.0/src/conforme/forecast/models/base.py +67 -0
- conforme-0.1.0/src/conforme/forecast/models/frames.py +83 -0
- conforme-0.1.0/src/conforme/forecast/models/mlforecast.py +100 -0
- conforme-0.1.0/src/conforme/forecast/models/naive.py +24 -0
- conforme-0.1.0/src/conforme/forecast/models/neuralforecast.py +77 -0
- conforme-0.1.0/src/conforme/forecast/models/statsforecast.py +32 -0
- conforme-0.1.0/src/conforme/forecast/reconcile.py +70 -0
- conforme-0.1.0/src/conforme/metrics.py +60 -0
- conforme-0.1.0/src/conforme/online/__init__.py +6 -0
- conforme-0.1.0/src/conforme/online/ledger.py +119 -0
- conforme-0.1.0/src/conforme/online/state.py +33 -0
- conforme-0.1.0/src/conforme/online/step.py +70 -0
- conforme-0.1.0/src/conforme/py.typed +0 -0
conforme-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Valentin Dusserre
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
conforme-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: conforme
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Conformal forecasting: point forecasts, hierarchy reconciliation, and calibrated bands.
|
|
5
|
+
Keywords: conformal prediction,forecasting,time series,hierarchical reconciliation,prediction intervals,inventory
|
|
6
|
+
Author: Valentin Dusserre
|
|
7
|
+
Author-email: Valentin Dusserre <valentindusserre@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Dist: numpy
|
|
20
|
+
Requires-Dist: pandas
|
|
21
|
+
Requires-Dist: scipy
|
|
22
|
+
Requires-Dist: mlforecast ; extra == 'ml'
|
|
23
|
+
Requires-Dist: neuralforecast ; extra == 'neural'
|
|
24
|
+
Requires-Dist: statsforecast ; extra == 'stats'
|
|
25
|
+
Requires-Python: >=3.12
|
|
26
|
+
Project-URL: Homepage, https://github.com/Vzlentin/conforme
|
|
27
|
+
Project-URL: Repository, https://github.com/Vzlentin/conforme
|
|
28
|
+
Project-URL: Issues, https://github.com/Vzlentin/conforme/issues
|
|
29
|
+
Project-URL: Documentation, https://github.com/Vzlentin/conforme/tree/main/docs
|
|
30
|
+
Project-URL: Changelog, https://github.com/Vzlentin/conforme/blob/main/CHANGELOG.md
|
|
31
|
+
Provides-Extra: ml
|
|
32
|
+
Provides-Extra: neural
|
|
33
|
+
Provides-Extra: stats
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# Conforme
|
|
37
|
+
|
|
38
|
+
[](https://github.com/Vzlentin/conforme/actions/workflows/ci.yml)
|
|
39
|
+
[](https://github.com/Vzlentin/conforme/blob/main/LICENSE)
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
Conforme is a conformal forecasting library for panels of time series. It makes point
|
|
43
|
+
forecasts at many origins, reconciles them over a hierarchy, and calibrates bands and
|
|
44
|
+
bounds from out-of-sample residuals.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
Install `conforme` from PyPI:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
pip install conforme
|
|
52
|
+
# or
|
|
53
|
+
uv add conforme
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The vendor model adapters need an extra: `stats` (statsforecast), `ml` (mlforecast), or
|
|
57
|
+
`neural` (neuralforecast).
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
pip install "conforme[stats]"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Quick start
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import numpy as np
|
|
67
|
+
import pandas as pd
|
|
68
|
+
|
|
69
|
+
from conforme import Absolute, BottomUp, Hierarchy, LeadTime, Panel, SeasonalNaive, Signed
|
|
70
|
+
from conforme import SplitQuantile, Step, replay, rolling_forecasts
|
|
71
|
+
from conforme.metrics import coverage
|
|
72
|
+
|
|
73
|
+
rng = np.random.default_rng(0)
|
|
74
|
+
series = np.array(["a", "b", "c"])
|
|
75
|
+
periods = pd.date_range("2024-01-01", periods=120, freq="D")
|
|
76
|
+
panel = Panel(series, periods, rng.poisson(5.0, (3, 120)), "D")
|
|
77
|
+
hierarchy = Hierarchy.from_attributes(
|
|
78
|
+
series, pd.DataFrame({"group": ["x", "x", "y"]}, index=series)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
origins = np.arange(60, 113) # each origin is the index of the last observed period
|
|
82
|
+
run = rolling_forecasts(panel, hierarchy, SeasonalNaive(7), BottomUp(hierarchy), origins, 7)
|
|
83
|
+
actuals = hierarchy.aggregate(panel.values)
|
|
84
|
+
|
|
85
|
+
calibrator = SplitQuantile(0.9, window=28)
|
|
86
|
+
bands = replay(run, actuals, target=Step(), score=Absolute(), calibrator=calibrator)
|
|
87
|
+
bound = replay(run, actuals, target=LeadTime(7), score=Signed(), calibrator=calibrator)
|
|
88
|
+
print(coverage(bands.target, bands.lower, bands.upper))
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Concepts
|
|
92
|
+
|
|
93
|
+
Shapes use these axes: B bottom series, T periods, N nodes with bottoms first,
|
|
94
|
+
S forecast series (B for `BottomUp`, N otherwise), O origins, H forecast steps, and
|
|
95
|
+
C target columns (H for `Step`, 1 for `LeadTime`).
|
|
96
|
+
|
|
97
|
+

|
|
98
|
+
|
|
99
|
+
| Stage | Names |
|
|
100
|
+
|---|---|
|
|
101
|
+
| Data | `Panel`, `Hierarchy`, `Covariate` |
|
|
102
|
+
| Forecast | `Forecaster`, `Reconciler` (`BottomUp`, `Identity`, `WlsStruct`), `rolling_forecasts` |
|
|
103
|
+
| Calibrate | `Target` (`Step`, `LeadTime`), `Score` (`Absolute`, `Signed`), `Loss` (`Miss`), `Calibrator` (`SplitQuantile`, `ACI`, `QuantileTracker`, `RiskControl`), `step`, `replay` |
|
|
104
|
+
| Decide | `critical_ratio`, `order_up_to`, `settle` |
|
|
105
|
+
| Measure | `conforme.metrics`: coverage, width, interval score, pinball, cost |
|
|
106
|
+
|
|
107
|
+
Two rules hold everywhere. A model reads only its window, so a later value cannot
|
|
108
|
+
change a point unless a covariate declares it known ahead. A calibrator sees a score
|
|
109
|
+
only once its target is known, and before the origin that knows it issues.
|
|
110
|
+
|
|
111
|
+
[Architecture](https://github.com/Vzlentin/conforme/blob/main/docs/architecture.md) has the shapes and the modules.
|
|
112
|
+
|
|
113
|
+
## Write a calibrator
|
|
114
|
+
|
|
115
|
+
A calibrator owns its target, a level or a loss, and is three functions of an explicit
|
|
116
|
+
state. `conforme.online` handles the origins, the delays, and the indexing. A new method
|
|
117
|
+
is one file in `conforme/conformal/calibrators/` that imports only `calibrators.base`,
|
|
118
|
+
`calibrators.ranks`, and `conformal.losses`. This is the whole of a quantile tracker:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
import numpy as np
|
|
122
|
+
|
|
123
|
+
from conforme.conformal.calibrators.base import Calibrator
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class Tracker(Calibrator):
|
|
127
|
+
def __init__(self, level, lr):
|
|
128
|
+
self.level = level
|
|
129
|
+
self.lr = lr
|
|
130
|
+
|
|
131
|
+
def initial_state(self, n_nodes, n_columns):
|
|
132
|
+
return {"q": np.zeros((n_nodes, n_columns))}
|
|
133
|
+
|
|
134
|
+
def update(self, state, feedback):
|
|
135
|
+
q = state["q"].copy()
|
|
136
|
+
for row, column in enumerate(feedback.column): # one row = one origin, one column
|
|
137
|
+
miss = feedback.scores[row] > feedback.issued[row]
|
|
138
|
+
q[:, column] += self.lr * (miss - (1 - self.level))
|
|
139
|
+
return {"q": q}
|
|
140
|
+
|
|
141
|
+
def threshold(self, state):
|
|
142
|
+
return state["q"].astype(np.float32)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Keep the state in numpy arrays. Then a product can save it after each origin with
|
|
146
|
+
`conforme.online.state.flatten` and continue from it.
|
|
147
|
+
|
|
148
|
+
## Models
|
|
149
|
+
|
|
150
|
+
| Model | Kind | Install |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| `conforme.forecast.models.naive.SeasonalNaive` | local | core |
|
|
153
|
+
| `conforme.forecast.models.statsforecast.StatsForecastModel` | local, any `statsforecast.models` model | `conforme[stats]` |
|
|
154
|
+
| `conforme.forecast.models.mlforecast.MLForecast` | global regressor with lags and covariates | `conforme[ml]` |
|
|
155
|
+
| `conforme.forecast.models.neuralforecast.NeuralForecast` | global network from `neuralforecast.models` | `conforme[neural]` |
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from lightgbm import LGBMRegressor
|
|
159
|
+
from conforme import BottomUp, Covariate, rolling_forecasts
|
|
160
|
+
from conforme.forecast.models.mlforecast import MLForecast
|
|
161
|
+
|
|
162
|
+
model = MLForecast(
|
|
163
|
+
LGBMRegressor(), lags=[7, 14, 28], features=["price"], fit_periods=365, lookback=84
|
|
164
|
+
)
|
|
165
|
+
price = Covariate(prices, known_ahead=True, aggregate="mean") # [B, T + H]
|
|
166
|
+
run = rolling_forecasts(
|
|
167
|
+
panel,
|
|
168
|
+
hierarchy,
|
|
169
|
+
model,
|
|
170
|
+
BottomUp(hierarchy),
|
|
171
|
+
origins,
|
|
172
|
+
28,
|
|
173
|
+
refit_every=7,
|
|
174
|
+
covariates={"price": price},
|
|
175
|
+
)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Benchmarks
|
|
179
|
+
|
|
180
|
+
`bench/` is a folder of scripts. They load benchmark data from a directory, run
|
|
181
|
+
benchmark protocols, and compare calibrators. They import the installed `conforme`.
|
|
182
|
+
Raw data is not in Git.
|
|
183
|
+
|
|
184
|
+
| Script | Content |
|
|
185
|
+
|---|---|
|
|
186
|
+
| `vn2.py` | `load(path)`: weekly VN2 sales with the out-of-stock mask, hierarchy, and starting stock. `play(data, bounds)`: six orders up to the bounds, eight settled weeks, holding 0.2 and shortage 1.0 |
|
|
187
|
+
| `m5.py` | `load(path)`: daily M5 sales and the 12-level hierarchy (42,840 nodes). `prices(horizon)`: the sell price covariate |
|
|
188
|
+
| `compare.py` | `compare(forecasts, actuals, calibrators, target, score, level)`: one row of metrics per method, on the cells where each method was ready |
|
|
189
|
+
|
|
190
|
+
`vn2_conformal.py` runs the whole VN2 path in about one second:
|
|
191
|
+
|
|
192
|
+
```sh
|
|
193
|
+
uv run python bench/vn2_conformal.py data/vn2
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Documents
|
|
197
|
+
|
|
198
|
+
- [Architecture](https://github.com/Vzlentin/conforme/blob/main/docs/architecture.md): modules, array contracts, and costs.
|
|
199
|
+
- [Semantics](https://github.com/Vzlentin/conforme/blob/main/docs/semantics.md): the calibration rules.
|
|
200
|
+
|
|
201
|
+
## Development
|
|
202
|
+
|
|
203
|
+
```sh
|
|
204
|
+
uv sync --group dev
|
|
205
|
+
uv run pytest
|
|
206
|
+
uv run ruff check .
|
|
207
|
+
uv run ruff format --check .
|
|
208
|
+
uv run ty check src/
|
|
209
|
+
uv build --no-sources
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
See [Contributing](https://github.com/Vzlentin/conforme/blob/main/CONTRIBUTING.md).
|
|
213
|
+
|
|
214
|
+
## License
|
|
215
|
+
|
|
216
|
+
[MIT](https://github.com/Vzlentin/conforme/blob/main/LICENSE)
|
conforme-0.1.0/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Conforme
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Vzlentin/conforme/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/Vzlentin/conforme/blob/main/LICENSE)
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
Conforme is a conformal forecasting library for panels of time series. It makes point
|
|
8
|
+
forecasts at many origins, reconciles them over a hierarchy, and calibrates bands and
|
|
9
|
+
bounds from out-of-sample residuals.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
Install `conforme` from PyPI:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pip install conforme
|
|
17
|
+
# or
|
|
18
|
+
uv add conforme
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The vendor model adapters need an extra: `stats` (statsforecast), `ml` (mlforecast), or
|
|
22
|
+
`neural` (neuralforecast).
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
pip install "conforme[stats]"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
import numpy as np
|
|
32
|
+
import pandas as pd
|
|
33
|
+
|
|
34
|
+
from conforme import Absolute, BottomUp, Hierarchy, LeadTime, Panel, SeasonalNaive, Signed
|
|
35
|
+
from conforme import SplitQuantile, Step, replay, rolling_forecasts
|
|
36
|
+
from conforme.metrics import coverage
|
|
37
|
+
|
|
38
|
+
rng = np.random.default_rng(0)
|
|
39
|
+
series = np.array(["a", "b", "c"])
|
|
40
|
+
periods = pd.date_range("2024-01-01", periods=120, freq="D")
|
|
41
|
+
panel = Panel(series, periods, rng.poisson(5.0, (3, 120)), "D")
|
|
42
|
+
hierarchy = Hierarchy.from_attributes(
|
|
43
|
+
series, pd.DataFrame({"group": ["x", "x", "y"]}, index=series)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
origins = np.arange(60, 113) # each origin is the index of the last observed period
|
|
47
|
+
run = rolling_forecasts(panel, hierarchy, SeasonalNaive(7), BottomUp(hierarchy), origins, 7)
|
|
48
|
+
actuals = hierarchy.aggregate(panel.values)
|
|
49
|
+
|
|
50
|
+
calibrator = SplitQuantile(0.9, window=28)
|
|
51
|
+
bands = replay(run, actuals, target=Step(), score=Absolute(), calibrator=calibrator)
|
|
52
|
+
bound = replay(run, actuals, target=LeadTime(7), score=Signed(), calibrator=calibrator)
|
|
53
|
+
print(coverage(bands.target, bands.lower, bands.upper))
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Concepts
|
|
57
|
+
|
|
58
|
+
Shapes use these axes: B bottom series, T periods, N nodes with bottoms first,
|
|
59
|
+
S forecast series (B for `BottomUp`, N otherwise), O origins, H forecast steps, and
|
|
60
|
+
C target columns (H for `Step`, 1 for `LeadTime`).
|
|
61
|
+
|
|
62
|
+

|
|
63
|
+
|
|
64
|
+
| Stage | Names |
|
|
65
|
+
|---|---|
|
|
66
|
+
| Data | `Panel`, `Hierarchy`, `Covariate` |
|
|
67
|
+
| Forecast | `Forecaster`, `Reconciler` (`BottomUp`, `Identity`, `WlsStruct`), `rolling_forecasts` |
|
|
68
|
+
| Calibrate | `Target` (`Step`, `LeadTime`), `Score` (`Absolute`, `Signed`), `Loss` (`Miss`), `Calibrator` (`SplitQuantile`, `ACI`, `QuantileTracker`, `RiskControl`), `step`, `replay` |
|
|
69
|
+
| Decide | `critical_ratio`, `order_up_to`, `settle` |
|
|
70
|
+
| Measure | `conforme.metrics`: coverage, width, interval score, pinball, cost |
|
|
71
|
+
|
|
72
|
+
Two rules hold everywhere. A model reads only its window, so a later value cannot
|
|
73
|
+
change a point unless a covariate declares it known ahead. A calibrator sees a score
|
|
74
|
+
only once its target is known, and before the origin that knows it issues.
|
|
75
|
+
|
|
76
|
+
[Architecture](https://github.com/Vzlentin/conforme/blob/main/docs/architecture.md) has the shapes and the modules.
|
|
77
|
+
|
|
78
|
+
## Write a calibrator
|
|
79
|
+
|
|
80
|
+
A calibrator owns its target, a level or a loss, and is three functions of an explicit
|
|
81
|
+
state. `conforme.online` handles the origins, the delays, and the indexing. A new method
|
|
82
|
+
is one file in `conforme/conformal/calibrators/` that imports only `calibrators.base`,
|
|
83
|
+
`calibrators.ranks`, and `conformal.losses`. This is the whole of a quantile tracker:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
import numpy as np
|
|
87
|
+
|
|
88
|
+
from conforme.conformal.calibrators.base import Calibrator
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Tracker(Calibrator):
|
|
92
|
+
def __init__(self, level, lr):
|
|
93
|
+
self.level = level
|
|
94
|
+
self.lr = lr
|
|
95
|
+
|
|
96
|
+
def initial_state(self, n_nodes, n_columns):
|
|
97
|
+
return {"q": np.zeros((n_nodes, n_columns))}
|
|
98
|
+
|
|
99
|
+
def update(self, state, feedback):
|
|
100
|
+
q = state["q"].copy()
|
|
101
|
+
for row, column in enumerate(feedback.column): # one row = one origin, one column
|
|
102
|
+
miss = feedback.scores[row] > feedback.issued[row]
|
|
103
|
+
q[:, column] += self.lr * (miss - (1 - self.level))
|
|
104
|
+
return {"q": q}
|
|
105
|
+
|
|
106
|
+
def threshold(self, state):
|
|
107
|
+
return state["q"].astype(np.float32)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Keep the state in numpy arrays. Then a product can save it after each origin with
|
|
111
|
+
`conforme.online.state.flatten` and continue from it.
|
|
112
|
+
|
|
113
|
+
## Models
|
|
114
|
+
|
|
115
|
+
| Model | Kind | Install |
|
|
116
|
+
|---|---|---|
|
|
117
|
+
| `conforme.forecast.models.naive.SeasonalNaive` | local | core |
|
|
118
|
+
| `conforme.forecast.models.statsforecast.StatsForecastModel` | local, any `statsforecast.models` model | `conforme[stats]` |
|
|
119
|
+
| `conforme.forecast.models.mlforecast.MLForecast` | global regressor with lags and covariates | `conforme[ml]` |
|
|
120
|
+
| `conforme.forecast.models.neuralforecast.NeuralForecast` | global network from `neuralforecast.models` | `conforme[neural]` |
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from lightgbm import LGBMRegressor
|
|
124
|
+
from conforme import BottomUp, Covariate, rolling_forecasts
|
|
125
|
+
from conforme.forecast.models.mlforecast import MLForecast
|
|
126
|
+
|
|
127
|
+
model = MLForecast(
|
|
128
|
+
LGBMRegressor(), lags=[7, 14, 28], features=["price"], fit_periods=365, lookback=84
|
|
129
|
+
)
|
|
130
|
+
price = Covariate(prices, known_ahead=True, aggregate="mean") # [B, T + H]
|
|
131
|
+
run = rolling_forecasts(
|
|
132
|
+
panel,
|
|
133
|
+
hierarchy,
|
|
134
|
+
model,
|
|
135
|
+
BottomUp(hierarchy),
|
|
136
|
+
origins,
|
|
137
|
+
28,
|
|
138
|
+
refit_every=7,
|
|
139
|
+
covariates={"price": price},
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Benchmarks
|
|
144
|
+
|
|
145
|
+
`bench/` is a folder of scripts. They load benchmark data from a directory, run
|
|
146
|
+
benchmark protocols, and compare calibrators. They import the installed `conforme`.
|
|
147
|
+
Raw data is not in Git.
|
|
148
|
+
|
|
149
|
+
| Script | Content |
|
|
150
|
+
|---|---|
|
|
151
|
+
| `vn2.py` | `load(path)`: weekly VN2 sales with the out-of-stock mask, hierarchy, and starting stock. `play(data, bounds)`: six orders up to the bounds, eight settled weeks, holding 0.2 and shortage 1.0 |
|
|
152
|
+
| `m5.py` | `load(path)`: daily M5 sales and the 12-level hierarchy (42,840 nodes). `prices(horizon)`: the sell price covariate |
|
|
153
|
+
| `compare.py` | `compare(forecasts, actuals, calibrators, target, score, level)`: one row of metrics per method, on the cells where each method was ready |
|
|
154
|
+
|
|
155
|
+
`vn2_conformal.py` runs the whole VN2 path in about one second:
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
uv run python bench/vn2_conformal.py data/vn2
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Documents
|
|
162
|
+
|
|
163
|
+
- [Architecture](https://github.com/Vzlentin/conforme/blob/main/docs/architecture.md): modules, array contracts, and costs.
|
|
164
|
+
- [Semantics](https://github.com/Vzlentin/conforme/blob/main/docs/semantics.md): the calibration rules.
|
|
165
|
+
|
|
166
|
+
## Development
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
uv sync --group dev
|
|
170
|
+
uv run pytest
|
|
171
|
+
uv run ruff check .
|
|
172
|
+
uv run ruff format --check .
|
|
173
|
+
uv run ty check src/
|
|
174
|
+
uv build --no-sources
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
See [Contributing](https://github.com/Vzlentin/conforme/blob/main/CONTRIBUTING.md).
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
[MIT](https://github.com/Vzlentin/conforme/blob/main/LICENSE)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "conforme"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Conformal forecasting: point forecasts, hierarchy reconciliation, and calibrated bands."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
keywords = [
|
|
10
|
+
"conformal prediction",
|
|
11
|
+
"forecasting",
|
|
12
|
+
"time series",
|
|
13
|
+
"hierarchical reconciliation",
|
|
14
|
+
"prediction intervals",
|
|
15
|
+
"inventory",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
26
|
+
"Typing :: Typed",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"numpy",
|
|
30
|
+
"pandas",
|
|
31
|
+
"scipy",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[[project.authors]]
|
|
35
|
+
name = "Valentin Dusserre"
|
|
36
|
+
email = "valentindusserre@gmail.com"
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/Vzlentin/conforme"
|
|
40
|
+
Repository = "https://github.com/Vzlentin/conforme"
|
|
41
|
+
Issues = "https://github.com/Vzlentin/conforme/issues"
|
|
42
|
+
Documentation = "https://github.com/Vzlentin/conforme/tree/main/docs"
|
|
43
|
+
Changelog = "https://github.com/Vzlentin/conforme/blob/main/CHANGELOG.md"
|
|
44
|
+
|
|
45
|
+
[project.optional-dependencies]
|
|
46
|
+
stats = ["statsforecast"]
|
|
47
|
+
ml = ["mlforecast"]
|
|
48
|
+
neural = ["neuralforecast"]
|
|
49
|
+
|
|
50
|
+
[dependency-groups]
|
|
51
|
+
dev = [
|
|
52
|
+
"ipykernel>=7.3.0",
|
|
53
|
+
"mlforecast",
|
|
54
|
+
"neuralforecast",
|
|
55
|
+
"pytest",
|
|
56
|
+
"statsforecast",
|
|
57
|
+
"ruff",
|
|
58
|
+
"ty",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
[build-system]
|
|
62
|
+
requires = ["uv_build>=0.12,<0.13"]
|
|
63
|
+
build-backend = "uv_build"
|
|
64
|
+
|
|
65
|
+
[tool.pytest.ini_options]
|
|
66
|
+
addopts = "-ra --strict-markers"
|
|
67
|
+
filterwarnings = [
|
|
68
|
+
"ignore:.*does not have many workers",
|
|
69
|
+
"ignore:GPU available but not used",
|
|
70
|
+
"ignore:.*LeafSpec.*is deprecated",
|
|
71
|
+
"ignore:The copy keyword is deprecated",
|
|
72
|
+
"ignore:The given NumPy array is not writable",
|
|
73
|
+
]
|
|
74
|
+
testpaths = ["tests"]
|
|
75
|
+
|
|
76
|
+
[tool.ruff]
|
|
77
|
+
line-length = 100
|
|
78
|
+
target-version = "py312"
|
|
79
|
+
src = [
|
|
80
|
+
"src",
|
|
81
|
+
"tests",
|
|
82
|
+
"bench",
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
[tool.ruff.lint]
|
|
86
|
+
select = [
|
|
87
|
+
"E",
|
|
88
|
+
"F",
|
|
89
|
+
"W",
|
|
90
|
+
"I",
|
|
91
|
+
"UP",
|
|
92
|
+
"B",
|
|
93
|
+
"SIM",
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
[tool.ty.environment]
|
|
97
|
+
python-version = "3.12"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "conforme"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Conformal forecasting: point forecasts, hierarchy reconciliation, and calibrated bands."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{ name = "Valentin Dusserre", email = "valentindusserre@gmail.com" }]
|
|
10
|
+
keywords = [
|
|
11
|
+
"conformal prediction",
|
|
12
|
+
"forecasting",
|
|
13
|
+
"time series",
|
|
14
|
+
"hierarchical reconciliation",
|
|
15
|
+
"prediction intervals",
|
|
16
|
+
"inventory",
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 3 - Alpha",
|
|
20
|
+
"Intended Audience :: Science/Research",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
27
|
+
"Typing :: Typed",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"numpy",
|
|
31
|
+
"pandas",
|
|
32
|
+
"scipy",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/Vzlentin/conforme"
|
|
37
|
+
Repository = "https://github.com/Vzlentin/conforme"
|
|
38
|
+
Issues = "https://github.com/Vzlentin/conforme/issues"
|
|
39
|
+
Documentation = "https://github.com/Vzlentin/conforme/tree/main/docs"
|
|
40
|
+
Changelog = "https://github.com/Vzlentin/conforme/blob/main/CHANGELOG.md"
|
|
41
|
+
|
|
42
|
+
[project.optional-dependencies]
|
|
43
|
+
stats = ["statsforecast"]
|
|
44
|
+
ml = ["mlforecast"]
|
|
45
|
+
neural = ["neuralforecast"]
|
|
46
|
+
|
|
47
|
+
[dependency-groups]
|
|
48
|
+
dev = [
|
|
49
|
+
"ipykernel>=7.3.0",
|
|
50
|
+
"mlforecast",
|
|
51
|
+
"neuralforecast",
|
|
52
|
+
"pytest",
|
|
53
|
+
"statsforecast",
|
|
54
|
+
"ruff",
|
|
55
|
+
"ty",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
[build-system]
|
|
60
|
+
requires = ["uv_build>=0.12,<0.13"]
|
|
61
|
+
build-backend = "uv_build"
|
|
62
|
+
|
|
63
|
+
[tool.pytest.ini_options]
|
|
64
|
+
addopts = "-ra --strict-markers"
|
|
65
|
+
# Warnings raised inside vendor libraries about their own internals.
|
|
66
|
+
filterwarnings = [
|
|
67
|
+
"ignore:.*does not have many workers",
|
|
68
|
+
"ignore:GPU available but not used",
|
|
69
|
+
"ignore:.*LeafSpec.*is deprecated",
|
|
70
|
+
"ignore:The copy keyword is deprecated",
|
|
71
|
+
"ignore:The given NumPy array is not writable",
|
|
72
|
+
]
|
|
73
|
+
testpaths = ["tests"]
|
|
74
|
+
|
|
75
|
+
[tool.ruff]
|
|
76
|
+
line-length = 100
|
|
77
|
+
target-version = "py312"
|
|
78
|
+
src = ["src", "tests", "bench"]
|
|
79
|
+
|
|
80
|
+
[tool.ruff.lint]
|
|
81
|
+
select = ["E", "F", "W", "I", "UP", "B", "SIM"]
|
|
82
|
+
|
|
83
|
+
[tool.ty.environment]
|
|
84
|
+
python-version = "3.12"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Conforme: point forecasts, hierarchy reconciliation, conformal bounds, and orders.
|
|
2
|
+
|
|
3
|
+
Vendor model adapters live in `conforme.forecast.models` and need their extra installed.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from conforme.backtest import Forecasts, Replay, replay, rolling_forecasts
|
|
7
|
+
from conforme.conformal import (
|
|
8
|
+
ACI,
|
|
9
|
+
Absolute,
|
|
10
|
+
Calibrator,
|
|
11
|
+
Feedback,
|
|
12
|
+
LeadTime,
|
|
13
|
+
Level,
|
|
14
|
+
Loss,
|
|
15
|
+
Miss,
|
|
16
|
+
QuantileCalibrator,
|
|
17
|
+
QuantileTracker,
|
|
18
|
+
RiskControl,
|
|
19
|
+
Score,
|
|
20
|
+
Signed,
|
|
21
|
+
SplitQuantile,
|
|
22
|
+
State,
|
|
23
|
+
Step,
|
|
24
|
+
Target,
|
|
25
|
+
)
|
|
26
|
+
from conforme.data import Hierarchy, Panel
|
|
27
|
+
from conforme.decision import Settlement, critical_ratio, order_up_to, settle
|
|
28
|
+
from conforme.forecast import (
|
|
29
|
+
BottomUp,
|
|
30
|
+
Covariate,
|
|
31
|
+
Fitted,
|
|
32
|
+
Forecaster,
|
|
33
|
+
Identity,
|
|
34
|
+
Reconciler,
|
|
35
|
+
SeasonalNaive,
|
|
36
|
+
Window,
|
|
37
|
+
WlsStruct,
|
|
38
|
+
)
|
|
39
|
+
from conforme.online import Issue, initial_state, step
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"ACI",
|
|
43
|
+
"Absolute",
|
|
44
|
+
"BottomUp",
|
|
45
|
+
"Calibrator",
|
|
46
|
+
"Covariate",
|
|
47
|
+
"Feedback",
|
|
48
|
+
"Fitted",
|
|
49
|
+
"Forecaster",
|
|
50
|
+
"Forecasts",
|
|
51
|
+
"Hierarchy",
|
|
52
|
+
"Identity",
|
|
53
|
+
"Issue",
|
|
54
|
+
"LeadTime",
|
|
55
|
+
"Level",
|
|
56
|
+
"Loss",
|
|
57
|
+
"Miss",
|
|
58
|
+
"QuantileCalibrator",
|
|
59
|
+
"Panel",
|
|
60
|
+
"QuantileTracker",
|
|
61
|
+
"RiskControl",
|
|
62
|
+
"Reconciler",
|
|
63
|
+
"Replay",
|
|
64
|
+
"Score",
|
|
65
|
+
"SeasonalNaive",
|
|
66
|
+
"Settlement",
|
|
67
|
+
"Signed",
|
|
68
|
+
"SplitQuantile",
|
|
69
|
+
"State",
|
|
70
|
+
"Step",
|
|
71
|
+
"Target",
|
|
72
|
+
"Window",
|
|
73
|
+
"WlsStruct",
|
|
74
|
+
"critical_ratio",
|
|
75
|
+
"initial_state",
|
|
76
|
+
"order_up_to",
|
|
77
|
+
"replay",
|
|
78
|
+
"rolling_forecasts",
|
|
79
|
+
"settle",
|
|
80
|
+
"step",
|
|
81
|
+
]
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Backtests: rolling-origin forecasts, and online calibration replayed over them."""
|
|
2
|
+
|
|
3
|
+
from conforme.backtest.forecasts import Forecasts, rolling_forecasts
|
|
4
|
+
from conforme.backtest.replay import Replay, replay
|
|
5
|
+
|
|
6
|
+
__all__ = ["Forecasts", "Replay", "replay", "rolling_forecasts"]
|