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.
skaters-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: skaters
3
+ Version: 0.1.0
4
+ Summary: Fast univariate time series models that run in Pyodide
5
+ Author: Peter Cotton
6
+ Author-email: Peter Cotton <peter.cotton@microprediction.com>
7
+ License-Expression: MIT
8
+ Requires-Dist: pytest ; extra == 'dev'
9
+ Requires-Python: >=3.10
10
+ Provides-Extra: dev
11
+ Description-Content-Type: text/markdown
12
+
13
+ # skaters
14
+
15
+ Fast univariate online time series models. Zero dependencies. Runs in [Pyodide](https://pyodide.org/).
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pip install skaters
21
+ ```
22
+
23
+ ## Quick start
24
+
25
+ ```python
26
+ from skaters import ensemble, envelope
27
+
28
+ # Create a precision-weighted ensemble of EMA models
29
+ f = ensemble(k=3) # predict 3 steps ahead
30
+
31
+ state = None
32
+ for y in observations:
33
+ x, state = f(y, state)
34
+ # x = [float, float, float] (forecasts for horizons 1, 2, 3)
35
+
36
+ # Want uncertainty bands? Wrap any skater with an envelope:
37
+ f = envelope(ensemble(k=3), k=3)
38
+
39
+ state = None
40
+ for y in observations:
41
+ out, state = f(y, state)
42
+ # out["mean"] = point forecasts
43
+ # out["std"] = empirical std of forecast error per horizon
44
+ ```
45
+
46
+ ## The skater convention
47
+
48
+ A skater is any callable with this signature:
49
+
50
+ ```python
51
+ x, state = f(y, state)
52
+ ```
53
+
54
+ | Argument | Type | Description |
55
+ |----------|------|-------------|
56
+ | `y` | `float` | New observation |
57
+ | `state` | `dict \| None` | Prior state (`None` on first call) |
58
+ | **Returns** | | |
59
+ | `x` | `list[float]` | Point forecasts for horizons 1..k |
60
+ | `state` | `dict` | Updated state (pass back next call) |
61
+
62
+ Skaters only predict. Uncertainty is handled separately by the [envelope](#envelope).
63
+
64
+ ## Built-in skaters
65
+
66
+ ### EMA
67
+
68
+ Exponential moving average. O(1) per observation.
69
+
70
+ ```python
71
+ from skaters import ema
72
+
73
+ f = ema(alpha=0.1, k=1)
74
+ ```
75
+
76
+ ### Convenience constructors
77
+
78
+ Pre-configured EMA speeds:
79
+
80
+ ```python
81
+ from skaters import rapidly, quickly, slowly, sluggishly
82
+
83
+ f = rapidly(k=1) # alpha=0.3
84
+ f = quickly(k=1) # alpha=0.1
85
+ f = slowly(k=1) # alpha=0.05
86
+ f = sluggishly(k=1) # alpha=0.01
87
+ ```
88
+
89
+ ### Precision-weighted ensemble
90
+
91
+ Combines multiple skaters, weighting each by 1/MSE of its forecast errors. Automatically favors models that are both accurate and unbiased.
92
+
93
+ ```python
94
+ from skaters import ensemble, precision_weighted_ensemble, ema
95
+
96
+ # Default: ensemble of EMAs at different speeds
97
+ f = ensemble(k=3)
98
+
99
+ # Custom: bring your own skaters
100
+ f = precision_weighted_ensemble(
101
+ skaters=[ema(alpha=0.05, k=3), ema(alpha=0.2, k=3)],
102
+ k=3,
103
+ )
104
+ ```
105
+
106
+ ## Envelope
107
+
108
+ The envelope wraps any skater and tracks empirical forecast errors at each horizon. It is model-independent — it doesn't care how the predictions are made.
109
+
110
+ ```python
111
+ from skaters import envelope, ema
112
+
113
+ # Welford's (uniform weight over all history)
114
+ f = envelope(ema(alpha=0.1, k=3), k=3)
115
+
116
+ # Exponentially weighted (forgets old errors, adapts to regime changes)
117
+ f = envelope(ema(alpha=0.1, k=3), k=3, decay=0.95)
118
+
119
+ state = None
120
+ for y in observations:
121
+ out, state = f(y, state)
122
+ print(out["mean"], out["std"])
123
+ ```
124
+
125
+ ## Writing your own skater
126
+
127
+ Any function matching the convention works:
128
+
129
+ ```python
130
+ def my_skater(y: float, state: dict | None) -> tuple[list[float], dict]:
131
+ if state is None:
132
+ state = {"last": y}
133
+ state["last"] = y
134
+ return [state["last"]], state # predict last value for k=1
135
+
136
+ # Use it standalone or in an ensemble:
137
+ from skaters import envelope, precision_weighted_ensemble
138
+
139
+ f = envelope(my_skater, k=1)
140
+ f = precision_weighted_ensemble([my_skater, ema(alpha=0.1, k=1)], k=1)
141
+ ```
142
+
143
+ ## Design
144
+
145
+ - **Online only** — O(1) per observation, no batch recomputation
146
+ - **Prediction and uncertainty are separate** — skaters predict, envelopes estimate error
147
+ - **Pure Python** — zero dependencies, runs anywhere Python runs
148
+ - **Pyodide compatible** — works in the browser via WebAssembly
149
+
150
+ ## Lineage
151
+
152
+ This package distills ideas from [timemachines](https://github.com/microprediction/timemachines), which provided a common skater interface for dozens of time series packages. This is a from-scratch rewrite focused on speed, simplicity, and browser compatibility.
@@ -0,0 +1,140 @@
1
+ # skaters
2
+
3
+ Fast univariate online time series models. Zero dependencies. Runs in [Pyodide](https://pyodide.org/).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install skaters
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```python
14
+ from skaters import ensemble, envelope
15
+
16
+ # Create a precision-weighted ensemble of EMA models
17
+ f = ensemble(k=3) # predict 3 steps ahead
18
+
19
+ state = None
20
+ for y in observations:
21
+ x, state = f(y, state)
22
+ # x = [float, float, float] (forecasts for horizons 1, 2, 3)
23
+
24
+ # Want uncertainty bands? Wrap any skater with an envelope:
25
+ f = envelope(ensemble(k=3), k=3)
26
+
27
+ state = None
28
+ for y in observations:
29
+ out, state = f(y, state)
30
+ # out["mean"] = point forecasts
31
+ # out["std"] = empirical std of forecast error per horizon
32
+ ```
33
+
34
+ ## The skater convention
35
+
36
+ A skater is any callable with this signature:
37
+
38
+ ```python
39
+ x, state = f(y, state)
40
+ ```
41
+
42
+ | Argument | Type | Description |
43
+ |----------|------|-------------|
44
+ | `y` | `float` | New observation |
45
+ | `state` | `dict \| None` | Prior state (`None` on first call) |
46
+ | **Returns** | | |
47
+ | `x` | `list[float]` | Point forecasts for horizons 1..k |
48
+ | `state` | `dict` | Updated state (pass back next call) |
49
+
50
+ Skaters only predict. Uncertainty is handled separately by the [envelope](#envelope).
51
+
52
+ ## Built-in skaters
53
+
54
+ ### EMA
55
+
56
+ Exponential moving average. O(1) per observation.
57
+
58
+ ```python
59
+ from skaters import ema
60
+
61
+ f = ema(alpha=0.1, k=1)
62
+ ```
63
+
64
+ ### Convenience constructors
65
+
66
+ Pre-configured EMA speeds:
67
+
68
+ ```python
69
+ from skaters import rapidly, quickly, slowly, sluggishly
70
+
71
+ f = rapidly(k=1) # alpha=0.3
72
+ f = quickly(k=1) # alpha=0.1
73
+ f = slowly(k=1) # alpha=0.05
74
+ f = sluggishly(k=1) # alpha=0.01
75
+ ```
76
+
77
+ ### Precision-weighted ensemble
78
+
79
+ Combines multiple skaters, weighting each by 1/MSE of its forecast errors. Automatically favors models that are both accurate and unbiased.
80
+
81
+ ```python
82
+ from skaters import ensemble, precision_weighted_ensemble, ema
83
+
84
+ # Default: ensemble of EMAs at different speeds
85
+ f = ensemble(k=3)
86
+
87
+ # Custom: bring your own skaters
88
+ f = precision_weighted_ensemble(
89
+ skaters=[ema(alpha=0.05, k=3), ema(alpha=0.2, k=3)],
90
+ k=3,
91
+ )
92
+ ```
93
+
94
+ ## Envelope
95
+
96
+ The envelope wraps any skater and tracks empirical forecast errors at each horizon. It is model-independent — it doesn't care how the predictions are made.
97
+
98
+ ```python
99
+ from skaters import envelope, ema
100
+
101
+ # Welford's (uniform weight over all history)
102
+ f = envelope(ema(alpha=0.1, k=3), k=3)
103
+
104
+ # Exponentially weighted (forgets old errors, adapts to regime changes)
105
+ f = envelope(ema(alpha=0.1, k=3), k=3, decay=0.95)
106
+
107
+ state = None
108
+ for y in observations:
109
+ out, state = f(y, state)
110
+ print(out["mean"], out["std"])
111
+ ```
112
+
113
+ ## Writing your own skater
114
+
115
+ Any function matching the convention works:
116
+
117
+ ```python
118
+ def my_skater(y: float, state: dict | None) -> tuple[list[float], dict]:
119
+ if state is None:
120
+ state = {"last": y}
121
+ state["last"] = y
122
+ return [state["last"]], state # predict last value for k=1
123
+
124
+ # Use it standalone or in an ensemble:
125
+ from skaters import envelope, precision_weighted_ensemble
126
+
127
+ f = envelope(my_skater, k=1)
128
+ f = precision_weighted_ensemble([my_skater, ema(alpha=0.1, k=1)], k=1)
129
+ ```
130
+
131
+ ## Design
132
+
133
+ - **Online only** — O(1) per observation, no batch recomputation
134
+ - **Prediction and uncertainty are separate** — skaters predict, envelopes estimate error
135
+ - **Pure Python** — zero dependencies, runs anywhere Python runs
136
+ - **Pyodide compatible** — works in the browser via WebAssembly
137
+
138
+ ## Lineage
139
+
140
+ This package distills ideas from [timemachines](https://github.com/microprediction/timemachines), which provided a common skater interface for dozens of time series packages. This is a from-scratch rewrite focused on speed, simplicity, and browser compatibility.
@@ -0,0 +1,23 @@
1
+ [project]
2
+ name = "skaters"
3
+ version = "0.1.0"
4
+ description = "Fast univariate time series models that run in Pyodide"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ authors = [
8
+ { name = "Peter Cotton", email = "peter.cotton@microprediction.com" }
9
+ ]
10
+ requires-python = ">=3.10"
11
+ dependencies = []
12
+
13
+ [project.optional-dependencies]
14
+ dev = ["pytest"]
15
+
16
+ [build-system]
17
+ requires = ["uv_build>=0.10.12,<0.11.0"]
18
+ build-backend = "uv_build"
19
+
20
+ [dependency-groups]
21
+ dev = [
22
+ "pytest>=9.0.2",
23
+ ]
@@ -0,0 +1,23 @@
1
+ """Fast univariate online time series models that run in Pyodide."""
2
+
3
+ from skaters.conventions import Skater
4
+ from skaters.ema import ema
5
+ from skaters.envelope import envelope
6
+ from skaters.ensemble import precision_weighted_ensemble
7
+ from skaters.api import (
8
+ quickly, slowly, sluggishly, rapidly,
9
+ ensemble, ensemble_with_envelope,
10
+ )
11
+
12
+ __all__ = [
13
+ "Skater",
14
+ "ema",
15
+ "envelope",
16
+ "precision_weighted_ensemble",
17
+ "quickly",
18
+ "slowly",
19
+ "sluggishly",
20
+ "rapidly",
21
+ "ensemble",
22
+ "ensemble_with_envelope",
23
+ ]
@@ -0,0 +1,43 @@
1
+ """Convenience constructors for common skater configurations."""
2
+
3
+ from __future__ import annotations
4
+ from skaters.ema import ema
5
+ from skaters.ensemble import precision_weighted_ensemble
6
+ from skaters.envelope import envelope
7
+
8
+
9
+ def rapidly(k: int = 1):
10
+ """EMA with alpha=0.3 — reacts fast to changes."""
11
+ return ema(alpha=0.3, k=k)
12
+
13
+
14
+ def quickly(k: int = 1):
15
+ """EMA with alpha=0.1 — moderate reactivity."""
16
+ return ema(alpha=0.1, k=k)
17
+
18
+
19
+ def slowly(k: int = 1):
20
+ """EMA with alpha=0.05 — smooth, slow-moving."""
21
+ return ema(alpha=0.05, k=k)
22
+
23
+
24
+ def sluggishly(k: int = 1):
25
+ """EMA with alpha=0.01 — very slow adaptation."""
26
+ return ema(alpha=0.01, k=k)
27
+
28
+
29
+ def ensemble(k: int = 1):
30
+ """Precision-weighted ensemble of EMA models at different speeds."""
31
+ return precision_weighted_ensemble(
32
+ skaters=[rapidly(k), quickly(k), slowly(k), sluggishly(k)],
33
+ k=k,
34
+ )
35
+
36
+
37
+ def ensemble_with_envelope(k: int = 1, decay: float | None = None):
38
+ """Precision-weighted ensemble wrapped with an empirical error envelope.
39
+
40
+ This is the recommended default — good predictions with calibrated
41
+ uncertainty bands.
42
+ """
43
+ return envelope(ensemble(k=k), k=k, decay=decay)
@@ -0,0 +1,23 @@
1
+ """Skater convention: an online univariate model is a callable:
2
+
3
+ x, state = f(y, state)
4
+
5
+ where:
6
+ y: float - new observation
7
+ state: dict | None - prior state (None on first call)
8
+ x: list[float] - point forecasts for horizons 1..k
9
+ state: dict - updated state (pass back next call)
10
+
11
+ Skaters only predict. Uncertainty estimation is handled separately
12
+ by the Envelope (see envelope.py), which wraps any skater and tracks
13
+ empirical forecast errors per horizon.
14
+ """
15
+
16
+ from __future__ import annotations
17
+ from typing import Protocol, runtime_checkable
18
+
19
+
20
+ @runtime_checkable
21
+ class Skater(Protocol):
22
+ def __call__(self, y: float, state: dict | None) -> tuple[list[float], dict]:
23
+ ...
@@ -0,0 +1,28 @@
1
+ """Exponential moving average skater.
2
+
3
+ Pure online, O(1) per observation. Returns only point forecasts.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+
9
+ def ema(alpha: float = 0.05, k: int = 1):
10
+ """Create an EMA skater.
11
+
12
+ Args:
13
+ alpha: smoothing factor in (0,1). Small = slow, large = fast.
14
+ k: forecast horizon (number of steps ahead).
15
+
16
+ Returns:
17
+ A skater callable: (y, state) -> (list[float], state)
18
+ """
19
+ assert 0 < alpha < 1
20
+
21
+ def _skater(y: float, state: dict | None) -> tuple[list[float], dict]:
22
+ if state is None:
23
+ return [y] * k, {"level": y}
24
+ level = state["level"] + alpha * (y - state["level"])
25
+ return [level] * k, {"level": level}
26
+
27
+ _skater.__name__ = f"ema(alpha={alpha}, k={k})"
28
+ return _skater
@@ -0,0 +1,74 @@
1
+ """Precision-weighted ensemble of skaters.
2
+
3
+ Combines multiple online skaters by weighting inversely proportional
4
+ to their empirical forecast variance. Each sub-model's errors are
5
+ tracked internally via parade-style queues.
6
+
7
+ The ensemble itself is a skater — it returns list[float] predictions.
8
+ Wrap it with an envelope if you also want uncertainty bands.
9
+ """
10
+
11
+ from __future__ import annotations
12
+ import math
13
+ from collections import deque
14
+ from skaters.runstats import running_var_init, running_var_update, running_mse_get
15
+
16
+
17
+ def precision_weighted_ensemble(skaters: list, k: int = 1, floor: float = 1e-6):
18
+ """Create a precision-weighted ensemble skater.
19
+
20
+ Args:
21
+ skaters: list of skater callables
22
+ k: forecast horizon
23
+ floor: minimum precision to prevent zero-weight
24
+
25
+ Returns:
26
+ A skater callable: (y, state) -> (list[float], state)
27
+ """
28
+ n = len(skaters)
29
+ assert n > 0
30
+
31
+ def _skater(y: float, state: dict | None) -> tuple[list[float], dict]:
32
+ if state is None:
33
+ state = {
34
+ "sub": [None] * n,
35
+ "queues": [[deque() for _ in range(k)] for _ in range(n)],
36
+ "stats": [[running_var_init() for _ in range(k)] for _ in range(n)],
37
+ }
38
+
39
+ # Run all sub-models and collect predictions
40
+ preds = []
41
+ for i, f in enumerate(skaters):
42
+ x_i, state["sub"][i] = f(y, state["sub"][i])
43
+ preds.append(x_i)
44
+
45
+ # Resolve pending predictions and update error stats
46
+ for i in range(n):
47
+ for h in range(k):
48
+ q = state["queues"][i][h]
49
+ if q:
50
+ error = y - q.popleft()
51
+ state["stats"][i][h] = running_var_update(state["stats"][i][h], error)
52
+ state["queues"][i][h].append(preds[i][h])
53
+
54
+ # Precision-weighted combination per horizon (weight by 1/MSE)
55
+ combined = []
56
+ for h in range(k):
57
+ weights = []
58
+ for i in range(n):
59
+ mse = running_mse_get(state["stats"][i][h])
60
+ if math.isfinite(mse) and mse > 0:
61
+ w = 1.0 / mse
62
+ else:
63
+ w = floor
64
+ weights.append(max(w, floor))
65
+
66
+ w_total = sum(weights)
67
+ combined.append(
68
+ sum(w * preds[i][h] for i, w in enumerate(weights)) / w_total
69
+ )
70
+
71
+ return combined, state
72
+
73
+ _skater.__name__ = f"precision_weighted_ensemble(n={n}, k={k})"
74
+ return _skater
@@ -0,0 +1,88 @@
1
+ """Model-independent empirical prediction envelope.
2
+
3
+ Wraps any skater and tracks forecast errors at each horizon using
4
+ online statistics. Provides regularized std estimates that can be
5
+ used as confidence bands.
6
+
7
+ The envelope is the sole source of uncertainty — skaters just predict.
8
+ """
9
+
10
+ from __future__ import annotations
11
+ import math
12
+ from collections import deque
13
+ from skaters.runstats import running_var_init, running_var_update, running_std_get
14
+
15
+
16
+ def envelope(skater, k: int = 1, decay: float | None = None):
17
+ """Wrap a skater with an empirical error envelope.
18
+
19
+ Args:
20
+ skater: any skater callable (y, state) -> (list[float], state)
21
+ k: forecast horizon (must match the skater's k)
22
+ decay: if set, use exponentially weighted variance with this
23
+ decay factor (0 < decay < 1, smaller = faster forgetting).
24
+ If None, use Welford's (uniform weighting over all history).
25
+
26
+ Returns:
27
+ A callable: (y, state) -> (x_dict, state) where x_dict contains
28
+ "mean" (point forecasts) and "std" (empirical error std per horizon).
29
+ """
30
+
31
+ def _enveloped(y: float, state: dict | None) -> tuple[dict, dict]:
32
+ if state is None:
33
+ state = {
34
+ "inner": None,
35
+ "queues": [deque() for _ in range(k)],
36
+ "error_stats": [running_var_init() for _ in range(k)],
37
+ }
38
+ if decay is not None:
39
+ state["ew_var"] = [{"mean": 0.0, "var": 0.0, "n": 0} for _ in range(k)]
40
+
41
+ # Run the inner skater
42
+ x, state["inner"] = skater(y, state["inner"])
43
+
44
+ # Resolve pending predictions against this observation
45
+ queues = state["queues"]
46
+ error_stats = state["error_stats"]
47
+ for h in range(k):
48
+ if queues[h]:
49
+ predicted = queues[h].popleft()
50
+ error = y - predicted
51
+ if decay is not None:
52
+ _ew_update(state["ew_var"][h], error, decay)
53
+ else:
54
+ error_stats[h] = running_var_update(error_stats[h], error)
55
+
56
+ # Enqueue new predictions
57
+ for h in range(k):
58
+ queues[h].append(x[h])
59
+
60
+ # Compute std
61
+ if decay is not None:
62
+ std = [_ew_std(state["ew_var"][h]) for h in range(k)]
63
+ else:
64
+ std = [running_std_get(s) for s in error_stats]
65
+
66
+ return {"mean": x, "std": std}, state
67
+
68
+ _enveloped.__name__ = f"envelope({getattr(skater, '__name__', '?')})"
69
+ return _enveloped
70
+
71
+
72
+ def _ew_update(ew: dict, x: float, decay: float) -> None:
73
+ """Exponentially weighted online variance update (in-place)."""
74
+ ew["n"] += 1
75
+ if ew["n"] == 1:
76
+ ew["mean"] = x
77
+ ew["var"] = 0.0
78
+ return
79
+ diff = x - ew["mean"]
80
+ ew["mean"] = decay * ew["mean"] + (1 - decay) * x
81
+ ew["var"] = decay * (ew["var"] + (1 - decay) * diff * diff)
82
+
83
+
84
+ def _ew_std(ew: dict) -> float:
85
+ """Get std from exponentially weighted state."""
86
+ if ew["n"] < 2:
87
+ return float("inf")
88
+ return math.sqrt(ew["var"]) if ew["var"] > 0 else float("inf")
@@ -0,0 +1,53 @@
1
+ """Parade: track forecast errors at each horizon incrementally.
2
+
3
+ A "parade" maintains k queues of pending predictions. When a new
4
+ observation arrives, it resolves the oldest prediction at each horizon,
5
+ computes the error, and updates running statistics.
6
+ """
7
+
8
+ from __future__ import annotations
9
+ from collections import deque
10
+ from skaters.runstats import running_var_init, running_var_update, running_std_get
11
+
12
+
13
+ def parade_init(k: int) -> dict:
14
+ """Initialize parade state for k horizons."""
15
+ return {
16
+ "k": k,
17
+ "queues": [deque() for _ in range(k)],
18
+ "error_stats": [running_var_init() for _ in range(k)],
19
+ }
20
+
21
+
22
+ def parade_update(state: dict, x: list[float], y: float) -> dict:
23
+ """Record new predictions x[0..k-1] and resolve against observation y.
24
+
25
+ Args:
26
+ state: parade state
27
+ x: predictions for horizons 1..k
28
+ y: the observation that just arrived
29
+
30
+ Returns:
31
+ updated state (std available via parade_std)
32
+ """
33
+ k = state["k"]
34
+ queues = state["queues"]
35
+ error_stats = state["error_stats"]
36
+
37
+ # Resolve: the observation y was predicted h steps ago by queue[h]
38
+ for h in range(k):
39
+ if queues[h]:
40
+ predicted = queues[h].popleft()
41
+ error = y - predicted
42
+ error_stats[h] = running_var_update(error_stats[h], error)
43
+
44
+ # Enqueue new predictions
45
+ for h in range(k):
46
+ queues[h].append(x[h])
47
+
48
+ return state
49
+
50
+
51
+ def parade_std(state: dict) -> list[float]:
52
+ """Return current std estimate at each horizon."""
53
+ return [running_std_get(s) for s in state["error_stats"]]
File without changes
@@ -0,0 +1,48 @@
1
+ """Lightweight online running statistics (pure Python, no dependencies).
2
+
3
+ Welford's algorithm for mean/variance, plus exponentially weighted variants.
4
+ """
5
+
6
+ from __future__ import annotations
7
+ import math
8
+
9
+
10
+ def running_var_init() -> dict:
11
+ """Initialize state for Welford's online variance."""
12
+ return {"n": 0, "mean": 0.0, "m2": 0.0}
13
+
14
+
15
+ def running_var_update(state: dict, x: float) -> dict:
16
+ """Update running mean/variance with a new observation (Welford)."""
17
+ n = state["n"] + 1
18
+ delta = x - state["mean"]
19
+ mean = state["mean"] + delta / n
20
+ delta2 = x - mean
21
+ m2 = state["m2"] + delta * delta2
22
+ return {"n": n, "mean": mean, "m2": m2}
23
+
24
+
25
+ def running_var_get(state: dict) -> tuple[float, float]:
26
+ """Return (mean, variance) from running stats state."""
27
+ if state["n"] < 2:
28
+ return state["mean"], float("inf")
29
+ return state["mean"], state["m2"] / (state["n"] - 1)
30
+
31
+
32
+ def running_std_get(state: dict) -> float:
33
+ """Return standard deviation from running stats state."""
34
+ _, var = running_var_get(state)
35
+ return math.sqrt(var) if math.isfinite(var) else float("inf")
36
+
37
+
38
+ def running_mse_get(state: dict) -> float:
39
+ """Return mean squared error (second moment) from running stats of errors.
40
+
41
+ MSE = bias² + variance, so it penalizes both systematic and random error.
42
+ """
43
+ if state["n"] < 1:
44
+ return float("inf")
45
+ mean, var = running_var_get(state)
46
+ if not math.isfinite(var):
47
+ return float("inf")
48
+ return mean * mean + var