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 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
- export function laplace(k = 1, objective = "crps", sticky = true, scales = null) {
165
- const f = multiscale((kk) => laplaceSingleScale(kk, objective, sticky), k, { scales });
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
@@ -22,6 +22,7 @@ export {
22
22
  buildCandidates, laplace,
23
23
  } from "./api.mjs";
24
24
  export { multiscale } from "./multiscale.mjs";
25
+ export { parade } from "./parade.mjs";
25
26
  export { sticky } from "./sticky.mjs";
26
27
  export {
27
28
  build, name as specName, toJson, fromJson,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skaters",
3
- "version": "0.11.4",
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
+ }