skaters 0.11.4 → 0.12.0
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 +11 -0
- package/api.mjs +14 -7
- package/index.mjs +1 -0
- package/package.json +5 -2
- package/parade.mjs +45 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ 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.12.0
|
|
9
|
+
|
|
10
|
+
Better default forecaster. `laplace`'s terminal-leaf `scaleAlpha` (the residual-variance
|
|
11
|
+
EWMA rate — how fast the predictive scale tracks changing volatility) now defaults to
|
|
12
|
+
**0.03** instead of 0.01. On the continuous FRED universe this beats the old default on
|
|
13
|
+
held-out **log-likelihood** (~+0.02 nats, ~79% of series) **and CRPS** (~80% of series), on
|
|
14
|
+
both non-price and price series — validated out-of-sample (see
|
|
15
|
+
`benchmarks/leaf_experiments/`). New optional `scaleAlpha` argument on `laplace`; pass
|
|
16
|
+
`scaleAlpha: 0.01` to reproduce the previous default. Minor bump because default predictions
|
|
17
|
+
change; Python/JS parity re-verified to 1e-6.
|
|
18
|
+
|
|
8
19
|
## 0.11.4
|
|
9
20
|
|
|
10
21
|
Docs: lead the README with the live in-browser race demo
|
package/api.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { terminalLeafEnsemble } from "./terminal.mjs";
|
|
|
8
8
|
import { bayesianEnsemble } from "./bayesian.mjs";
|
|
9
9
|
import { sticky as project } from "./sticky.mjs";
|
|
10
10
|
import { multiscale } from "./multiscale.mjs";
|
|
11
|
+
import { parade } from "./parade.mjs";
|
|
11
12
|
import {
|
|
12
13
|
difference, fractionalDifference, standardize, emaTransform, ouTransform, drift,
|
|
13
14
|
holtLinear, ar, theta, seasonalDifference, garch, powerTransform, yeoJohnson,
|
|
@@ -142,16 +143,16 @@ export function buildCandidates(k) {
|
|
|
142
143
|
// Policies
|
|
143
144
|
// ---------------------------------------------------------------------------
|
|
144
145
|
|
|
145
|
-
function objectiveLeaf(objective) {
|
|
146
|
-
if (objective === "crps") return crpsLeaf;
|
|
147
|
-
if (objective === "likelihood") return scaleMixtureLeaf;
|
|
146
|
+
function objectiveLeaf(objective, scaleAlpha) {
|
|
147
|
+
if (objective === "crps") return (k) => crpsLeaf(k, undefined, scaleAlpha);
|
|
148
|
+
if (objective === "likelihood") return (k) => scaleMixtureLeaf(k, undefined, scaleAlpha);
|
|
148
149
|
throw new Error(`objective must be 'crps' or 'likelihood', got ${objective}`);
|
|
149
150
|
}
|
|
150
151
|
|
|
151
|
-
function laplaceSingleScale(k, objective, sticky) {
|
|
152
|
+
function laplaceSingleScale(k, objective, sticky, scaleAlpha) {
|
|
152
153
|
const [candidates, depths] = buildCandidates(k);
|
|
153
154
|
let f = terminalLeafEnsemble(candidates, {
|
|
154
|
-
k, leafFn: objectiveLeaf(objective), learningRate: 0.8, complexityPenalty: 0.005, depths, maxComponents: 20,
|
|
155
|
+
k, leafFn: objectiveLeaf(objective, scaleAlpha), learningRate: 0.8, complexityPenalty: 0.005, depths, maxComponents: 20,
|
|
155
156
|
forget: 0.99,
|
|
156
157
|
});
|
|
157
158
|
if (sticky) f = project(f, k);
|
|
@@ -161,8 +162,14 @@ function laplaceSingleScale(k, objective, sticky) {
|
|
|
161
162
|
// Multi-scale by default at k > 1: one instance per decimation stride
|
|
162
163
|
// (default {1, ceil(sqrt(k)), k}), horizons mix eligible scales by likelihood.
|
|
163
164
|
// Pass scales = [1] for the single-scale (native fan-out) variant.
|
|
164
|
-
|
|
165
|
-
|
|
165
|
+
//
|
|
166
|
+
// scaleAlpha is the terminal leaf's residual-variance EWMA rate (how fast the
|
|
167
|
+
// predictive scale tracks volatility). Default 0.03 beats the older 0.01 on
|
|
168
|
+
// held-out log-likelihood AND CRPS across the continuous FRED universe; pass
|
|
169
|
+
// scaleAlpha = 0.01 to reproduce the earlier default.
|
|
170
|
+
export function laplace(k = 1, objective = "crps", sticky = true, scales = null, scaleAlpha = 0.03) {
|
|
171
|
+
// parade adds state.pit / state.z calibration diagnostics (see parade.mjs)
|
|
172
|
+
const f = parade(multiscale((kk) => laplaceSingleScale(kk, objective, sticky, scaleAlpha), k, { scales }), k);
|
|
166
173
|
f.skaterName = `laplace(k=${k})`;
|
|
167
174
|
return f;
|
|
168
175
|
}
|
package/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skaters",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
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",
|
|
@@ -35,7 +35,10 @@
|
|
|
35
35
|
"garch",
|
|
36
36
|
"zero-dependency",
|
|
37
37
|
"browser",
|
|
38
|
-
"microprediction"
|
|
38
|
+
"microprediction",
|
|
39
|
+
"anomaly-detection",
|
|
40
|
+
"calibration",
|
|
41
|
+
"pit"
|
|
39
42
|
],
|
|
40
43
|
"homepage": "https://skaters.microprediction.org",
|
|
41
44
|
"repository": {
|
package/parade.mjs
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// The prediction parade — JS port of skaters/parade.py.
|
|
2
|
+
//
|
|
3
|
+
// Resolve each arriving observation against the predictions previously made
|
|
4
|
+
// for it: state.pit[m-1] is the PIT of y under the m-step-ahead predictive
|
|
5
|
+
// issued m steps ago (roughly Uniform(0,1) when calibrated); state.z[m-1] is
|
|
6
|
+
// the same through the standard-normal quantile (roughly N(0,1)). Entries are
|
|
7
|
+
// null until horizon m has matured. Pass-through for the forecasts themselves.
|
|
8
|
+
// Named for the prediction parade of the timemachines package.
|
|
9
|
+
|
|
10
|
+
import { Dist } from "./dist.mjs";
|
|
11
|
+
|
|
12
|
+
const STD_NORMAL = Dist.gaussian(0.0, 1.0);
|
|
13
|
+
// Clamp the PIT away from {0,1}: |z| is then bounded by ~7.03 and the
|
|
14
|
+
// bisection in Dist.quantile stays inside its +-8 sigma bracket. No input can
|
|
15
|
+
// produce an infinite z. Non-finite CDF values leave the entry null.
|
|
16
|
+
const EPS = 1e-12;
|
|
17
|
+
|
|
18
|
+
export function parade(base, k) {
|
|
19
|
+
return function skater(y, state) {
|
|
20
|
+
if (state === null || state === undefined) {
|
|
21
|
+
state = { base: null, pending: [], pit: new Array(k).fill(null), z: new Array(k).fill(null) };
|
|
22
|
+
}
|
|
23
|
+
const pend = state.pending;
|
|
24
|
+
const n = pend.length;
|
|
25
|
+
const pit = new Array(k).fill(null);
|
|
26
|
+
const z = new Array(k).fill(null);
|
|
27
|
+
for (let m = 1; m <= k; m++) {
|
|
28
|
+
if (m <= n) {
|
|
29
|
+
const d = pend[n - m][m - 1]; // issued m steps ago, horizon m
|
|
30
|
+
let u = d.cdf(y);
|
|
31
|
+
if (!Number.isFinite(u)) continue; // degenerate predictive or bad y
|
|
32
|
+
u = Math.min(Math.max(u, EPS), 1.0 - EPS);
|
|
33
|
+
pit[m - 1] = u;
|
|
34
|
+
z[m - 1] = STD_NORMAL.quantile(u);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const [dists, st] = base(y, state.base);
|
|
38
|
+
state.base = st;
|
|
39
|
+
pend.push(dists.slice());
|
|
40
|
+
if (pend.length > k) pend.shift();
|
|
41
|
+
state.pit = pit;
|
|
42
|
+
state.z = z;
|
|
43
|
+
return [dists, state];
|
|
44
|
+
};
|
|
45
|
+
}
|