ice-skaters 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021-2026 Peter Cotton
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.
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: ice-skaters
3
+ Version: 0.1.0
4
+ Summary: skaters on a river: calibrated forecast features for streaming ML pipelines. Each numeric stream becomes what the forecaster expected and how surprising the value was.
5
+ Author-email: Peter Cotton <peter.cotton@microprediction.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/microprediction/ice-skaters
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: skaters>=0.12.1
12
+ Requires-Dist: river>=0.21
13
+ Dynamic: license-file
14
+
15
+ # ice-skaters
16
+
17
+ [skaters](https://github.com/microprediction/skaters) on a
18
+ [river](https://riverml.xyz): calibrated forecast features for streaming
19
+ ML pipelines.
20
+
21
+ Every numeric stream is replaced by two scalars from its own online
22
+ Laplace forecaster: the predictive mean (what the forecaster expected
23
+ this value to be) and the standardized surprise z (how unexpected the
24
+ actual value was, bounded near |z| = 7 by construction). The mean
25
+ carries the level, the z carries the news. A wild observation can move
26
+ the pair only so far, and that bounded influence is where the robustness
27
+ comes from.
28
+
29
+ ```
30
+ pip install ice-skaters
31
+ ```
32
+
33
+ ```python
34
+ from river import datasets, linear_model, metrics, preprocessing
35
+ from ice_skaters import LaplaceFeatures, LaplaceTarget
36
+
37
+ model = LaplaceTarget(
38
+ regressor=preprocessing.TargetStandardScaler(
39
+ regressor=LaplaceFeatures()
40
+ | preprocessing.StandardScaler()
41
+ | linear_model.LinearRegression()))
42
+
43
+ mae = metrics.MAE()
44
+ for x, y in datasets.TrumpApproval():
45
+ pred = model.predict_one(x)
46
+ mae.update(y, pred if pred is not None else 0.0)
47
+ model.learn_one(x, y)
48
+ ```
49
+
50
+ `LaplaceFeatures` is a river Transformer for the input streams.
51
+ `LaplaceTarget` wraps any regressor, in the style of
52
+ `TargetStandardScaler`, to add the target's own (mean, surprise) pair,
53
+ which a transformer cannot do since it never sees y. The target itself
54
+ stays raw. Both estimators pipe, pickle and deep-copy like any river
55
+ estimator. Non-numeric values pass through untouched, and NaN is imputed
56
+ by the forecast itself with z = 0: the model receives "expected value,
57
+ no news" instead of a poisoned pipeline.
58
+
59
+ ## Why
60
+
61
+ On TrumpApproval with river's recommended pipeline (progressive
62
+ validation MAE, burn-in 100, `examples/trump_approval.py`):
63
+
64
+ | | clean | 2% corrupted readings |
65
+ |---|---|---|
66
+ | `StandardScaler` pipeline | 0.328 | 0.597 |
67
+ | + Laplace front-end | 0.382 | 0.407 |
68
+
69
+ The front-end pays a small toll on clean data and holds its footing when
70
+ the inputs misbehave. In controlled simulation the same substitution
71
+ beats raw features, a running z-score, a median/MAD winsorizer and a
72
+ Huberised loss 30/30 seeds under every contamination type tested, at a
73
+ small clean-data toll. Full protocols, numbers and the study design live
74
+ in the [timemachines](https://github.com/microprediction/timemachines)
75
+ repo, `benchmarks/RESULTS.md` section 6.
76
+
77
+ ## Boundaries, stated plainly
78
+
79
+ - Distance-based learners (KNN) do not benefit: neighbour averaging is
80
+ already spike-robust and the extra dimensions degrade the metric.
81
+ - Entity-interleaved streams (many units multiplexed into one key) want
82
+ per-entity bodies; a single body per key is handicapped there.
83
+ - If your heavy tails are signal rather than noise, taming them costs
84
+ accuracy. Whether the extremes are informative decides the coordinates.
85
+
86
+ ## Relation to the stack
87
+
88
+ `skaters` does one thing: fast univariate distributional forecasting,
89
+ stdlib-only, and this package is deliberately a thin adapter over it.
90
+ `timemachines` builds anomaly detection on the same calibrated surprise
91
+ streams. ice-skaters is the bridge from those streams to river's
92
+ estimator protocol, and nothing more.
@@ -0,0 +1,78 @@
1
+ # ice-skaters
2
+
3
+ [skaters](https://github.com/microprediction/skaters) on a
4
+ [river](https://riverml.xyz): calibrated forecast features for streaming
5
+ ML pipelines.
6
+
7
+ Every numeric stream is replaced by two scalars from its own online
8
+ Laplace forecaster: the predictive mean (what the forecaster expected
9
+ this value to be) and the standardized surprise z (how unexpected the
10
+ actual value was, bounded near |z| = 7 by construction). The mean
11
+ carries the level, the z carries the news. A wild observation can move
12
+ the pair only so far, and that bounded influence is where the robustness
13
+ comes from.
14
+
15
+ ```
16
+ pip install ice-skaters
17
+ ```
18
+
19
+ ```python
20
+ from river import datasets, linear_model, metrics, preprocessing
21
+ from ice_skaters import LaplaceFeatures, LaplaceTarget
22
+
23
+ model = LaplaceTarget(
24
+ regressor=preprocessing.TargetStandardScaler(
25
+ regressor=LaplaceFeatures()
26
+ | preprocessing.StandardScaler()
27
+ | linear_model.LinearRegression()))
28
+
29
+ mae = metrics.MAE()
30
+ for x, y in datasets.TrumpApproval():
31
+ pred = model.predict_one(x)
32
+ mae.update(y, pred if pred is not None else 0.0)
33
+ model.learn_one(x, y)
34
+ ```
35
+
36
+ `LaplaceFeatures` is a river Transformer for the input streams.
37
+ `LaplaceTarget` wraps any regressor, in the style of
38
+ `TargetStandardScaler`, to add the target's own (mean, surprise) pair,
39
+ which a transformer cannot do since it never sees y. The target itself
40
+ stays raw. Both estimators pipe, pickle and deep-copy like any river
41
+ estimator. Non-numeric values pass through untouched, and NaN is imputed
42
+ by the forecast itself with z = 0: the model receives "expected value,
43
+ no news" instead of a poisoned pipeline.
44
+
45
+ ## Why
46
+
47
+ On TrumpApproval with river's recommended pipeline (progressive
48
+ validation MAE, burn-in 100, `examples/trump_approval.py`):
49
+
50
+ | | clean | 2% corrupted readings |
51
+ |---|---|---|
52
+ | `StandardScaler` pipeline | 0.328 | 0.597 |
53
+ | + Laplace front-end | 0.382 | 0.407 |
54
+
55
+ The front-end pays a small toll on clean data and holds its footing when
56
+ the inputs misbehave. In controlled simulation the same substitution
57
+ beats raw features, a running z-score, a median/MAD winsorizer and a
58
+ Huberised loss 30/30 seeds under every contamination type tested, at a
59
+ small clean-data toll. Full protocols, numbers and the study design live
60
+ in the [timemachines](https://github.com/microprediction/timemachines)
61
+ repo, `benchmarks/RESULTS.md` section 6.
62
+
63
+ ## Boundaries, stated plainly
64
+
65
+ - Distance-based learners (KNN) do not benefit: neighbour averaging is
66
+ already spike-robust and the extra dimensions degrade the metric.
67
+ - Entity-interleaved streams (many units multiplexed into one key) want
68
+ per-entity bodies; a single body per key is handicapped there.
69
+ - If your heavy tails are signal rather than noise, taming them costs
70
+ accuracy. Whether the extremes are informative decides the coordinates.
71
+
72
+ ## Relation to the stack
73
+
74
+ `skaters` does one thing: fast univariate distributional forecasting,
75
+ stdlib-only, and this package is deliberately a thin adapter over it.
76
+ `timemachines` builds anomaly detection on the same calibrated surprise
77
+ streams. ice-skaters is the bridge from those streams to river's
78
+ estimator protocol, and nothing more.
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ice-skaters"
7
+ version = "0.1.0"
8
+ description = "skaters on a river: calibrated forecast features for streaming ML pipelines. Each numeric stream becomes what the forecaster expected and how surprising the value was."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [
12
+ { name = "Peter Cotton", email = "peter.cotton@microprediction.com" }
13
+ ]
14
+ requires-python = ">=3.10"
15
+ dependencies = [
16
+ "skaters>=0.12.1",
17
+ "river>=0.21",
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/microprediction/ice-skaters"
22
+
23
+ [tool.setuptools.packages.find]
24
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,205 @@
1
+ """ice-skaters: skaters on a river.
2
+
3
+ Calibrated forecast features for `river <https://riverml.xyz>`_
4
+ pipelines, built on the online Laplace forecaster of the ``skaters``
5
+ package. ``LaplaceFeatures`` replaces each numeric feature stream with
6
+ two scalars from its own forecaster: the one-step predictive mean (what
7
+ the forecaster expected this value to be) and the standardized surprise
8
+ z (how unexpected the actual value was, bounded by construction near
9
+ |z| = 7). The mean carries the level, the z carries the news, and a wild
10
+ input can move the pair only so far, which is where the robustness comes
11
+ from. Non-numeric values pass through untouched; non-finite numeric
12
+ values are imputed by the forecast itself (mean = the body's prediction,
13
+ z = 0: no news), which is more than ``StandardScaler`` can say for NaN.
14
+
15
+ from river import linear_model, preprocessing
16
+ from ice_skaters import LaplaceFeatures, LaplaceTarget
17
+
18
+ model = LaplaceTarget(
19
+ regressor=preprocessing.TargetStandardScaler(
20
+ regressor=LaplaceFeatures()
21
+ | preprocessing.StandardScaler()
22
+ | linear_model.LinearRegression()))
23
+ # ...then the usual river loop:
24
+ # model.predict_one(x); model.learn_one(x, y)
25
+
26
+ ``LaplaceTarget`` adds the same pair for the target's own history, which
27
+ a transformer cannot do since it never sees y; it wraps any regressor in
28
+ the style of ``TargetStandardScaler`` and leaves the target itself raw.
29
+
30
+ Evidence, protocols and boundaries (distance-based learners do not
31
+ benefit; entity-interleaved streams want per-entity bodies; genuinely
32
+ informative heavy tails should not be tamed): the regression front-end
33
+ study in the timemachines repo, ``benchmarks/RESULTS.md`` section 6.
34
+
35
+ Implementation notes:
36
+
37
+ - river's ``Pipeline.learn_one`` updates unsupervised transformers
38
+ *before* calling ``transform_one`` for the downstream steps. A naive
39
+ stateful transformer would therefore hand the model fresher features
40
+ at learn time than it predicted with. ``LaplaceFeatures`` caches the
41
+ pre-update feature dict during ``learn_one`` and serves it to the
42
+ immediately following ``transform_one`` for the same sample, so the
43
+ predict path and the learn path see identical features. Bodies advance
44
+ exactly once per sample, in ``learn_one``; ``transform_one`` alone
45
+ (the predict path) is pure.
46
+ - Instances hold only plain state data (dicts, Dists), never the skater
47
+ closure itself: skaters' state purity lets one module-level ``laplace``
48
+ callable serve every stream with state passed per stream, so both
49
+ estimators pickle and deep-copy like any river estimator.
50
+ - Observations are winsorized before a body consumes them (1e60
51
+ absolute, plus a magnitude-relative window twelve orders above the
52
+ current predictive level). skaters >= 0.13 carries the same gate in
53
+ the parade; keeping it here too protects users on earlier releases.
54
+ """
55
+
56
+ from __future__ import annotations
57
+ import math
58
+ from river import base
59
+ from skaters import laplace
60
+ from skaters.dist import Dist
61
+
62
+ __version__ = "0.1.0"
63
+ __all__ = ["LaplaceFeatures", "LaplaceTarget"]
64
+
65
+ _STD_NORMAL = Dist.gaussian(0.0, 1.0)
66
+ _EPS = 1e-12 # parade's PIT clamp: |z| <= ~7.03, never infinite
67
+
68
+ _SKATER = None # shared, stateless given per-stream state; not pickled
69
+
70
+
71
+ def _skater():
72
+ global _SKATER
73
+ if _SKATER is None:
74
+ _SKATER = laplace(1)
75
+ return _SKATER
76
+
77
+
78
+ def _is_number(v) -> bool:
79
+ return isinstance(v, (int, float)) and not isinstance(v, bool)
80
+
81
+
82
+ def _winsorize(v: float, pending) -> float:
83
+ """Clamp absurd finite magnitudes before a body consumes them.
84
+
85
+ Magnitude-relative, NOT sigma-relative: after a degenerate-variance
86
+ stretch (missing-data zeros, say) a legitimate value sits billions of
87
+ sigmas out and must pass. Twelve orders above the current level is
88
+ unreachable by data, far below the ~1e77 jump ratio where double
89
+ arithmetic actually dies, so this is exact identity on sane streams.
90
+ """
91
+ v = min(max(v, -1e60), 1e60)
92
+ if pending is not None:
93
+ m, s = pending.mean, pending.std
94
+ if math.isfinite(m) and math.isfinite(s):
95
+ w = 1e12 * (1.0 + abs(m) + s)
96
+ v = min(max(v, m - w), m + w)
97
+ return v
98
+
99
+
100
+ class LaplaceFeatures(base.Transformer):
101
+ """Replace each numeric feature with (predictive mean, surprise z).
102
+
103
+ Args:
104
+ emit: which scalars to emit per stream, ``"both"`` (default),
105
+ ``"mu"`` or ``"z"``.
106
+ prefix: feature-name prefixes, ``(mean_prefix, z_prefix)``.
107
+ """
108
+
109
+ def __init__(self, emit: str = "both", prefix=("mu_", "z_")):
110
+ if emit not in ("both", "mu", "z"):
111
+ raise ValueError(f"emit must be 'both', 'mu' or 'z', got {emit!r}")
112
+ self.emit = emit
113
+ self.prefix = prefix
114
+ self._bodies = {} # key -> (state dict, pending Dist)
115
+ self._pending_out = None # features cached by learn_one
116
+ self._last_x = None
117
+
118
+ def _features(self, x):
119
+ """Pure: read the pending predictives, never advance a body."""
120
+ out = {}
121
+ mu_p, z_p = self.prefix
122
+ for k, v in x.items():
123
+ if not _is_number(v):
124
+ out[k] = v
125
+ continue
126
+ entry = self._bodies.get(k)
127
+ pending = entry[1] if entry else None
128
+ if pending is None:
129
+ mu = float(v) if math.isfinite(v) else 0.0
130
+ z = 0.0
131
+ elif not math.isfinite(v):
132
+ mu, z = pending.mean, 0.0 # forecast-imputed, no news
133
+ else:
134
+ mu = pending.mean
135
+ u = pending.cdf(float(v))
136
+ u = min(max(u, _EPS), 1.0 - _EPS)
137
+ z = _STD_NORMAL.quantile(u)
138
+ if self.emit in ("both", "mu"):
139
+ out[mu_p + k] = mu
140
+ if self.emit in ("both", "z"):
141
+ out[z_p + k] = z
142
+ return out
143
+
144
+ def learn_one(self, x):
145
+ self._pending_out = self._features(x)
146
+ self._last_x = dict(x)
147
+ f = _skater()
148
+ for k, v in x.items():
149
+ if not _is_number(v) or not math.isfinite(v):
150
+ continue # non-finite: no update
151
+ st, pending = self._bodies.get(k) or (None, None)
152
+ dists, st = f(_winsorize(float(v), pending), st)
153
+ self._bodies[k] = (st, dists[0])
154
+
155
+ def transform_one(self, x):
156
+ if self._pending_out is not None and x == self._last_x:
157
+ out, self._pending_out = self._pending_out, None
158
+ return out
159
+ return self._features(x)
160
+
161
+
162
+ class LaplaceTarget(base.Regressor):
163
+ """Augment features with the TARGET stream's own (mean, surprise) pair.
164
+
165
+ Transformers never see y, so the target's calibrated history features
166
+ need a regressor wrapper, in the style of ``TargetStandardScaler``:
167
+
168
+ model = LaplaceTarget(
169
+ regressor=LaplaceFeatures()
170
+ | preprocessing.StandardScaler()
171
+ | linear_model.LinearRegression())
172
+
173
+ At prediction time the inner regressor receives, alongside x, the
174
+ body's forecast of the y it is about to predict (``mu_y``) and the
175
+ surprise of the previous y (``zy``). The same pre-update pair is used
176
+ at learn time, then the body consumes the new y. The target itself
177
+ stays raw: this wrapper never transforms y, it only adds features.
178
+ """
179
+
180
+ def __init__(self, regressor, keys=("mu_y", "zy")):
181
+ self.regressor = regressor
182
+ self.keys = keys
183
+ self._state = None
184
+ self._pending = None # predictive Dist for the next y
185
+ self._zy = 0.0
186
+
187
+ def _augment(self, x):
188
+ mu_key, z_key = self.keys
189
+ mu = self._pending.mean if self._pending is not None else 0.0
190
+ return {**x, mu_key: mu, z_key: self._zy}
191
+
192
+ def predict_one(self, x):
193
+ return self.regressor.predict_one(self._augment(x))
194
+
195
+ def learn_one(self, x, y):
196
+ self.regressor.learn_one(self._augment(x), y)
197
+ y = float(y)
198
+ if math.isfinite(y):
199
+ # laplace is parade-wrapped: state["z"][0] is the surprise of
200
+ # this y against the predictive issued before it arrived
201
+ dists, self._state = _skater()(
202
+ _winsorize(y, self._pending), self._state)
203
+ self._pending = dists[0]
204
+ z = self._state["z"][0]
205
+ self._zy = z if z is not None else 0.0
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: ice-skaters
3
+ Version: 0.1.0
4
+ Summary: skaters on a river: calibrated forecast features for streaming ML pipelines. Each numeric stream becomes what the forecaster expected and how surprising the value was.
5
+ Author-email: Peter Cotton <peter.cotton@microprediction.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/microprediction/ice-skaters
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: skaters>=0.12.1
12
+ Requires-Dist: river>=0.21
13
+ Dynamic: license-file
14
+
15
+ # ice-skaters
16
+
17
+ [skaters](https://github.com/microprediction/skaters) on a
18
+ [river](https://riverml.xyz): calibrated forecast features for streaming
19
+ ML pipelines.
20
+
21
+ Every numeric stream is replaced by two scalars from its own online
22
+ Laplace forecaster: the predictive mean (what the forecaster expected
23
+ this value to be) and the standardized surprise z (how unexpected the
24
+ actual value was, bounded near |z| = 7 by construction). The mean
25
+ carries the level, the z carries the news. A wild observation can move
26
+ the pair only so far, and that bounded influence is where the robustness
27
+ comes from.
28
+
29
+ ```
30
+ pip install ice-skaters
31
+ ```
32
+
33
+ ```python
34
+ from river import datasets, linear_model, metrics, preprocessing
35
+ from ice_skaters import LaplaceFeatures, LaplaceTarget
36
+
37
+ model = LaplaceTarget(
38
+ regressor=preprocessing.TargetStandardScaler(
39
+ regressor=LaplaceFeatures()
40
+ | preprocessing.StandardScaler()
41
+ | linear_model.LinearRegression()))
42
+
43
+ mae = metrics.MAE()
44
+ for x, y in datasets.TrumpApproval():
45
+ pred = model.predict_one(x)
46
+ mae.update(y, pred if pred is not None else 0.0)
47
+ model.learn_one(x, y)
48
+ ```
49
+
50
+ `LaplaceFeatures` is a river Transformer for the input streams.
51
+ `LaplaceTarget` wraps any regressor, in the style of
52
+ `TargetStandardScaler`, to add the target's own (mean, surprise) pair,
53
+ which a transformer cannot do since it never sees y. The target itself
54
+ stays raw. Both estimators pipe, pickle and deep-copy like any river
55
+ estimator. Non-numeric values pass through untouched, and NaN is imputed
56
+ by the forecast itself with z = 0: the model receives "expected value,
57
+ no news" instead of a poisoned pipeline.
58
+
59
+ ## Why
60
+
61
+ On TrumpApproval with river's recommended pipeline (progressive
62
+ validation MAE, burn-in 100, `examples/trump_approval.py`):
63
+
64
+ | | clean | 2% corrupted readings |
65
+ |---|---|---|
66
+ | `StandardScaler` pipeline | 0.328 | 0.597 |
67
+ | + Laplace front-end | 0.382 | 0.407 |
68
+
69
+ The front-end pays a small toll on clean data and holds its footing when
70
+ the inputs misbehave. In controlled simulation the same substitution
71
+ beats raw features, a running z-score, a median/MAD winsorizer and a
72
+ Huberised loss 30/30 seeds under every contamination type tested, at a
73
+ small clean-data toll. Full protocols, numbers and the study design live
74
+ in the [timemachines](https://github.com/microprediction/timemachines)
75
+ repo, `benchmarks/RESULTS.md` section 6.
76
+
77
+ ## Boundaries, stated plainly
78
+
79
+ - Distance-based learners (KNN) do not benefit: neighbour averaging is
80
+ already spike-robust and the extra dimensions degrade the metric.
81
+ - Entity-interleaved streams (many units multiplexed into one key) want
82
+ per-entity bodies; a single body per key is handicapped there.
83
+ - If your heavy tails are signal rather than noise, taming them costs
84
+ accuracy. Whether the extremes are informative decides the coordinates.
85
+
86
+ ## Relation to the stack
87
+
88
+ `skaters` does one thing: fast univariate distributional forecasting,
89
+ stdlib-only, and this package is deliberately a thin adapter over it.
90
+ `timemachines` builds anomaly detection on the same calibrated surprise
91
+ streams. ice-skaters is the bridge from those streams to river's
92
+ estimator protocol, and nothing more.
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/ice_skaters/__init__.py
5
+ src/ice_skaters.egg-info/PKG-INFO
6
+ src/ice_skaters.egg-info/SOURCES.txt
7
+ src/ice_skaters.egg-info/dependency_links.txt
8
+ src/ice_skaters.egg-info/requires.txt
9
+ src/ice_skaters.egg-info/top_level.txt
10
+ tests/test_ice_skaters.py
@@ -0,0 +1,2 @@
1
+ skaters>=0.12.1
2
+ river>=0.21
@@ -0,0 +1 @@
1
+ ice_skaters
@@ -0,0 +1,113 @@
1
+ """Alignment, robustness, serialization."""
2
+
3
+ import copy
4
+ import math
5
+ import pickle
6
+ import random
7
+
8
+ import pytest
9
+ from river import linear_model, preprocessing
10
+
11
+ from ice_skaters import LaplaceFeatures, LaplaceTarget
12
+
13
+
14
+ def _pipeline():
15
+ return LaplaceTarget(
16
+ regressor=preprocessing.TargetStandardScaler(
17
+ regressor=LaplaceFeatures()
18
+ | preprocessing.StandardScaler()
19
+ | linear_model.LinearRegression()))
20
+
21
+
22
+ def test_predict_and_learn_see_identical_features():
23
+ """river's Pipeline updates transformers before transforming in
24
+ learn_one; the cache must hand both paths the same features."""
25
+ seen = []
26
+ orig = LaplaceFeatures.transform_one
27
+
28
+ def spy(self, x):
29
+ out = orig(self, x)
30
+ seen.append(dict(out))
31
+ return out
32
+
33
+ lf = LaplaceFeatures()
34
+ pipe = lf | preprocessing.StandardScaler() | linear_model.LinearRegression()
35
+ rng = random.Random(0)
36
+ LaplaceFeatures.transform_one = spy
37
+ try:
38
+ for _ in range(30):
39
+ x = {"a": rng.gauss(0, 1), "b": rng.gauss(5, 2)}
40
+ pipe.predict_one(x)
41
+ pipe.learn_one(x, rng.gauss(0, 1))
42
+ finally:
43
+ LaplaceFeatures.transform_one = orig
44
+ predict_path, learn_path = seen[0::2], seen[1::2]
45
+ assert predict_path == learn_path
46
+
47
+
48
+ def test_surprise_is_bounded_on_wild_spike():
49
+ lf = LaplaceFeatures()
50
+ for t in range(100):
51
+ lf.learn_one({"a": math.sin(t)})
52
+ out = lf.transform_one({"a": 1e9})
53
+ assert abs(out["z_a"]) < 7.1
54
+ assert math.isfinite(out["mu_a"])
55
+
56
+
57
+ def test_non_finite_values_are_forecast_imputed_and_ignored():
58
+ lf = LaplaceFeatures()
59
+ for t in range(50):
60
+ lf.learn_one({"a": float(t % 5)})
61
+ out = lf.transform_one({"a": float("nan")})
62
+ assert math.isfinite(out["mu_a"]) and out["z_a"] == 0.0
63
+ lf.learn_one({"a": float("inf")}) # must not corrupt the body
64
+ out = lf.transform_one({"a": 1.0})
65
+ assert all(math.isfinite(v) for v in out.values())
66
+
67
+
68
+ def test_non_numeric_passthrough():
69
+ lf = LaplaceFeatures()
70
+ lf.learn_one({"a": 1.0, "tag": "x", "flag": True})
71
+ out = lf.transform_one({"a": 1.1, "tag": "x", "flag": True})
72
+ assert out["tag"] == "x" and out["flag"] is True
73
+ assert set(out) == {"tag", "flag", "mu_a", "z_a"}
74
+
75
+
76
+ def test_emit_modes_and_validation():
77
+ assert set(LaplaceFeatures(emit="mu")._features({"a": 1.0})) == {"mu_a"}
78
+ assert set(LaplaceFeatures(emit="z")._features({"a": 1.0})) == {"z_a"}
79
+ with pytest.raises(ValueError):
80
+ LaplaceFeatures(emit="nope")
81
+
82
+
83
+ def test_full_model_pickles_and_deepcopies():
84
+ model = _pipeline()
85
+ rng = random.Random(1)
86
+ for _ in range(40):
87
+ x = {"a": rng.gauss(0, 1)}
88
+ model.predict_one(x)
89
+ model.learn_one(x, rng.gauss(0, 1))
90
+ probe = {"a": 0.3}
91
+ expected = model.predict_one(probe)
92
+ assert pickle.loads(pickle.dumps(model)).predict_one(probe) == expected
93
+ assert copy.deepcopy(model).predict_one(probe) == expected
94
+
95
+
96
+ def test_keys_may_appear_and_vanish_mid_stream():
97
+ lf = LaplaceFeatures()
98
+ lf.learn_one({"a": 1.0})
99
+ lf.learn_one({"a": 1.1, "c": 9.0})
100
+ out = lf.transform_one({"c": 9.1})
101
+ assert set(out) == {"mu_c", "z_c"}
102
+
103
+
104
+ def test_target_wrapper_learns_and_predicts_finite():
105
+ model = _pipeline()
106
+ rng = random.Random(2)
107
+ preds = []
108
+ for t in range(120):
109
+ x = {"a": rng.gauss(0, 1)}
110
+ p = model.predict_one(x)
111
+ preds.append(p if p is not None else 0.0)
112
+ model.learn_one(x, 0.5 * x["a"] + rng.gauss(0, 0.1))
113
+ assert all(math.isfinite(p) for p in preds)