@cloudglides/nox 1.1.5 → 2.0.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.
Files changed (58) hide show
  1. package/README.md +9 -9
  2. package/example/src/App.css +89 -84
  3. package/example/src/App.jsx +375 -151
  4. package/package.json +7 -6
  5. package/src/core.browser.js +10 -2
  6. package/src/core.js +32 -4
  7. package/src/generators/logistic.js +30 -25
  8. package/src/generators/mixer.js +7 -7
  9. package/src/generators/mt19937.js +10 -7
  10. package/src/generators/pcg64.js +23 -12
  11. package/src/generators/splitmix64.js +12 -6
  12. package/src/generators/tent.js +12 -7
  13. package/src/generators/xorshift64.js +6 -3
  14. package/src/index.d.ts +68 -4
  15. package/src/index.js +154 -2
  16. package/src/rng.browser.js +21 -10
  17. package/src/rng.js +95 -82
  18. package/src/utils/arrays.js +149 -0
  19. package/src/utils/bits.js +146 -21
  20. package/src/utils/categorical.js +68 -31
  21. package/src/utils/combinatorics.js +113 -69
  22. package/src/utils/confidence.js +145 -0
  23. package/src/utils/decomposition.js +204 -0
  24. package/src/utils/distributions-advanced.js +122 -0
  25. package/src/utils/distributions-extra.js +102 -11
  26. package/src/utils/distributions-special.js +77 -20
  27. package/src/utils/distributions.js +99 -35
  28. package/src/utils/effects.js +172 -0
  29. package/src/utils/entropy.browser.js +29 -26
  30. package/src/utils/entropy.js +18 -8
  31. package/src/utils/helpers.js +64 -0
  32. package/src/utils/hypothesis.js +167 -0
  33. package/src/utils/integration.js +137 -0
  34. package/src/utils/interpolation.js +221 -0
  35. package/src/utils/matrix.js +242 -0
  36. package/src/utils/noise.js +36 -22
  37. package/src/utils/odesolvers.js +176 -0
  38. package/src/utils/optimization.js +215 -0
  39. package/src/utils/precomputed.js +166 -0
  40. package/src/utils/probability.js +199 -0
  41. package/src/utils/regression.js +170 -0
  42. package/src/utils/resampling.js +112 -0
  43. package/src/utils/rootfinding.js +158 -0
  44. package/src/utils/sampling.js +86 -77
  45. package/src/utils/seed.js +10 -4
  46. package/src/utils/seeding.js +24 -12
  47. package/src/utils/sequence.js +116 -32
  48. package/src/utils/state.js +48 -36
  49. package/src/utils/statistics.js +64 -2
  50. package/src/utils/stochastic.js +91 -31
  51. package/src/utils/stratified.js +108 -0
  52. package/src/utils/timeseries.js +166 -0
  53. package/src/utils/transforms.js +146 -0
  54. package/test/comprehensive-new.js +126 -0
  55. package/test/comprehensive.js +4 -3
  56. package/test/error-handling.js +49 -0
  57. package/test/new-features.js +52 -0
  58. package/IMPROVEMENTS.md +0 -58
@@ -0,0 +1,166 @@
1
+ export class WeightedDistribution {
2
+ constructor(items, weights) {
3
+ if (!Array.isArray(items) || items.length === 0) {
4
+ throw new TypeError('items must be non-empty array');
5
+ }
6
+ if (!Array.isArray(weights) || weights.length !== items.length) {
7
+ throw new TypeError('weights length must match items');
8
+ }
9
+
10
+ this.items = items;
11
+ this.cumsum = new Array(items.length);
12
+ let total = 0;
13
+
14
+ for (let i = 0; i < weights.length; i++) {
15
+ if (typeof weights[i] !== 'number' || weights[i] < 0) {
16
+ throw new RangeError('all weights must be non-negative numbers');
17
+ }
18
+ total += weights[i];
19
+ this.cumsum[i] = total;
20
+ }
21
+
22
+ if (total <= 0) {
23
+ throw new RangeError('weights must sum to positive value');
24
+ }
25
+
26
+ this.total = total;
27
+ }
28
+
29
+ pick(rng) {
30
+ if (!rng || typeof rng.nextFloat !== 'function') {
31
+ throw new TypeError('rng must be an RNG instance');
32
+ }
33
+
34
+ const rand = rng.nextFloat() * this.total;
35
+ let left = 0, right = this.cumsum.length;
36
+
37
+ while (left < right) {
38
+ const mid = (left + right) >> 1;
39
+ if (rand < this.cumsum[mid]) {
40
+ right = mid;
41
+ } else {
42
+ left = mid + 1;
43
+ }
44
+ }
45
+
46
+ return this.items[left];
47
+ }
48
+
49
+ sample(rng, n) {
50
+ if (typeof n !== 'number' || n < 1) {
51
+ throw new RangeError('n must be positive integer');
52
+ }
53
+
54
+ const result = new Array(n);
55
+ for (let i = 0; i < n; i++) {
56
+ result[i] = this.pick(rng);
57
+ }
58
+ return result;
59
+ }
60
+ }
61
+
62
+ export class CategoricalDistribution {
63
+ constructor(probabilities) {
64
+ if (!Array.isArray(probabilities) || probabilities.length === 0) {
65
+ throw new TypeError('probabilities must be non-empty array');
66
+ }
67
+
68
+ this.cumsum = new Array(probabilities.length);
69
+ let total = 0;
70
+
71
+ for (let i = 0; i < probabilities.length; i++) {
72
+ if (typeof probabilities[i] !== 'number' || probabilities[i] < 0) {
73
+ throw new RangeError('all probabilities must be non-negative');
74
+ }
75
+ total += probabilities[i];
76
+ this.cumsum[i] = total;
77
+ }
78
+
79
+ if (Math.abs(total - 1) > 1e-10) {
80
+ for (let i = 0; i < this.cumsum.length; i++) {
81
+ this.cumsum[i] /= total;
82
+ }
83
+ }
84
+
85
+ this.n = probabilities.length;
86
+ }
87
+
88
+ sample(rng) {
89
+ if (!rng || typeof rng.nextFloat !== 'function') {
90
+ throw new TypeError('rng must be an RNG instance');
91
+ }
92
+
93
+ const u = rng.nextFloat();
94
+ let left = 0, right = this.cumsum.length;
95
+
96
+ while (left < right) {
97
+ const mid = (left + right) >> 1;
98
+ if (u < this.cumsum[mid]) {
99
+ right = mid;
100
+ } else {
101
+ left = mid + 1;
102
+ }
103
+ }
104
+
105
+ return left;
106
+ }
107
+
108
+ samples(rng, n) {
109
+ if (typeof n !== 'number' || n < 1) {
110
+ throw new RangeError('n must be positive integer');
111
+ }
112
+
113
+ const result = new Array(n);
114
+ for (let i = 0; i < n; i++) {
115
+ result[i] = this.sample(rng);
116
+ }
117
+ return result;
118
+ }
119
+ }
120
+
121
+ export class NormalDistribution {
122
+ constructor(mean = 0, stddev = 1) {
123
+ if (typeof mean !== 'number') {
124
+ throw new TypeError('mean must be number');
125
+ }
126
+ if (typeof stddev !== 'number' || stddev <= 0) {
127
+ throw new RangeError('stddev must be positive');
128
+ }
129
+
130
+ this.mean = mean;
131
+ this.stddev = stddev;
132
+ this.hasSpare = false;
133
+ this.spare = 0;
134
+ }
135
+
136
+ sample(rng) {
137
+ if (!rng || typeof rng.nextFloat !== 'function') {
138
+ throw new TypeError('rng must be an RNG instance');
139
+ }
140
+
141
+ if (this.hasSpare) {
142
+ this.hasSpare = false;
143
+ return this.spare * this.stddev + this.mean;
144
+ }
145
+
146
+ this.hasSpare = true;
147
+ const u1 = rng.nextFloat();
148
+ const u2 = rng.nextFloat();
149
+ const mag = Math.sqrt(-2 * Math.log(u1 > 1e-10 ? u1 : 1e-10));
150
+
151
+ this.spare = Math.sin(2 * Math.PI * u2) * mag;
152
+ return Math.cos(2 * Math.PI * u2) * mag * this.stddev + this.mean;
153
+ }
154
+
155
+ samples(rng, n) {
156
+ if (typeof n !== 'number' || n < 1) {
157
+ throw new RangeError('n must be positive integer');
158
+ }
159
+
160
+ const result = new Array(n);
161
+ for (let i = 0; i < n; i++) {
162
+ result[i] = this.sample(rng);
163
+ }
164
+ return result;
165
+ }
166
+ }
@@ -0,0 +1,199 @@
1
+ export function cdf(x, distribution = 'uniform', ...params) {
2
+ if (typeof x !== 'number') {
3
+ throw new TypeError('x must be a number');
4
+ }
5
+
6
+ switch (distribution.toLowerCase()) {
7
+ case 'uniform': {
8
+ const [min = 0, max = 1] = params;
9
+ if (x < min) return 0;
10
+ if (x > max) return 1;
11
+ return (x - min) / (max - min);
12
+ }
13
+
14
+ case 'normal': {
15
+ const [mu = 0, sigma = 1] = params;
16
+ const z = (x - mu) / sigma;
17
+ return 0.5 * (1 + erf(z / Math.sqrt(2)));
18
+ }
19
+
20
+ case 'exponential': {
21
+ const [lambda = 1] = params;
22
+ if (x < 0) return 0;
23
+ return 1 - Math.exp(-lambda * x);
24
+ }
25
+
26
+ case 'poisson': {
27
+ const [lambda = 1] = params;
28
+ if (x < 0) return 0;
29
+ const k = Math.floor(x);
30
+ let sum = 0;
31
+ const expNeg = Math.exp(-lambda);
32
+ let term = expNeg;
33
+
34
+ for (let i = 0; i <= k; i++) {
35
+ sum += term;
36
+ if (i < k) term *= lambda / (i + 1);
37
+ }
38
+ return sum;
39
+ }
40
+
41
+ case 'beta': {
42
+ const [a = 1, b = 1] = params;
43
+ if (x <= 0) return 0;
44
+ if (x >= 1) return 1;
45
+ return incompleteBeta(x, a, b);
46
+ }
47
+
48
+ default:
49
+ throw new RangeError(`unknown distribution: ${distribution}`);
50
+ }
51
+ }
52
+
53
+ export function ppf(p, distribution = 'uniform', ...params) {
54
+ if (typeof p !== 'number' || p < 0 || p > 1) {
55
+ throw new RangeError('p must be in [0, 1]');
56
+ }
57
+
58
+ switch (distribution.toLowerCase()) {
59
+ case 'uniform': {
60
+ const [min = 0, max = 1] = params;
61
+ return min + p * (max - min);
62
+ }
63
+
64
+ case 'normal': {
65
+ const [mu = 0, sigma = 1] = params;
66
+ return mu + sigma * invNormal(p);
67
+ }
68
+
69
+ case 'exponential': {
70
+ const [lambda = 1] = params;
71
+ if (p === 0) return 0;
72
+ if (p === 1) return Infinity;
73
+ return -Math.log(1 - p) / lambda;
74
+ }
75
+
76
+ case 'beta': {
77
+ const [a = 1, b = 1] = params;
78
+ if (p === 0) return 0;
79
+ if (p === 1) return 1;
80
+ return invIncompleteBeta(p, a, b);
81
+ }
82
+
83
+ default:
84
+ throw new RangeError(`unknown distribution: ${distribution}`);
85
+ }
86
+ }
87
+
88
+ export function sf(x, distribution = 'uniform', ...params) {
89
+ if (typeof x !== 'number') {
90
+ throw new TypeError('x must be a number');
91
+ }
92
+
93
+ return 1 - cdf(x, distribution, ...params);
94
+ }
95
+
96
+ function erf(x) {
97
+ const a1 = 0.254829592;
98
+ const a2 = -0.284496736;
99
+ const a3 = 1.421413741;
100
+ const a4 = -1.453152027;
101
+ const a5 = 1.061405429;
102
+ const p = 0.3275911;
103
+
104
+ const sign = x < 0 ? -1 : 1;
105
+ const absX = Math.abs(x);
106
+ const t = 1 / (1 + p * absX);
107
+ const t2 = t * t;
108
+ const t3 = t2 * t;
109
+ const t4 = t3 * t;
110
+ const t5 = t4 * t;
111
+
112
+ return sign * (1 - (a1 * t + a2 * t2 + a3 * t3 + a4 * t4 + a5 * t5) * Math.exp(-absX * absX));
113
+ }
114
+
115
+ function invNormal(p) {
116
+ if (p <= 0.5) {
117
+ const t = Math.sqrt(-2 * Math.log(p));
118
+ const c0 = 2.515517;
119
+ const c1 = 0.802853;
120
+ const c2 = 0.010328;
121
+ const d1 = 1.432788;
122
+ const d2 = 0.189269;
123
+ const d3 = 0.001308;
124
+ return -(t - (c0 + c1 * t + c2 * t * t) / (1 + d1 * t + d2 * t * t + d3 * t * t * t));
125
+ } else {
126
+ const t = Math.sqrt(-2 * Math.log(1 - p));
127
+ const c0 = 2.515517;
128
+ const c1 = 0.802853;
129
+ const c2 = 0.010328;
130
+ const d1 = 1.432788;
131
+ const d2 = 0.189269;
132
+ const d3 = 0.001308;
133
+ return t - (c0 + c1 * t + c2 * t * t) / (1 + d1 * t + d2 * t * t + d3 * t * t * t);
134
+ }
135
+ }
136
+
137
+ function incompleteBeta(x, a, b) {
138
+ const maxIterations = 100;
139
+ const epsilon = 1e-10;
140
+
141
+ let sum = 1;
142
+ let term = 1;
143
+
144
+ for (let i = 1; i <= maxIterations; i++) {
145
+ term *= (a + b - 1) * x / ((a + i - 1) * (1 - x / (a + i)));
146
+ const nextSum = sum + term;
147
+
148
+ if (Math.abs(nextSum - sum) < epsilon) {
149
+ break;
150
+ }
151
+ sum = nextSum;
152
+ }
153
+
154
+ return sum * Math.exp(a * Math.log(x) + b * Math.log(1 - x) - logBeta(a, b)) / a;
155
+ }
156
+
157
+ function invIncompleteBeta(p, a, b) {
158
+ let x = 0.5;
159
+
160
+ for (let i = 0; i < 20; i++) {
161
+ const fx = incompleteBeta(x, a, b) - p;
162
+ const absX = Math.log(x / (1 - x));
163
+ const denom = (Math.exp(a * absX - logBeta(a, b)) * Math.exp(b * Math.log(1 - x))) / (a + b);
164
+ const dx = fx / denom;
165
+
166
+ x = Math.max(0.001, Math.min(0.999, x - dx));
167
+
168
+ if (Math.abs(dx) < 1e-10) break;
169
+ }
170
+
171
+ return x;
172
+ }
173
+
174
+ function logBeta(a, b) {
175
+ return logGamma(a) + logGamma(b) - logGamma(a + b);
176
+ }
177
+
178
+ function logGamma(x) {
179
+ const coefficients = [
180
+ 676.5203681218851,
181
+ -1259.1392167224028,
182
+ 771.32342877765313,
183
+ -176.61502916214059,
184
+ 12.507343278686905,
185
+ -0.13857109526572012,
186
+ 9.9843695780195716e-6,
187
+ 1.5056327351493116e-7
188
+ ];
189
+
190
+ const g = 7;
191
+ const z = x - 1;
192
+ let sum = 0.99999999999980993;
193
+
194
+ for (let i = 0; i < coefficients.length; i++) {
195
+ sum += coefficients[i] / (z + i + 1);
196
+ }
197
+
198
+ return 0.5 * Math.log(2 * Math.PI) + (z + 0.5) * Math.log(z + g + 0.5) - (z + g + 0.5) + Math.log(sum);
199
+ }
@@ -0,0 +1,170 @@
1
+ export class LinearRegression {
2
+ constructor(x, y) {
3
+ if (!Array.isArray(x) || !Array.isArray(y)) {
4
+ throw new TypeError('x and y must be arrays');
5
+ }
6
+ if (x.length !== y.length || x.length < 2) {
7
+ throw new RangeError('x and y must have same length >= 2');
8
+ }
9
+
10
+ const n = x.length;
11
+ const sumX = x.reduce((a, b) => a + b, 0);
12
+ const sumY = y.reduce((a, b) => a + b, 0);
13
+ const sumXY = x.reduce((sum, xi, i) => sum + xi * y[i], 0);
14
+ const sumX2 = x.reduce((sum, xi) => sum + xi * xi, 0);
15
+
16
+ const meanX = sumX / n;
17
+ const meanY = sumY / n;
18
+
19
+ this.slope = (sumXY - n * meanX * meanY) / (sumX2 - n * meanX * meanX);
20
+ this.intercept = meanY - this.slope * meanX;
21
+
22
+ const yPred = x.map(xi => this.slope * xi + this.intercept);
23
+
24
+ let ssRes = 0, ssTot = 0;
25
+ for (let i = 0; i < n; i++) {
26
+ ssRes += (y[i] - yPred[i]) ** 2;
27
+ ssTot += (y[i] - meanY) ** 2;
28
+ }
29
+
30
+ this.rSquared = 1 - ssRes / ssTot;
31
+ this.rmse = Math.sqrt(ssRes / n);
32
+ this.residuals = y.map((yi, i) => yi - yPred[i]);
33
+ this.predictions = yPred;
34
+ }
35
+
36
+ predict(x) {
37
+ if (typeof x === 'number') {
38
+ return this.slope * x + this.intercept;
39
+ }
40
+ if (Array.isArray(x)) {
41
+ return x.map(xi => this.slope * xi + this.intercept);
42
+ }
43
+ throw new TypeError('x must be number or array');
44
+ }
45
+ }
46
+
47
+ export class MultipleRegression {
48
+ constructor(X, y) {
49
+ if (!Array.isArray(X) || !Array.isArray(y)) {
50
+ throw new TypeError('X and y must be arrays');
51
+ }
52
+ if (X.length !== y.length || X.length < 2) {
53
+ throw new RangeError('X and y must have same length >= 2');
54
+ }
55
+ if (!Array.isArray(X[0])) {
56
+ throw new TypeError('X must be array of arrays (features)');
57
+ }
58
+
59
+ const n = X.length;
60
+ const p = X[0].length;
61
+
62
+ const matX = X.map(row => [1, ...row]);
63
+
64
+ const XtX = matmul(transpose(matX), matX);
65
+ const Xty = matmul(transpose(matX), y.map(yi => [yi]));
66
+
67
+ this.coefficients = solve(XtX, Xty).flat();
68
+ this.intercept = this.coefficients[0];
69
+
70
+ const yPred = X.map((row, i) => {
71
+ let pred = this.intercept;
72
+ for (let j = 0; j < p; j++) {
73
+ pred += this.coefficients[j + 1] * row[j];
74
+ }
75
+ return pred;
76
+ });
77
+
78
+ const meanY = y.reduce((a, b) => a + b, 0) / n;
79
+ let ssRes = 0, ssTot = 0;
80
+
81
+ for (let i = 0; i < n; i++) {
82
+ ssRes += (y[i] - yPred[i]) ** 2;
83
+ ssTot += (y[i] - meanY) ** 2;
84
+ }
85
+
86
+ this.rSquared = 1 - ssRes / ssTot;
87
+ this.adjRSquared = 1 - (1 - this.rSquared) * (n - 1) / (n - p - 1);
88
+ this.rmse = Math.sqrt(ssRes / n);
89
+ this.predictions = yPred;
90
+ this.residuals = y.map((yi, i) => yi - yPred[i]);
91
+ }
92
+
93
+ predict(X) {
94
+ if (typeof X[0] === 'number') {
95
+ let pred = this.intercept;
96
+ for (let i = 0; i < X.length; i++) {
97
+ pred += this.coefficients[i + 1] * X[i];
98
+ }
99
+ return pred;
100
+ }
101
+
102
+ return X.map(row => {
103
+ let pred = this.intercept;
104
+ for (let i = 0; i < row.length; i++) {
105
+ pred += this.coefficients[i + 1] * row[i];
106
+ }
107
+ return pred;
108
+ });
109
+ }
110
+ }
111
+
112
+ function transpose(matrix) {
113
+ const result = [];
114
+ for (let j = 0; j < matrix[0].length; j++) {
115
+ result[j] = [];
116
+ for (let i = 0; i < matrix.length; i++) {
117
+ result[j].push(matrix[i][j]);
118
+ }
119
+ }
120
+ return result;
121
+ }
122
+
123
+ function matmul(A, B) {
124
+ const result = [];
125
+ for (let i = 0; i < A.length; i++) {
126
+ result[i] = [];
127
+ for (let j = 0; j < B[0].length; j++) {
128
+ let sum = 0;
129
+ for (let k = 0; k < A[0].length; k++) {
130
+ sum += A[i][k] * B[k][j];
131
+ }
132
+ result[i][j] = sum;
133
+ }
134
+ }
135
+ return result;
136
+ }
137
+
138
+ function solve(A, b) {
139
+ const n = A.length;
140
+ const aug = A.map((row, i) => [...row, b[i][0]]);
141
+
142
+ for (let i = 0; i < n; i++) {
143
+ let maxRow = i;
144
+ for (let k = i + 1; k < n; k++) {
145
+ if (Math.abs(aug[k][i]) > Math.abs(aug[maxRow][i])) {
146
+ maxRow = k;
147
+ }
148
+ }
149
+
150
+ [aug[i], aug[maxRow]] = [aug[maxRow], aug[i]];
151
+
152
+ for (let k = i + 1; k < n; k++) {
153
+ const factor = aug[k][i] / aug[i][i];
154
+ for (let j = i; j < n + 1; j++) {
155
+ aug[k][j] -= factor * aug[i][j];
156
+ }
157
+ }
158
+ }
159
+
160
+ const x = new Array(n);
161
+ for (let i = n - 1; i >= 0; i--) {
162
+ x[i] = aug[i][n];
163
+ for (let j = i + 1; j < n; j++) {
164
+ x[i] -= aug[i][j] * x[j];
165
+ }
166
+ x[i] /= aug[i][i];
167
+ }
168
+
169
+ return x.map(xi => [xi]);
170
+ }
@@ -0,0 +1,112 @@
1
+ export function bootstrap(r, data, n, statistic) {
2
+ if (!Array.isArray(data) || data.length === 0) {
3
+ throw new TypeError('data must be a non-empty array');
4
+ }
5
+ if (typeof n !== 'number' || n < 1) {
6
+ throw new RangeError('n must be a positive integer');
7
+ }
8
+ if (typeof statistic !== 'function') {
9
+ throw new TypeError('statistic must be a function');
10
+ }
11
+
12
+ const samples = new Array(n);
13
+ const size = data.length;
14
+
15
+ for (let i = 0; i < n; i++) {
16
+ const resample = new Array(size);
17
+ for (let j = 0; j < size; j++) {
18
+ resample[j] = data[r.nextInt(size)];
19
+ }
20
+ samples[i] = statistic(resample);
21
+ }
22
+
23
+ return samples;
24
+ }
25
+
26
+ export function jackknife(data, statistic) {
27
+ if (!Array.isArray(data) || data.length === 0) {
28
+ throw new TypeError('data must be a non-empty array');
29
+ }
30
+ if (typeof statistic !== 'function') {
31
+ throw new TypeError('statistic must be a function');
32
+ }
33
+
34
+ const n = data.length;
35
+ const samples = new Array(n);
36
+
37
+ for (let i = 0; i < n; i++) {
38
+ const subset = data.slice(0, i).concat(data.slice(i + 1));
39
+ samples[i] = statistic(subset);
40
+ }
41
+
42
+ return samples;
43
+ }
44
+
45
+ export function crossValidation(r, data, k, trainFn, testFn) {
46
+ if (!Array.isArray(data) || data.length === 0) {
47
+ throw new TypeError('data must be a non-empty array');
48
+ }
49
+ if (typeof k !== 'number' || k < 2 || k !== Math.floor(k)) {
50
+ throw new RangeError('k must be an integer >= 2');
51
+ }
52
+ if (typeof trainFn !== 'function' || typeof testFn !== 'function') {
53
+ throw new TypeError('trainFn and testFn must be functions');
54
+ }
55
+
56
+ const n = data.length;
57
+ const foldSize = Math.floor(n / k);
58
+ const shuffled = [...data];
59
+
60
+ for (let i = n - 1; i > 0; i--) {
61
+ const j = r.nextInt(i + 1);
62
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
63
+ }
64
+
65
+ const scores = new Array(k);
66
+
67
+ for (let fold = 0; fold < k; fold++) {
68
+ const start = fold * foldSize;
69
+ const end = fold === k - 1 ? n : (fold + 1) * foldSize;
70
+
71
+ const testSet = shuffled.slice(start, end);
72
+ const trainSet = shuffled.slice(0, start).concat(shuffled.slice(end));
73
+
74
+ const model = trainFn(trainSet);
75
+ scores[fold] = testFn(model, testSet);
76
+ }
77
+
78
+ return scores;
79
+ }
80
+
81
+ export function permutationTest(r, data1, data2, testStatistic, n) {
82
+ if (!Array.isArray(data1) || !Array.isArray(data2)) {
83
+ throw new TypeError('data1 and data2 must be arrays');
84
+ }
85
+ if (typeof testStatistic !== 'function') {
86
+ throw new TypeError('testStatistic must be a function');
87
+ }
88
+ if (typeof n !== 'number' || n < 1) {
89
+ throw new RangeError('n must be a positive integer');
90
+ }
91
+
92
+ const observed = testStatistic(data1, data2);
93
+ const combined = [...data1, ...data2];
94
+ const n1 = data1.length;
95
+ let count = 0;
96
+
97
+ for (let i = 0; i < n; i++) {
98
+ for (let j = combined.length - 1; j > 0; j--) {
99
+ const k = r.nextInt(j + 1);
100
+ [combined[j], combined[k]] = [combined[k], combined[j]];
101
+ }
102
+
103
+ const perm1 = combined.slice(0, n1);
104
+ const perm2 = combined.slice(n1);
105
+
106
+ if (Math.abs(testStatistic(perm1, perm2)) >= Math.abs(observed)) {
107
+ count++;
108
+ }
109
+ }
110
+
111
+ return count / n;
112
+ }