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/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/api.mjs +168 -0
- package/bayesian.mjs +84 -0
- package/conjugate.mjs +23 -0
- package/cov.mjs +95 -0
- package/dist.mjs +276 -0
- package/ema.mjs +12 -0
- package/ensemble.mjs +56 -0
- package/index.mjs +31 -0
- package/leaf.mjs +211 -0
- package/multiscale.mjs +70 -0
- package/package.json +51 -0
- package/periodicity.mjs +58 -0
- package/runstats.mjs +33 -0
- package/search.mjs +236 -0
- package/spec.mjs +98 -0
- package/sticky.mjs +64 -0
- package/terminal.mjs +82 -0
- package/transform.mjs +598 -0
package/transform.mjs
ADDED
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
// Invertible online transforms — JS port of skaters/transform.py.
|
|
2
|
+
//
|
|
3
|
+
// A transform is { forward, inverseK }:
|
|
4
|
+
// [yPrime, state] = forward(y, state) // scalar in, scalar out
|
|
5
|
+
// dists = inverseK(dists, state) // map k Dists back
|
|
6
|
+
//
|
|
7
|
+
// forward runs on every observation; inverseK maps k Dist predictions in
|
|
8
|
+
// the transformed space back to the original space.
|
|
9
|
+
|
|
10
|
+
import { fsum, Dist } from "./dist.mjs";
|
|
11
|
+
|
|
12
|
+
// --- small helpers ---
|
|
13
|
+
|
|
14
|
+
function copysign(a, b) {
|
|
15
|
+
const m = Math.abs(a);
|
|
16
|
+
return b < 0 || Object.is(b, -0) ? -m : m;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function matVec(M, v, n) {
|
|
20
|
+
const out = new Array(n).fill(0.0);
|
|
21
|
+
for (let i = 0; i < n; i++) {
|
|
22
|
+
let s = 0.0;
|
|
23
|
+
for (let j = 0; j < n; j++) s += M[i * n + j] * v[j];
|
|
24
|
+
out[i] = s;
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function dot(a, b, n) {
|
|
30
|
+
const terms = new Array(n);
|
|
31
|
+
for (let i = 0; i < n; i++) terms[i] = a[i] * b[i];
|
|
32
|
+
return fsum(terms);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function eye(n, scale = 1.0) {
|
|
36
|
+
const m = new Array(n * n).fill(0.0);
|
|
37
|
+
for (let i = 0; i < n; i++) m[i * n + i] = scale;
|
|
38
|
+
return m;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Differencing: y'_t = y_t - y_{t-1}
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
export function difference() {
|
|
46
|
+
function forward(y, state) {
|
|
47
|
+
if (state === null || state === undefined) return [0.0, { last: y }];
|
|
48
|
+
return [y - state.last, { last: y }];
|
|
49
|
+
}
|
|
50
|
+
function inverseK(dists, state) {
|
|
51
|
+
const anchor = state.last;
|
|
52
|
+
const result = [];
|
|
53
|
+
let cumsumMean = 0.0;
|
|
54
|
+
let cumsumVar = 0.0;
|
|
55
|
+
for (const d of dists) {
|
|
56
|
+
cumsumMean += d.mean;
|
|
57
|
+
cumsumVar += d.var;
|
|
58
|
+
const std = cumsumVar > 0 ? Math.sqrt(cumsumVar) : Math.max(d.std, 1e-12);
|
|
59
|
+
result.push(Dist.gaussian(anchor + cumsumMean, std));
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
return { forward, inverseK };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Fractional differencing: (1 - B)^d
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
function fracDiffWeights(d, window) {
|
|
71
|
+
const w = [1.0];
|
|
72
|
+
for (let i = 1; i < window; i++) w.push((-w[w.length - 1] * (d - i + 1)) / i);
|
|
73
|
+
return w;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function fractionalDifference(d = 0.4, window = 50) {
|
|
77
|
+
const wFwd = fracDiffWeights(d, window);
|
|
78
|
+
|
|
79
|
+
function forward(y, state) {
|
|
80
|
+
if (state === null || state === undefined) state = { buffer: [] };
|
|
81
|
+
const buf = state.buffer;
|
|
82
|
+
buf.push(y);
|
|
83
|
+
if (buf.length > window) buf.shift();
|
|
84
|
+
const n = buf.length;
|
|
85
|
+
const yPrime = fsum(Array.from({ length: n }, (_, j) => wFwd[j] * buf[n - 1 - j]));
|
|
86
|
+
return [yPrime, state];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function inverseK(dists, state) {
|
|
90
|
+
const buf = state.buffer.slice();
|
|
91
|
+
const result = [];
|
|
92
|
+
for (const dIn of dists) {
|
|
93
|
+
buf.push(0.0);
|
|
94
|
+
const n = buf.length;
|
|
95
|
+
let shift = 0.0;
|
|
96
|
+
const upper = Math.min(n, window);
|
|
97
|
+
for (let j = 1; j < upper; j++) shift -= wFwd[j] * buf[n - 1 - j];
|
|
98
|
+
const recoveredMean = dIn.mean + shift;
|
|
99
|
+
buf[buf.length - 1] = recoveredMean;
|
|
100
|
+
result.push(Dist.gaussian(recoveredMean, dIn.std));
|
|
101
|
+
if (buf.length > window) buf.shift();
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
return { forward, inverseK };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Running standardization
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
export function standardize(alpha = 0.05, eps = 1e-8) {
|
|
113
|
+
function forward(y, state) {
|
|
114
|
+
if (state === null || state === undefined) return [0.0, { mu: y, var: 0.0 }];
|
|
115
|
+
const mu = state.mu;
|
|
116
|
+
let varr = state.var;
|
|
117
|
+
const diff = y - mu;
|
|
118
|
+
// Center against the PRIOR mean (post-update centering shrinks the residual
|
|
119
|
+
// by (1-alpha) -> overconfident intervals); standard EWMA variance recursion.
|
|
120
|
+
const muNew = mu + alpha * diff;
|
|
121
|
+
varr = (1 - alpha) * varr + alpha * diff * diff;
|
|
122
|
+
const sigma = varr > eps * eps ? Math.sqrt(varr) : eps;
|
|
123
|
+
const yPrime = diff / sigma;
|
|
124
|
+
return [yPrime, { mu: muNew, var: varr }];
|
|
125
|
+
}
|
|
126
|
+
function inverseK(dists, state) {
|
|
127
|
+
const varr = state.var;
|
|
128
|
+
const sigma = varr > 1e-16 ? Math.sqrt(varr) : 1e-8;
|
|
129
|
+
return dists.map((d) => d.affine(sigma, state.mu));
|
|
130
|
+
}
|
|
131
|
+
return { forward, inverseK };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// EMA as a transform: subtract the running level
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
export function emaTransform(alpha = 0.05) {
|
|
139
|
+
function forward(y, state) {
|
|
140
|
+
if (state === null || state === undefined) return [0.0, { level: y }];
|
|
141
|
+
// Residual is the one-step forecast error (against the PRIOR level); using the
|
|
142
|
+
// post-update level would shrink it by (1-alpha) -> overconfident intervals.
|
|
143
|
+
const residual = y - state.level;
|
|
144
|
+
const level = state.level + alpha * residual;
|
|
145
|
+
return [residual, { level }];
|
|
146
|
+
}
|
|
147
|
+
function inverseK(dists, state) {
|
|
148
|
+
return dists.map((d) => d.shift(state.level));
|
|
149
|
+
}
|
|
150
|
+
return { forward, inverseK };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Ornstein-Uhlenbeck mean-reversion (JS port of transform.ou_transform).
|
|
154
|
+
// Reverts toward a running mean at speed kappa; phi = 1 - kappa. Multi-step
|
|
155
|
+
// inverse uses the exact OU moments (phi^h mean decay, saturating variance).
|
|
156
|
+
export function ouTransform(kappa = 0.1, alpha = 0.02) {
|
|
157
|
+
const phi = 1.0 - kappa;
|
|
158
|
+
function forward(y, state) {
|
|
159
|
+
if (state === null || state === undefined || !Number.isFinite(y)) {
|
|
160
|
+
const y0 = Number.isFinite(y) ? y : 0.0;
|
|
161
|
+
return [0.0, { m: y0, fc: y0, y: y0 }];
|
|
162
|
+
}
|
|
163
|
+
let resid = y - state.fc;
|
|
164
|
+
if (!Number.isFinite(resid)) resid = 0.0;
|
|
165
|
+
const m = state.m + alpha * (y - state.m);
|
|
166
|
+
const fc = m + phi * (y - m);
|
|
167
|
+
return [resid, { m, fc, y }];
|
|
168
|
+
}
|
|
169
|
+
function inverseK(dists, state) {
|
|
170
|
+
const m = state.m;
|
|
171
|
+
const ylast = state.y;
|
|
172
|
+
return dists.map((d, i) => {
|
|
173
|
+
const h = i + 1;
|
|
174
|
+
const center = m + Math.pow(phi, h) * (ylast - m);
|
|
175
|
+
const g = phi < 1.0 - 1e-9
|
|
176
|
+
? Math.sqrt((1.0 - Math.pow(phi, 2 * h)) / (1.0 - phi * phi))
|
|
177
|
+
: Math.sqrt(h);
|
|
178
|
+
return d.scale(g).shift(center);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return { forward, inverseK };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Theta method (Assimakopoulos & Nikolopoulos, 2000)
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
export function theta(alpha = 0.1) {
|
|
189
|
+
function forward(y, state) {
|
|
190
|
+
if (state === null || state === undefined) {
|
|
191
|
+
return [0.0, { ses: y, t: 1, sum_t: 1.0, sum_t2: 1.0, sum_y: y, sum_ty: y }];
|
|
192
|
+
}
|
|
193
|
+
const s = state;
|
|
194
|
+
s.t += 1;
|
|
195
|
+
const t = s.t;
|
|
196
|
+
// Forecast from the PRIOR state, then update (forecasting after folding y into
|
|
197
|
+
// ses/slope would leak y into its own residual -> overconfident interval).
|
|
198
|
+
const forecast = s.ses + (s.slope === undefined ? 0.0 : s.slope) / 2;
|
|
199
|
+
const residual = y - forecast;
|
|
200
|
+
s.ses = alpha * y + (1 - alpha) * s.ses;
|
|
201
|
+
s.sum_t += t;
|
|
202
|
+
s.sum_t2 += t * t;
|
|
203
|
+
s.sum_y += y;
|
|
204
|
+
s.sum_ty += t * y;
|
|
205
|
+
const n = t;
|
|
206
|
+
const denom = n * s.sum_t2 - s.sum_t * s.sum_t;
|
|
207
|
+
const slope = Math.abs(denom) > 1e-12 ? (n * s.sum_ty - s.sum_t * s.sum_y) / denom : 0.0;
|
|
208
|
+
s.slope = slope;
|
|
209
|
+
return [residual, s];
|
|
210
|
+
}
|
|
211
|
+
function inverseK(dists, state) {
|
|
212
|
+
const ses = state.ses;
|
|
213
|
+
const slope = state.slope === undefined ? 0.0 : state.slope;
|
|
214
|
+
const result = [];
|
|
215
|
+
let cumsumVar = 0.0;
|
|
216
|
+
for (let h = 0; h < dists.length; h++) {
|
|
217
|
+
const d = dists[h];
|
|
218
|
+
cumsumVar += d.var;
|
|
219
|
+
const forecast = ses + ((h + 1) * slope) / 2 + d.mean;
|
|
220
|
+
const std = cumsumVar > 0 ? Math.sqrt(cumsumVar) : Math.max(d.std, 1e-12);
|
|
221
|
+
result.push(Dist.gaussian(forecast, std));
|
|
222
|
+
}
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
return { forward, inverseK };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// Random walk with drift
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
export function drift(alpha = 0.002, shrinkage = 0.001) {
|
|
233
|
+
const decay = 1 - alpha - shrinkage;
|
|
234
|
+
function forward(y, state) {
|
|
235
|
+
if (state === null || state === undefined) return [0.0, { last: y, mu: 0.0 }];
|
|
236
|
+
const dy = y - state.last;
|
|
237
|
+
// Increment minus the PRIOR drift (one-step forecast error); the post-update
|
|
238
|
+
// drift would leak dy into its own residual -> overconfident interval.
|
|
239
|
+
const residual = dy - state.mu;
|
|
240
|
+
const mu = decay * state.mu + alpha * dy;
|
|
241
|
+
return [residual, { last: y, mu }];
|
|
242
|
+
}
|
|
243
|
+
function inverseK(dists, state) {
|
|
244
|
+
const anchor = state.last;
|
|
245
|
+
const mu = state.mu;
|
|
246
|
+
const result = [];
|
|
247
|
+
let cumsumMean = 0.0;
|
|
248
|
+
let cumsumVar = 0.0;
|
|
249
|
+
for (let h = 0; h < dists.length; h++) {
|
|
250
|
+
const d = dists[h];
|
|
251
|
+
cumsumMean += d.mean;
|
|
252
|
+
cumsumVar += d.var;
|
|
253
|
+
const totalMean = anchor + (h + 1) * mu + cumsumMean;
|
|
254
|
+
const totalStd = cumsumVar > 0 ? Math.sqrt(cumsumVar) : Math.max(d.std, 1e-12);
|
|
255
|
+
result.push(Dist.gaussian(totalMean, totalStd));
|
|
256
|
+
}
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
259
|
+
return { forward, inverseK };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
// Holt linear (level + trend)
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
export function holtLinear(alpha = 0.1, beta = 0.05) {
|
|
267
|
+
function forward(y, state) {
|
|
268
|
+
if (state === null || state === undefined) return [0.0, { level: y, trend: 0.0 }];
|
|
269
|
+
const lPrev = state.level;
|
|
270
|
+
const bPrev = state.trend;
|
|
271
|
+
const lNew = alpha * y + (1 - alpha) * (lPrev + bPrev);
|
|
272
|
+
const bNew = beta * (lNew - lPrev) + (1 - beta) * bPrev;
|
|
273
|
+
return [y - (lPrev + bPrev), { level: lNew, trend: bNew }];
|
|
274
|
+
}
|
|
275
|
+
function inverseK(dists, state) {
|
|
276
|
+
const level = state.level;
|
|
277
|
+
const trend = state.trend;
|
|
278
|
+
const result = [];
|
|
279
|
+
let cumsumVar = 0.0;
|
|
280
|
+
for (let h = 0; h < dists.length; h++) {
|
|
281
|
+
const d = dists[h];
|
|
282
|
+
cumsumVar += d.var;
|
|
283
|
+
const forecast = level + (h + 1) * trend + d.mean;
|
|
284
|
+
const std = cumsumVar > 0 ? Math.sqrt(cumsumVar) : Math.max(d.std, 1e-12);
|
|
285
|
+
result.push(Dist.gaussian(forecast, std));
|
|
286
|
+
}
|
|
287
|
+
return result;
|
|
288
|
+
}
|
|
289
|
+
return { forward, inverseK };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
// GARCH(1,1) volatility scaling
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
export function garch(omega = 0.01, alpha = 0.1, beta = 0.85, eps = 1e-8) {
|
|
297
|
+
function forward(y, state) {
|
|
298
|
+
if (state === null || state === undefined) {
|
|
299
|
+
const persist = alpha + beta;
|
|
300
|
+
const var0 = persist < 1 ? omega / (1 - persist) : omega / eps;
|
|
301
|
+
return [y / Math.max(Math.sqrt(var0), eps), { var: var0, last_y: y }];
|
|
302
|
+
}
|
|
303
|
+
const varr = omega + alpha * state.last_y * state.last_y + beta * state.var;
|
|
304
|
+
const sigma = varr > eps * eps ? Math.sqrt(varr) : eps;
|
|
305
|
+
return [y / sigma, { var: varr, last_y: y }];
|
|
306
|
+
}
|
|
307
|
+
function inverseK(dists, state) {
|
|
308
|
+
const sigma = state.var > 1e-16 ? Math.sqrt(state.var) : 1e-8;
|
|
309
|
+
return dists.map((d) => d.scale(sigma));
|
|
310
|
+
}
|
|
311
|
+
return { forward, inverseK };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
// Seasonal differencing: y'_t = y_t - y_{t-s}
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
export function seasonalDifference(period = 12) {
|
|
319
|
+
function forward(y, state) {
|
|
320
|
+
if (state === null || state === undefined) return [0.0, { buffer: [y] }];
|
|
321
|
+
const buf = state.buffer;
|
|
322
|
+
let yPrime;
|
|
323
|
+
if (buf.length >= period) yPrime = y - buf[buf.length - period];
|
|
324
|
+
else yPrime = 0.0;
|
|
325
|
+
buf.push(y);
|
|
326
|
+
if (buf.length > 2 * period) buf.shift();
|
|
327
|
+
return [yPrime, state];
|
|
328
|
+
}
|
|
329
|
+
function inverseK(dists, state) {
|
|
330
|
+
const buf = state.buffer.slice();
|
|
331
|
+
const recoveredMeans = [];
|
|
332
|
+
const result = [];
|
|
333
|
+
for (let h = 0; h < dists.length; h++) {
|
|
334
|
+
const lagIdx = h - period;
|
|
335
|
+
let anchor;
|
|
336
|
+
if (lagIdx < 0) {
|
|
337
|
+
const bufIdx = buf.length - period + h;
|
|
338
|
+
anchor = bufIdx >= 0 && bufIdx < buf.length ? buf[bufIdx] : 0.0;
|
|
339
|
+
} else {
|
|
340
|
+
anchor = recoveredMeans[lagIdx];
|
|
341
|
+
}
|
|
342
|
+
recoveredMeans.push(dists[h].mean + anchor);
|
|
343
|
+
result.push(dists[h].shift(anchor));
|
|
344
|
+
}
|
|
345
|
+
return result;
|
|
346
|
+
}
|
|
347
|
+
return { forward, inverseK };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
// Signed power transform
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
|
|
354
|
+
export function powerTransform(p = 0.5) {
|
|
355
|
+
const invP = 1.0 / p;
|
|
356
|
+
const fwd = (y) => copysign(Math.pow(Math.abs(y), p), y);
|
|
357
|
+
const inv = (yp) => copysign(Math.pow(Math.abs(yp), invP), yp);
|
|
358
|
+
|
|
359
|
+
function forward(y, state) {
|
|
360
|
+
return [fwd(y), state || {}];
|
|
361
|
+
}
|
|
362
|
+
function inverseK(dists, state) {
|
|
363
|
+
const result = [];
|
|
364
|
+
for (const d of dists) {
|
|
365
|
+
const components = [];
|
|
366
|
+
for (const [w, mu, sigma] of d.components) {
|
|
367
|
+
const origMean = inv(mu);
|
|
368
|
+
const absMu = Math.abs(mu);
|
|
369
|
+
const deriv = absMu > 1e-12 ? invP * Math.pow(absMu, invP - 1) : invP;
|
|
370
|
+
const origStd = Math.max(sigma * deriv, 1e-12);
|
|
371
|
+
components.push([w, origMean, origStd]);
|
|
372
|
+
}
|
|
373
|
+
result.push(new Dist(components));
|
|
374
|
+
}
|
|
375
|
+
return result;
|
|
376
|
+
}
|
|
377
|
+
return { forward, inverseK };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Yeo-Johnson coordinate transform — signed Box-Cox family. JS port of
|
|
381
|
+
// skaters/transform.py:yeo_johnson. Learns the coordinate the series is simple
|
|
382
|
+
// in (lambda=0 -> log1p / non-negative, 1 -> identity, 0.5 -> compression).
|
|
383
|
+
export function yeoJohnson(lmbda = 0.0) {
|
|
384
|
+
const L = lmbda;
|
|
385
|
+
const fwd = (y) => {
|
|
386
|
+
if (y >= 0.0) return L === 0.0 ? Math.log1p(y) : (Math.pow(y + 1.0, L) - 1.0) / L;
|
|
387
|
+
if (L === 2.0) return -Math.log1p(-y);
|
|
388
|
+
return -((Math.pow(-y + 1.0, 2.0 - L) - 1.0) / (2.0 - L));
|
|
389
|
+
};
|
|
390
|
+
// clamp exp args so squaring the result in var() stays finite (exp(350)~1e152).
|
|
391
|
+
const inv = (yp) => {
|
|
392
|
+
if (yp >= 0.0) {
|
|
393
|
+
if (L === 0.0) return Math.expm1(Math.min(yp, 350.0));
|
|
394
|
+
return Math.pow(Math.max(L * yp + 1.0, 1e-12), 1.0 / L) - 1.0;
|
|
395
|
+
}
|
|
396
|
+
if (L === 2.0) return 1.0 - Math.exp(Math.min(-yp, 350.0));
|
|
397
|
+
return 1.0 - Math.pow(Math.max(-(2.0 - L) * yp + 1.0, 1e-12), 1.0 / (2.0 - L));
|
|
398
|
+
};
|
|
399
|
+
const dinv = (yp) => {
|
|
400
|
+
if (yp >= 0.0) {
|
|
401
|
+
if (L === 0.0) return Math.exp(Math.min(yp, 350.0));
|
|
402
|
+
return Math.pow(Math.max(L * yp + 1.0, 1e-12), 1.0 / L - 1.0);
|
|
403
|
+
}
|
|
404
|
+
if (L === 2.0) return Math.exp(Math.min(-yp, 350.0));
|
|
405
|
+
return Math.pow(Math.max(-(2.0 - L) * yp + 1.0, 1e-12), 1.0 / (2.0 - L) - 1.0);
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
function forward(y, state) {
|
|
409
|
+
return [fwd(y), state || {}];
|
|
410
|
+
}
|
|
411
|
+
function inverseK(dists, state) {
|
|
412
|
+
const result = [];
|
|
413
|
+
for (const d of dists) {
|
|
414
|
+
const components = [];
|
|
415
|
+
for (const [w, mu, sigma] of d.components) {
|
|
416
|
+
components.push([w, inv(mu), Math.max(sigma * dinv(mu), 1e-12)]);
|
|
417
|
+
}
|
|
418
|
+
result.push(new Dist(components));
|
|
419
|
+
}
|
|
420
|
+
return result;
|
|
421
|
+
}
|
|
422
|
+
return { forward, inverseK };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ---------------------------------------------------------------------------
|
|
426
|
+
// AR(p) with online recursive least squares
|
|
427
|
+
// ---------------------------------------------------------------------------
|
|
428
|
+
|
|
429
|
+
export function ar(order = 2, lam = 0.99, ridge = 1.0, decay = 0.0) {
|
|
430
|
+
const p = order;
|
|
431
|
+
|
|
432
|
+
function initP() {
|
|
433
|
+
const P = new Array(p * p).fill(0.0);
|
|
434
|
+
for (let j = 0; j < p; j++) {
|
|
435
|
+
P[j * p + j] = decay > 0 ? ridge / Math.pow(j + 1, decay) : ridge;
|
|
436
|
+
}
|
|
437
|
+
return P;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function forward(y, state) {
|
|
441
|
+
if (state === null || state === undefined) {
|
|
442
|
+
state = { buffer: [], phi: new Array(p).fill(0.0), P: initP(), n: 0 };
|
|
443
|
+
}
|
|
444
|
+
const buf = state.buffer;
|
|
445
|
+
const phi = state.phi;
|
|
446
|
+
state.n += 1;
|
|
447
|
+
let residual;
|
|
448
|
+
if (buf.length >= p) {
|
|
449
|
+
const x = new Array(p);
|
|
450
|
+
for (let i = 0; i < p; i++) x[i] = buf[buf.length - 1 - i];
|
|
451
|
+
const prediction = fsum(Array.from({ length: p }, (_, i) => phi[i] * x[i]));
|
|
452
|
+
residual = y - prediction;
|
|
453
|
+
const P = state.P;
|
|
454
|
+
const Px = matVec(P, x, p);
|
|
455
|
+
const denom = lam + dot(x, Px, p);
|
|
456
|
+
if (Math.abs(denom) > 1e-15) {
|
|
457
|
+
const K = Px.map((px) => px / denom);
|
|
458
|
+
for (let i = 0; i < p; i++) phi[i] += K[i] * residual;
|
|
459
|
+
for (let i = 0; i < p; i++) {
|
|
460
|
+
for (let j = 0; j < p; j++) {
|
|
461
|
+
P[i * p + j] = (P[i * p + j] - K[i] * Px[j]) / lam;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
} else {
|
|
466
|
+
residual = y;
|
|
467
|
+
}
|
|
468
|
+
buf.push(y);
|
|
469
|
+
if (buf.length > 2 * p + 10) buf.shift();
|
|
470
|
+
return [residual, state];
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function inverseK(dists, state) {
|
|
474
|
+
const buf = state.buffer.slice();
|
|
475
|
+
const phi = state.phi;
|
|
476
|
+
const recoveredMeans = [];
|
|
477
|
+
const recoveredVars = [];
|
|
478
|
+
const result = [];
|
|
479
|
+
for (let h = 0; h < dists.length; h++) {
|
|
480
|
+
let arMean = 0.0;
|
|
481
|
+
let arVar = 0.0;
|
|
482
|
+
for (let j = 0; j < p; j++) {
|
|
483
|
+
const lagH = h - j - 1;
|
|
484
|
+
if (lagH < 0) {
|
|
485
|
+
const bufIdx = buf.length + lagH;
|
|
486
|
+
if (bufIdx >= 0 && bufIdx < buf.length) arMean += phi[j] * buf[bufIdx];
|
|
487
|
+
} else if (lagH < recoveredMeans.length) {
|
|
488
|
+
arMean += phi[j] * recoveredMeans[lagH];
|
|
489
|
+
arVar += phi[j] * phi[j] * recoveredVars[lagH];
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const totalMean = dists[h].mean + arMean;
|
|
493
|
+
const totalVar = dists[h].var + arVar;
|
|
494
|
+
const totalStd = totalVar > 0 ? Math.sqrt(totalVar) : Math.max(dists[h].std, 1e-12);
|
|
495
|
+
recoveredMeans.push(totalMean);
|
|
496
|
+
recoveredVars.push(totalVar);
|
|
497
|
+
result.push(Dist.gaussian(totalMean, totalStd));
|
|
498
|
+
}
|
|
499
|
+
return result;
|
|
500
|
+
}
|
|
501
|
+
return { forward, inverseK };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ---------------------------------------------------------------------------
|
|
505
|
+
// Grouped AR with geometrically grouped coefficients
|
|
506
|
+
// ---------------------------------------------------------------------------
|
|
507
|
+
|
|
508
|
+
function buildGroups(maxLag) {
|
|
509
|
+
const groups = [];
|
|
510
|
+
let g = 0;
|
|
511
|
+
let size = 1;
|
|
512
|
+
let assigned = 0;
|
|
513
|
+
while (assigned < maxLag) {
|
|
514
|
+
for (let i = 0; i < size; i++) {
|
|
515
|
+
if (assigned >= maxLag) break;
|
|
516
|
+
groups.push(g);
|
|
517
|
+
assigned += 1;
|
|
518
|
+
}
|
|
519
|
+
g += 1;
|
|
520
|
+
size *= 2;
|
|
521
|
+
}
|
|
522
|
+
return groups;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function groupRegressor(buf, groups, nGroups, maxLag) {
|
|
526
|
+
const x = new Array(nGroups).fill(0.0);
|
|
527
|
+
for (let j = 0; j < maxLag; j++) x[groups[j]] += buf[buf.length - 1 - j];
|
|
528
|
+
return x;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export function groupedAr(maxLag = 16, lam = 0.99, ridge = 1.0) {
|
|
532
|
+
const groups = buildGroups(maxLag);
|
|
533
|
+
const nGroups = Math.max(...groups) + 1;
|
|
534
|
+
|
|
535
|
+
function forward(y, state) {
|
|
536
|
+
if (state === null || state === undefined) {
|
|
537
|
+
state = { buffer: [], theta: new Array(nGroups).fill(0.0), P: eye(nGroups, ridge), n: 0 };
|
|
538
|
+
}
|
|
539
|
+
const buf = state.buffer;
|
|
540
|
+
const th = state.theta;
|
|
541
|
+
state.n += 1;
|
|
542
|
+
let residual;
|
|
543
|
+
if (buf.length >= maxLag) {
|
|
544
|
+
const x = groupRegressor(buf, groups, nGroups, maxLag);
|
|
545
|
+
const prediction = fsum(Array.from({ length: nGroups }, (_, g) => th[g] * x[g]));
|
|
546
|
+
residual = y - prediction;
|
|
547
|
+
const P = state.P;
|
|
548
|
+
const Px = matVec(P, x, nGroups);
|
|
549
|
+
const denom = lam + dot(x, Px, nGroups);
|
|
550
|
+
if (Math.abs(denom) > 1e-15) {
|
|
551
|
+
const K = Px.map((px) => px / denom);
|
|
552
|
+
for (let g = 0; g < nGroups; g++) th[g] += K[g] * residual;
|
|
553
|
+
for (let i = 0; i < nGroups; i++) {
|
|
554
|
+
for (let j = 0; j < nGroups; j++) {
|
|
555
|
+
P[i * nGroups + j] = (P[i * nGroups + j] - K[i] * Px[j]) / lam;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
} else {
|
|
560
|
+
residual = y;
|
|
561
|
+
}
|
|
562
|
+
buf.push(y);
|
|
563
|
+
if (buf.length > maxLag + 10) buf.shift();
|
|
564
|
+
return [residual, state];
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function inverseK(dists, state) {
|
|
568
|
+
const buf = state.buffer.slice();
|
|
569
|
+
const th = state.theta;
|
|
570
|
+
const phi = [];
|
|
571
|
+
for (let j = 0; j < maxLag; j++) phi.push(th[groups[j]]);
|
|
572
|
+
const recoveredMeans = [];
|
|
573
|
+
const recoveredVars = [];
|
|
574
|
+
const result = [];
|
|
575
|
+
for (let h = 0; h < dists.length; h++) {
|
|
576
|
+
let arMean = 0.0;
|
|
577
|
+
let arVar = 0.0;
|
|
578
|
+
for (let j = 0; j < maxLag; j++) {
|
|
579
|
+
const lagH = h - j - 1;
|
|
580
|
+
if (lagH < 0) {
|
|
581
|
+
const bufIdx = buf.length + lagH;
|
|
582
|
+
if (bufIdx >= 0 && bufIdx < buf.length) arMean += phi[j] * buf[bufIdx];
|
|
583
|
+
} else if (lagH < recoveredMeans.length) {
|
|
584
|
+
arMean += phi[j] * recoveredMeans[lagH];
|
|
585
|
+
arVar += phi[j] * phi[j] * recoveredVars[lagH];
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
const totalMean = dists[h].mean + arMean;
|
|
589
|
+
const totalVar = dists[h].var + arVar;
|
|
590
|
+
const totalStd = totalVar > 0 ? Math.sqrt(totalVar) : Math.max(dists[h].std, 1e-12);
|
|
591
|
+
recoveredMeans.push(totalMean);
|
|
592
|
+
recoveredVars.push(totalVar);
|
|
593
|
+
result.push(Dist.gaussian(totalMean, totalStd));
|
|
594
|
+
}
|
|
595
|
+
return result;
|
|
596
|
+
}
|
|
597
|
+
return { forward, inverseK };
|
|
598
|
+
}
|