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/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `skaters` npm package (the JavaScript port). The port
|
|
4
|
+
tracks the Python [`skaters`](https://pypi.org/project/skaters/) package and is
|
|
5
|
+
kept numerically identical to it within `1e-6`, enforced by the parity checker on
|
|
6
|
+
every release.
|
|
7
|
+
|
|
8
|
+
## 0.11.0
|
|
9
|
+
|
|
10
|
+
Initial npm release of the JavaScript port. Zero-dependency ES modules for Node
|
|
11
|
+
and the browser. Exports `laplace` and `buildCandidates`, the `Dist` object,
|
|
12
|
+
transforms (`difference`, `standardize`, `garch`, `ar`, `holtLinear`,
|
|
13
|
+
`powerTransform`, …), ensembles, leaves (`scaleMixtureLeaf`, `crpsLeaf`,
|
|
14
|
+
`garchLeaf`), `multiscale`, `sticky`, periodicity and covariance helpers, and the
|
|
15
|
+
spec (de)serialisers. Version chosen to align with the Python package.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Peter Cotton
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# skaters
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/skaters)
|
|
4
|
+
[](https://www.npmjs.com/package/skaters)
|
|
5
|
+
[](https://github.com/microprediction/skaters/blob/main/LICENSE)
|
|
6
|
+
|
|
7
|
+
Fast univariate **online distributional** time-series forecasting. Zero
|
|
8
|
+
dependencies, runs in Node or the browser. This is the JavaScript port of the
|
|
9
|
+
Python [`skaters`](https://pypi.org/project/skaters/) package, numerically
|
|
10
|
+
identical to it within `1e-6` (enforced on every release by a parity checker
|
|
11
|
+
against the Python reference).
|
|
12
|
+
|
|
13
|
+
Every prediction is a full predictive **distribution** (a `Dist`), so it can be
|
|
14
|
+
scored on log-likelihood — not just a point or an interval.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
npm install skaters
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quickstart
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import { laplace } from "skaters";
|
|
26
|
+
|
|
27
|
+
const f = laplace(1); // online, O(1) per step, the general-purpose default
|
|
28
|
+
let state = null;
|
|
29
|
+
for (const y of stream) {
|
|
30
|
+
let dists;
|
|
31
|
+
[dists, state] = f(y, state);
|
|
32
|
+
const d = dists[0];
|
|
33
|
+
d.mean; // point forecast
|
|
34
|
+
d.std; // uncertainty
|
|
35
|
+
d.quantile(0.975); // 97.5th percentile
|
|
36
|
+
d.logpdf(y); // a real density — scorable on log-likelihood
|
|
37
|
+
d.crps(y); // ...and on CRPS
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
In the browser, import the hosted module directly — no build step:
|
|
42
|
+
|
|
43
|
+
```html
|
|
44
|
+
<script type="module">
|
|
45
|
+
import { laplace } from "https://skaters.microprediction.org/js/skaters/index.mjs";
|
|
46
|
+
</script>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## What's exported
|
|
50
|
+
|
|
51
|
+
`laplace` and `buildCandidates`; the `Dist` object; transforms (`difference`,
|
|
52
|
+
`standardize`, `garch`, `ar`, `holtLinear`, `powerTransform`, …); ensembles
|
|
53
|
+
(`precisionWeightedEnsemble`, `bayesianEnsemble`, `terminalLeafEnsemble`); leaves
|
|
54
|
+
(`scaleMixtureLeaf`, `crpsLeaf`, `garchLeaf`); `multiscale`, `sticky`, periodicity
|
|
55
|
+
and covariance helpers; and spec (de)serialisers. See `index.mjs` for the full
|
|
56
|
+
surface.
|
|
57
|
+
|
|
58
|
+
## Notes
|
|
59
|
+
|
|
60
|
+
- `laplace` is the general-purpose default; its signature is
|
|
61
|
+
`laplace(k = 1, objective = "crps", sticky = true, scales = null)`. On
|
|
62
|
+
price/return series with volatility clustering there is no free lunch — a
|
|
63
|
+
dedicated GARCH-t model is the right tool there.
|
|
64
|
+
- The port is a faithful mirror of the Python package; the two agree to `1e-6`.
|
|
65
|
+
|
|
66
|
+
## Links
|
|
67
|
+
|
|
68
|
+
- Docs & benchmarks: <https://skaters.microprediction.org>
|
|
69
|
+
- Paper: *Transforms All the Way Down — Automatic Online Distributional Forecasting by Conjugation*
|
|
70
|
+
- Python package: <https://pypi.org/project/skaters/>
|
|
71
|
+
- Source: <https://github.com/microprediction/skaters>
|
|
72
|
+
|
|
73
|
+
MIT © Peter Cotton
|
package/api.mjs
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// User-facing API — JS port of skaters/api.py (named search policies).
|
|
2
|
+
//
|
|
3
|
+
// One forecaster: laplace (general). Volatility/mean-reversion are composable transforms.
|
|
4
|
+
|
|
5
|
+
import { leaf, scaleMixtureLeaf, crpsLeaf } from "./leaf.mjs";
|
|
6
|
+
import { conjugate } from "./conjugate.mjs";
|
|
7
|
+
import { terminalLeafEnsemble } from "./terminal.mjs";
|
|
8
|
+
import { bayesianEnsemble } from "./bayesian.mjs";
|
|
9
|
+
import { sticky as project } from "./sticky.mjs";
|
|
10
|
+
import { multiscale } from "./multiscale.mjs";
|
|
11
|
+
import {
|
|
12
|
+
difference, fractionalDifference, standardize, emaTransform, ouTransform, drift,
|
|
13
|
+
holtLinear, ar, theta, seasonalDifference, garch, powerTransform, yeoJohnson,
|
|
14
|
+
} from "./transform.mjs";
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Shared candidate population
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export function buildCandidates(k) {
|
|
21
|
+
const candidates = [];
|
|
22
|
+
const depths = [];
|
|
23
|
+
const groups = {};
|
|
24
|
+
const families = []; // human family label per candidate (for introspection)
|
|
25
|
+
const push = (c, d, fam) => {
|
|
26
|
+
candidates.push(c);
|
|
27
|
+
depths.push(d);
|
|
28
|
+
families.push(fam);
|
|
29
|
+
return candidates.length - 1;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Depth 0: baseline noise
|
|
33
|
+
push(leaf(k), 0, "baseline");
|
|
34
|
+
|
|
35
|
+
// Depth 1: single EMA at various speeds
|
|
36
|
+
for (const alpha of [0.01, 0.05, 0.1, 0.3]) push(conjugate(leaf(k), emaTransform(alpha), k), 1, "EMA");
|
|
37
|
+
|
|
38
|
+
// Depth 1: differencing (pure random walk)
|
|
39
|
+
groups.diff = [];
|
|
40
|
+
groups.diff.push(push(conjugate(leaf(k), difference(), k), 1, "random walk"));
|
|
41
|
+
|
|
42
|
+
// Depth 1: drift (random walk with adaptive drift)
|
|
43
|
+
groups.drift = [];
|
|
44
|
+
for (const [a, s] of [[0.05, 0.01], [0.01, 0.002], [0.002, 0.001], [0.0005, 0.0002]]) {
|
|
45
|
+
groups.drift.push(push(conjugate(leaf(k), drift(a, s), k), 1, "drift"));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Depth 1: Theta
|
|
49
|
+
for (const a of [0.05, 0.1, 0.3]) push(conjugate(leaf(k), theta(a), k), 1, "Theta");
|
|
50
|
+
|
|
51
|
+
// Depth 1: AR
|
|
52
|
+
push(conjugate(leaf(k), ar(1), k), 1, "AR");
|
|
53
|
+
push(conjugate(leaf(k), ar(2, 0.99, 1.0, 1), k), 1, "AR");
|
|
54
|
+
|
|
55
|
+
// Depth 1: Holt linear
|
|
56
|
+
groups.holt = [];
|
|
57
|
+
for (const [a, b] of [[0.1, 0.02], [0.1, 0.05], [0.3, 0.1]]) {
|
|
58
|
+
groups.holt.push(push(conjugate(leaf(k), holtLinear(a, b), k), 1, "Holt trend"));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Depth 1: Seasonal differencing
|
|
62
|
+
for (const period of [7, 12, 24]) push(conjugate(leaf(k), seasonalDifference(period), k), 1, "seasonal");
|
|
63
|
+
|
|
64
|
+
// Depth 2: Seasonal differencing + EMA
|
|
65
|
+
for (const period of [7, 12, 24]) {
|
|
66
|
+
for (const alpha of [0.05, 0.1]) {
|
|
67
|
+
push(conjugate(conjugate(leaf(k), emaTransform(alpha), k), seasonalDifference(period), k), 2, "seasonal → EMA");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Depth 2: differencing + EMA
|
|
72
|
+
for (const alpha of [0.05, 0.1, 0.3]) {
|
|
73
|
+
groups.diff.push(push(conjugate(conjugate(leaf(k), emaTransform(alpha), k), difference(), k), 2, "random walk → EMA"));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Depth 2: standardize + EMA
|
|
77
|
+
for (const alpha of [0.05, 0.1]) {
|
|
78
|
+
push(conjugate(conjugate(leaf(k), emaTransform(alpha), k), standardize(), k), 2, "standardize → EMA");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Depth 2: fractional diff + EMA
|
|
82
|
+
groups.frac = [];
|
|
83
|
+
for (const d of [0.2, 0.4]) {
|
|
84
|
+
groups.frac.push(push(conjugate(conjugate(leaf(k), emaTransform(0.1), k), fractionalDifference(d, 30), k), 2, "frac-diff → EMA"));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Depth 2: drift + EMA
|
|
88
|
+
for (const [aDrift, sDrift] of [[0.002, 0.001], [0.0005, 0.0002]]) {
|
|
89
|
+
for (const aEma of [0.05, 0.1]) {
|
|
90
|
+
groups.drift.push(push(conjugate(conjugate(leaf(k), emaTransform(aEma), k), drift(aDrift, sDrift), k), 2, "drift → EMA"));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Depth 2: drift + Holt linear
|
|
95
|
+
{
|
|
96
|
+
const idx = push(conjugate(conjugate(leaf(k), holtLinear(0.1, 0.05), k), drift(0.001, 0.0005), k), 2, "drift → Holt");
|
|
97
|
+
groups.drift.push(idx);
|
|
98
|
+
groups.holt.push(idx);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Depth 2: GARCH + EMA
|
|
102
|
+
push(conjugate(conjugate(leaf(k), emaTransform(0.1), k), garch(), k), 2, "GARCH vol → EMA");
|
|
103
|
+
|
|
104
|
+
// Depth 2: power transform + EMA
|
|
105
|
+
push(conjugate(conjugate(leaf(k), emaTransform(0.1), k), powerTransform(0.5), k), 2, "power → EMA");
|
|
106
|
+
|
|
107
|
+
// Depth 2: thinking fast and slow (fast tracker outside, slow scale inside)
|
|
108
|
+
const fastTrackers = () => [
|
|
109
|
+
emaTransform(0.3), emaTransform(0.5), holtLinear(0.4, 0.2), ar(1), drift(0.05, 0.01), difference(),
|
|
110
|
+
];
|
|
111
|
+
groups.fast_slow = [];
|
|
112
|
+
for (const scaleAlpha of [0.02, 0.05]) {
|
|
113
|
+
for (const tracker of fastTrackers()) {
|
|
114
|
+
groups.fast_slow.push(push(conjugate(conjugate(leaf(k), standardize(scaleAlpha), k), tracker, k), 2, "fast/slow"));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Coordinate prior (Yeo-Johnson): learn the coordinate the series is simple in.
|
|
119
|
+
groups.coordinate = [];
|
|
120
|
+
for (const L of [0.0, 0.5]) {
|
|
121
|
+
for (const innerTx of [difference(), emaTransform(0.1)]) {
|
|
122
|
+
groups.coordinate.push(push(conjugate(conjugate(leaf(k), innerTx, k), yeoJohnson(L), k), 2, "coordinate (Yeo-Johnson)"));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Mean-reversion prior (Ornstein-Uhlenbeck), MULTI-STEP ONLY: redundant with
|
|
127
|
+
// the ema/random-walk mix at one step, so gated on k > 1 (see api.py).
|
|
128
|
+
groups.mean_revert = [];
|
|
129
|
+
if (k > 1) {
|
|
130
|
+
for (const L of [0.0, 0.5]) {
|
|
131
|
+
for (const kappa of [0.03, 0.1, 0.3]) {
|
|
132
|
+
groups.mean_revert.push(push(
|
|
133
|
+
conjugate(conjugate(leaf(k), ouTransform(kappa, 0.02), k), yeoJohnson(L), k), 2, "mean reversion (OU)"));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return [candidates, depths, groups, families];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// Policies
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
function objectiveLeaf(objective) {
|
|
146
|
+
if (objective === "crps") return crpsLeaf;
|
|
147
|
+
if (objective === "likelihood") return scaleMixtureLeaf;
|
|
148
|
+
throw new Error(`objective must be 'crps' or 'likelihood', got ${objective}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function laplaceSingleScale(k, objective, sticky) {
|
|
152
|
+
const [candidates, depths] = buildCandidates(k);
|
|
153
|
+
let f = terminalLeafEnsemble(candidates, {
|
|
154
|
+
k, leafFn: objectiveLeaf(objective), learningRate: 0.8, complexityPenalty: 0.005, depths, maxComponents: 20,
|
|
155
|
+
forget: 0.99,
|
|
156
|
+
});
|
|
157
|
+
if (sticky) f = project(f, k);
|
|
158
|
+
return f;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Multi-scale by default at k > 1: one instance per decimation stride
|
|
162
|
+
// (default {1, ceil(sqrt(k)), k}), horizons mix eligible scales by likelihood.
|
|
163
|
+
// 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 });
|
|
166
|
+
f.skaterName = `laplace(k=${k})`;
|
|
167
|
+
return f;
|
|
168
|
+
}
|
package/bayesian.mjs
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Bayesian model-averaging ensemble — JS port of skaters/bayesian.py.
|
|
2
|
+
//
|
|
3
|
+
// Weights models by cumulative shrunk log-likelihood minus a per-depth
|
|
4
|
+
// complexity penalty, combining predictions via Dist.combine with softmax
|
|
5
|
+
// weights (XGBoost-inspired regularization).
|
|
6
|
+
|
|
7
|
+
import { Dist } from "./dist.mjs";
|
|
8
|
+
|
|
9
|
+
export function bayesianEnsemble(
|
|
10
|
+
skaters,
|
|
11
|
+
{
|
|
12
|
+
k = 1,
|
|
13
|
+
learningRate = 0.5,
|
|
14
|
+
complexityPenalty = 0.0,
|
|
15
|
+
depths = null,
|
|
16
|
+
priorLogWeights = null,
|
|
17
|
+
maxComponents = 20,
|
|
18
|
+
} = {}
|
|
19
|
+
) {
|
|
20
|
+
const n = skaters.length;
|
|
21
|
+
if (n <= 0) throw new Error("ensemble needs at least one skater");
|
|
22
|
+
if (!(learningRate > 0 && learningRate <= 1)) throw new Error("learningRate in (0,1]");
|
|
23
|
+
if (complexityPenalty < 0) throw new Error("complexityPenalty >= 0");
|
|
24
|
+
|
|
25
|
+
const d = depths === null ? new Array(n).fill(0) : depths;
|
|
26
|
+
const prior = priorLogWeights === null ? new Array(n).fill(0.0) : priorLogWeights;
|
|
27
|
+
|
|
28
|
+
function _skater(y, state) {
|
|
29
|
+
if (state === null || state === undefined) {
|
|
30
|
+
state = {
|
|
31
|
+
sub: new Array(n).fill(null),
|
|
32
|
+
queues: Array.from({ length: n }, () => Array.from({ length: k }, () => [])),
|
|
33
|
+
log_w: Array.from({ length: n }, (_, i) => new Array(k).fill(prior[i])),
|
|
34
|
+
n_obs: 0,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
state.n_obs += 1;
|
|
38
|
+
|
|
39
|
+
const allDists = [];
|
|
40
|
+
for (let i = 0; i < n; i++) {
|
|
41
|
+
const [distsI, sub] = skaters[i](y, state.sub[i]);
|
|
42
|
+
state.sub[i] = sub;
|
|
43
|
+
allDists.push(distsI);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Resolve pending predictions: score each model's past Dist against y.
|
|
47
|
+
for (let i = 0; i < n; i++) {
|
|
48
|
+
for (let h = 0; h < k; h++) {
|
|
49
|
+
const q = state.queues[i][h];
|
|
50
|
+
if (q.length) {
|
|
51
|
+
const pastDist = q.shift();
|
|
52
|
+
let lp = pastDist.logpdf(y);
|
|
53
|
+
lp = Math.max(lp, -20.0); // bounded loss (mixability)
|
|
54
|
+
state.log_w[i][h] += learningRate * lp - complexityPenalty * d[i];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Enqueue current predictions for future scoring.
|
|
60
|
+
for (let i = 0; i < n; i++) {
|
|
61
|
+
for (let h = 0; h < k; h++) state.queues[i][h].push(allDists[i][h]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Softmax weights per horizon, then combine.
|
|
65
|
+
const combined = [];
|
|
66
|
+
for (let h = 0; h < k; h++) {
|
|
67
|
+
const logWs = [];
|
|
68
|
+
for (let i = 0; i < n; i++) logWs.push(state.log_w[i][h]);
|
|
69
|
+
const maxLw = Math.max(...logWs);
|
|
70
|
+
let weights;
|
|
71
|
+
if (Number.isFinite(maxLw)) weights = logWs.map((lw) => Math.exp(lw - maxLw));
|
|
72
|
+
else weights = new Array(n).fill(1.0);
|
|
73
|
+
|
|
74
|
+
const horizonDists = [];
|
|
75
|
+
for (let i = 0; i < n; i++) horizonDists.push(allDists[i][h]);
|
|
76
|
+
let dist = Dist.combine(horizonDists, weights);
|
|
77
|
+
if (dist.length > maxComponents) dist = dist.prune(maxComponents);
|
|
78
|
+
combined.push(dist);
|
|
79
|
+
}
|
|
80
|
+
return [combined, state];
|
|
81
|
+
}
|
|
82
|
+
_skater.skaterName = `bayesian_ensemble(n=${n}, k=${k}, eta=${learningRate})`;
|
|
83
|
+
return _skater;
|
|
84
|
+
}
|
package/conjugate.mjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Conjugation — JS port of skaters/conjugate.py.
|
|
2
|
+
//
|
|
3
|
+
// Given an invertible transform T = {forward, inverseK} and a skater f,
|
|
4
|
+
// produce a new skater that transforms each observation, predicts in the
|
|
5
|
+
// transformed space, and inverts the predictions back.
|
|
6
|
+
|
|
7
|
+
export function conjugate(skater, transform, k = 1) {
|
|
8
|
+
const { forward, inverseK } = transform;
|
|
9
|
+
|
|
10
|
+
function _conjugated(y, state) {
|
|
11
|
+
if (state === null || state === undefined) {
|
|
12
|
+
state = { tState: null, sState: null };
|
|
13
|
+
}
|
|
14
|
+
const [yPrime, tState] = forward(y, state.tState);
|
|
15
|
+
state.tState = tState;
|
|
16
|
+
const [distsPrime, sState] = skater(yPrime, state.sState);
|
|
17
|
+
state.sState = sState;
|
|
18
|
+
const dists = inverseK(distsPrime, state.tState);
|
|
19
|
+
return [dists, state];
|
|
20
|
+
}
|
|
21
|
+
_conjugated.skaterName = `conjugate(${skater.skaterName || "?"})`;
|
|
22
|
+
return _conjugated;
|
|
23
|
+
}
|
package/cov.mjs
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Online covariance estimators — JS port of skaters/cov/*.py.
|
|
2
|
+
//
|
|
3
|
+
// API: [mean, cov, state] = f(y, state)
|
|
4
|
+
// y is an array of floats; cov is a flat row-major n*n array.
|
|
5
|
+
|
|
6
|
+
export function runningCov(y, state) {
|
|
7
|
+
const n = y.length;
|
|
8
|
+
if (state === null || state === undefined) {
|
|
9
|
+
state = { n: 0, mean: new Array(n).fill(0.0), C: new Array(n * n).fill(0.0) };
|
|
10
|
+
}
|
|
11
|
+
state.n += 1;
|
|
12
|
+
const k = state.n;
|
|
13
|
+
const mean = state.mean;
|
|
14
|
+
const C = state.C;
|
|
15
|
+
|
|
16
|
+
const delta = new Array(n);
|
|
17
|
+
for (let i = 0; i < n; i++) delta[i] = y[i] - mean[i];
|
|
18
|
+
for (let i = 0; i < n; i++) mean[i] += delta[i] / k;
|
|
19
|
+
const delta2 = new Array(n);
|
|
20
|
+
for (let i = 0; i < n; i++) delta2[i] = y[i] - mean[i];
|
|
21
|
+
for (let i = 0; i < n; i++) {
|
|
22
|
+
for (let j = 0; j < n; j++) C[i * n + j] += delta[i] * delta2[j];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let cov;
|
|
26
|
+
if (k < 2) cov = new Array(n * n).fill(0.0);
|
|
27
|
+
else cov = C.map((c) => c / (k - 1));
|
|
28
|
+
return [mean.slice(), cov, state];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function emaCov(y, state, alpha = 0.05) {
|
|
32
|
+
const n = y.length;
|
|
33
|
+
if (state === null || state === undefined) {
|
|
34
|
+
state = { mean: y.slice(), cov: new Array(n * n).fill(0.0), n: 1 };
|
|
35
|
+
return [y.slice(), new Array(n * n).fill(0.0), state];
|
|
36
|
+
}
|
|
37
|
+
const mean = state.mean;
|
|
38
|
+
const cov = state.cov;
|
|
39
|
+
state.n += 1;
|
|
40
|
+
|
|
41
|
+
const delta = new Array(n);
|
|
42
|
+
for (let i = 0; i < n; i++) delta[i] = y[i] - mean[i];
|
|
43
|
+
for (let i = 0; i < n; i++) mean[i] += alpha * delta[i];
|
|
44
|
+
for (let i = 0; i < n; i++) {
|
|
45
|
+
for (let j = 0; j < n; j++) {
|
|
46
|
+
cov[i * n + j] = (1 - alpha) * (cov[i * n + j] + alpha * delta[i] * delta[j]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return [mean.slice(), cov.slice(), state];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function ledoitWolfCov(y, state, alpha = 0.05, shrinkage = 0.5) {
|
|
53
|
+
const n = y.length;
|
|
54
|
+
if (state === null || state === undefined) {
|
|
55
|
+
const corr = new Array(n * n);
|
|
56
|
+
for (let i = 0; i < n; i++) for (let j = 0; j < n; j++) corr[i * n + j] = i === j ? 1.0 : 0.0;
|
|
57
|
+
state = { mean: y.slice(), var: new Array(n).fill(0.0), corr, n: 1 };
|
|
58
|
+
return [y.slice(), new Array(n * n).fill(0.0), state];
|
|
59
|
+
}
|
|
60
|
+
const mean = state.mean;
|
|
61
|
+
const varr = state.var;
|
|
62
|
+
const corr = state.corr;
|
|
63
|
+
state.n += 1;
|
|
64
|
+
|
|
65
|
+
const delta = new Array(n);
|
|
66
|
+
for (let i = 0; i < n; i++) delta[i] = y[i] - mean[i];
|
|
67
|
+
for (let i = 0; i < n; i++) mean[i] += alpha * delta[i];
|
|
68
|
+
const delta2 = new Array(n);
|
|
69
|
+
for (let i = 0; i < n; i++) delta2[i] = y[i] - mean[i];
|
|
70
|
+
for (let i = 0; i < n; i++) varr[i] = (1 - alpha) * varr[i] + alpha * delta[i] * delta2[i];
|
|
71
|
+
|
|
72
|
+
for (let i = 0; i < n; i++) {
|
|
73
|
+
const si = varr[i] > 1e-16 ? Math.sqrt(varr[i]) : 1e-8;
|
|
74
|
+
for (let j = i + 1; j < n; j++) {
|
|
75
|
+
const sj = varr[j] > 1e-16 ? Math.sqrt(varr[j]) : 1e-8;
|
|
76
|
+
const zCross = (delta[i] / si) * (delta[j] / sj);
|
|
77
|
+
const idx = i * n + j;
|
|
78
|
+
let c = (1 - alpha) * corr[idx] + alpha * zCross;
|
|
79
|
+
c = Math.max(-1.0, Math.min(1.0, c));
|
|
80
|
+
corr[idx] = c;
|
|
81
|
+
corr[j * n + i] = c;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const shrunkCov = new Array(n * n).fill(0.0);
|
|
86
|
+
for (let i = 0; i < n; i++) {
|
|
87
|
+
const si = varr[i] > 1e-16 ? Math.sqrt(varr[i]) : 1e-8;
|
|
88
|
+
for (let j = 0; j < n; j++) {
|
|
89
|
+
const sj = varr[j] > 1e-16 ? Math.sqrt(varr[j]) : 1e-8;
|
|
90
|
+
if (i === j) shrunkCov[i * n + j] = varr[i];
|
|
91
|
+
else shrunkCov[i * n + j] = (1 - shrinkage) * corr[i * n + j] * si * sj;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return [mean.slice(), shrunkCov, state];
|
|
95
|
+
}
|