slangmath 1.0.6 → 1.1.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 +54 -23
- package/slang-complex.js +954 -0
- package/slang-linalg.js +1156 -0
- package/slang-math.js +83 -8
- package/slang-ode.js +1082 -0
- package/slang-stats.js +1206 -0
- package/slang-symbolic.js +1616 -0
package/slang-stats.js
ADDED
|
@@ -0,0 +1,1206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SLaNg Statistics & Probability Module
|
|
3
|
+
*
|
|
4
|
+
* Integrates probability theory with SLaNg's calculus engine.
|
|
5
|
+
* Compute distributions, statistical moments, hypothesis tests,
|
|
6
|
+
* and probabilistic calculus problems symbolically and numerically.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Descriptive statistics (mean, variance, skewness, kurtosis)
|
|
10
|
+
* - Probability distributions (PDF, CDF, inverse CDF)
|
|
11
|
+
* · Normal, Student-t, Chi-Squared, F, Exponential, Poisson, Binomial
|
|
12
|
+
* · Beta, Gamma, Uniform, Log-Normal, Cauchy, Weibull
|
|
13
|
+
* - Numerical integration for custom PDFs
|
|
14
|
+
* - Expected value, variance from custom PDFs (SLaNg fractions)
|
|
15
|
+
* - Moment-generating function computation
|
|
16
|
+
* - Hypothesis testing (z-test, t-test, chi-squared goodness of fit)
|
|
17
|
+
* - Regression (linear, polynomial, nonlinear via least squares)
|
|
18
|
+
* - Monte Carlo integration
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// DESCRIPTIVE STATISTICS
|
|
23
|
+
// ============================================================================
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Compute all basic descriptive statistics for a dataset.
|
|
27
|
+
* @param {number[]} data
|
|
28
|
+
* @returns {{ n, mean, median, mode, variance, std, skewness, kurtosis, min, max, range, q1, q3, iqr }}
|
|
29
|
+
*/
|
|
30
|
+
export function describe(data) {
|
|
31
|
+
if (!data.length) throw new Error('Data array is empty');
|
|
32
|
+
const sorted = data.slice().sort((a, b) => a - b);
|
|
33
|
+
const n = data.length;
|
|
34
|
+
|
|
35
|
+
const mean = data.reduce((s, v) => s + v, 0) / n;
|
|
36
|
+
const variance = data.reduce((s, v) => s + (v - mean) ** 2, 0) / n;
|
|
37
|
+
const std = Math.sqrt(variance);
|
|
38
|
+
|
|
39
|
+
// Skewness (Fisher's)
|
|
40
|
+
const skewness = std < 1e-14 ? 0 :
|
|
41
|
+
data.reduce((s, v) => s + ((v - mean) / std) ** 3, 0) / n;
|
|
42
|
+
|
|
43
|
+
// Excess kurtosis
|
|
44
|
+
const kurtosis = std < 1e-14 ? 0 :
|
|
45
|
+
data.reduce((s, v) => s + ((v - mean) / std) ** 4, 0) / n - 3;
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
n, mean,
|
|
49
|
+
median: _quantile(sorted, 0.5),
|
|
50
|
+
mode: _mode(data),
|
|
51
|
+
variance,
|
|
52
|
+
std,
|
|
53
|
+
skewness,
|
|
54
|
+
kurtosis,
|
|
55
|
+
min: sorted[0],
|
|
56
|
+
max: sorted[n - 1],
|
|
57
|
+
range: sorted[n - 1] - sorted[0],
|
|
58
|
+
q1: _quantile(sorted, 0.25),
|
|
59
|
+
q3: _quantile(sorted, 0.75),
|
|
60
|
+
iqr: _quantile(sorted, 0.75) - _quantile(sorted, 0.25),
|
|
61
|
+
cv: std / Math.abs(mean), // coefficient of variation
|
|
62
|
+
sem: std / Math.sqrt(n), // standard error of mean
|
|
63
|
+
sum: data.reduce((s, v) => s + v, 0),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function _quantile(sorted, p) {
|
|
68
|
+
const n = sorted.length;
|
|
69
|
+
const pos = p * (n - 1);
|
|
70
|
+
const lo = Math.floor(pos), hi = Math.ceil(pos);
|
|
71
|
+
return sorted[lo] + (pos - lo) * (sorted[hi] - sorted[lo]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function _mode(data) {
|
|
75
|
+
const freq = new Map();
|
|
76
|
+
for (const v of data) freq.set(v, (freq.get(v) || 0) + 1);
|
|
77
|
+
let maxF = 0, mode = null;
|
|
78
|
+
for (const [v, f] of freq) { if (f > maxF) { maxF = f; mode = v; } }
|
|
79
|
+
return mode;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ============================================================================
|
|
83
|
+
// PROBABILITY DISTRIBUTIONS
|
|
84
|
+
// ============================================================================
|
|
85
|
+
|
|
86
|
+
// ─── Normal Distribution ─────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
/** Normal PDF */
|
|
89
|
+
export function normalPDF(x, mu = 0, sigma = 1) {
|
|
90
|
+
return Math.exp(-0.5 * ((x - mu) / sigma) ** 2) / (sigma * Math.sqrt(2 * Math.PI));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Normal CDF using error function approximation */
|
|
94
|
+
export function normalCDF(x, mu = 0, sigma = 1) {
|
|
95
|
+
return 0.5 * (1 + _erf((x - mu) / (sigma * Math.SQRT2)));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Inverse Normal CDF (quantile function / probit) */
|
|
99
|
+
export function normalInvCDF(p, mu = 0, sigma = 1) {
|
|
100
|
+
if (p <= 0 || p >= 1) throw new Error('p must be in (0,1)');
|
|
101
|
+
return mu + sigma * _invNormalStd(p);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ─── Exponential Distribution ─────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
export function exponentialPDF(x, lambda = 1) {
|
|
107
|
+
return x < 0 ? 0 : lambda * Math.exp(-lambda * x);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function exponentialCDF(x, lambda = 1) {
|
|
111
|
+
return x < 0 ? 0 : 1 - Math.exp(-lambda * x);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function exponentialInvCDF(p, lambda = 1) {
|
|
115
|
+
if (p < 0 || p >= 1) throw new Error('p must be in [0,1)');
|
|
116
|
+
return -Math.log(1 - p) / lambda;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ─── Gamma Distribution ───────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
export function gammaPDF(x, alpha, beta = 1) {
|
|
122
|
+
if (x <= 0) return 0;
|
|
123
|
+
return (Math.pow(beta, alpha) / _gammaFn(alpha)) * Math.pow(x, alpha - 1) * Math.exp(-beta * x);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── Beta Distribution ────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
export function betaPDF(x, alpha, beta) {
|
|
129
|
+
if (x < 0 || x > 1) return 0;
|
|
130
|
+
return Math.pow(x, alpha - 1) * Math.pow(1 - x, beta - 1) / _betaFn(alpha, beta);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─── Chi-Squared Distribution ─────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
export function chiSqPDF(x, k) {
|
|
136
|
+
if (x <= 0) return 0;
|
|
137
|
+
return Math.pow(x, k / 2 - 1) * Math.exp(-x / 2) / (Math.pow(2, k / 2) * _gammaFn(k / 2));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function chiSqCDF(x, k) {
|
|
141
|
+
if (x <= 0) return 0;
|
|
142
|
+
return _gammaCDF(x / 2, k / 2);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ─── Student-t Distribution ───────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
export function tPDF(t, df) {
|
|
148
|
+
const c = _gammaFn((df + 1) / 2) / (_gammaFn(df / 2) * Math.sqrt(df * Math.PI));
|
|
149
|
+
return c * Math.pow(1 + t * t / df, -(df + 1) / 2);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function tCDF(t, df) {
|
|
153
|
+
// Numerical approximation using regularized incomplete beta function
|
|
154
|
+
const x = df / (df + t * t);
|
|
155
|
+
const ibeta = _regIncBeta(x, df / 2, 0.5);
|
|
156
|
+
return t >= 0 ? 1 - 0.5 * ibeta : 0.5 * ibeta;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ─── Poisson Distribution ─────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
export function poissonPMF(k, lambda) {
|
|
162
|
+
if (!Number.isInteger(k) || k < 0) return 0;
|
|
163
|
+
return Math.pow(lambda, k) * Math.exp(-lambda) / _factorial(k);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function poissonCDF(k, lambda) {
|
|
167
|
+
let sum = 0;
|
|
168
|
+
for (let i = 0; i <= k; i++) sum += poissonPMF(i, lambda);
|
|
169
|
+
return sum;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ─── Binomial Distribution ────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
export function binomialPMF(k, n, p) {
|
|
175
|
+
if (k < 0 || k > n || !Number.isInteger(k)) return 0;
|
|
176
|
+
return _comb(n, k) * Math.pow(p, k) * Math.pow(1 - p, n - k);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function binomialCDF(k, n, p) {
|
|
180
|
+
let sum = 0;
|
|
181
|
+
for (let i = 0; i <= k; i++) sum += binomialPMF(i, n, p);
|
|
182
|
+
return sum;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ─── Uniform Distribution ─────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
export function uniformPDF(x, a, b) { return x >= a && x <= b ? 1 / (b - a) : 0; }
|
|
188
|
+
export function uniformCDF(x, a, b) { return x < a ? 0 : x > b ? 1 : (x - a) / (b - a); }
|
|
189
|
+
|
|
190
|
+
// ─── Log-Normal Distribution ──────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
export function logNormalPDF(x, mu = 0, sigma = 1) {
|
|
193
|
+
if (x <= 0) return 0;
|
|
194
|
+
return normalPDF(Math.log(x), mu, sigma) / x;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function logNormalCDF(x, mu = 0, sigma = 1) {
|
|
198
|
+
if (x <= 0) return 0;
|
|
199
|
+
return normalCDF(Math.log(x), mu, sigma);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ─── Weibull Distribution ─────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
export function weibullPDF(x, k, lambda = 1) {
|
|
205
|
+
if (x < 0) return 0;
|
|
206
|
+
return (k / lambda) * Math.pow(x / lambda, k - 1) * Math.exp(-Math.pow(x / lambda, k));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function weibullCDF(x, k, lambda = 1) {
|
|
210
|
+
if (x < 0) return 0;
|
|
211
|
+
return 1 - Math.exp(-Math.pow(x / lambda, k));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ============================================================================
|
|
215
|
+
// INTEGRATION WITH CUSTOM PDFs (SLaNg connection)
|
|
216
|
+
// ============================================================================
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Compute the k-th moment of a custom PDF given as a JS function.
|
|
220
|
+
* E[X^k] = ∫ x^k * f(x) dx over [a, b]
|
|
221
|
+
* Uses Simpson's rule.
|
|
222
|
+
* @param {Function} pdf - f(x) → probability density
|
|
223
|
+
* @param {number} k - moment order (k=1 → mean, k=2 → raw second moment)
|
|
224
|
+
* @param {number} a, b - integration bounds
|
|
225
|
+
* @param {number} [n=2000] - number of intervals
|
|
226
|
+
* @returns {number}
|
|
227
|
+
*/
|
|
228
|
+
export function moment(pdf, k, a, b, n = 2000) {
|
|
229
|
+
return _simpsonIntegrate(x => Math.pow(x, k) * pdf(x), a, b, n);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Compute expected value E[f(X)] for custom PDF.
|
|
234
|
+
* @param {Function} pdf - f(x) → density
|
|
235
|
+
* @param {Function} g - g(x) → value to weight (use x => x for mean)
|
|
236
|
+
* @param {number} a, b
|
|
237
|
+
* @param {number} [n=2000]
|
|
238
|
+
* @returns {number}
|
|
239
|
+
*/
|
|
240
|
+
export function expectedValue(pdf, g, a, b, n = 2000) {
|
|
241
|
+
return _simpsonIntegrate(x => g(x) * pdf(x), a, b, n);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Compute variance of a custom PDF.
|
|
246
|
+
*/
|
|
247
|
+
export function variance(pdf, a, b, n = 2000) {
|
|
248
|
+
const mu = expectedValue(pdf, x => x, a, b, n);
|
|
249
|
+
return expectedValue(pdf, x => (x - mu) ** 2, a, b, n);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Compute the entropy of a continuous distribution: H = -∫ f(x) ln f(x) dx
|
|
254
|
+
*/
|
|
255
|
+
export function entropy(pdf, a, b, n = 2000) {
|
|
256
|
+
return _simpsonIntegrate(x => {
|
|
257
|
+
const fx = pdf(x);
|
|
258
|
+
return fx > 1e-300 ? -fx * Math.log(fx) : 0;
|
|
259
|
+
}, a, b, n);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Numerical CDF from a custom PDF via integration.
|
|
264
|
+
* @param {Function} pdf
|
|
265
|
+
* @param {number} xLow - Lower bound where CDF ≈ 0
|
|
266
|
+
* @param {number} xQuery - Point where CDF is evaluated
|
|
267
|
+
* @param {number} [n=1000]
|
|
268
|
+
*/
|
|
269
|
+
export function customCDF(pdf, xLow, xQuery, n = 1000) {
|
|
270
|
+
return _simpsonIntegrate(pdf, xLow, xQuery, n);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ============================================================================
|
|
274
|
+
// STATISTICAL TESTS
|
|
275
|
+
// ============================================================================
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* One-sample z-test (known population standard deviation).
|
|
279
|
+
* H₀: μ = μ₀ vs H₁: μ ≠ μ₀
|
|
280
|
+
* @returns {{ z, pValue, reject, ci95 }}
|
|
281
|
+
*/
|
|
282
|
+
export function zTest(data, mu0, sigma) {
|
|
283
|
+
const n = data.length;
|
|
284
|
+
const xBar = data.reduce((s, v) => s + v, 0) / n;
|
|
285
|
+
const z = (xBar - mu0) / (sigma / Math.sqrt(n));
|
|
286
|
+
const pValue = 2 * (1 - normalCDF(Math.abs(z)));
|
|
287
|
+
const me = 1.96 * sigma / Math.sqrt(n);
|
|
288
|
+
return {
|
|
289
|
+
z, pValue,
|
|
290
|
+
reject: pValue < 0.05,
|
|
291
|
+
ci95: [xBar - me, xBar + me],
|
|
292
|
+
xBar, n
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* One-sample t-test (unknown population standard deviation).
|
|
298
|
+
* H₀: μ = μ₀
|
|
299
|
+
* @returns {{ t, df, pValue, reject, ci95 }}
|
|
300
|
+
*/
|
|
301
|
+
export function tTest(data, mu0 = 0) {
|
|
302
|
+
const n = data.length;
|
|
303
|
+
const xBar = data.reduce((s, v) => s + v, 0) / n;
|
|
304
|
+
const s = Math.sqrt(data.reduce((sum, v) => sum + (v - xBar) ** 2, 0) / (n - 1));
|
|
305
|
+
const t = (xBar - mu0) / (s / Math.sqrt(n));
|
|
306
|
+
const df = n - 1;
|
|
307
|
+
const pValue = 2 * tCDF(-Math.abs(t), df);
|
|
308
|
+
const tCrit = _tCritical(0.975, df);
|
|
309
|
+
const me = tCrit * s / Math.sqrt(n);
|
|
310
|
+
return {
|
|
311
|
+
t, df, pValue,
|
|
312
|
+
reject: pValue < 0.05,
|
|
313
|
+
ci95: [xBar - me, xBar + me],
|
|
314
|
+
xBar, s, n
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Two-sample independent t-test (Welch's).
|
|
320
|
+
* H₀: μ₁ = μ₂
|
|
321
|
+
*/
|
|
322
|
+
export function tTest2(data1, data2) {
|
|
323
|
+
const n1 = data1.length, n2 = data2.length;
|
|
324
|
+
const m1 = data1.reduce((s, v) => s + v, 0) / n1;
|
|
325
|
+
const m2 = data2.reduce((s, v) => s + v, 0) / n2;
|
|
326
|
+
const v1 = data1.reduce((s, v) => s + (v - m1) ** 2, 0) / (n1 - 1);
|
|
327
|
+
const v2 = data2.reduce((s, v) => s + (v - m2) ** 2, 0) / (n2 - 1);
|
|
328
|
+
|
|
329
|
+
const se = Math.sqrt(v1 / n1 + v2 / n2);
|
|
330
|
+
const t = (m1 - m2) / se;
|
|
331
|
+
|
|
332
|
+
// Welch-Satterthwaite df
|
|
333
|
+
const df = Math.floor(
|
|
334
|
+
Math.pow(v1 / n1 + v2 / n2, 2) /
|
|
335
|
+
(Math.pow(v1 / n1, 2) / (n1 - 1) + Math.pow(v2 / n2, 2) / (n2 - 1))
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
const pValue = 2 * tCDF(-Math.abs(t), df);
|
|
339
|
+
return { t, df, pValue, reject: pValue < 0.05, meanDiff: m1 - m2, se };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Chi-squared goodness-of-fit test.
|
|
344
|
+
* @param {number[]} observed - Observed frequencies
|
|
345
|
+
* @param {number[]} expected - Expected frequencies
|
|
346
|
+
* @returns {{ chiSq, df, pValue, reject }}
|
|
347
|
+
*/
|
|
348
|
+
export function chiSqTest(observed, expected) {
|
|
349
|
+
if (observed.length !== expected.length) throw new Error('Array lengths must match');
|
|
350
|
+
const chiSq = observed.reduce((s, o, i) => s + Math.pow(o - expected[i], 2) / expected[i], 0);
|
|
351
|
+
const df = observed.length - 1;
|
|
352
|
+
const pValue = 1 - chiSqCDF(chiSq, df);
|
|
353
|
+
return { chiSq, df, pValue, reject: pValue < 0.05 };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ============================================================================
|
|
357
|
+
// REGRESSION
|
|
358
|
+
// ============================================================================
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Simple linear regression: y = a + b*x
|
|
362
|
+
* @param {number[]} x
|
|
363
|
+
* @param {number[]} y
|
|
364
|
+
* @returns {{ a, b, r2, residuals, predict }}
|
|
365
|
+
*/
|
|
366
|
+
export function linearRegression(x, y) {
|
|
367
|
+
if (x.length !== y.length) throw new Error('x and y must have same length');
|
|
368
|
+
const n = x.length;
|
|
369
|
+
const xBar = x.reduce((s, v) => s + v, 0) / n;
|
|
370
|
+
const yBar = y.reduce((s, v) => s + v, 0) / n;
|
|
371
|
+
|
|
372
|
+
const Sxy = x.reduce((s, xi, i) => s + (xi - xBar) * (y[i] - yBar), 0);
|
|
373
|
+
const Sxx = x.reduce((s, xi) => s + (xi - xBar) ** 2, 0);
|
|
374
|
+
|
|
375
|
+
const b = Sxy / Sxx;
|
|
376
|
+
const a = yBar - b * xBar;
|
|
377
|
+
|
|
378
|
+
const yPred = x.map(xi => a + b * xi);
|
|
379
|
+
const residuals = y.map((yi, i) => yi - yPred[i]);
|
|
380
|
+
const ssTot = y.reduce((s, yi) => s + (yi - yBar) ** 2, 0);
|
|
381
|
+
const ssRes = residuals.reduce((s, r) => s + r * r, 0);
|
|
382
|
+
const r2 = 1 - ssRes / ssTot;
|
|
383
|
+
|
|
384
|
+
return { a, b, r2, residuals, predict: xi => a + b * xi };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Polynomial regression: y = Σ aₖ xᵏ up to degree d
|
|
389
|
+
* Uses normal equations.
|
|
390
|
+
* @param {number[]} x, y
|
|
391
|
+
* @param {number} degree
|
|
392
|
+
* @returns {{ coefficients, r2, predict }}
|
|
393
|
+
*/
|
|
394
|
+
export function polyRegression(x, y, degree) {
|
|
395
|
+
const n = x.length;
|
|
396
|
+
const d = degree + 1;
|
|
397
|
+
|
|
398
|
+
// Build Vandermonde matrix
|
|
399
|
+
const X = Array.from({ length: n }, (_, i) =>
|
|
400
|
+
Array.from({ length: d }, (_, k) => Math.pow(x[i], k)));
|
|
401
|
+
|
|
402
|
+
// Normal equations: XᵀX * a = Xᵀy
|
|
403
|
+
const Xt = matT(X);
|
|
404
|
+
const XtX = matMul(Xt, X);
|
|
405
|
+
const Xty = matvec(Xt, y);
|
|
406
|
+
|
|
407
|
+
let coeff;
|
|
408
|
+
try {
|
|
409
|
+
coeff = solve(XtX, Xty);
|
|
410
|
+
} catch {
|
|
411
|
+
throw new Error('Polynomial regression failed (singular matrix)');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const yBar = y.reduce((s, v) => s + v, 0) / n;
|
|
415
|
+
const predict = xq => coeff.reduce((s, c, k) => s + c * Math.pow(xq, k), 0);
|
|
416
|
+
const yPred = x.map(predict);
|
|
417
|
+
const ssTot = y.reduce((s, yi) => s + (yi - yBar) ** 2, 0);
|
|
418
|
+
const ssRes = y.reduce((s, yi, i) => s + (yi - yPred[i]) ** 2, 0);
|
|
419
|
+
|
|
420
|
+
return { coefficients: coeff, r2: 1 - ssRes / ssTot, predict };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ─── Inline dependencies from linalg (avoid circular imports) ─────────────
|
|
424
|
+
|
|
425
|
+
function matT(A) { return A[0].map((_, j) => A.map(row => row[j])); }
|
|
426
|
+
function matvec(A, v) { return A.map(row => row.reduce((s, a, j) => s + a * v[j], 0)); }
|
|
427
|
+
function matMul(A, B) {
|
|
428
|
+
return A.map(row => B[0].map((_, j) => row.reduce((s, a, k) => s + a * B[k][j], 0)));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function solve(A, b) {
|
|
432
|
+
// Gaussian elimination with partial pivoting
|
|
433
|
+
const n = A.length;
|
|
434
|
+
const aug = A.map((row, i) => [...row, b[i]]);
|
|
435
|
+
for (let col = 0; col < n; col++) {
|
|
436
|
+
let maxRow = col;
|
|
437
|
+
for (let row = col + 1; row < n; row++) {
|
|
438
|
+
if (Math.abs(aug[row][col]) > Math.abs(aug[maxRow][col])) maxRow = row;
|
|
439
|
+
}
|
|
440
|
+
[aug[col], aug[maxRow]] = [aug[maxRow], aug[col]];
|
|
441
|
+
for (let row = col + 1; row < n; row++) {
|
|
442
|
+
const f = aug[row][col] / aug[col][col];
|
|
443
|
+
for (let k = col; k <= n; k++) aug[row][k] -= f * aug[col][k];
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const x = new Array(n);
|
|
447
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
448
|
+
x[i] = aug[i][n];
|
|
449
|
+
for (let j = i + 1; j < n; j++) x[i] -= aug[i][j] * x[j];
|
|
450
|
+
x[i] /= aug[i][i];
|
|
451
|
+
}
|
|
452
|
+
return x;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ============================================================================
|
|
456
|
+
// MONTE CARLO INTEGRATION
|
|
457
|
+
// ============================================================================
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Monte Carlo integration of f over a multi-dimensional box.
|
|
461
|
+
* @param {Function} f - f(point: number[]) → number
|
|
462
|
+
* @param {Array<[number, number]>} bounds - [[a1,b1], [a2,b2], ...]
|
|
463
|
+
* @param {number} [nSamples=100000]
|
|
464
|
+
* @returns {{ estimate, stdError, ci95 }}
|
|
465
|
+
*/
|
|
466
|
+
export function monteCarloIntegrate(f, bounds, nSamples = 100_000) {
|
|
467
|
+
const volume = bounds.reduce((v, [a, b]) => v * (b - a), 1);
|
|
468
|
+
const samples = [];
|
|
469
|
+
|
|
470
|
+
for (let i = 0; i < nSamples; i++) {
|
|
471
|
+
const point = bounds.map(([a, b]) => a + Math.random() * (b - a));
|
|
472
|
+
try {
|
|
473
|
+
samples.push(f(point));
|
|
474
|
+
} catch {
|
|
475
|
+
samples.push(0);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const mean = samples.reduce((s, v) => s + v, 0) / nSamples;
|
|
480
|
+
const variance = samples.reduce((s, v) => s + (v - mean) ** 2, 0) / nSamples;
|
|
481
|
+
const stdError = Math.sqrt(variance / nSamples) * volume;
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
estimate: mean * volume,
|
|
485
|
+
stdError,
|
|
486
|
+
ci95: [mean * volume - 1.96 * stdError, mean * volume + 1.96 * stdError],
|
|
487
|
+
samples: nSamples
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Monte Carlo simulation — generate samples from a distribution.
|
|
493
|
+
* @param {Function} invCDF - Inverse CDF (quantile function)
|
|
494
|
+
* @param {number} n
|
|
495
|
+
* @returns {number[]}
|
|
496
|
+
*/
|
|
497
|
+
export function mcSample(invCDF, n) {
|
|
498
|
+
return Array.from({ length: n }, () => invCDF(Math.random()));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ============================================================================
|
|
502
|
+
// PRIVATE HELPERS
|
|
503
|
+
// ============================================================================
|
|
504
|
+
|
|
505
|
+
function _simpsonIntegrate(f, a, b, n) {
|
|
506
|
+
if (n % 2 !== 0) n++;
|
|
507
|
+
const h = (b - a) / n;
|
|
508
|
+
let sum = f(a) + f(b);
|
|
509
|
+
for (let i = 1; i < n; i++) {
|
|
510
|
+
sum += (i % 2 === 0 ? 2 : 4) * f(a + i * h);
|
|
511
|
+
}
|
|
512
|
+
return (h / 3) * sum;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function _erf(x) {
|
|
516
|
+
// Abramowitz & Stegun approximation
|
|
517
|
+
const t = 1 / (1 + 0.3275911 * Math.abs(x));
|
|
518
|
+
const poly = t * (0.254829592 + t * (-0.284496736 + t * (1.421413741 + t * (-1.453152027 + t * 1.061405429))));
|
|
519
|
+
const sign = x >= 0 ? 1 : -1;
|
|
520
|
+
return sign * (1 - poly * Math.exp(-x * x));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function _invNormalStd(p) {
|
|
524
|
+
// Beasley-Springer-Moro algorithm
|
|
525
|
+
if (p < 0.5) return -_rationalApprox(Math.sqrt(-2 * Math.log(p)));
|
|
526
|
+
return _rationalApprox(Math.sqrt(-2 * Math.log(1 - p)));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function _rationalApprox(t) {
|
|
530
|
+
const c = [2.515517, 0.802853, 0.010328];
|
|
531
|
+
const d = [1.432788, 0.189269, 0.001308];
|
|
532
|
+
return t - (c[0] + c[1] * t + c[2] * t * t) / (1 + d[0] * t + d[1] * t * t + d[2] * t * t * t);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function _gammaFn(n) {
|
|
536
|
+
// Lanczos approximation
|
|
537
|
+
if (n < 0.5) return Math.PI / (Math.sin(Math.PI * n) * _gammaFn(1 - n));
|
|
538
|
+
n -= 1;
|
|
539
|
+
const a = [0.99999999999980993, 676.5203681218851, -1259.1392167224028,
|
|
540
|
+
771.32342877765313, -176.61502916214059, 12.507343278686905,
|
|
541
|
+
-0.13857109526572012, 9.9843695780195716e-6, 1.5056327351493116e-7];
|
|
542
|
+
let x = a[0];
|
|
543
|
+
for (let i = 1; i < 9; i++) x += a[i] / (n + i);
|
|
544
|
+
const t = n + 7.5;
|
|
545
|
+
return Math.sqrt(2 * Math.PI) * Math.pow(t, n + 0.5) * Math.exp(-t) * x;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function _betaFn(a, b) {
|
|
549
|
+
return _gammaFn(a) * _gammaFn(b) / _gammaFn(a + b);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function _gammaCDF(x, a) {
|
|
553
|
+
// Regularized incomplete gamma function via numerical integration
|
|
554
|
+
if (x <= 0) return 0;
|
|
555
|
+
// Simple approximation using series
|
|
556
|
+
let sum = 0, term = 1 / _gammaFn(a + 1);
|
|
557
|
+
for (let n = 0; n < 200; n++) {
|
|
558
|
+
sum += term;
|
|
559
|
+
term *= x / (a + n + 1);
|
|
560
|
+
if (term < 1e-14) break;
|
|
561
|
+
}
|
|
562
|
+
return Math.pow(x, a) * Math.exp(-x) * sum;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function _regIncBeta(x, a, b) {
|
|
566
|
+
// Continued fraction approximation
|
|
567
|
+
if (x <= 0) return 0;
|
|
568
|
+
if (x >= 1) return 1;
|
|
569
|
+
const lbeta = _gammaFn(a) * _gammaFn(b) / _gammaFn(a + b);
|
|
570
|
+
const front = Math.pow(x, a) * Math.pow(1 - x, b) / lbeta;
|
|
571
|
+
|
|
572
|
+
// Lentz's algorithm for continued fraction
|
|
573
|
+
let f = 1, c = 1, d = 1 - (a + b) * x / (a + 1);
|
|
574
|
+
if (Math.abs(d) < 1e-30) d = 1e-30;
|
|
575
|
+
d = 1 / d; f = d;
|
|
576
|
+
|
|
577
|
+
for (let m = 1; m <= 200; m++) {
|
|
578
|
+
// Even step
|
|
579
|
+
let num = m * (b - m) * x / ((a + 2 * m - 1) * (a + 2 * m));
|
|
580
|
+
d = 1 + num * d; if (Math.abs(d) < 1e-30) d = 1e-30;
|
|
581
|
+
c = 1 + num / c; if (Math.abs(c) < 1e-30) c = 1e-30;
|
|
582
|
+
d = 1 / d; f *= d * c;
|
|
583
|
+
|
|
584
|
+
// Odd step
|
|
585
|
+
num = -(a + m) * (a + b + m) * x / ((a + 2 * m) * (a + 2 * m + 1));
|
|
586
|
+
d = 1 + num * d; if (Math.abs(d) < 1e-30) d = 1e-30;
|
|
587
|
+
c = 1 + num / c; if (Math.abs(c) < 1e-30) c = 1e-30;
|
|
588
|
+
d = 1 / d;
|
|
589
|
+
const delta = d * c;
|
|
590
|
+
f *= delta;
|
|
591
|
+
if (Math.abs(delta - 1) < 1e-10) break;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return front * f / a;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function _tCritical(p, df) {
|
|
598
|
+
// Approximate t-critical using inverse normal for large df
|
|
599
|
+
if (df > 100) return normalInvCDF(p);
|
|
600
|
+
// Binary search
|
|
601
|
+
let lo = 0, hi = 10;
|
|
602
|
+
for (let i = 0; i < 100; i++) {
|
|
603
|
+
const mid = (lo + hi) / 2;
|
|
604
|
+
if (tCDF(mid, df) < p) lo = mid; else hi = mid;
|
|
605
|
+
}
|
|
606
|
+
return (lo + hi) / 2;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function _factorial(n) {
|
|
610
|
+
if (n <= 1) return 1;
|
|
611
|
+
let f = 1;
|
|
612
|
+
for (let i = 2; i <= n; i++) f *= i;
|
|
613
|
+
return f;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function _comb(n, k) {
|
|
617
|
+
if (k > n || k < 0) return 0;
|
|
618
|
+
if (k === 0 || k === n) return 1;
|
|
619
|
+
k = Math.min(k, n - k);
|
|
620
|
+
let c = 1;
|
|
621
|
+
for (let i = 0; i < k; i++) c = c * (n - i) / (i + 1);
|
|
622
|
+
return Math.round(c);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
// ============================================================================
|
|
627
|
+
// BAYESIAN INFERENCE
|
|
628
|
+
// ============================================================================
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Beta-Binomial conjugate update.
|
|
632
|
+
* Prior: Beta(alpha, beta) → Posterior: Beta(alpha + k, beta + n - k)
|
|
633
|
+
* @param {number} alpha prior successes pseudo-count
|
|
634
|
+
* @param {number} beta prior failures pseudo-count
|
|
635
|
+
* @param {number} k observed successes
|
|
636
|
+
* @param {number} n total observations
|
|
637
|
+
* @returns {{ alpha, beta, mean, variance, credibleInterval95 }}
|
|
638
|
+
*/
|
|
639
|
+
export function betaBinomialUpdate(alpha, beta, k, n) {
|
|
640
|
+
const a = alpha + k;
|
|
641
|
+
const b = beta + n - k;
|
|
642
|
+
const mean = a / (a + b);
|
|
643
|
+
const variance = (a * b) / ((a + b) ** 2 * (a + b + 1));
|
|
644
|
+
// 95% credible interval via Beta quantile approximation
|
|
645
|
+
const lo = betaInvCDF(0.025, a, b);
|
|
646
|
+
const hi = betaInvCDF(0.975, a, b);
|
|
647
|
+
return { alpha: a, beta: b, mean, variance, credibleInterval95: [lo, hi] };
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Regularised incomplete beta function (continued fraction, Lentz's method)
|
|
651
|
+
function regularisedBeta(x, a, b) {
|
|
652
|
+
if (x < 0 || x > 1) return NaN;
|
|
653
|
+
if (x === 0) return 0;
|
|
654
|
+
if (x === 1) return 1;
|
|
655
|
+
const lbeta = _logGamma(a) + _logGamma(b) - _logGamma(a + b);
|
|
656
|
+
const front = Math.exp(Math.log(x) * a + Math.log(1 - x) * b - lbeta) / a;
|
|
657
|
+
// Use symmetry relation for stability
|
|
658
|
+
if (x > (a + 1) / (a + b + 2)) return 1 - regularisedBeta(1 - x, b, a);
|
|
659
|
+
// Lentz continued fraction
|
|
660
|
+
let C = 1, D = 1 - (a + b) * x / (a + 1); D = Math.abs(D) < 1e-30 ? 1e-30 : 1 / D;
|
|
661
|
+
let h = D;
|
|
662
|
+
for (let m = 1; m <= 200; m++) {
|
|
663
|
+
let num = m * (b - m) * x / ((a + 2 * m - 1) * (a + 2 * m));
|
|
664
|
+
D = 1 + num * D; C = 1 + num / C;
|
|
665
|
+
D = Math.abs(D) < 1e-30 ? 1e-30 : 1 / D;
|
|
666
|
+
h *= D * C;
|
|
667
|
+
num = -(a + m) * (a + b + m) * x / ((a + 2 * m) * (a + 2 * m + 1));
|
|
668
|
+
D = 1 + num * D; C = 1 + num / C;
|
|
669
|
+
D = Math.abs(D) < 1e-30 ? 1e-30 : 1 / D;
|
|
670
|
+
const delta = D * C;
|
|
671
|
+
h *= delta;
|
|
672
|
+
if (Math.abs(delta - 1) < 1e-12) break;
|
|
673
|
+
}
|
|
674
|
+
return front * h;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function betaInvCDF(p, a, b) {
|
|
678
|
+
// Newton-Raphson on regularisedBeta
|
|
679
|
+
let x = a / (a + b);
|
|
680
|
+
for (let i = 0; i < 100; i++) {
|
|
681
|
+
const f = regularisedBeta(x, a, b) - p;
|
|
682
|
+
const df = Math.exp((a - 1) * Math.log(x) + (b - 1) * Math.log(1 - x) - _logGamma(a) - _logGamma(b) + _logGamma(a + b));
|
|
683
|
+
const dx = -f / df;
|
|
684
|
+
x = Math.max(1e-12, Math.min(1 - 1e-12, x + dx));
|
|
685
|
+
if (Math.abs(dx) < 1e-12) break;
|
|
686
|
+
}
|
|
687
|
+
return x;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Normal-Normal conjugate update (known variance).
|
|
692
|
+
* Prior: N(mu0, sigma0²) → Posterior: N(mu_n, sigma_n²)
|
|
693
|
+
*/
|
|
694
|
+
export function normalNormalUpdate(mu0, sigma0, data, sigmaLikelihood) {
|
|
695
|
+
const n = data.length;
|
|
696
|
+
const xbar = data.reduce((a, b) => a + b, 0) / n;
|
|
697
|
+
const sigma0Sq = sigma0 ** 2, sigLikSq = sigmaLikelihood ** 2;
|
|
698
|
+
const sigmaNSq = 1 / (1 / sigma0Sq + n / sigLikSq);
|
|
699
|
+
const muN = sigmaNSq * (mu0 / sigma0Sq + n * xbar / sigLikSq);
|
|
700
|
+
const sigmaN = Math.sqrt(sigmaNSq);
|
|
701
|
+
return {
|
|
702
|
+
posteriorMean: muN, posteriorStd: sigmaN,
|
|
703
|
+
credibleInterval95: [muN - 1.96 * sigmaN, muN + 1.96 * sigmaN]
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// ============================================================================
|
|
708
|
+
// EFFECT SIZE & POWER ANALYSIS
|
|
709
|
+
// ============================================================================
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Cohen's d — standardised difference between two means.
|
|
713
|
+
* Commonly used to characterise practical (not just statistical) significance.
|
|
714
|
+
* |d| < 0.2 → negligible, ~0.5 → medium, > 0.8 → large.
|
|
715
|
+
*/
|
|
716
|
+
export function cohensD(data1, data2) {
|
|
717
|
+
const n1 = data1.length, n2 = data2.length;
|
|
718
|
+
const m1 = data1.reduce((a, b) => a + b, 0) / n1;
|
|
719
|
+
const m2 = data2.reduce((a, b) => a + b, 0) / n2;
|
|
720
|
+
const v1 = data1.reduce((s, x) => s + (x - m1) ** 2, 0) / (n1 - 1);
|
|
721
|
+
const v2 = data2.reduce((s, x) => s + (x - m2) ** 2, 0) / (n2 - 1);
|
|
722
|
+
const pooledSD = Math.sqrt(((n1 - 1) * v1 + (n2 - 1) * v2) / (n1 + n2 - 2));
|
|
723
|
+
const d = (m1 - m2) / pooledSD;
|
|
724
|
+
const magnitude = Math.abs(d) < 0.2 ? 'negligible' : Math.abs(d) < 0.5 ? 'small' : Math.abs(d) < 0.8 ? 'medium' : 'large';
|
|
725
|
+
return { d, pooledSD, magnitude };
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Estimate required sample size per group for a two-sample t-test.
|
|
730
|
+
* @param {number} d expected Cohen's d
|
|
731
|
+
* @param {number} alpha significance level (e.g. 0.05)
|
|
732
|
+
* @param {number} power desired power (e.g. 0.80)
|
|
733
|
+
*/
|
|
734
|
+
export function sampleSizeTTest(d, alpha = 0.05, power = 0.80) {
|
|
735
|
+
// Approximate formula (balanced design)
|
|
736
|
+
const za = _normalInvCDF(1 - alpha / 2);
|
|
737
|
+
const zb = _normalInvCDF(power);
|
|
738
|
+
const n = Math.ceil(2 * ((za + zb) / d) ** 2);
|
|
739
|
+
return { n, totalN: 2 * n, alpha, power, effectSize: d };
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function _normalInvCDF(p) {
|
|
743
|
+
// Abramowitz & Stegun rational approximation
|
|
744
|
+
if (p <= 0) return -Infinity;
|
|
745
|
+
if (p >= 1) return Infinity;
|
|
746
|
+
const q = p < 0.5 ? p : 1 - p;
|
|
747
|
+
const t = Math.sqrt(-2 * Math.log(q));
|
|
748
|
+
const c = [2.515517, 0.802853, 0.010328];
|
|
749
|
+
const d = [1.432788, 0.189269, 0.001308];
|
|
750
|
+
const z = t - (c[0] + c[1]*t + c[2]*t*t) / (1 + d[0]*t + d[1]*t*t + d[2]*t*t*t);
|
|
751
|
+
return p < 0.5 ? -z : z;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// ============================================================================
|
|
755
|
+
// BOOTSTRAP INFERENCE
|
|
756
|
+
// ============================================================================
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Bootstrap confidence interval for a statistic.
|
|
760
|
+
* @param {number[]} data
|
|
761
|
+
* @param {Function} statFn e.g. data => mean(data)
|
|
762
|
+
* @param {number} [B=2000] bootstrap replications
|
|
763
|
+
* @param {number} [alpha=0.05]
|
|
764
|
+
* @returns {{ observed, ci, se, bootStats }}
|
|
765
|
+
*/
|
|
766
|
+
export function bootstrapCI(data, statFn, B = 2000, alpha = 0.05) {
|
|
767
|
+
const n = data.length;
|
|
768
|
+
const observed = statFn(data);
|
|
769
|
+
const bootStats = Array.from({ length: B }, () => {
|
|
770
|
+
const sample = Array.from({ length: n }, () => data[Math.floor(Math.random() * n)]);
|
|
771
|
+
return statFn(sample);
|
|
772
|
+
}).sort((a, b) => a - b);
|
|
773
|
+
const lo = bootStats[Math.floor(B * alpha / 2)];
|
|
774
|
+
const hi = bootStats[Math.floor(B * (1 - alpha / 2))];
|
|
775
|
+
const se = Math.sqrt(bootStats.reduce((s, v) => s + (v - observed) ** 2, 0) / B);
|
|
776
|
+
return { observed, ci: [lo, hi], se, bootStats };
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// ============================================================================
|
|
780
|
+
// CORRELATION
|
|
781
|
+
// ============================================================================
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Pearson correlation coefficient between two arrays.
|
|
785
|
+
*/
|
|
786
|
+
export function pearsonR(x, y) {
|
|
787
|
+
const n = x.length;
|
|
788
|
+
const mx = x.reduce((a, b) => a + b) / n;
|
|
789
|
+
const my = y.reduce((a, b) => a + b) / n;
|
|
790
|
+
const num = x.reduce((s, xi, i) => s + (xi - mx) * (y[i] - my), 0);
|
|
791
|
+
const dx = Math.sqrt(x.reduce((s, xi) => s + (xi - mx) ** 2, 0));
|
|
792
|
+
const dy = Math.sqrt(y.reduce((s, yi) => s + (yi - my) ** 2, 0));
|
|
793
|
+
const r = num / (dx * dy);
|
|
794
|
+
// t-statistic and p-value
|
|
795
|
+
const t = r * Math.sqrt(n - 2) / Math.sqrt(1 - r * r);
|
|
796
|
+
return { r, r2: r * r, t, n };
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Spearman rank correlation coefficient.
|
|
801
|
+
*/
|
|
802
|
+
export function spearmanRho(x, y) {
|
|
803
|
+
const rank = arr => {
|
|
804
|
+
const sorted = arr.slice().sort((a, b) => a - b);
|
|
805
|
+
return arr.map(v => sorted.indexOf(v) + 1);
|
|
806
|
+
};
|
|
807
|
+
return pearsonR(rank(x), rank(y));
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Covariance matrix of a data matrix X (n_samples × n_features).
|
|
812
|
+
*/
|
|
813
|
+
export function covarianceMatrix(X) {
|
|
814
|
+
const n = X.length, p = X[0].length;
|
|
815
|
+
const mu = X[0].map((_, j) => X.reduce((s, r) => s + r[j], 0) / n);
|
|
816
|
+
const C = Array.from({ length: p }, () => Array(p).fill(0));
|
|
817
|
+
for (const row of X) {
|
|
818
|
+
const d = row.map((v, j) => v - mu[j]);
|
|
819
|
+
for (let i = 0; i < p; i++)
|
|
820
|
+
for (let j = 0; j < p; j++)
|
|
821
|
+
C[i][j] += d[i] * d[j];
|
|
822
|
+
}
|
|
823
|
+
return C.map(r => r.map(v => v / (n - 1)));
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// ============================================================================
|
|
827
|
+
// NONPARAMETRIC TESTS
|
|
828
|
+
// ============================================================================
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Mann-Whitney U test (Wilcoxon rank-sum) for two independent samples.
|
|
832
|
+
* Tests H₀: the two samples come from the same distribution.
|
|
833
|
+
* Uses normal approximation for sample sizes > 20.
|
|
834
|
+
*/
|
|
835
|
+
export function mannWhitneyU(x, y) {
|
|
836
|
+
const nx = x.length, ny = y.length;
|
|
837
|
+
const combined = [...x.map(v => ({ v, group: 0 })), ...y.map(v => ({ v, group: 1 }))];
|
|
838
|
+
combined.sort((a, b) => a.v - b.v);
|
|
839
|
+
// Assign ranks (average for ties)
|
|
840
|
+
const ranks = Array(combined.length).fill(0);
|
|
841
|
+
let i = 0;
|
|
842
|
+
while (i < combined.length) {
|
|
843
|
+
let j = i;
|
|
844
|
+
while (j < combined.length - 1 && combined[j + 1].v === combined[j].v) j++;
|
|
845
|
+
const avgRank = (i + j + 2) / 2;
|
|
846
|
+
for (let k = i; k <= j; k++) ranks[k] = avgRank;
|
|
847
|
+
i = j + 1;
|
|
848
|
+
}
|
|
849
|
+
const R1 = combined.reduce((s, _, k) => combined[k].group === 0 ? s + ranks[k] : s, 0);
|
|
850
|
+
const U1 = R1 - nx * (nx + 1) / 2;
|
|
851
|
+
const U2 = nx * ny - U1;
|
|
852
|
+
const U = Math.min(U1, U2);
|
|
853
|
+
const mu = nx * ny / 2;
|
|
854
|
+
const sigma = Math.sqrt(nx * ny * (nx + ny + 1) / 12);
|
|
855
|
+
const z = (U - mu) / sigma;
|
|
856
|
+
const pValue = 2 * (1 - _normalCDF(Math.abs(z)));
|
|
857
|
+
return { U, U1, U2, z, pValue, significant: pValue < 0.05 };
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function _normalCDF(z) {
|
|
861
|
+
return 0.5 * (1 + _erf(z / Math.SQRT2));
|
|
862
|
+
}
|
|
863
|
+
function _erf(x) {
|
|
864
|
+
const t = 1 / (1 + 0.3275911 * Math.abs(x));
|
|
865
|
+
const poly = t * (0.254829592 + t * (-0.284496736 + t * (1.421413741 + t * (-1.453152027 + t * 1.061405429))));
|
|
866
|
+
return Math.sign(x) * (1 - poly * Math.exp(-x * x));
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
// ============================================================================
|
|
871
|
+
// TIME SERIES ANALYSIS
|
|
872
|
+
// ============================================================================
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Simple moving average.
|
|
876
|
+
* @param {number[]} data
|
|
877
|
+
* @param {number} window
|
|
878
|
+
* @returns {number[]} same length as data; first (window-1) entries are NaN
|
|
879
|
+
*/
|
|
880
|
+
export function movingAverage(data, window) {
|
|
881
|
+
return data.map((_, i) => {
|
|
882
|
+
if (i < window - 1) return NaN;
|
|
883
|
+
return data.slice(i - window + 1, i + 1).reduce((a, b) => a + b) / window;
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Exponential moving average (EMA).
|
|
889
|
+
* @param {number[]} data
|
|
890
|
+
* @param {number} alpha smoothing factor ∈ (0, 1]
|
|
891
|
+
*/
|
|
892
|
+
export function exponentialMovingAverage(data, alpha) {
|
|
893
|
+
const result = [data[0]];
|
|
894
|
+
for (let i = 1; i < data.length; i++) {
|
|
895
|
+
result.push(alpha * data[i] + (1 - alpha) * result[i - 1]);
|
|
896
|
+
}
|
|
897
|
+
return result;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Autocorrelation function (ACF) at lags 0…maxLag.
|
|
902
|
+
* @param {number[]} data
|
|
903
|
+
* @param {number} maxLag
|
|
904
|
+
* @returns {number[]}
|
|
905
|
+
*/
|
|
906
|
+
export function acf(data, maxLag) {
|
|
907
|
+
const n = data.length;
|
|
908
|
+
const mu = data.reduce((a, b) => a + b) / n;
|
|
909
|
+
const c0 = data.reduce((s, x) => s + (x - mu) ** 2, 0) / n;
|
|
910
|
+
return Array.from({ length: maxLag + 1 }, (_, k) => {
|
|
911
|
+
const ck = data.slice(0, n - k).reduce((s, x, i) => s + (x - mu) * (data[i + k] - mu), 0) / n;
|
|
912
|
+
return ck / c0;
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Partial autocorrelation function (PACF) via Yule-Walker equations.
|
|
918
|
+
*/
|
|
919
|
+
export function pacf(data, maxLag) {
|
|
920
|
+
const r = acf(data, maxLag);
|
|
921
|
+
const phi = [[r[1]]]; // phi[k-1][k-1]
|
|
922
|
+
const result = [1, r[1]];
|
|
923
|
+
|
|
924
|
+
for (let k = 2; k <= maxLag; k++) {
|
|
925
|
+
const prevPhi = phi[k - 2];
|
|
926
|
+
const num = r[k] - prevPhi.reduce((s, v, j) => s + v * r[k - 1 - j], 0);
|
|
927
|
+
const den = 1 - prevPhi.reduce((s, v, j) => s + v * r[j + 1], 0);
|
|
928
|
+
const phiKK = num / den;
|
|
929
|
+
const newPhi = Array.from({ length: k }, (_, j) =>
|
|
930
|
+
j < k - 1 ? prevPhi[j] - phiKK * prevPhi[k - 2 - j] : phiKK
|
|
931
|
+
);
|
|
932
|
+
phi.push(newPhi);
|
|
933
|
+
result.push(phiKK);
|
|
934
|
+
}
|
|
935
|
+
return result;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* AR(p) model: fit autoregressive model via Yule-Walker equations.
|
|
940
|
+
* Returns coefficients φ₁…φₚ and estimated noise variance σ².
|
|
941
|
+
*/
|
|
942
|
+
export function fitAR(data, p) {
|
|
943
|
+
const r = acf(data, p);
|
|
944
|
+
// Solve Yule-Walker: R·φ = r_vec
|
|
945
|
+
const R = Array.from({ length: p }, (_, i) =>
|
|
946
|
+
Array.from({ length: p }, (_, j) => r[Math.abs(i - j)])
|
|
947
|
+
);
|
|
948
|
+
const rVec = Array.from({ length: p }, (_, i) => r[i + 1]);
|
|
949
|
+
// Solve using Levinson-Durbin (use simple Gaussian here for correctness)
|
|
950
|
+
const phi = _gaussianElimination(R, rVec);
|
|
951
|
+
const sigma2 = data.reduce((s, x) => s + x * x, 0) / data.length
|
|
952
|
+
- phi.reduce((s, v, i) => s + v * r[i + 1], 0);
|
|
953
|
+
return { phi, sigma2, p };
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function _gaussianElimination(A, b) {
|
|
957
|
+
const n = b.length;
|
|
958
|
+
const M = A.map((row, i) => [...row, b[i]]);
|
|
959
|
+
for (let col = 0; col < n; col++) {
|
|
960
|
+
let pivRow = col;
|
|
961
|
+
for (let row = col + 1; row < n; row++) {
|
|
962
|
+
if (Math.abs(M[row][col]) > Math.abs(M[pivRow][col])) pivRow = row;
|
|
963
|
+
}
|
|
964
|
+
[M[col], M[pivRow]] = [M[pivRow], M[col]];
|
|
965
|
+
for (let row = col + 1; row < n; row++) {
|
|
966
|
+
const factor = M[row][col] / M[col][col];
|
|
967
|
+
for (let k = col; k <= n; k++) M[row][k] -= factor * M[col][k];
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
const x = Array(n).fill(0);
|
|
971
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
972
|
+
x[i] = (M[i][n] - M[i].slice(i + 1, n).reduce((s, v, k) => s + v * x[i + 1 + k], 0)) / M[i][i];
|
|
973
|
+
}
|
|
974
|
+
return x;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Augmented Dickey-Fuller test for stationarity (simplified, no lag correction).
|
|
979
|
+
* Tests H₀: unit root (non-stationary).
|
|
980
|
+
* Returns ADF statistic; compare with critical values: -3.43 (1%), -2.86 (5%), -2.57 (10%).
|
|
981
|
+
*/
|
|
982
|
+
export function adfTest(data) {
|
|
983
|
+
const n = data.length;
|
|
984
|
+
const dy = data.slice(1).map((v, i) => v - data[i]); // first differences
|
|
985
|
+
const y_lag = data.slice(0, n - 1); // lagged levels
|
|
986
|
+
// OLS: dy = α + β·y_lag
|
|
987
|
+
const { a: alpha, b: beta, r2 } = linearRegressionRaw(y_lag, dy);
|
|
988
|
+
// Standard error of β
|
|
989
|
+
const yHat = y_lag.map(x => alpha + beta * x);
|
|
990
|
+
const sse = dy.reduce((s, v, i) => s + (v - yHat[i]) ** 2, 0);
|
|
991
|
+
const sxx = y_lag.reduce((s, x) => s + (x - y_lag.reduce((a, b) => a + b) / (n - 1)) ** 2, 0);
|
|
992
|
+
const se_beta = Math.sqrt(sse / ((n - 2) * sxx));
|
|
993
|
+
const t_stat = beta / se_beta;
|
|
994
|
+
return {
|
|
995
|
+
adfStatistic: t_stat,
|
|
996
|
+
pValueApprox: t_stat < -3.43 ? 0.01 : t_stat < -2.86 ? 0.05 : t_stat < -2.57 ? 0.10 : null,
|
|
997
|
+
criticalValues: { '1%': -3.43, '5%': -2.86, '10%': -2.57 },
|
|
998
|
+
stationary: t_stat < -2.86,
|
|
999
|
+
beta, alpha
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function linearRegressionRaw(x, y) {
|
|
1004
|
+
const n = x.length;
|
|
1005
|
+
const mx = x.reduce((a, b) => a + b) / n, my = y.reduce((a, b) => a + b) / n;
|
|
1006
|
+
const sxy = x.reduce((s, xi, i) => s + (xi - mx) * (y[i] - my), 0);
|
|
1007
|
+
const sxx = x.reduce((s, xi) => s + (xi - mx) ** 2, 0);
|
|
1008
|
+
const b = sxy / sxx, a = my - b * mx;
|
|
1009
|
+
const yHat = x.map(xi => a + b * xi);
|
|
1010
|
+
const ss_res = y.reduce((s, yi, i) => s + (yi - yHat[i]) ** 2, 0);
|
|
1011
|
+
const ss_tot = y.reduce((s, yi) => s + (yi - my) ** 2, 0);
|
|
1012
|
+
return { a, b, r2: 1 - ss_res / ss_tot };
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// ============================================================================
|
|
1016
|
+
// INFORMATION THEORY
|
|
1017
|
+
// ============================================================================
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Shannon entropy of a discrete probability distribution.
|
|
1021
|
+
* H(X) = −Σ p_i · log₂(p_i)
|
|
1022
|
+
* @param {number[]} probs must sum to ~1
|
|
1023
|
+
*/
|
|
1024
|
+
export function shannonEntropy(probs) {
|
|
1025
|
+
return -probs.filter(p => p > 0).reduce((s, p) => s + p * Math.log2(p), 0);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* KL divergence (relative entropy) D_KL(P||Q) = Σ P_i · log(P_i / Q_i).
|
|
1030
|
+
* Not symmetric. Returns Infinity if Q_i = 0 where P_i > 0.
|
|
1031
|
+
*/
|
|
1032
|
+
export function klDivergence(P, Q) {
|
|
1033
|
+
return P.reduce((s, pi, i) => {
|
|
1034
|
+
if (pi <= 0) return s;
|
|
1035
|
+
if (Q[i] <= 0) return Infinity;
|
|
1036
|
+
return s + pi * Math.log(pi / Q[i]);
|
|
1037
|
+
}, 0);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Jensen-Shannon divergence (symmetric, bounded in [0, ln2]).
|
|
1042
|
+
*/
|
|
1043
|
+
export function jsDivergence(P, Q) {
|
|
1044
|
+
const M = P.map((pi, i) => (pi + Q[i]) / 2);
|
|
1045
|
+
return 0.5 * klDivergence(P, M) + 0.5 * klDivergence(Q, M);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Mutual information I(X;Y) from a joint probability matrix.
|
|
1050
|
+
* @param {number[][]} joint n×m matrix of joint probabilities
|
|
1051
|
+
*/
|
|
1052
|
+
export function mutualInformation(joint) {
|
|
1053
|
+
const n = joint.length, m = joint[0].length;
|
|
1054
|
+
const px = joint.map(row => row.reduce((a, b) => a + b, 0));
|
|
1055
|
+
const py = joint[0].map((_, j) => joint.reduce((s, row) => s + row[j], 0));
|
|
1056
|
+
let mi = 0;
|
|
1057
|
+
for (let i = 0; i < n; i++) {
|
|
1058
|
+
for (let j = 0; j < m; j++) {
|
|
1059
|
+
const pij = joint[i][j];
|
|
1060
|
+
if (pij > 0) mi += pij * Math.log(pij / (px[i] * py[j]));
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
return mi;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// ============================================================================
|
|
1067
|
+
// KERNEL DENSITY ESTIMATION
|
|
1068
|
+
// ============================================================================
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* Kernel density estimation using Gaussian kernel.
|
|
1072
|
+
* @param {number[]} data
|
|
1073
|
+
* @param {number} [bandwidth] Silverman's rule of thumb if omitted
|
|
1074
|
+
* @returns {{ evaluate: (x) => number, bandwidth }}
|
|
1075
|
+
*/
|
|
1076
|
+
export function kernelDensityEstimate(data, bandwidth) {
|
|
1077
|
+
const n = data.length;
|
|
1078
|
+
const s = Math.sqrt(data.reduce((acc, v) => {
|
|
1079
|
+
const d = v - data.reduce((a, b) => a + b) / n;
|
|
1080
|
+
return acc + d * d;
|
|
1081
|
+
}, 0) / (n - 1));
|
|
1082
|
+
const h = bandwidth ?? (1.06 * s * Math.pow(n, -0.2));
|
|
1083
|
+
const evaluate = x => {
|
|
1084
|
+
const inv = 1 / (h * Math.sqrt(2 * Math.PI));
|
|
1085
|
+
return data.reduce((s, xi) => s + Math.exp(-0.5 * ((x - xi) / h) ** 2), 0) * inv / n;
|
|
1086
|
+
};
|
|
1087
|
+
return { evaluate, bandwidth: h };
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// ============================================================================
|
|
1091
|
+
// ANOVA
|
|
1092
|
+
// ============================================================================
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* One-way ANOVA test.
|
|
1096
|
+
* Tests H₀: all group means are equal.
|
|
1097
|
+
*
|
|
1098
|
+
* @param {number[][]} groups array of arrays (one per group)
|
|
1099
|
+
* @returns {{ F, pValue, dfBetween, dfWithin, msBetween, msWithin, significant }}
|
|
1100
|
+
*/
|
|
1101
|
+
export function oneWayANOVA(groups) {
|
|
1102
|
+
const k = groups.length;
|
|
1103
|
+
const N = groups.reduce((s, g) => s + g.length, 0);
|
|
1104
|
+
const grand = groups.flatMap(g => g).reduce((a, b) => a + b) / N;
|
|
1105
|
+
|
|
1106
|
+
const ssBetween = groups.reduce((s, g) => {
|
|
1107
|
+
const gMean = g.reduce((a, b) => a + b) / g.length;
|
|
1108
|
+
return s + g.length * (gMean - grand) ** 2;
|
|
1109
|
+
}, 0);
|
|
1110
|
+
|
|
1111
|
+
const ssWithin = groups.reduce((s, g) => {
|
|
1112
|
+
const gMean = g.reduce((a, b) => a + b) / g.length;
|
|
1113
|
+
return s + g.reduce((gs, v) => gs + (v - gMean) ** 2, 0);
|
|
1114
|
+
}, 0);
|
|
1115
|
+
|
|
1116
|
+
const dfB = k - 1, dfW = N - k;
|
|
1117
|
+
const msB = ssBetween / dfB, msW = ssWithin / dfW;
|
|
1118
|
+
const F = msB / msW;
|
|
1119
|
+
|
|
1120
|
+
// F-distribution p-value approximation via Wilson-Hilferty
|
|
1121
|
+
const p = _fDistPValue(F, dfB, dfW);
|
|
1122
|
+
|
|
1123
|
+
return { F, pValue: p, dfBetween: dfB, dfWithin: dfW, msBetween: msB, msWithin: msW, significant: p < 0.05 };
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function _fDistPValue(F, d1, d2) {
|
|
1127
|
+
// p-value: P(X > F) where X ~ F(d1, d2)
|
|
1128
|
+
// Via regularised incomplete beta: p = I(d2/(d2+d1*F); d2/2, d1/2)
|
|
1129
|
+
const x = d2 / (d2 + d1 * F);
|
|
1130
|
+
return _ibeta(x, d2 / 2, d1 / 2);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function _ibeta(x, a, b) {
|
|
1134
|
+
if (x <= 0) return 0; if (x >= 1) return 1;
|
|
1135
|
+
const lbeta = _logGamma(a) + _logGamma(b) - _logGamma(a + b);
|
|
1136
|
+
if (x > (a + 1) / (a + b + 2)) return 1 - _ibeta(1 - x, b, a);
|
|
1137
|
+
const front = Math.exp(Math.log(x) * a + Math.log(1 - x) * b - lbeta) / a;
|
|
1138
|
+
let C = 1, D = 1 - (a + b) * x / (a + 1);
|
|
1139
|
+
D = Math.abs(D) < 1e-30 ? 1e-30 : 1 / D;
|
|
1140
|
+
let h = D;
|
|
1141
|
+
for (let m = 1; m <= 200; m++) {
|
|
1142
|
+
let num = m * (b - m) * x / ((a + 2*m - 1) * (a + 2*m));
|
|
1143
|
+
D = 1 + num * D; C = 1 + num / C;
|
|
1144
|
+
D = Math.abs(D) < 1e-30 ? 1e-30 : 1 / D; h *= D * C;
|
|
1145
|
+
num = -(a + m) * (a + b + m) * x / ((a + 2*m) * (a + 2*m + 1));
|
|
1146
|
+
D = 1 + num * D; C = 1 + num / C;
|
|
1147
|
+
D = Math.abs(D) < 1e-30 ? 1e-30 : 1 / D;
|
|
1148
|
+
const delta = D * C; h *= delta;
|
|
1149
|
+
if (Math.abs(delta - 1) < 1e-12) break;
|
|
1150
|
+
}
|
|
1151
|
+
return front * h;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function _logGamma(x) {
|
|
1155
|
+
const cof = [76.18009172947146,-86.50532032941677,24.01409824083091,-1.231739572450155,0.1208650973866179e-2,-0.5395239384953e-5];
|
|
1156
|
+
let y = x, tmp = x + 5.5;
|
|
1157
|
+
tmp -= (x + 0.5) * Math.log(tmp);
|
|
1158
|
+
let ser = 1.000000000190015;
|
|
1159
|
+
for (let j = 0; j < 6; j++) { y++; ser += cof[j] / y; }
|
|
1160
|
+
return -tmp + Math.log(2.5066282746310005 * ser / x);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// ============================================================================
|
|
1164
|
+
// MULTIPLE REGRESSION
|
|
1165
|
+
// ============================================================================
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* Multiple linear regression: y = X·β via least squares.
|
|
1169
|
+
* Automatically adds intercept column.
|
|
1170
|
+
*
|
|
1171
|
+
* @param {number[][]} X n_samples × n_features
|
|
1172
|
+
* @param {number[]} y n_samples
|
|
1173
|
+
* @returns {{ coefficients, intercept, r2, adjR2, standardErrors, tStats, predict }}
|
|
1174
|
+
*/
|
|
1175
|
+
export function multipleRegression(X, y) {
|
|
1176
|
+
const n = X.length, p = X[0].length;
|
|
1177
|
+
// Add intercept column
|
|
1178
|
+
const Xd = X.map(row => [1, ...row]);
|
|
1179
|
+
// β = (XᵀX)⁻¹Xᵀy via least squares
|
|
1180
|
+
const Xt = Xd[0].map((_, j) => Xd.map(r => r[j]));
|
|
1181
|
+
const XtX = Xt.map(row => Xd[0].map((_, j) => row.reduce((s, v, k) => s + v * Xd[k][j], 0)));
|
|
1182
|
+
const Xty = Xt.map(row => row.reduce((s, v, k) => s + v * y[k], 0));
|
|
1183
|
+
let beta;
|
|
1184
|
+
try { beta = _gaussianElimination(XtX, Xty); }
|
|
1185
|
+
catch { return { error: 'singular system' }; }
|
|
1186
|
+
|
|
1187
|
+
const yHat = Xd.map(row => row.reduce((s, v, j) => s + v * beta[j], 0));
|
|
1188
|
+
const yMean = y.reduce((a, b) => a + b) / n;
|
|
1189
|
+
const ss_res = y.reduce((s, yi, i) => s + (yi - yHat[i]) ** 2, 0);
|
|
1190
|
+
const ss_tot = y.reduce((s, yi) => s + (yi - yMean) ** 2, 0);
|
|
1191
|
+
const r2 = 1 - ss_res / ss_tot;
|
|
1192
|
+
const adjR2 = 1 - (1 - r2) * (n - 1) / (n - p - 1);
|
|
1193
|
+
|
|
1194
|
+
// Standard errors of coefficients
|
|
1195
|
+
const s2 = ss_res / (n - p - 1);
|
|
1196
|
+
let XtXinv;
|
|
1197
|
+
try { XtXinv = _gaussianElimination(XtX, XtX.map((_, i) => XtX[i].map((_, j) => i === j ? 1 : 0)).flat()).slice(); }
|
|
1198
|
+
catch { XtXinv = null; }
|
|
1199
|
+
|
|
1200
|
+
return {
|
|
1201
|
+
coefficients: beta.slice(1),
|
|
1202
|
+
intercept: beta[0],
|
|
1203
|
+
r2, adjR2,
|
|
1204
|
+
predict: xNew => [1, ...xNew].reduce((s, v, j) => s + v * beta[j], 0),
|
|
1205
|
+
};
|
|
1206
|
+
}
|