skaters 0.11.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/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "skaters",
3
+ "version": "0.11.0",
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
+ "type": "module",
6
+ "main": "./index.mjs",
7
+ "module": "./index.mjs",
8
+ "exports": {
9
+ ".": "./index.mjs",
10
+ "./package.json": "./package.json",
11
+ "./*.mjs": "./*.mjs",
12
+ "./*": "./*.mjs"
13
+ },
14
+ "files": [
15
+ "*.mjs",
16
+ "README.md",
17
+ "CHANGELOG.md",
18
+ "LICENSE"
19
+ ],
20
+ "sideEffects": false,
21
+ "engines": {
22
+ "node": ">=16"
23
+ },
24
+ "keywords": [
25
+ "time-series",
26
+ "forecasting",
27
+ "online",
28
+ "distributional",
29
+ "probabilistic",
30
+ "density",
31
+ "quantile",
32
+ "crps",
33
+ "log-likelihood",
34
+ "laplace",
35
+ "garch",
36
+ "zero-dependency",
37
+ "browser",
38
+ "microprediction"
39
+ ],
40
+ "homepage": "https://skaters.microprediction.org",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/microprediction/skaters.git",
44
+ "directory": "docs/js/skaters"
45
+ },
46
+ "bugs": {
47
+ "url": "https://github.com/microprediction/skaters/issues"
48
+ },
49
+ "author": "Peter Cotton",
50
+ "license": "MIT"
51
+ }
@@ -0,0 +1,58 @@
1
+ // Online periodicity detection — JS port of skaters/periodicity.py.
2
+
3
+ export const DEFAULT_LAGS = [2, 3, 4, 5, 6, 7, 12, 14, 24, 28, 30, 52, 60, 90, 168, 365];
4
+
5
+ export function periodDetector(lags = null, alpha = 0.01, minObservations = 50) {
6
+ if (lags === null) lags = DEFAULT_LAGS.slice();
7
+ const maxLag = Math.max(...lags);
8
+
9
+ function _detect(y, state) {
10
+ if (state === null || state === undefined) {
11
+ const cross = {};
12
+ for (const L of lags) cross[L] = 0.0;
13
+ state = { buffer: [], n: 0, mean: 0.0, var: 0.0, cross };
14
+ }
15
+ const buf = state.buffer;
16
+ buf.push(y);
17
+ state.n += 1;
18
+
19
+ const diff = y - state.mean;
20
+ state.mean += alpha * diff;
21
+ state.var = (1 - alpha) * (state.var + alpha * diff * diff);
22
+
23
+ const mu = state.mean;
24
+ const varr = state.var;
25
+
26
+ for (const L of lags) {
27
+ if (buf.length > L) {
28
+ const yLagged = buf[buf.length - 1 - L];
29
+ const cross = (y - mu) * (yLagged - mu);
30
+ state.cross[L] = (1 - alpha) * state.cross[L] + alpha * cross;
31
+ }
32
+ }
33
+
34
+ if (buf.length > maxLag + 1) buf.shift();
35
+
36
+ if (state.n < minObservations || varr < 1e-12) return [[], state];
37
+
38
+ const scores = [];
39
+ for (let li = 0; li < lags.length; li++) {
40
+ const L = lags[li];
41
+ if (state.n > L) {
42
+ const acf = varr > 0 ? state.cross[L] / varr : 0.0;
43
+ scores.push([L, acf, li]); // li carried to keep the stable-sort order
44
+ }
45
+ }
46
+ // Sort by |acf| descending, ties keep original (lags) order.
47
+ scores.sort((a, b) => {
48
+ const d = Math.abs(b[1]) - Math.abs(a[1]);
49
+ return d !== 0 ? d : a[2] - b[2];
50
+ });
51
+ return [scores.map(([L, acf]) => [L, acf]), state];
52
+ }
53
+ return _detect;
54
+ }
55
+
56
+ export function topPeriods(scores, threshold = 0.3, maxPeriods = 3) {
57
+ return scores.slice(0, maxPeriods).filter(([, acf]) => Math.abs(acf) >= threshold).map(([lag]) => lag);
58
+ }
package/runstats.mjs ADDED
@@ -0,0 +1,33 @@
1
+ // Lightweight online running statistics — JS port of skaters/runstats.py.
2
+ // Welford's algorithm for mean/variance. Pure JS, no dependencies.
3
+
4
+ export function runningVarInit() {
5
+ return { n: 0, mean: 0.0, m2: 0.0 };
6
+ }
7
+
8
+ export function runningVarUpdate(state, x) {
9
+ const n = state.n + 1;
10
+ const delta = x - state.mean;
11
+ const mean = state.mean + delta / n;
12
+ const delta2 = x - mean;
13
+ const m2 = state.m2 + delta * delta2;
14
+ return { n, mean, m2 };
15
+ }
16
+
17
+ export function runningVarGet(state) {
18
+ // Returns [mean, variance]. Variance is +Infinity until n >= 2.
19
+ if (state.n < 2) return [state.mean, Infinity];
20
+ return [state.mean, state.m2 / (state.n - 1)];
21
+ }
22
+
23
+ export function runningStdGet(state) {
24
+ const [, v] = runningVarGet(state);
25
+ return Number.isFinite(v) ? Math.sqrt(v) : Infinity;
26
+ }
27
+
28
+ export function runningMseGet(state) {
29
+ if (state.n < 1) return Infinity;
30
+ const [mean, v] = runningVarGet(state);
31
+ if (!Number.isFinite(v)) return Infinity;
32
+ return mean * mean + v;
33
+ }
package/search.mjs ADDED
@@ -0,0 +1,236 @@
1
+ // Adaptive search over the transform tree — JS port of skaters/search.py.
2
+
3
+ import { Dist } from "./dist.mjs";
4
+ import { leaf } from "./leaf.mjs";
5
+ import { conjugate } from "./conjugate.mjs";
6
+ import {
7
+ difference, fractionalDifference, standardize, emaTransform, garch,
8
+ seasonalDifference, powerTransform, ar, groupedAr, drift, holtLinear, theta,
9
+ } from "./transform.mjs";
10
+ import { periodDetector, topPeriods } from "./periodicity.mjs";
11
+
12
+ // Grammar: [name, factory, costPerObs]. Order matters for parity.
13
+ export const TRANSFORMS = [
14
+ ["ema_t(0.05)", () => emaTransform(0.05), 1],
15
+ ["ema_t(0.1)", () => emaTransform(0.1), 1],
16
+ ["ema_t(0.3)", () => emaTransform(0.3), 1],
17
+ ["diff", () => difference(), 1],
18
+ ["std(0.05)", () => standardize(0.05), 1],
19
+ ["frac(0.3)", () => fractionalDifference(0.3, 30), 3],
20
+ ["garch", () => garch(), 1],
21
+ ["pow(0.5)", () => powerTransform(0.5), 1],
22
+ ["ar(2)", () => ar(2), 2],
23
+ ["ar(5)", () => ar(5, 0.99, 1.0, 1), 3],
24
+ ["gar(16)", () => groupedAr(16), 2],
25
+ ["theta(0.1)", () => theta(0.1), 1],
26
+ ["theta(0.3)", () => theta(0.3), 1],
27
+ ["drift", () => drift(), 1],
28
+ ["drift(0.01)", () => drift(0.01, 0.002), 1],
29
+ ["holt(0.1,0.05)", () => holtLinear(0.1, 0.05), 1],
30
+ ["holt(0.3,0.1)", () => holtLinear(0.3, 0.1), 1],
31
+ ["seas(7)", () => seasonalDifference(7), 1],
32
+ ["seas(12)", () => seasonalDifference(12), 1],
33
+ ["seas(24)", () => seasonalDifference(24), 1],
34
+ ];
35
+
36
+ function avgW(entry, k) {
37
+ let s = 0.0;
38
+ for (const w of entry.log_w) s += w;
39
+ return s / k;
40
+ }
41
+
42
+ function makeEntry(skaterFn, depth, recipe, k, cost = 0.0) {
43
+ return {
44
+ f: skaterFn,
45
+ s: null,
46
+ depth,
47
+ recipe,
48
+ cost,
49
+ age: 0,
50
+ warmed: false,
51
+ log_w: new Array(k).fill(0.0),
52
+ queues: Array.from({ length: k }, () => []),
53
+ dists: null,
54
+ };
55
+ }
56
+
57
+ function warmup(entry, buffer, k) {
58
+ for (const y of buffer) {
59
+ const [dists, s] = entry.f(y, entry.s);
60
+ entry.s = s;
61
+ entry.dists = dists;
62
+ entry.age += 1;
63
+ }
64
+ if (entry.dists !== null) {
65
+ for (let h = 0; h < k; h++) {
66
+ entry.queues[h].length = 0;
67
+ entry.queues[h].push(entry.dists[h]);
68
+ }
69
+ }
70
+ entry.warmed = true;
71
+ }
72
+
73
+ function initPool(k, costBudget) {
74
+ const pool = [];
75
+ const leafEntry = makeEntry(leaf(k), 0, [], k, 1.0);
76
+ leafEntry.warmed = true;
77
+ pool.push(leafEntry);
78
+ for (const [tName, tFactory, tCost] of TRANSFORMS) {
79
+ const candidateCost = 1.0 + tCost;
80
+ if (candidateCost > costBudget) continue;
81
+ const f = conjugate(leaf(k), tFactory(), k);
82
+ f.skaterName = `${tName}|leaf`;
83
+ const entry = makeEntry(f, 1, [tName], k, candidateCost);
84
+ entry.warmed = true;
85
+ pool.push(entry);
86
+ }
87
+ return pool;
88
+ }
89
+
90
+ function buildFromRecipe(recipe, k, transforms) {
91
+ const lookup = new Map(transforms.map(([name, factory]) => [name, factory]));
92
+ let f = leaf(k);
93
+ for (const tName of recipe) f = conjugate(f, lookup.get(tName)(), k);
94
+ return f;
95
+ }
96
+
97
+ function expand(pool, k, topN, maxDepth, transforms, costBudget) {
98
+ const scored = pool.map((e, i) => ({ avg: avgW(e, k), i, e }));
99
+ scored.sort((a, b) => (b.avg - a.avg) || (b.i - a.i));
100
+
101
+ const existing = new Set(pool.map((e) => e.recipe.join("|")));
102
+ const children = [];
103
+ for (const { e: parent } of scored.slice(0, topN)) {
104
+ if (parent.depth >= maxDepth) continue;
105
+ for (const [tName, , tCost] of transforms) {
106
+ const childCost = parent.cost + tCost;
107
+ if (childCost > costBudget) continue;
108
+ if (parent.recipe.length && parent.recipe[parent.recipe.length - 1] === tName) continue;
109
+ const newRecipe = parent.recipe.concat([tName]);
110
+ const key = newRecipe.join("|");
111
+ if (existing.has(key)) continue;
112
+ existing.add(key);
113
+ const childFn = buildFromRecipe(newRecipe, k, transforms);
114
+ childFn.skaterName = newRecipe.join("|") + "|leaf";
115
+ children.push(makeEntry(childFn, newRecipe.length, newRecipe, k, childCost));
116
+ }
117
+ }
118
+ return children;
119
+ }
120
+
121
+ function prune(pool, threshold, maxPool, k) {
122
+ if (pool.length <= 1) return;
123
+ let best = -Infinity;
124
+ for (const e of pool) best = Math.max(best, avgW(e, k));
125
+
126
+ let i = 0;
127
+ while (i < pool.length) {
128
+ if (avgW(pool[i], k) < best + threshold && pool.length > 1) pool.splice(i, 1);
129
+ else i += 1;
130
+ }
131
+ while (pool.length > maxPool) {
132
+ let worstIdx = 0;
133
+ let worst = Infinity;
134
+ for (let j = 0; j < pool.length; j++) {
135
+ const a = avgW(pool[j], k);
136
+ if (a < worst) {
137
+ worst = a;
138
+ worstIdx = j;
139
+ }
140
+ }
141
+ pool.splice(worstIdx, 1);
142
+ }
143
+ }
144
+
145
+ export function search({
146
+ k = 1,
147
+ learningRate = 0.5,
148
+ complexityPenalty = 0.02,
149
+ maxPool = 30,
150
+ expandInterval = 100,
151
+ expandTopN = 3,
152
+ maxDepth = 3,
153
+ replayBuffer = 500,
154
+ pruneThreshold = -50.0,
155
+ maxComponents = 20,
156
+ costBudget = Infinity,
157
+ } = {}) {
158
+ const pdFunc = periodDetector();
159
+
160
+ function _search(y, state) {
161
+ if (state === null || state === undefined) {
162
+ state = {
163
+ pool: initPool(k, costBudget),
164
+ n_obs: 0,
165
+ buffer: [],
166
+ pd_state: null,
167
+ detected_periods: new Set(),
168
+ transforms: TRANSFORMS.slice(),
169
+ };
170
+ }
171
+
172
+ state.n_obs += 1;
173
+ state.buffer.push(y);
174
+ if (state.buffer.length > replayBuffer) state.buffer.shift();
175
+
176
+ const pool = state.pool;
177
+
178
+ for (const entry of pool) {
179
+ const [dists, s] = entry.f(y, entry.s);
180
+ entry.s = s;
181
+ entry.dists = dists;
182
+ entry.age += 1;
183
+ }
184
+
185
+ for (const entry of pool) {
186
+ for (let h = 0; h < k; h++) {
187
+ const q = entry.queues[h];
188
+ if (q.length) {
189
+ const pastDist = q.shift();
190
+ if (entry.warmed) {
191
+ const lp = Math.max(pastDist.logpdf(y), -20.0);
192
+ entry.log_w[h] += learningRate * lp - complexityPenalty * entry.depth;
193
+ }
194
+ }
195
+ }
196
+ }
197
+
198
+ for (const entry of pool) {
199
+ for (let h = 0; h < k; h++) entry.queues[h].push(entry.dists[h]);
200
+ }
201
+
202
+ const [scores, pdState] = pdFunc(y, state.pd_state);
203
+ state.pd_state = pdState;
204
+
205
+ if (state.n_obs % expandInterval === 0 && state.n_obs > 10) {
206
+ const detected = topPeriods(scores, 0.3, 3);
207
+ for (const period of detected) {
208
+ if (!state.detected_periods.has(period)) {
209
+ state.detected_periods.add(period);
210
+ const tName = `seas(${period})`;
211
+ state.transforms.push([tName, () => seasonalDifference(period), 2]);
212
+ }
213
+ }
214
+ const newChildren = expand(pool, k, expandTopN, maxDepth, state.transforms, costBudget);
215
+ for (const child of newChildren) warmup(child, state.buffer, k);
216
+ for (const child of newChildren) pool.push(child);
217
+ prune(pool, pruneThreshold, maxPool, k);
218
+ }
219
+
220
+ const combined = [];
221
+ for (let h = 0; h < k; h++) {
222
+ const logWs = pool.map((e) => e.log_w[h]);
223
+ const maxLw = Math.max(...logWs);
224
+ let weights;
225
+ if (Number.isFinite(maxLw)) weights = logWs.map((lw) => Math.exp(lw - maxLw));
226
+ else weights = new Array(pool.length).fill(1.0);
227
+ const horizonDists = pool.map((e) => e.dists[h]);
228
+ let dist = Dist.combine(horizonDists, weights);
229
+ if (dist.length > maxComponents) dist = dist.prune(maxComponents);
230
+ combined.push(dist);
231
+ }
232
+ return [combined, state];
233
+ }
234
+ _search.skaterName = `search(k=${k})`;
235
+ return _search;
236
+ }
package/spec.mjs ADDED
@@ -0,0 +1,98 @@
1
+ // Symbolic spec for skater pipelines — JS port of skaters/spec.py.
2
+
3
+ import { leaf } from "./leaf.mjs";
4
+ import { ema } from "./ema.mjs";
5
+ import { precisionWeightedEnsemble } from "./ensemble.mjs";
6
+ import { conjugate } from "./conjugate.mjs";
7
+ import { difference, fractionalDifference, standardize, emaTransform } from "./transform.mjs";
8
+
9
+ export function build(spec) {
10
+ const op = spec.op;
11
+ let f;
12
+ if (op === "leaf") {
13
+ f = leaf(spec.k);
14
+ } else if (op === "ema") {
15
+ f = ema(spec.alpha, spec.k);
16
+ } else if (op === "ensemble") {
17
+ const subs = spec.skaters.map(build);
18
+ f = precisionWeightedEnsemble(subs, spec.k, spec.floor === undefined ? 1e-6 : spec.floor);
19
+ } else if (op === "conjugate") {
20
+ const inner = build(spec.skater);
21
+ const t = buildTransform(spec.transform);
22
+ f = conjugate(inner, t, inferK(spec.skater));
23
+ } else {
24
+ throw new Error(`Unknown op: ${op}`);
25
+ }
26
+ f.skaterName = name(spec);
27
+ return f;
28
+ }
29
+
30
+ function buildTransform(spec) {
31
+ const op = spec.op;
32
+ if (op === "diff") return difference();
33
+ if (op === "frac") return fractionalDifference(spec.d, spec.window === undefined ? 50 : spec.window);
34
+ if (op === "std") return standardize(spec.alpha === undefined ? 0.05 : spec.alpha);
35
+ if (op === "ema_t") return emaTransform(spec.alpha);
36
+ throw new Error(`Unknown transform op: ${op}`);
37
+ }
38
+
39
+ function inferK(spec) {
40
+ if ("k" in spec) return spec.k;
41
+ if ("skater" in spec) return inferK(spec.skater);
42
+ if ("skaters" in spec) return inferK(spec.skaters[0]);
43
+ throw new Error("Cannot infer k from spec");
44
+ }
45
+
46
+ // --- canonical name ---
47
+
48
+ function fmt(x) {
49
+ // Mimic Python's "%.6g" with integer collapse.
50
+ if (x === Math.trunc(x)) return String(Math.trunc(x));
51
+ let s = x.toPrecision(6);
52
+ if (s.indexOf(".") >= 0 && s.indexOf("e") < 0 && s.indexOf("E") < 0) {
53
+ s = s.replace(/0+$/, "").replace(/\.$/, "");
54
+ }
55
+ return s;
56
+ }
57
+
58
+ export function name(spec) {
59
+ const op = spec.op;
60
+ if (op === "leaf") return "leaf";
61
+ if (op === "ema") return `ema(${fmt(spec.alpha)})`;
62
+ if (op === "ensemble") return `ensemble(${spec.skaters.map(name).join(",")})`;
63
+ if (op === "conjugate") return `${transformName(spec.transform)}|${name(spec.skater)}`;
64
+ throw new Error(`Unknown op: ${op}`);
65
+ }
66
+
67
+ function transformName(spec) {
68
+ const op = spec.op;
69
+ if (op === "diff") return "diff";
70
+ if (op === "frac") {
71
+ const w = spec.window === undefined ? 50 : spec.window;
72
+ return w === 50 ? `frac(${fmt(spec.d)})` : `frac(${fmt(spec.d)},w=${w})`;
73
+ }
74
+ if (op === "std") return `std(${fmt(spec.alpha === undefined ? 0.05 : spec.alpha)})`;
75
+ if (op === "ema_t") return `ema_t(${fmt(spec.alpha)})`;
76
+ throw new Error(`Unknown transform op: ${op}`);
77
+ }
78
+
79
+ // --- serialization ---
80
+
81
+ export function toJson(spec) {
82
+ return JSON.stringify(spec);
83
+ }
84
+
85
+ export function fromJson(s) {
86
+ return JSON.parse(s);
87
+ }
88
+
89
+ // --- spec constructors ---
90
+
91
+ export const leafSpec = (k = 1) => ({ op: "leaf", k });
92
+ export const emaSpec = (alpha = 0.05, k = 1) => ({ op: "ema", alpha, k });
93
+ export const ensembleSpec = (skaterSpecs, k = 1) => ({ op: "ensemble", k, skaters: skaterSpecs });
94
+ export const conjugateSpec = (skaterSpec, transformSpec) => ({ op: "conjugate", skater: skaterSpec, transform: transformSpec });
95
+ export const diffSpec = () => ({ op: "diff" });
96
+ export const fracSpec = (d = 0.4, window = 50) => ({ op: "frac", d, window });
97
+ export const stdSpec = (alpha = 0.05) => ({ op: "std", alpha });
98
+ export const emaTSpec = (alpha = 0.05) => ({ op: "ema_t", alpha });
package/sticky.mjs ADDED
@@ -0,0 +1,64 @@
1
+ // Sticky / lattice projection — JS port of skaters/sticky.py.
2
+ // Recency-weighted frequency table of exact values; atoms fire at the values
3
+ // revisited above a noise floor (top-k by frequency), mean-preserving. The
4
+ // single-spike behaviour is the max_atoms=1 case. Uses a Map so numeric-key
5
+ // insertion order (hence the sort tie-break) matches Python's dict.
6
+
7
+ import { fsum, Dist } from "./dist.mjs";
8
+
9
+ export function sticky(base, k = 1, propensityAlpha = 0.05, spikeFrac = 0.005,
10
+ threshMult = 1.8, maxAtoms = 6, pruneEps = 1e-6) {
11
+ function _skater(y, state) {
12
+ if (state === null || state === undefined) {
13
+ state = { base: null, counts: new Map() };
14
+ }
15
+ const [dists, baseState] = base(y, state.base);
16
+ state.base = baseState;
17
+
18
+ // recency-weighted frequency table of exact values.
19
+ const c = state.counts;
20
+ const drop = [];
21
+ for (const key of c.keys()) {
22
+ const v = c.get(key) * (1.0 - propensityAlpha);
23
+ if (v < pruneEps) drop.push(key);
24
+ else c.set(key, v);
25
+ }
26
+ for (const key of drop) c.delete(key);
27
+ c.set(y, (c.get(y) || 0.0) + propensityAlpha);
28
+
29
+ // lattice atoms = revisited values above the floor, top-k by weight.
30
+ const thr = threshMult * propensityAlpha;
31
+ let atoms = [];
32
+ for (const [v, w] of c) if (w > thr) atoms.push([v, w]);
33
+ // stable sort by descending weight; ties keep insertion order (matches Python)
34
+ atoms = atoms.map((a, i) => [a, i])
35
+ .sort((A, B) => (B[0][1] - A[0][1]) || (A[1] - B[1]))
36
+ .map((p) => p[0])
37
+ .slice(0, maxAtoms);
38
+
39
+ const out = [];
40
+ for (const d of dists) {
41
+ if (atoms.length === 0) {
42
+ out.push(d);
43
+ continue;
44
+ }
45
+ const sw = fsum(atoms.map((a) => a[1]));
46
+ const P = Math.min(sw, 0.999);
47
+ const pc = 1.0 - P;
48
+ const atomMean = fsum(atoms.map((a) => a[1] * a[0])) / sw;
49
+ const spikeStd = Math.max(spikeFrac * d.std, 1e-9);
50
+ if (pc <= 1e-9) {
51
+ out.push(new Dist(atoms.map(([v, w]) => [w / sw, v, spikeStd])));
52
+ continue;
53
+ }
54
+ const mu = d.mean;
55
+ const delta = (P * (mu - atomMean)) / pc;
56
+ const comps = atoms.map(([v, w]) => [P * (w / sw), v, spikeStd]);
57
+ for (const [w, m, s] of d.components) comps.push([pc * w, m + delta, s]);
58
+ out.push(new Dist(comps));
59
+ }
60
+ return [out, state];
61
+ }
62
+ _skater.skaterName = `sticky(${base.skaterName || "?"})`;
63
+ return _skater;
64
+ }
package/terminal.mjs ADDED
@@ -0,0 +1,82 @@
1
+ // Terminal-leaf ensemble — JS port of skaters/terminal.py.
2
+ // Mix the sub-models for the MEAN; model the combined residual with one
3
+ // terminal leaf (default the scale-mixture leaf), so its shape reaches the
4
+ // output undiluted.
5
+
6
+ import { fsum, Dist } from "./dist.mjs";
7
+ import { crpsLeaf } from "./leaf.mjs";
8
+
9
+ export function terminalLeafEnsemble(skaters, {
10
+ leafFn = crpsLeaf,
11
+ k = 1,
12
+ learningRate = 0.5,
13
+ complexityPenalty = 0.0,
14
+ depths = null,
15
+ priorLogWeights = null,
16
+ maxComponents = 20,
17
+ forget = 1.0,
18
+ } = {}) {
19
+ const n = skaters.length;
20
+ const d = depths === null ? new Array(n).fill(0) : depths;
21
+ const prior = priorLogWeights === null ? new Array(n).fill(0.0) : priorLogWeights;
22
+
23
+ function _skater(y, state) {
24
+ if (state === null || state === undefined) {
25
+ state = {
26
+ sub: new Array(n).fill(null),
27
+ qdist: Array.from({ length: n }, () => []),
28
+ log_w: prior.slice(),
29
+ tleaf: Array.from({ length: k }, () => leafFn(1)),
30
+ leafState: new Array(k).fill(null),
31
+ leafPred: new Array(k).fill(null),
32
+ meanQ: Array.from({ length: k }, () => []),
33
+ };
34
+ }
35
+
36
+ const allDists = [];
37
+ for (let i = 0; i < n; i++) {
38
+ const [di, s] = skaters[i](y, state.sub[i]);
39
+ state.sub[i] = s;
40
+ allDists.push(di);
41
+ }
42
+
43
+ for (let i = 0; i < n; i++) {
44
+ const q = state.qdist[i];
45
+ if (q.length) {
46
+ const lp = Math.max(q.shift().logpdf(y), -20.0);
47
+ state.log_w[i] = forget * state.log_w[i] + learningRate * lp - complexityPenalty * d[i];
48
+ }
49
+ q.push(allDists[i][0]);
50
+ }
51
+
52
+ const maxLw = Math.max(...state.log_w);
53
+ const w = state.log_w.map((lw) => Math.exp(lw - maxLw));
54
+ const tot = fsum(w);
55
+
56
+ const combined = [];
57
+ for (let h = 0; h < k; h++) {
58
+ const muH = fsum(w.map((wi, i) => wi * allDists[i][h].mean)) / tot;
59
+
60
+ const mq = state.meanQ[h];
61
+ if (mq.length >= h + 1) {
62
+ const r = y - mq.shift();
63
+ const [ld, ls] = state.tleaf[h](r, state.leafState[h]);
64
+ state.leafState[h] = ls;
65
+ state.leafPred[h] = ld[0];
66
+ }
67
+
68
+ let pred;
69
+ if (state.leafPred[h] !== null) {
70
+ pred = state.leafPred[h].shift(muH);
71
+ } else {
72
+ pred = Dist.combine(allDists.map((di) => di[h]), w);
73
+ if (pred.components.length > maxComponents) pred = pred.prune(maxComponents);
74
+ }
75
+ combined.push(pred);
76
+ mq.push(muH);
77
+ }
78
+ return [combined, state];
79
+ }
80
+ _skater.skaterName = `terminal_leaf_ensemble(n=${n}, k=${k})`;
81
+ return _skater;
82
+ }