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/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
+ }