skaters 0.11.0 → 0.11.4
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.
- package/CHANGELOG.md +35 -0
- package/README.md +9 -0
- package/dist.mjs +23 -2
- package/leaf.mjs +14 -3
- package/package.json +1 -1
- package/transform.mjs +22 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,41 @@ tracks the Python [`skaters`](https://pypi.org/project/skaters/) package and is
|
|
|
5
5
|
kept numerically identical to it within `1e-6`, enforced by the parity checker on
|
|
6
6
|
every release.
|
|
7
7
|
|
|
8
|
+
## 0.11.4
|
|
9
|
+
|
|
10
|
+
Docs: lead the README with the live in-browser race demo
|
|
11
|
+
(<https://skaters.microprediction.org/demos/race.html>) — the JS port racing
|
|
12
|
+
`arima`, `@bsull/augurs` (ETS and MSTL), and Prophet (Stan compiled to WASM) on
|
|
13
|
+
real FRED series, scored on held-out log-likelihood. No code changes.
|
|
14
|
+
|
|
15
|
+
## 0.11.3
|
|
16
|
+
|
|
17
|
+
Fix: `Dist.logpdf` returned `-Infinity` for finite inputs when the scale had
|
|
18
|
+
collapsed near a Dirac (e.g. on a near-constant stream), because `log(sum(w *
|
|
19
|
+
pdf))` underflowed each `exp(-z**2/2)` to `0`. A Gaussian mixture is strictly
|
|
20
|
+
positive everywhere, so this is now computed as a log-sum-exp over the
|
|
21
|
+
per-component log densities and is always finite for finite `x`. Numerically
|
|
22
|
+
identical to the old path wherever it did not underflow (parity vectors
|
|
23
|
+
unchanged); only the former `-Infinity` cases now return the correct large
|
|
24
|
+
finite value.
|
|
25
|
+
|
|
26
|
+
## 0.11.2
|
|
27
|
+
|
|
28
|
+
Fix: RLS covariance windup in the `ar` and `grouped_ar` transforms. Under
|
|
29
|
+
low-excitation input the RLS `P` matrix inflated by `1/lam` each step and
|
|
30
|
+
eventually overflowed to `Inf` (~74k steps), giving `NaN` coefficients and a
|
|
31
|
+
non-finite forecast. `P` is now reset if it grows implausibly large or turns
|
|
32
|
+
non-finite (keeping the coefficients); it never triggers on well-excited data, so
|
|
33
|
+
results and Python↔JS parity are unchanged.
|
|
34
|
+
|
|
35
|
+
## 0.11.1
|
|
36
|
+
|
|
37
|
+
Fix: the CRPS leaf's exponentiated-gradient weight update could overflow `exp()`
|
|
38
|
+
to `Inf` on some streams, normalizing to `NaN` weights and throwing in the `Dist`
|
|
39
|
+
constructor. The step is now stabilized by subtracting the max exponent
|
|
40
|
+
(mathematically invariant, so results and Python↔JS parity are unchanged), with a
|
|
41
|
+
guard that keeps the current weights on any degenerate update.
|
|
42
|
+
|
|
8
43
|
## 0.11.0
|
|
9
44
|
|
|
10
45
|
Initial npm release of the JavaScript port. Zero-dependency ES modules for Node
|
package/README.md
CHANGED
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/skaters)
|
|
5
5
|
[](https://github.com/microprediction/skaters/blob/main/LICENSE)
|
|
6
6
|
|
|
7
|
+
### ▶ [Watch it race live in your browser](https://skaters.microprediction.org/demos/race.html)
|
|
8
|
+
|
|
9
|
+
The actual JavaScript port racing `arima`, `@bsull/augurs` (ETS and MSTL), and
|
|
10
|
+
Prophet (Stan compiled to WASM) on 150 real FRED series — every method scored on
|
|
11
|
+
held-out log-likelihood, rolling one-step-ahead, no server. `laplace` keeps up
|
|
12
|
+
with a moving average while the refit baselines grind. **[Start here →](https://skaters.microprediction.org/demos/race.html)**
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
7
16
|
Fast univariate **online distributional** time-series forecasting. Zero
|
|
8
17
|
dependencies, runs in Node or the browser. This is the JavaScript port of the
|
|
9
18
|
Python [`skaters`](https://pypi.org/project/skaters/) package, numerically
|
package/dist.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
const SQRT2 = Math.SQRT2;
|
|
10
10
|
const SQRT2PI = Math.sqrt(2.0 * Math.PI);
|
|
11
|
+
const LOG_SQRT2PI = 0.5 * Math.log(2.0 * Math.PI);
|
|
11
12
|
const LGAMMA_HALF = 0.5723649429247001; // ln(Gamma(1/2)) = ln(sqrt(pi))
|
|
12
13
|
|
|
13
14
|
// --- erf via the regularized lower incomplete gamma P(1/2, x^2) ---
|
|
@@ -134,8 +135,28 @@ export class Dist {
|
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
logpdf(x) {
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
// Log density accumulated in log-space (log-sum-exp). A Gaussian mixture is
|
|
139
|
+
// strictly positive everywhere, so for finite x this is always finite;
|
|
140
|
+
// log(sum(w * pdf)) would underflow to -Infinity once x sits many standard
|
|
141
|
+
// deviations from every component (e.g. after the scale collapses on a
|
|
142
|
+
// near-constant stream), because each exp(-z*z/2) rounds to 0.
|
|
143
|
+
let best = -Infinity;
|
|
144
|
+
const terms = [];
|
|
145
|
+
for (const [w, m, s] of this.components) {
|
|
146
|
+
if (w <= 0.0) continue;
|
|
147
|
+
if (s <= 0.0) {
|
|
148
|
+
if (x === m) return Infinity;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const z = (x - m) / s;
|
|
152
|
+
const t = Math.log(w) - 0.5 * z * z - Math.log(s) - LOG_SQRT2PI;
|
|
153
|
+
terms.push(t);
|
|
154
|
+
if (t > best) best = t;
|
|
155
|
+
}
|
|
156
|
+
if (best === -Infinity) return -Infinity;
|
|
157
|
+
let acc = 0.0;
|
|
158
|
+
for (const t of terms) acc += Math.exp(t - best);
|
|
159
|
+
return best + Math.log(acc);
|
|
139
160
|
}
|
|
140
161
|
|
|
141
162
|
cdf(x) {
|
package/leaf.mjs
CHANGED
|
@@ -114,10 +114,21 @@ export function crpsLeaf(k = 1, eta = 1.0, scaleAlpha = 0.01, scales = FINE) {
|
|
|
114
114
|
g[c] = absNormal(-z, C[c]) - dot;
|
|
115
115
|
}
|
|
116
116
|
const gm = fsum(g) / K;
|
|
117
|
+
// Exponentiated-gradient step, stabilized by subtracting the max exponent so
|
|
118
|
+
// exp() cannot overflow to Inf (which would normalize to NaN weights and
|
|
119
|
+
// crash Dist). Subtracting a constant from every exponent leaves the
|
|
120
|
+
// normalized weights unchanged.
|
|
121
|
+
const e = new Array(K);
|
|
122
|
+
for (let c = 0; c < K; c++) e[c] = -eta * (g[c] - gm);
|
|
123
|
+
let emax = -Infinity;
|
|
124
|
+
for (let c = 0; c < K; c++) if (e[c] > emax) emax = e[c];
|
|
117
125
|
const nw = new Array(K);
|
|
118
|
-
let
|
|
119
|
-
|
|
120
|
-
Z
|
|
126
|
+
for (let c = 0; c < K; c++) nw[c] = w[c] * Math.exp(e[c] - emax);
|
|
127
|
+
let Z = fsum(nw);
|
|
128
|
+
if (!(Z > 0.0) || !Number.isFinite(Z)) {
|
|
129
|
+
for (let c = 0; c < K; c++) nw[c] = w[c]; // degenerate update: keep weights
|
|
130
|
+
Z = fsum(nw);
|
|
131
|
+
}
|
|
121
132
|
state.w = nw.map((x) => x / Z);
|
|
122
133
|
const comps = C.map((c, i) => [state.w[i], 0.0, c * sig]);
|
|
123
134
|
return [new Array(k).fill(new Dist(comps)), state];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skaters",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.4",
|
|
4
4
|
"description": "Fast univariate online distributional time-series forecasting: a zero-dependency JavaScript port of the Python skaters package, numerically identical to 1e-6.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
package/transform.mjs
CHANGED
|
@@ -461,6 +461,18 @@ export function ar(order = 2, lam = 0.99, ridge = 1.0, decay = 0.0) {
|
|
|
461
461
|
P[i * p + j] = (P[i * p + j] - K[i] * Px[j]) / lam;
|
|
462
462
|
}
|
|
463
463
|
}
|
|
464
|
+
// Guard against RLS covariance windup (see the Python note): under
|
|
465
|
+
// low-excitation input P inflates by 1/lam each step and overflows to
|
|
466
|
+
// Inf, giving nan coefficients. Reset the covariance (keeping phi) if it
|
|
467
|
+
// grows implausibly large or non-finite. No effect on well-excited data.
|
|
468
|
+
let bad = false, pmax = 0.0;
|
|
469
|
+
for (let m = 0; m < P.length; m++) {
|
|
470
|
+
const v = P[m];
|
|
471
|
+
if (!Number.isFinite(v)) { bad = true; break; }
|
|
472
|
+
const av = Math.abs(v);
|
|
473
|
+
if (av > pmax) pmax = av;
|
|
474
|
+
}
|
|
475
|
+
if (bad || pmax > 1e10) { const np = initP(); for (let m = 0; m < P.length; m++) P[m] = np[m]; }
|
|
464
476
|
}
|
|
465
477
|
} else {
|
|
466
478
|
residual = y;
|
|
@@ -555,6 +567,16 @@ export function groupedAr(maxLag = 16, lam = 0.99, ridge = 1.0) {
|
|
|
555
567
|
P[i * nGroups + j] = (P[i * nGroups + j] - K[i] * Px[j]) / lam;
|
|
556
568
|
}
|
|
557
569
|
}
|
|
570
|
+
// RLS covariance windup guard (see ar()): reset P if it inflates
|
|
571
|
+
// implausibly or turns non-finite; keeps theta. No effect on well-excited data.
|
|
572
|
+
let bad = false, pmax = 0.0;
|
|
573
|
+
for (let m = 0; m < P.length; m++) {
|
|
574
|
+
const v = P[m];
|
|
575
|
+
if (!Number.isFinite(v)) { bad = true; break; }
|
|
576
|
+
const av = Math.abs(v);
|
|
577
|
+
if (av > pmax) pmax = av;
|
|
578
|
+
}
|
|
579
|
+
if (bad || pmax > 1e10) { const np = eye(nGroups, ridge); for (let m = 0; m < P.length; m++) P[m] = np[m]; }
|
|
558
580
|
}
|
|
559
581
|
} else {
|
|
560
582
|
residual = y;
|