regressio 0.0.1
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/README.md +325 -0
- package/dist/index.cjs +2505 -0
- package/dist/index.d.cts +545 -0
- package/dist/index.d.ts +545 -0
- package/dist/index.js +2502 -0
- package/package.json +68 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2505 @@
|
|
|
1
|
+
var import_node_module = require("node:module");
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
function __accessProp(key) {
|
|
7
|
+
return this[key];
|
|
8
|
+
}
|
|
9
|
+
var __toCommonJS = (from) => {
|
|
10
|
+
var entry = (__moduleCache ??= new WeakMap).get(from), desc;
|
|
11
|
+
if (entry)
|
|
12
|
+
return entry;
|
|
13
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (var key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(entry, key))
|
|
17
|
+
__defProp(entry, key, {
|
|
18
|
+
get: __accessProp.bind(from, key),
|
|
19
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
__moduleCache.set(from, entry);
|
|
23
|
+
return entry;
|
|
24
|
+
};
|
|
25
|
+
var __moduleCache;
|
|
26
|
+
var __returnValue = (v) => v;
|
|
27
|
+
function __exportSetter(name, newValue) {
|
|
28
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
29
|
+
}
|
|
30
|
+
var __export = (target, all) => {
|
|
31
|
+
for (var name in all)
|
|
32
|
+
__defProp(target, name, {
|
|
33
|
+
get: all[name],
|
|
34
|
+
enumerable: true,
|
|
35
|
+
configurable: true,
|
|
36
|
+
set: __exportSetter.bind(all, name)
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
40
|
+
|
|
41
|
+
// src/core/distributions.ts
|
|
42
|
+
var exports_distributions = {};
|
|
43
|
+
__export(exports_distributions, {
|
|
44
|
+
tTestPValue: () => tTestPValue,
|
|
45
|
+
tPDF: () => tPDF,
|
|
46
|
+
tInverseCDF: () => tInverseCDF,
|
|
47
|
+
tCDF: () => tCDF,
|
|
48
|
+
regularizedIncompleteBeta: () => regularizedIncompleteBeta,
|
|
49
|
+
normalPDF: () => normalPDF,
|
|
50
|
+
normalInverseCDF: () => normalInverseCDF,
|
|
51
|
+
normalCDF: () => normalCDF,
|
|
52
|
+
logGamma: () => logGamma,
|
|
53
|
+
logBeta: () => logBeta,
|
|
54
|
+
inverseErf: () => inverseErf,
|
|
55
|
+
fTestPValue: () => fTestPValue,
|
|
56
|
+
fPDF: () => fPDF,
|
|
57
|
+
fInverseCDF: () => fInverseCDF,
|
|
58
|
+
fCDF: () => fCDF,
|
|
59
|
+
erf: () => erf,
|
|
60
|
+
chi2TestPValue: () => chi2TestPValue,
|
|
61
|
+
chi2PDF: () => chi2PDF,
|
|
62
|
+
chi2InverseCDF: () => chi2InverseCDF,
|
|
63
|
+
chi2CDFExact: () => chi2CDFExact,
|
|
64
|
+
chi2CDF: () => chi2CDF
|
|
65
|
+
});
|
|
66
|
+
function logGamma(x) {
|
|
67
|
+
if (x <= 0)
|
|
68
|
+
throw new Error("logGamma: x must be positive");
|
|
69
|
+
if (x < 0.5) {
|
|
70
|
+
return Math.log(Math.PI / Math.sin(Math.PI * x)) - logGamma(1 - x);
|
|
71
|
+
}
|
|
72
|
+
x -= 1;
|
|
73
|
+
let a = LANCZOS_COEFFS[0];
|
|
74
|
+
for (let i = 1;i < LANCZOS_G + 2; i++) {
|
|
75
|
+
a += LANCZOS_COEFFS[i] / (x + i);
|
|
76
|
+
}
|
|
77
|
+
const t = x + LANCZOS_G + 0.5;
|
|
78
|
+
return 0.5 * Math.log(2 * Math.PI) + (x + 0.5) * Math.log(t) - t + Math.log(a);
|
|
79
|
+
}
|
|
80
|
+
function logBeta(a, b) {
|
|
81
|
+
return logGamma(a) + logGamma(b) - logGamma(a + b);
|
|
82
|
+
}
|
|
83
|
+
function regularizedIncompleteBeta(x, a, b) {
|
|
84
|
+
if (x <= 0)
|
|
85
|
+
return 0;
|
|
86
|
+
if (x >= 1)
|
|
87
|
+
return 1;
|
|
88
|
+
if (x > (a + 1) / (a + b + 2)) {
|
|
89
|
+
return 1 - regularizedIncompleteBeta(1 - x, b, a);
|
|
90
|
+
}
|
|
91
|
+
const lnPrefix = a * Math.log(x) + b * Math.log(1 - x) - logBeta(a, b) - Math.log(a);
|
|
92
|
+
const maxIter = 200;
|
|
93
|
+
const eps = 0.00000000000001;
|
|
94
|
+
const tiny = 0.000000000000000000000000000001;
|
|
95
|
+
let c = 1;
|
|
96
|
+
let d = 1 - (a + b) * x / (a + 1);
|
|
97
|
+
if (Math.abs(d) < tiny)
|
|
98
|
+
d = tiny;
|
|
99
|
+
d = 1 / d;
|
|
100
|
+
let h = d;
|
|
101
|
+
for (let m = 1;m <= maxIter; m++) {
|
|
102
|
+
const m2 = 2 * m;
|
|
103
|
+
let num = m * (b - m) * x / ((a + m2 - 1) * (a + m2));
|
|
104
|
+
d = 1 + num * d;
|
|
105
|
+
if (Math.abs(d) < tiny)
|
|
106
|
+
d = tiny;
|
|
107
|
+
c = 1 + num / c;
|
|
108
|
+
if (Math.abs(c) < tiny)
|
|
109
|
+
c = tiny;
|
|
110
|
+
d = 1 / d;
|
|
111
|
+
h *= d * c;
|
|
112
|
+
num = -((a + m) * (a + b + m) * x) / ((a + m2) * (a + m2 + 1));
|
|
113
|
+
d = 1 + num * d;
|
|
114
|
+
if (Math.abs(d) < tiny)
|
|
115
|
+
d = tiny;
|
|
116
|
+
c = 1 + num / c;
|
|
117
|
+
if (Math.abs(c) < tiny)
|
|
118
|
+
c = tiny;
|
|
119
|
+
d = 1 / d;
|
|
120
|
+
const delta = d * c;
|
|
121
|
+
h *= delta;
|
|
122
|
+
if (Math.abs(delta - 1) < eps)
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
return Math.exp(lnPrefix) * h;
|
|
126
|
+
}
|
|
127
|
+
function erf(x) {
|
|
128
|
+
if (x === 0)
|
|
129
|
+
return 0;
|
|
130
|
+
const sign = x >= 0 ? 1 : -1;
|
|
131
|
+
const ax = Math.abs(x);
|
|
132
|
+
if (ax < 0.5) {
|
|
133
|
+
const x2 = ax * ax;
|
|
134
|
+
let term = ax;
|
|
135
|
+
let sum = ax;
|
|
136
|
+
for (let n = 1;n < 30; n++) {
|
|
137
|
+
term *= -x2 / n;
|
|
138
|
+
sum += term / (2 * n + 1);
|
|
139
|
+
}
|
|
140
|
+
return sign * (2 / Math.sqrt(Math.PI)) * sum;
|
|
141
|
+
}
|
|
142
|
+
const t = 1 / (1 + 0.3275911 * ax);
|
|
143
|
+
const poly = t * (0.254829592 + t * (-0.284496736 + t * (1.421413741 + t * (-1.453152027 + t * 1.061405429))));
|
|
144
|
+
return sign * (1 - poly * Math.exp(-ax * ax));
|
|
145
|
+
}
|
|
146
|
+
function inverseErf(x) {
|
|
147
|
+
if (x <= -1)
|
|
148
|
+
return -Infinity;
|
|
149
|
+
if (x >= 1)
|
|
150
|
+
return Infinity;
|
|
151
|
+
const a = 0.147;
|
|
152
|
+
const ln1mx2 = Math.log(1 - x * x);
|
|
153
|
+
const term = 2 / (Math.PI * a) + ln1mx2 / 2;
|
|
154
|
+
const sign = x >= 0 ? 1 : -1;
|
|
155
|
+
return sign * Math.sqrt(Math.sqrt(term * term - ln1mx2 / a) - term);
|
|
156
|
+
}
|
|
157
|
+
function normalPDF(x, mu = 0, sigma = 1) {
|
|
158
|
+
const z = (x - mu) / sigma;
|
|
159
|
+
return Math.exp(-0.5 * z * z) / (sigma * Math.sqrt(2 * Math.PI));
|
|
160
|
+
}
|
|
161
|
+
function normalCDF(x, mu = 0, sigma = 1) {
|
|
162
|
+
return 0.5 * (1 + erf((x - mu) / (sigma * Math.SQRT2)));
|
|
163
|
+
}
|
|
164
|
+
function normalInverseCDF(p, mu = 0, sigma = 1) {
|
|
165
|
+
return mu + sigma * Math.SQRT2 * inverseErf(2 * p - 1);
|
|
166
|
+
}
|
|
167
|
+
function tPDF(t, df) {
|
|
168
|
+
const coeff = Math.exp(logGamma((df + 1) / 2) - logGamma(df / 2));
|
|
169
|
+
return coeff / Math.sqrt(df * Math.PI) * (1 + t * t / df) ** (-(df + 1) / 2);
|
|
170
|
+
}
|
|
171
|
+
function tCDF(t, df) {
|
|
172
|
+
if (df <= 0)
|
|
173
|
+
throw new Error("tCDF: df must be positive");
|
|
174
|
+
const x = df / (t * t + df);
|
|
175
|
+
const ib = regularizedIncompleteBeta(x, df / 2, 0.5);
|
|
176
|
+
return t >= 0 ? 1 - 0.5 * ib : 0.5 * ib;
|
|
177
|
+
}
|
|
178
|
+
function tInverseCDF(p, df) {
|
|
179
|
+
if (p <= 0)
|
|
180
|
+
return -Infinity;
|
|
181
|
+
if (p >= 1)
|
|
182
|
+
return Infinity;
|
|
183
|
+
if (Math.abs(p - 0.5) < 0.000000000000001)
|
|
184
|
+
return 0;
|
|
185
|
+
let lo = -1000;
|
|
186
|
+
let hi = 1000;
|
|
187
|
+
for (let i = 0;i < 100; i++) {
|
|
188
|
+
const mid = (lo + hi) / 2;
|
|
189
|
+
if (tCDF(mid, df) < p) {
|
|
190
|
+
lo = mid;
|
|
191
|
+
} else {
|
|
192
|
+
hi = mid;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return (lo + hi) / 2;
|
|
196
|
+
}
|
|
197
|
+
function tTestPValue(tStat, df) {
|
|
198
|
+
return 2 * (1 - tCDF(Math.abs(tStat), df));
|
|
199
|
+
}
|
|
200
|
+
function fPDF(x, df1, df2) {
|
|
201
|
+
if (x <= 0)
|
|
202
|
+
return 0;
|
|
203
|
+
const half1 = df1 / 2;
|
|
204
|
+
const half2 = df2 / 2;
|
|
205
|
+
const lnCoeff = half1 * Math.log(df1) + half2 * Math.log(df2) - logBeta(half1, half2);
|
|
206
|
+
return Math.exp(lnCoeff + (half1 - 1) * Math.log(x) - (half1 + half2) * Math.log(df1 * x + df2));
|
|
207
|
+
}
|
|
208
|
+
function fCDF(x, df1, df2) {
|
|
209
|
+
if (x <= 0)
|
|
210
|
+
return 0;
|
|
211
|
+
const z = df1 * x / (df1 * x + df2);
|
|
212
|
+
return regularizedIncompleteBeta(z, df1 / 2, df2 / 2);
|
|
213
|
+
}
|
|
214
|
+
function fInverseCDF(p, df1, df2) {
|
|
215
|
+
if (p <= 0)
|
|
216
|
+
return 0;
|
|
217
|
+
if (p >= 1)
|
|
218
|
+
return Infinity;
|
|
219
|
+
let lo = 0;
|
|
220
|
+
let hi = 1000;
|
|
221
|
+
while (fCDF(hi, df1, df2) < p)
|
|
222
|
+
hi *= 2;
|
|
223
|
+
for (let i = 0;i < 100; i++) {
|
|
224
|
+
const mid = (lo + hi) / 2;
|
|
225
|
+
if (fCDF(mid, df1, df2) < p) {
|
|
226
|
+
lo = mid;
|
|
227
|
+
} else {
|
|
228
|
+
hi = mid;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return (lo + hi) / 2;
|
|
232
|
+
}
|
|
233
|
+
function fTestPValue(fStat, df1, df2) {
|
|
234
|
+
return 1 - fCDF(fStat, df1, df2);
|
|
235
|
+
}
|
|
236
|
+
function chi2PDF(x, df) {
|
|
237
|
+
if (x <= 0)
|
|
238
|
+
return 0;
|
|
239
|
+
const k2 = df / 2;
|
|
240
|
+
return Math.exp((k2 - 1) * Math.log(x) - x / 2 - k2 * Math.LN2 - logGamma(k2));
|
|
241
|
+
}
|
|
242
|
+
function chi2CDF(x, df) {
|
|
243
|
+
return chi2CDFExact(x, df);
|
|
244
|
+
}
|
|
245
|
+
function chi2CDFExact(x, df) {
|
|
246
|
+
if (x <= 0)
|
|
247
|
+
return 0;
|
|
248
|
+
return regularizedLowerGamma(df / 2, x / 2);
|
|
249
|
+
}
|
|
250
|
+
function regularizedLowerGamma(a, x) {
|
|
251
|
+
if (x <= 0)
|
|
252
|
+
return 0;
|
|
253
|
+
if (x < a + 1) {
|
|
254
|
+
return lowerGammaSeries(a, x);
|
|
255
|
+
}
|
|
256
|
+
return 1 - upperGammaCF(a, x);
|
|
257
|
+
}
|
|
258
|
+
function lowerGammaSeries(a, x) {
|
|
259
|
+
let term = 1 / a;
|
|
260
|
+
let sum = term;
|
|
261
|
+
for (let n = 1;n < 300; n++) {
|
|
262
|
+
term *= x / (a + n);
|
|
263
|
+
sum += term;
|
|
264
|
+
if (Math.abs(term) < 0.000000000000001 * Math.abs(sum))
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
return sum * Math.exp(-x + a * Math.log(x) - logGamma(a));
|
|
268
|
+
}
|
|
269
|
+
function upperGammaCF(a, x) {
|
|
270
|
+
const tiny = 0.000000000000000000000000000001;
|
|
271
|
+
let b = x + 1 - a;
|
|
272
|
+
let c = 1 / tiny;
|
|
273
|
+
let d = 1 / b;
|
|
274
|
+
let h = d;
|
|
275
|
+
for (let n = 1;n < 300; n++) {
|
|
276
|
+
const an = -n * (n - a);
|
|
277
|
+
b += 2;
|
|
278
|
+
d = an * d + b;
|
|
279
|
+
if (Math.abs(d) < tiny)
|
|
280
|
+
d = tiny;
|
|
281
|
+
c = b + an / c;
|
|
282
|
+
if (Math.abs(c) < tiny)
|
|
283
|
+
c = tiny;
|
|
284
|
+
d = 1 / d;
|
|
285
|
+
const delta = d * c;
|
|
286
|
+
h *= delta;
|
|
287
|
+
if (Math.abs(delta - 1) < 0.000000000000001)
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
return h * Math.exp(-x + a * Math.log(x) - logGamma(a));
|
|
291
|
+
}
|
|
292
|
+
function chi2InverseCDF(p, df) {
|
|
293
|
+
if (p <= 0)
|
|
294
|
+
return 0;
|
|
295
|
+
if (p >= 1)
|
|
296
|
+
return Infinity;
|
|
297
|
+
let lo = 0;
|
|
298
|
+
let hi = Math.max(df * 4, 100);
|
|
299
|
+
while (chi2CDFExact(hi, df) < p)
|
|
300
|
+
hi *= 2;
|
|
301
|
+
for (let i = 0;i < 100; i++) {
|
|
302
|
+
const mid = (lo + hi) / 2;
|
|
303
|
+
if (chi2CDFExact(mid, df) < p) {
|
|
304
|
+
lo = mid;
|
|
305
|
+
} else {
|
|
306
|
+
hi = mid;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return (lo + hi) / 2;
|
|
310
|
+
}
|
|
311
|
+
function chi2TestPValue(stat, df) {
|
|
312
|
+
return 1 - chi2CDFExact(stat, df);
|
|
313
|
+
}
|
|
314
|
+
var LANCZOS_G = 7, LANCZOS_COEFFS;
|
|
315
|
+
var init_distributions = __esm(() => {
|
|
316
|
+
LANCZOS_COEFFS = [
|
|
317
|
+
0.9999999999998099,
|
|
318
|
+
676.5203681218851,
|
|
319
|
+
-1259.1392167224028,
|
|
320
|
+
771.3234287776531,
|
|
321
|
+
-176.6150291621406,
|
|
322
|
+
12.507343278686905,
|
|
323
|
+
-0.13857109526572012,
|
|
324
|
+
0.000009984369578019572,
|
|
325
|
+
0.00000015056327351493116
|
|
326
|
+
];
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// src/index.ts
|
|
330
|
+
var exports_src = {};
|
|
331
|
+
__export(exports_src, {
|
|
332
|
+
vif: () => vif,
|
|
333
|
+
useWasmModule: () => useWasmModule,
|
|
334
|
+
useWasmEngine: () => useWasmEngine,
|
|
335
|
+
useTypescriptEngine: () => useTypescriptEngine,
|
|
336
|
+
unstandardize: () => unstandardize,
|
|
337
|
+
unnormalize: () => unnormalize,
|
|
338
|
+
studentizedResiduals: () => studentizedResiduals,
|
|
339
|
+
standardize: () => standardize,
|
|
340
|
+
shapiroWilk: () => shapiroWilk,
|
|
341
|
+
residualDiagnostics: () => residualDiagnostics,
|
|
342
|
+
predictionInterval: () => predictionInterval,
|
|
343
|
+
polynomialFeatures: () => polynomialFeatures,
|
|
344
|
+
oneHotEncode: () => oneHotEncode,
|
|
345
|
+
normalize: () => normalize,
|
|
346
|
+
leverage: () => leverage,
|
|
347
|
+
isWasmActive: () => isWasmActive,
|
|
348
|
+
interactionFeatures: () => interactionFeatures,
|
|
349
|
+
imputeMedian: () => imputeMedian,
|
|
350
|
+
imputeMean: () => imputeMean,
|
|
351
|
+
getEngine: () => getEngine,
|
|
352
|
+
durbinWatson: () => durbinWatson,
|
|
353
|
+
dropMissing: () => dropMissing,
|
|
354
|
+
correlationMatrix: () => correlationMatrix,
|
|
355
|
+
cooksDistance: () => cooksDistance,
|
|
356
|
+
confidenceInterval: () => confidenceInterval,
|
|
357
|
+
conditionNumber: () => conditionNumber,
|
|
358
|
+
breuschPagan: () => breuschPagan,
|
|
359
|
+
bootstrapCoefficients: () => bootstrapCoefficients,
|
|
360
|
+
WeightedRegression: () => WeightedRegression,
|
|
361
|
+
RobustRegression: () => RobustRegression,
|
|
362
|
+
RidgeRegression: () => RidgeRegression,
|
|
363
|
+
PolynomialRegression: () => PolynomialRegression,
|
|
364
|
+
NeuralNetwork: () => NeuralNetwork,
|
|
365
|
+
MulticlassLogisticRegression: () => MulticlassLogisticRegression,
|
|
366
|
+
Matrix: () => Matrix,
|
|
367
|
+
LogisticRegression: () => LogisticRegression,
|
|
368
|
+
LinearRegression: () => LinearRegression,
|
|
369
|
+
LassoRegression: () => LassoRegression,
|
|
370
|
+
KNearestNeighbors: () => KNearestNeighbors,
|
|
371
|
+
ElasticNet: () => ElasticNet
|
|
372
|
+
});
|
|
373
|
+
module.exports = __toCommonJS(exports_src);
|
|
374
|
+
|
|
375
|
+
// src/core/engine.ts
|
|
376
|
+
var currentEngine = { name: "typescript" };
|
|
377
|
+
function getEngine() {
|
|
378
|
+
return currentEngine;
|
|
379
|
+
}
|
|
380
|
+
function isWasmActive() {
|
|
381
|
+
return currentEngine.name === "wasm" && currentEngine.wasm != null;
|
|
382
|
+
}
|
|
383
|
+
async function useWasmEngine() {
|
|
384
|
+
try {
|
|
385
|
+
const wasm = await import("../../pkg");
|
|
386
|
+
currentEngine = { name: "wasm", wasm };
|
|
387
|
+
} catch {
|
|
388
|
+
throw new Error("WASM engine not available. Build it with: cd rust && wasm-pack build --target bundler --out-dir ../pkg");
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
function useWasmModule(wasmModule) {
|
|
392
|
+
currentEngine = { name: "wasm", wasm: wasmModule };
|
|
393
|
+
}
|
|
394
|
+
function useTypescriptEngine() {
|
|
395
|
+
currentEngine = { name: "typescript" };
|
|
396
|
+
}
|
|
397
|
+
function engineMatrixMultiply(a, aRows, aCols, b, bRows, bCols) {
|
|
398
|
+
if (currentEngine.wasm) {
|
|
399
|
+
return currentEngine.wasm.matrix_multiply(a, aRows, aCols, b, bRows, bCols);
|
|
400
|
+
}
|
|
401
|
+
const result = new Float64Array(aRows * bCols);
|
|
402
|
+
for (let i = 0;i < aRows; i++) {
|
|
403
|
+
for (let k = 0;k < aCols; k++) {
|
|
404
|
+
const aik = a[i * aCols + k];
|
|
405
|
+
for (let j = 0;j < bCols; j++) {
|
|
406
|
+
result[i * bCols + j] += aik * b[k * bCols + j];
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return result;
|
|
411
|
+
}
|
|
412
|
+
function engineQR(data, rows, cols) {
|
|
413
|
+
if (currentEngine.wasm) {
|
|
414
|
+
const result = currentEngine.wasm.qr_decompose(data, rows, cols);
|
|
415
|
+
const qSize = rows * rows;
|
|
416
|
+
const Q = result.slice(0, qSize);
|
|
417
|
+
const R = result.slice(qSize);
|
|
418
|
+
return { Q, R };
|
|
419
|
+
}
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
function engineCholesky(data, n) {
|
|
423
|
+
if (currentEngine.wasm) {
|
|
424
|
+
return currentEngine.wasm.cholesky(data, n);
|
|
425
|
+
}
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
function engineSolveTriangular(r, b, n) {
|
|
429
|
+
if (currentEngine.wasm) {
|
|
430
|
+
return currentEngine.wasm.solve_triangular(r, b, n);
|
|
431
|
+
}
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
// src/core/matrix.ts
|
|
435
|
+
class Matrix {
|
|
436
|
+
rows;
|
|
437
|
+
cols;
|
|
438
|
+
data;
|
|
439
|
+
constructor(rows, cols, data) {
|
|
440
|
+
this.rows = rows;
|
|
441
|
+
this.cols = cols;
|
|
442
|
+
if (data) {
|
|
443
|
+
this.data = data instanceof Float64Array ? data : new Float64Array(data);
|
|
444
|
+
} else {
|
|
445
|
+
this.data = new Float64Array(rows * cols);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
static fromArray(arr) {
|
|
449
|
+
const rows = arr.length;
|
|
450
|
+
if (rows === 0)
|
|
451
|
+
return new Matrix(0, 0);
|
|
452
|
+
const cols = arr[0].length;
|
|
453
|
+
const data = new Float64Array(rows * cols);
|
|
454
|
+
for (let i = 0;i < rows; i++) {
|
|
455
|
+
const row = arr[i];
|
|
456
|
+
if (row.length !== cols) {
|
|
457
|
+
throw new Error(`Row ${i} has ${row.length} columns, expected ${cols}`);
|
|
458
|
+
}
|
|
459
|
+
for (let j = 0;j < cols; j++) {
|
|
460
|
+
data[i * cols + j] = row[j];
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return new Matrix(rows, cols, data);
|
|
464
|
+
}
|
|
465
|
+
static zeros(rows, cols) {
|
|
466
|
+
return new Matrix(rows, cols);
|
|
467
|
+
}
|
|
468
|
+
static ones(rows, cols) {
|
|
469
|
+
const data = new Float64Array(rows * cols).fill(1);
|
|
470
|
+
return new Matrix(rows, cols, data);
|
|
471
|
+
}
|
|
472
|
+
static identity(n) {
|
|
473
|
+
const m = new Matrix(n, n);
|
|
474
|
+
for (let i = 0;i < n; i++) {
|
|
475
|
+
m.data[i * n + i] = 1;
|
|
476
|
+
}
|
|
477
|
+
return m;
|
|
478
|
+
}
|
|
479
|
+
static columnVector(arr) {
|
|
480
|
+
return new Matrix(arr.length, 1, new Float64Array(arr));
|
|
481
|
+
}
|
|
482
|
+
static rowVector(arr) {
|
|
483
|
+
return new Matrix(1, arr.length, new Float64Array(arr));
|
|
484
|
+
}
|
|
485
|
+
static diagonal(values) {
|
|
486
|
+
const n = values.length;
|
|
487
|
+
const m = new Matrix(n, n);
|
|
488
|
+
for (let i = 0;i < n; i++) {
|
|
489
|
+
m.data[i * n + i] = values[i];
|
|
490
|
+
}
|
|
491
|
+
return m;
|
|
492
|
+
}
|
|
493
|
+
get(i, j) {
|
|
494
|
+
return this.data[i * this.cols + j];
|
|
495
|
+
}
|
|
496
|
+
set(i, j, value) {
|
|
497
|
+
this.data[i * this.cols + j] = value;
|
|
498
|
+
}
|
|
499
|
+
getColumn(j) {
|
|
500
|
+
const col = new Float64Array(this.rows);
|
|
501
|
+
for (let i = 0;i < this.rows; i++) {
|
|
502
|
+
col[i] = this.data[i * this.cols + j];
|
|
503
|
+
}
|
|
504
|
+
return new Matrix(this.rows, 1, col);
|
|
505
|
+
}
|
|
506
|
+
getRow(i) {
|
|
507
|
+
const start = i * this.cols;
|
|
508
|
+
return new Matrix(1, this.cols, this.data.slice(start, start + this.cols));
|
|
509
|
+
}
|
|
510
|
+
setColumn(j, col) {
|
|
511
|
+
for (let i = 0;i < this.rows; i++) {
|
|
512
|
+
this.data[i * this.cols + j] = col.data[i];
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
transpose() {
|
|
516
|
+
const result = new Matrix(this.cols, this.rows);
|
|
517
|
+
for (let i = 0;i < this.rows; i++) {
|
|
518
|
+
for (let j = 0;j < this.cols; j++) {
|
|
519
|
+
result.data[j * this.rows + i] = this.data[i * this.cols + j];
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return result;
|
|
523
|
+
}
|
|
524
|
+
multiply(other) {
|
|
525
|
+
if (this.cols !== other.rows) {
|
|
526
|
+
throw new Error(`Matrix multiply: incompatible dimensions (${this.rows}x${this.cols}) * (${other.rows}x${other.cols})`);
|
|
527
|
+
}
|
|
528
|
+
const resultData = engineMatrixMultiply(this.data, this.rows, this.cols, other.data, other.rows, other.cols);
|
|
529
|
+
return new Matrix(this.rows, other.cols, resultData);
|
|
530
|
+
}
|
|
531
|
+
add(other) {
|
|
532
|
+
this.assertSameDimensions(other, "add");
|
|
533
|
+
const result = new Matrix(this.rows, this.cols);
|
|
534
|
+
for (let i = 0;i < this.data.length; i++) {
|
|
535
|
+
result.data[i] = this.data[i] + other.data[i];
|
|
536
|
+
}
|
|
537
|
+
return result;
|
|
538
|
+
}
|
|
539
|
+
subtract(other) {
|
|
540
|
+
this.assertSameDimensions(other, "subtract");
|
|
541
|
+
const result = new Matrix(this.rows, this.cols);
|
|
542
|
+
for (let i = 0;i < this.data.length; i++) {
|
|
543
|
+
result.data[i] = this.data[i] - other.data[i];
|
|
544
|
+
}
|
|
545
|
+
return result;
|
|
546
|
+
}
|
|
547
|
+
scale(scalar) {
|
|
548
|
+
const result = new Matrix(this.rows, this.cols);
|
|
549
|
+
for (let i = 0;i < this.data.length; i++) {
|
|
550
|
+
result.data[i] = this.data[i] * scalar;
|
|
551
|
+
}
|
|
552
|
+
return result;
|
|
553
|
+
}
|
|
554
|
+
addInPlace(other) {
|
|
555
|
+
this.assertSameDimensions(other, "addInPlace");
|
|
556
|
+
for (let i = 0;i < this.data.length; i++) {
|
|
557
|
+
this.data[i] += other.data[i];
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
subtractInPlace(other) {
|
|
561
|
+
this.assertSameDimensions(other, "subtractInPlace");
|
|
562
|
+
for (let i = 0;i < this.data.length; i++) {
|
|
563
|
+
this.data[i] -= other.data[i];
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
scaleInPlace(scalar) {
|
|
567
|
+
for (let i = 0;i < this.data.length; i++) {
|
|
568
|
+
this.data[i] *= scalar;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
norm() {
|
|
572
|
+
let sum = 0;
|
|
573
|
+
for (let i = 0;i < this.data.length; i++) {
|
|
574
|
+
sum += this.data[i] * this.data[i];
|
|
575
|
+
}
|
|
576
|
+
return Math.sqrt(sum);
|
|
577
|
+
}
|
|
578
|
+
trace() {
|
|
579
|
+
if (this.rows !== this.cols) {
|
|
580
|
+
throw new Error("Trace is only defined for square matrices");
|
|
581
|
+
}
|
|
582
|
+
let sum = 0;
|
|
583
|
+
for (let i = 0;i < this.rows; i++) {
|
|
584
|
+
sum += this.data[i * this.cols + i];
|
|
585
|
+
}
|
|
586
|
+
return sum;
|
|
587
|
+
}
|
|
588
|
+
determinant() {
|
|
589
|
+
if (this.rows !== this.cols) {
|
|
590
|
+
throw new Error("Determinant is only defined for square matrices");
|
|
591
|
+
}
|
|
592
|
+
const n = this.rows;
|
|
593
|
+
if (n === 0)
|
|
594
|
+
return 1;
|
|
595
|
+
if (n === 1)
|
|
596
|
+
return this.data[0];
|
|
597
|
+
if (n === 2)
|
|
598
|
+
return this.data[0] * this.data[3] - this.data[1] * this.data[2];
|
|
599
|
+
const a = new Float64Array(this.data);
|
|
600
|
+
let det = 1;
|
|
601
|
+
for (let col = 0;col < n; col++) {
|
|
602
|
+
let maxVal = Math.abs(a[col * n + col]);
|
|
603
|
+
let maxRow = col;
|
|
604
|
+
for (let row = col + 1;row < n; row++) {
|
|
605
|
+
const val = Math.abs(a[row * n + col]);
|
|
606
|
+
if (val > maxVal) {
|
|
607
|
+
maxVal = val;
|
|
608
|
+
maxRow = row;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (maxVal < 0.000000000000001)
|
|
612
|
+
return 0;
|
|
613
|
+
if (maxRow !== col) {
|
|
614
|
+
det = -det;
|
|
615
|
+
for (let j = 0;j < n; j++) {
|
|
616
|
+
const tmp = a[col * n + j];
|
|
617
|
+
a[col * n + j] = a[maxRow * n + j];
|
|
618
|
+
a[maxRow * n + j] = tmp;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
det *= a[col * n + col];
|
|
622
|
+
const pivot = a[col * n + col];
|
|
623
|
+
for (let row = col + 1;row < n; row++) {
|
|
624
|
+
const factor = a[row * n + col] / pivot;
|
|
625
|
+
for (let j = col + 1;j < n; j++) {
|
|
626
|
+
a[row * n + j] -= factor * a[col * n + j];
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return det;
|
|
631
|
+
}
|
|
632
|
+
submatrix(rowStart, rowEnd, colStart, colEnd) {
|
|
633
|
+
const rows = rowEnd - rowStart;
|
|
634
|
+
const cols = colEnd - colStart;
|
|
635
|
+
const result = new Matrix(rows, cols);
|
|
636
|
+
for (let i = 0;i < rows; i++) {
|
|
637
|
+
for (let j = 0;j < cols; j++) {
|
|
638
|
+
result.data[i * cols + j] = this.data[(rowStart + i) * this.cols + (colStart + j)];
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return result;
|
|
642
|
+
}
|
|
643
|
+
clone() {
|
|
644
|
+
return new Matrix(this.rows, this.cols, new Float64Array(this.data));
|
|
645
|
+
}
|
|
646
|
+
toArray() {
|
|
647
|
+
const result = [];
|
|
648
|
+
for (let i = 0;i < this.rows; i++) {
|
|
649
|
+
const row = [];
|
|
650
|
+
for (let j = 0;j < this.cols; j++) {
|
|
651
|
+
row.push(this.data[i * this.cols + j]);
|
|
652
|
+
}
|
|
653
|
+
result.push(row);
|
|
654
|
+
}
|
|
655
|
+
return result;
|
|
656
|
+
}
|
|
657
|
+
toFlatArray() {
|
|
658
|
+
return Array.from(this.data);
|
|
659
|
+
}
|
|
660
|
+
dot(other) {
|
|
661
|
+
let sum = 0;
|
|
662
|
+
for (let i = 0;i < this.data.length; i++) {
|
|
663
|
+
sum += this.data[i] * other.data[i];
|
|
664
|
+
}
|
|
665
|
+
return sum;
|
|
666
|
+
}
|
|
667
|
+
assertSameDimensions(other, op) {
|
|
668
|
+
if (this.rows !== other.rows || this.cols !== other.cols) {
|
|
669
|
+
throw new Error(`Matrix ${op}: incompatible dimensions (${this.rows}x${this.cols}) vs (${other.rows}x${other.cols})`);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
// src/core/decompositions.ts
|
|
674
|
+
function qrDecomposition(A) {
|
|
675
|
+
const m = A.rows;
|
|
676
|
+
const n = A.cols;
|
|
677
|
+
if (isWasmActive()) {
|
|
678
|
+
const wasmResult = engineQR(A.data, m, n);
|
|
679
|
+
if (wasmResult) {
|
|
680
|
+
return {
|
|
681
|
+
Q: new Matrix(m, m, wasmResult.Q),
|
|
682
|
+
R: new Matrix(m, n, wasmResult.R)
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
const R = A.clone();
|
|
687
|
+
const Q = Matrix.identity(m);
|
|
688
|
+
const minMN = Math.min(m, n);
|
|
689
|
+
for (let k = 0;k < minMN; k++) {
|
|
690
|
+
const x = new Float64Array(m - k);
|
|
691
|
+
for (let i = 0;i < m - k; i++) {
|
|
692
|
+
x[i] = R.get(k + i, k);
|
|
693
|
+
}
|
|
694
|
+
let xNorm = 0;
|
|
695
|
+
for (let i = 0;i < x.length; i++) {
|
|
696
|
+
xNorm += x[i] * x[i];
|
|
697
|
+
}
|
|
698
|
+
xNorm = Math.sqrt(xNorm);
|
|
699
|
+
if (xNorm < 0.000000000000001)
|
|
700
|
+
continue;
|
|
701
|
+
const alpha = x[0] >= 0 ? -xNorm : xNorm;
|
|
702
|
+
const v = new Float64Array(x);
|
|
703
|
+
v[0] -= alpha;
|
|
704
|
+
let vNorm = 0;
|
|
705
|
+
for (let i = 0;i < v.length; i++) {
|
|
706
|
+
vNorm += v[i] * v[i];
|
|
707
|
+
}
|
|
708
|
+
vNorm = Math.sqrt(vNorm);
|
|
709
|
+
if (vNorm < 0.000000000000001)
|
|
710
|
+
continue;
|
|
711
|
+
for (let i = 0;i < v.length; i++) {
|
|
712
|
+
v[i] /= vNorm;
|
|
713
|
+
}
|
|
714
|
+
for (let j = k;j < n; j++) {
|
|
715
|
+
let dot = 0;
|
|
716
|
+
for (let i = 0;i < v.length; i++) {
|
|
717
|
+
dot += v[i] * R.get(k + i, j);
|
|
718
|
+
}
|
|
719
|
+
for (let i = 0;i < v.length; i++) {
|
|
720
|
+
R.set(k + i, j, R.get(k + i, j) - 2 * v[i] * dot);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
for (let i = 0;i < m; i++) {
|
|
724
|
+
let dot = 0;
|
|
725
|
+
for (let j = 0;j < v.length; j++) {
|
|
726
|
+
dot += Q.get(i, k + j) * v[j];
|
|
727
|
+
}
|
|
728
|
+
for (let j = 0;j < v.length; j++) {
|
|
729
|
+
Q.set(i, k + j, Q.get(i, k + j) - 2 * dot * v[j]);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return { Q, R };
|
|
734
|
+
}
|
|
735
|
+
function backSubstitution(R, b) {
|
|
736
|
+
const n = R.cols;
|
|
737
|
+
if (isWasmActive()) {
|
|
738
|
+
const bFlat = new Float64Array(n);
|
|
739
|
+
for (let i = 0;i < n; i++)
|
|
740
|
+
bFlat[i] = b.get(i, 0);
|
|
741
|
+
const wasmResult = engineSolveTriangular(R.data, bFlat, n);
|
|
742
|
+
if (wasmResult) {
|
|
743
|
+
return Matrix.columnVector(Array.from(wasmResult));
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
const x = new Float64Array(n);
|
|
747
|
+
for (let i = n - 1;i >= 0; i--) {
|
|
748
|
+
let sum = b.get(i, 0);
|
|
749
|
+
for (let j = i + 1;j < n; j++) {
|
|
750
|
+
sum -= R.get(i, j) * x[j];
|
|
751
|
+
}
|
|
752
|
+
const diag = R.get(i, i);
|
|
753
|
+
if (Math.abs(diag) < 0.000000000000001) {
|
|
754
|
+
throw new Error("Matrix is singular or near-singular (zero diagonal in R)");
|
|
755
|
+
}
|
|
756
|
+
x[i] = sum / diag;
|
|
757
|
+
}
|
|
758
|
+
return Matrix.columnVector(Array.from(x));
|
|
759
|
+
}
|
|
760
|
+
function solveQR(Q, R, b) {
|
|
761
|
+
const Qtb = Q.transpose().multiply(b);
|
|
762
|
+
const n = R.cols;
|
|
763
|
+
const Rsq = R.submatrix(0, n, 0, n);
|
|
764
|
+
const bTop = Qtb.submatrix(0, n, 0, 1);
|
|
765
|
+
return backSubstitution(Rsq, bTop);
|
|
766
|
+
}
|
|
767
|
+
function choleskyDecomposition(A) {
|
|
768
|
+
if (A.rows !== A.cols) {
|
|
769
|
+
throw new Error("Cholesky: matrix must be square");
|
|
770
|
+
}
|
|
771
|
+
const n = A.rows;
|
|
772
|
+
if (isWasmActive()) {
|
|
773
|
+
const wasmResult = engineCholesky(A.data, n);
|
|
774
|
+
if (wasmResult) {
|
|
775
|
+
return { L: new Matrix(n, n, wasmResult) };
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
const L = Matrix.zeros(n, n);
|
|
779
|
+
for (let j = 0;j < n; j++) {
|
|
780
|
+
let sum = 0;
|
|
781
|
+
for (let k = 0;k < j; k++) {
|
|
782
|
+
sum += L.get(j, k) * L.get(j, k);
|
|
783
|
+
}
|
|
784
|
+
const diag = A.get(j, j) - sum;
|
|
785
|
+
if (diag <= 0) {
|
|
786
|
+
throw new Error("Cholesky: matrix is not positive-definite");
|
|
787
|
+
}
|
|
788
|
+
L.set(j, j, Math.sqrt(diag));
|
|
789
|
+
for (let i = j + 1;i < n; i++) {
|
|
790
|
+
let s = 0;
|
|
791
|
+
for (let k = 0;k < j; k++) {
|
|
792
|
+
s += L.get(i, k) * L.get(j, k);
|
|
793
|
+
}
|
|
794
|
+
L.set(i, j, (A.get(i, j) - s) / L.get(j, j));
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return { L };
|
|
798
|
+
}
|
|
799
|
+
function forwardSubstitution(L, b) {
|
|
800
|
+
const n = L.rows;
|
|
801
|
+
const y = new Float64Array(n);
|
|
802
|
+
for (let i = 0;i < n; i++) {
|
|
803
|
+
let sum = b.get(i, 0);
|
|
804
|
+
for (let j = 0;j < i; j++) {
|
|
805
|
+
sum -= L.get(i, j) * y[j];
|
|
806
|
+
}
|
|
807
|
+
y[i] = sum / L.get(i, i);
|
|
808
|
+
}
|
|
809
|
+
return Matrix.columnVector(Array.from(y));
|
|
810
|
+
}
|
|
811
|
+
function solveCholesky(L, b) {
|
|
812
|
+
const y = forwardSubstitution(L, b);
|
|
813
|
+
return backSubstitution(L.transpose(), y);
|
|
814
|
+
}
|
|
815
|
+
function svd(A) {
|
|
816
|
+
const m = A.rows;
|
|
817
|
+
const n = A.cols;
|
|
818
|
+
const k = Math.min(m, n);
|
|
819
|
+
const W = A.clone();
|
|
820
|
+
const V = Matrix.identity(n);
|
|
821
|
+
const maxIter = 100;
|
|
822
|
+
const tol = 0.000000000001;
|
|
823
|
+
for (let iter = 0;iter < maxIter; iter++) {
|
|
824
|
+
let converged = true;
|
|
825
|
+
for (let p = 0;p < n - 1; p++) {
|
|
826
|
+
for (let q = p + 1;q < n; q++) {
|
|
827
|
+
let app = 0;
|
|
828
|
+
let aqq = 0;
|
|
829
|
+
let apq = 0;
|
|
830
|
+
for (let i = 0;i < m; i++) {
|
|
831
|
+
const wp = W.get(i, p);
|
|
832
|
+
const wq = W.get(i, q);
|
|
833
|
+
app += wp * wp;
|
|
834
|
+
aqq += wq * wq;
|
|
835
|
+
apq += wp * wq;
|
|
836
|
+
}
|
|
837
|
+
if (Math.abs(apq) < tol * Math.sqrt(app * aqq))
|
|
838
|
+
continue;
|
|
839
|
+
converged = false;
|
|
840
|
+
const tau = (aqq - app) / (2 * apq);
|
|
841
|
+
const t = Math.sign(tau) / (Math.abs(tau) + Math.sqrt(1 + tau * tau));
|
|
842
|
+
const c = 1 / Math.sqrt(1 + t * t);
|
|
843
|
+
const s = t * c;
|
|
844
|
+
for (let i = 0;i < m; i++) {
|
|
845
|
+
const wp = W.get(i, p);
|
|
846
|
+
const wq = W.get(i, q);
|
|
847
|
+
W.set(i, p, c * wp - s * wq);
|
|
848
|
+
W.set(i, q, s * wp + c * wq);
|
|
849
|
+
}
|
|
850
|
+
for (let i = 0;i < n; i++) {
|
|
851
|
+
const vp = V.get(i, p);
|
|
852
|
+
const vq = V.get(i, q);
|
|
853
|
+
V.set(i, p, c * vp - s * vq);
|
|
854
|
+
V.set(i, q, s * vp + c * vq);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (converged)
|
|
859
|
+
break;
|
|
860
|
+
}
|
|
861
|
+
const S = [];
|
|
862
|
+
const U = Matrix.zeros(m, k);
|
|
863
|
+
for (let j = 0;j < k; j++) {
|
|
864
|
+
let norm = 0;
|
|
865
|
+
for (let i = 0;i < m; i++) {
|
|
866
|
+
norm += W.get(i, j) * W.get(i, j);
|
|
867
|
+
}
|
|
868
|
+
norm = Math.sqrt(norm);
|
|
869
|
+
S.push(norm);
|
|
870
|
+
if (norm > 0.000000000000001) {
|
|
871
|
+
for (let i = 0;i < m; i++) {
|
|
872
|
+
U.set(i, j, W.get(i, j) / norm);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
const indices = S.map((_, i) => i).sort((a, b) => S[b] - S[a]);
|
|
877
|
+
const sortedS = indices.map((i) => S[i]);
|
|
878
|
+
const sortedU = Matrix.zeros(m, k);
|
|
879
|
+
const sortedV = Matrix.zeros(n, k);
|
|
880
|
+
for (let j = 0;j < k; j++) {
|
|
881
|
+
const srcIdx = indices[j];
|
|
882
|
+
for (let i = 0;i < m; i++) {
|
|
883
|
+
sortedU.set(i, j, U.get(i, srcIdx));
|
|
884
|
+
}
|
|
885
|
+
for (let i = 0;i < n; i++) {
|
|
886
|
+
sortedV.set(i, j, V.get(i, srcIdx));
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return { U: sortedU, S: sortedS, V: sortedV };
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// src/models/base.ts
|
|
893
|
+
init_distributions();
|
|
894
|
+
class BaseRegression {
|
|
895
|
+
_coefficients = [];
|
|
896
|
+
_intercept = 0;
|
|
897
|
+
_fitted = false;
|
|
898
|
+
_fitIntercept;
|
|
899
|
+
_X = [];
|
|
900
|
+
_y = [];
|
|
901
|
+
_yHat = [];
|
|
902
|
+
constructor(options = {}) {
|
|
903
|
+
this._fitIntercept = options.fitIntercept ?? true;
|
|
904
|
+
}
|
|
905
|
+
get coefficients() {
|
|
906
|
+
this.assertFitted();
|
|
907
|
+
return this._coefficients;
|
|
908
|
+
}
|
|
909
|
+
get intercept() {
|
|
910
|
+
this.assertFitted();
|
|
911
|
+
return this._intercept;
|
|
912
|
+
}
|
|
913
|
+
residuals() {
|
|
914
|
+
this.assertFitted();
|
|
915
|
+
return this._y.map((yi, i) => yi - this._yHat[i]);
|
|
916
|
+
}
|
|
917
|
+
statistics() {
|
|
918
|
+
this.assertFitted();
|
|
919
|
+
const n = this._y.length;
|
|
920
|
+
const p = this._coefficients.length;
|
|
921
|
+
const k = p + (this._fitIntercept ? 1 : 0);
|
|
922
|
+
const dfResidual = n - k;
|
|
923
|
+
if (dfResidual <= 0) {
|
|
924
|
+
throw new Error("Not enough observations for statistical inference");
|
|
925
|
+
}
|
|
926
|
+
let yMean = 0;
|
|
927
|
+
for (let i = 0;i < n; i++)
|
|
928
|
+
yMean += this._y[i];
|
|
929
|
+
yMean /= n;
|
|
930
|
+
let rss = 0;
|
|
931
|
+
let tss = 0;
|
|
932
|
+
for (let i = 0;i < n; i++) {
|
|
933
|
+
const residual = this._y[i] - this._yHat[i];
|
|
934
|
+
rss += residual * residual;
|
|
935
|
+
const diff = this._y[i] - yMean;
|
|
936
|
+
tss += diff * diff;
|
|
937
|
+
}
|
|
938
|
+
const rSquared = 1 - rss / tss;
|
|
939
|
+
const adjustedRSquared = 1 - (1 - rSquared) * (n - 1) / dfResidual;
|
|
940
|
+
const mse = rss / dfResidual;
|
|
941
|
+
const residualStandardError = Math.sqrt(mse);
|
|
942
|
+
const Xdesign = this._fitIntercept ? this.addInterceptColumn(this._X) : this._X;
|
|
943
|
+
const A = Matrix.fromArray(Xdesign);
|
|
944
|
+
const { R } = qrDecomposition(A);
|
|
945
|
+
const Rsq = R.submatrix(0, k, 0, k);
|
|
946
|
+
const Rinv = Matrix.zeros(k, k);
|
|
947
|
+
for (let j = 0;j < k; j++) {
|
|
948
|
+
const ej = Matrix.zeros(k, 1);
|
|
949
|
+
ej.set(j, 0, 1);
|
|
950
|
+
const col = backSubstitution(Rsq, ej);
|
|
951
|
+
for (let i = 0;i < k; i++) {
|
|
952
|
+
Rinv.set(i, j, col.get(i, 0));
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
const XtXinv = Rinv.multiply(Rinv.transpose());
|
|
956
|
+
const standardErrors = [];
|
|
957
|
+
const tStatistics = [];
|
|
958
|
+
const pValues = [];
|
|
959
|
+
const confidenceIntervals = [];
|
|
960
|
+
const tCrit = tInverseCDF(0.975, dfResidual);
|
|
961
|
+
const allCoeffs = this._fitIntercept ? [this._intercept, ...this._coefficients] : [...this._coefficients];
|
|
962
|
+
for (let i = 0;i < k; i++) {
|
|
963
|
+
const se = Math.sqrt(mse * XtXinv.get(i, i));
|
|
964
|
+
const coeff = allCoeffs[i];
|
|
965
|
+
const tStat = coeff / se;
|
|
966
|
+
const pVal = tTestPValue(tStat, dfResidual);
|
|
967
|
+
standardErrors.push(se);
|
|
968
|
+
tStatistics.push(tStat);
|
|
969
|
+
pValues.push(pVal);
|
|
970
|
+
confidenceIntervals.push([coeff - tCrit * se, coeff + tCrit * se]);
|
|
971
|
+
}
|
|
972
|
+
const ess = tss - rss;
|
|
973
|
+
const fStatistic = p > 0 ? ess / p / (rss / dfResidual) : 0;
|
|
974
|
+
const fPValue = p > 0 ? fTestPValue(fStatistic, p, dfResidual) : 1;
|
|
975
|
+
const aic = n * Math.log(rss / n) + 2 * k;
|
|
976
|
+
const bic = n * Math.log(rss / n) + Math.log(n) * k;
|
|
977
|
+
return {
|
|
978
|
+
rSquared,
|
|
979
|
+
adjustedRSquared,
|
|
980
|
+
standardErrors,
|
|
981
|
+
tStatistics,
|
|
982
|
+
pValues,
|
|
983
|
+
confidenceIntervals,
|
|
984
|
+
fStatistic,
|
|
985
|
+
fPValue,
|
|
986
|
+
residualStandardError,
|
|
987
|
+
aic,
|
|
988
|
+
bic,
|
|
989
|
+
degreesOfFreedom: dfResidual,
|
|
990
|
+
nObservations: n
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
summary() {
|
|
994
|
+
const stats = this.statistics();
|
|
995
|
+
const allCoeffs = this._fitIntercept ? [this._intercept, ...this._coefficients] : [...this._coefficients];
|
|
996
|
+
const names = this._fitIntercept ? ["(Intercept)", ...this._coefficients.map((_, i) => `x${i + 1}`)] : this._coefficients.map((_, i) => `x${i + 1}`);
|
|
997
|
+
const lines = ["Coefficients:"];
|
|
998
|
+
lines.push(padRight("", 15) + padRight("Estimate", 12) + padRight("Std. Error", 12) + padRight("t value", 10) + padRight("Pr(>|t|)", 12) + "");
|
|
999
|
+
for (let i = 0;i < allCoeffs.length; i++) {
|
|
1000
|
+
const stars = significanceStars(stats.pValues[i]);
|
|
1001
|
+
lines.push(padRight(names[i], 15) + padRight(allCoeffs[i].toFixed(4), 12) + padRight(stats.standardErrors[i].toFixed(4), 12) + padRight(stats.tStatistics[i].toFixed(2), 10) + padRight(stats.pValues[i].toFixed(4), 12) + stars);
|
|
1002
|
+
}
|
|
1003
|
+
lines.push("---");
|
|
1004
|
+
lines.push(`Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1`);
|
|
1005
|
+
lines.push("");
|
|
1006
|
+
lines.push(`Residual standard error: ${stats.residualStandardError.toFixed(4)} on ${stats.degreesOfFreedom} degrees of freedom`);
|
|
1007
|
+
lines.push(`Multiple R-squared: ${stats.rSquared.toFixed(4)}, Adjusted R-squared: ${stats.adjustedRSquared.toFixed(4)}`);
|
|
1008
|
+
lines.push(`F-statistic: ${stats.fStatistic.toFixed(2)} on ${this._coefficients.length} and ${stats.degreesOfFreedom} DF, p-value: ${stats.fPValue.toFixed(6)}`);
|
|
1009
|
+
return lines.join(`
|
|
1010
|
+
`);
|
|
1011
|
+
}
|
|
1012
|
+
normalizeInput(X) {
|
|
1013
|
+
if (X.length === 0)
|
|
1014
|
+
throw new Error("Input data cannot be empty");
|
|
1015
|
+
if (typeof X[0] === "number") {
|
|
1016
|
+
return X.map((v) => [v]);
|
|
1017
|
+
}
|
|
1018
|
+
return X;
|
|
1019
|
+
}
|
|
1020
|
+
addInterceptColumn(X) {
|
|
1021
|
+
return X.map((row) => [1, ...row]);
|
|
1022
|
+
}
|
|
1023
|
+
validateFitInput(X, y) {
|
|
1024
|
+
if (X.length !== y.length) {
|
|
1025
|
+
throw new Error(`X has ${X.length} rows but y has ${y.length} elements`);
|
|
1026
|
+
}
|
|
1027
|
+
if (X.length === 0)
|
|
1028
|
+
throw new Error("Input data cannot be empty");
|
|
1029
|
+
}
|
|
1030
|
+
assertFitted() {
|
|
1031
|
+
if (!this._fitted) {
|
|
1032
|
+
throw new Error("Model has not been fitted. Call fit() first.");
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
function padRight(s, len) {
|
|
1037
|
+
return s.length >= len ? s : s + " ".repeat(len - s.length);
|
|
1038
|
+
}
|
|
1039
|
+
function significanceStars(p) {
|
|
1040
|
+
if (p < 0.001)
|
|
1041
|
+
return " ***";
|
|
1042
|
+
if (p < 0.01)
|
|
1043
|
+
return " **";
|
|
1044
|
+
if (p < 0.05)
|
|
1045
|
+
return " *";
|
|
1046
|
+
if (p < 0.1)
|
|
1047
|
+
return " .";
|
|
1048
|
+
return "";
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// src/models/linear-regression.ts
|
|
1052
|
+
class LinearRegression extends BaseRegression {
|
|
1053
|
+
constructor(options = {}) {
|
|
1054
|
+
super(options);
|
|
1055
|
+
}
|
|
1056
|
+
fit(X, y) {
|
|
1057
|
+
const Xmat = this.normalizeInput(X);
|
|
1058
|
+
this.validateFitInput(Xmat, y);
|
|
1059
|
+
this._X = Xmat;
|
|
1060
|
+
this._y = y;
|
|
1061
|
+
const Xdesign = this._fitIntercept ? this.addInterceptColumn(Xmat) : Xmat;
|
|
1062
|
+
const A = Matrix.fromArray(Xdesign);
|
|
1063
|
+
const b = Matrix.columnVector(y);
|
|
1064
|
+
const { Q, R } = qrDecomposition(A);
|
|
1065
|
+
const beta = solveQR(Q, R, b);
|
|
1066
|
+
const betaArray = beta.toFlatArray();
|
|
1067
|
+
if (this._fitIntercept) {
|
|
1068
|
+
this._intercept = betaArray[0];
|
|
1069
|
+
this._coefficients = betaArray.slice(1);
|
|
1070
|
+
} else {
|
|
1071
|
+
this._intercept = 0;
|
|
1072
|
+
this._coefficients = betaArray;
|
|
1073
|
+
}
|
|
1074
|
+
this._yHat = this.predict(Xmat);
|
|
1075
|
+
this._fitted = true;
|
|
1076
|
+
return this;
|
|
1077
|
+
}
|
|
1078
|
+
predict(X) {
|
|
1079
|
+
const Xmat = this.normalizeInput(X);
|
|
1080
|
+
return Xmat.map((row) => {
|
|
1081
|
+
let sum = this._intercept;
|
|
1082
|
+
for (let j = 0;j < this._coefficients.length; j++) {
|
|
1083
|
+
sum += row[j] * this._coefficients[j];
|
|
1084
|
+
}
|
|
1085
|
+
return sum;
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// src/diagnostics/multicollinearity.ts
|
|
1091
|
+
function vif(X) {
|
|
1092
|
+
const p = X[0].length;
|
|
1093
|
+
if (p < 2)
|
|
1094
|
+
return [1];
|
|
1095
|
+
const result = [];
|
|
1096
|
+
for (let j = 0;j < p; j++) {
|
|
1097
|
+
const yCol = X.map((row) => row[j]);
|
|
1098
|
+
const XOther = X.map((row) => row.filter((_, idx) => idx !== j));
|
|
1099
|
+
const model = new LinearRegression;
|
|
1100
|
+
model.fit(XOther, yCol);
|
|
1101
|
+
const stats = model.statistics();
|
|
1102
|
+
const r2 = stats.rSquared;
|
|
1103
|
+
result.push(1 / Math.max(1 - r2, 0.000000000000001));
|
|
1104
|
+
}
|
|
1105
|
+
return result;
|
|
1106
|
+
}
|
|
1107
|
+
function correlationMatrix(X) {
|
|
1108
|
+
const n = X.length;
|
|
1109
|
+
const p = X[0].length;
|
|
1110
|
+
const means = [];
|
|
1111
|
+
for (let j = 0;j < p; j++) {
|
|
1112
|
+
let sum = 0;
|
|
1113
|
+
for (let i = 0;i < n; i++)
|
|
1114
|
+
sum += X[i][j];
|
|
1115
|
+
means.push(sum / n);
|
|
1116
|
+
}
|
|
1117
|
+
const stds = [];
|
|
1118
|
+
for (let j = 0;j < p; j++) {
|
|
1119
|
+
let sum = 0;
|
|
1120
|
+
for (let i = 0;i < n; i++) {
|
|
1121
|
+
const diff = X[i][j] - means[j];
|
|
1122
|
+
sum += diff * diff;
|
|
1123
|
+
}
|
|
1124
|
+
stds.push(Math.sqrt(sum / n));
|
|
1125
|
+
}
|
|
1126
|
+
const corr = [];
|
|
1127
|
+
for (let j1 = 0;j1 < p; j1++) {
|
|
1128
|
+
const row = [];
|
|
1129
|
+
for (let j2 = 0;j2 < p; j2++) {
|
|
1130
|
+
if (j1 === j2) {
|
|
1131
|
+
row.push(1);
|
|
1132
|
+
} else {
|
|
1133
|
+
let sum = 0;
|
|
1134
|
+
for (let i = 0;i < n; i++) {
|
|
1135
|
+
sum += (X[i][j1] - means[j1]) / Math.max(stds[j1], 0.000000000000001) * ((X[i][j2] - means[j2]) / Math.max(stds[j2], 0.000000000000001));
|
|
1136
|
+
}
|
|
1137
|
+
row.push(sum / n);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
corr.push(row);
|
|
1141
|
+
}
|
|
1142
|
+
return corr;
|
|
1143
|
+
}
|
|
1144
|
+
function conditionNumber(X) {
|
|
1145
|
+
const A = Matrix.fromArray(X);
|
|
1146
|
+
const { S } = svd(A);
|
|
1147
|
+
const sMax = S[0] ?? 1;
|
|
1148
|
+
const sMin = S[S.length - 1] ?? 0;
|
|
1149
|
+
return sMin > 0.000000000000001 ? sMax / sMin : Infinity;
|
|
1150
|
+
}
|
|
1151
|
+
// src/diagnostics/residuals.ts
|
|
1152
|
+
function residualDiagnostics(X, y, yHat, fitIntercept = true) {
|
|
1153
|
+
const n = y.length;
|
|
1154
|
+
const raw = y.map((yi, i) => yi - yHat[i]);
|
|
1155
|
+
const Xdesign = fitIntercept ? X.map((row) => [1, ...row]) : X;
|
|
1156
|
+
const A = Matrix.fromArray(Xdesign);
|
|
1157
|
+
const k = A.cols;
|
|
1158
|
+
const { Q } = qrDecomposition(A);
|
|
1159
|
+
const leverageValues = [];
|
|
1160
|
+
for (let i = 0;i < n; i++) {
|
|
1161
|
+
let h = 0;
|
|
1162
|
+
for (let j = 0;j < k; j++) {
|
|
1163
|
+
h += Q.get(i, j) * Q.get(i, j);
|
|
1164
|
+
}
|
|
1165
|
+
leverageValues.push(h);
|
|
1166
|
+
}
|
|
1167
|
+
let rss = 0;
|
|
1168
|
+
for (const r of raw)
|
|
1169
|
+
rss += r * r;
|
|
1170
|
+
const s = Math.sqrt(rss / (n - k));
|
|
1171
|
+
const studentized = raw.map((r, i) => {
|
|
1172
|
+
const h = leverageValues[i];
|
|
1173
|
+
const denom = s * Math.sqrt(Math.max(1 - h, 0.000000000000001));
|
|
1174
|
+
return r / denom;
|
|
1175
|
+
});
|
|
1176
|
+
const cooksDistance = studentized.map((eStar, i) => {
|
|
1177
|
+
const h = leverageValues[i];
|
|
1178
|
+
return eStar * eStar * h / (k * Math.max(1 - h, 0.000000000000001));
|
|
1179
|
+
});
|
|
1180
|
+
return {
|
|
1181
|
+
raw,
|
|
1182
|
+
studentized,
|
|
1183
|
+
cooksDistance,
|
|
1184
|
+
leverage: leverageValues
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
function leverage(X, fitIntercept = true) {
|
|
1188
|
+
const Xdesign = fitIntercept ? X.map((row) => [1, ...row]) : X;
|
|
1189
|
+
const A = Matrix.fromArray(Xdesign);
|
|
1190
|
+
const k = A.cols;
|
|
1191
|
+
const { Q } = qrDecomposition(A);
|
|
1192
|
+
const result = [];
|
|
1193
|
+
for (let i = 0;i < A.rows; i++) {
|
|
1194
|
+
let h = 0;
|
|
1195
|
+
for (let j = 0;j < k; j++) {
|
|
1196
|
+
h += Q.get(i, j) * Q.get(i, j);
|
|
1197
|
+
}
|
|
1198
|
+
result.push(h);
|
|
1199
|
+
}
|
|
1200
|
+
return result;
|
|
1201
|
+
}
|
|
1202
|
+
function cooksDistance(X, y, yHat, fitIntercept = true) {
|
|
1203
|
+
return residualDiagnostics(X, y, yHat, fitIntercept).cooksDistance;
|
|
1204
|
+
}
|
|
1205
|
+
function studentizedResiduals(X, y, yHat, fitIntercept = true) {
|
|
1206
|
+
return residualDiagnostics(X, y, yHat, fitIntercept).studentized;
|
|
1207
|
+
}
|
|
1208
|
+
// src/diagnostics/tests.ts
|
|
1209
|
+
init_distributions();
|
|
1210
|
+
function durbinWatson(residuals) {
|
|
1211
|
+
const n = residuals.length;
|
|
1212
|
+
let num = 0;
|
|
1213
|
+
let den = 0;
|
|
1214
|
+
for (let i = 0;i < n; i++) {
|
|
1215
|
+
den += residuals[i] * residuals[i];
|
|
1216
|
+
if (i > 0) {
|
|
1217
|
+
const diff = residuals[i] - residuals[i - 1];
|
|
1218
|
+
num += diff * diff;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
const statistic = den > 0 ? num / den : 2;
|
|
1222
|
+
const pValue = Math.exp(-Math.abs(statistic - 2) * Math.sqrt(n) * 0.5);
|
|
1223
|
+
return { statistic, pValue };
|
|
1224
|
+
}
|
|
1225
|
+
function breuschPagan(X, residuals) {
|
|
1226
|
+
const n = residuals.length;
|
|
1227
|
+
const squaredResiduals = residuals.map((r) => r * r);
|
|
1228
|
+
const auxModel = new LinearRegression;
|
|
1229
|
+
auxModel.fit(X, squaredResiduals);
|
|
1230
|
+
const stats = auxModel.statistics();
|
|
1231
|
+
const statistic = n * stats.rSquared;
|
|
1232
|
+
const df = X[0].length;
|
|
1233
|
+
const pValue = chi2TestPValue(statistic, df);
|
|
1234
|
+
return { statistic, pValue };
|
|
1235
|
+
}
|
|
1236
|
+
function shapiroWilk(data) {
|
|
1237
|
+
const n = data.length;
|
|
1238
|
+
if (n < 3)
|
|
1239
|
+
throw new Error("Shapiro-Wilk requires at least 3 observations");
|
|
1240
|
+
const sorted = [...data].sort((a2, b) => a2 - b);
|
|
1241
|
+
let mean = 0;
|
|
1242
|
+
for (const x of sorted)
|
|
1243
|
+
mean += x;
|
|
1244
|
+
mean /= n;
|
|
1245
|
+
let ss = 0;
|
|
1246
|
+
for (const x of sorted)
|
|
1247
|
+
ss += (x - mean) ** 2;
|
|
1248
|
+
if (ss < 0.000000000000001) {
|
|
1249
|
+
return { statistic: 1, pValue: 1 };
|
|
1250
|
+
}
|
|
1251
|
+
const { normalInverseCDF: normalInverseCDF2 } = (init_distributions(), __toCommonJS(exports_distributions));
|
|
1252
|
+
const m = [];
|
|
1253
|
+
for (let i = 0;i < n; i++) {
|
|
1254
|
+
const p = (i + 1 - 0.375) / (n + 0.25);
|
|
1255
|
+
m.push(normalInverseCDF2(p));
|
|
1256
|
+
}
|
|
1257
|
+
let mSqSum = 0;
|
|
1258
|
+
for (const mi of m)
|
|
1259
|
+
mSqSum += mi * mi;
|
|
1260
|
+
const mNorm = Math.sqrt(mSqSum);
|
|
1261
|
+
const a = m.map((mi) => mi / mNorm);
|
|
1262
|
+
let aTimesX = 0;
|
|
1263
|
+
for (let i = 0;i < n; i++) {
|
|
1264
|
+
aTimesX += a[i] * sorted[i];
|
|
1265
|
+
}
|
|
1266
|
+
const statistic = aTimesX * aTimesX / ss;
|
|
1267
|
+
const lnW = Math.log(1 - statistic);
|
|
1268
|
+
const mu = -1.2725 + 1.0521 * Math.log(n);
|
|
1269
|
+
const sigma = 1.0308 - 0.26758 * Math.log(n);
|
|
1270
|
+
const z = (lnW - mu) / sigma;
|
|
1271
|
+
const { normalCDF: normCDF } = (init_distributions(), __toCommonJS(exports_distributions));
|
|
1272
|
+
const pValue = 1 - normCDF(z);
|
|
1273
|
+
return { statistic, pValue: Math.max(0, Math.min(1, pValue)) };
|
|
1274
|
+
}
|
|
1275
|
+
// src/models/lasso-regression.ts
|
|
1276
|
+
class LassoRegression extends BaseRegression {
|
|
1277
|
+
_alpha;
|
|
1278
|
+
_maxIterations;
|
|
1279
|
+
_tolerance;
|
|
1280
|
+
constructor(options = {}) {
|
|
1281
|
+
super(options);
|
|
1282
|
+
this._alpha = options.alpha ?? 1;
|
|
1283
|
+
this._maxIterations = options.maxIterations ?? 1000;
|
|
1284
|
+
this._tolerance = options.tolerance ?? 0.0001;
|
|
1285
|
+
}
|
|
1286
|
+
fit(X, y) {
|
|
1287
|
+
const Xmat = this.normalizeInput(X);
|
|
1288
|
+
this.validateFitInput(Xmat, y);
|
|
1289
|
+
this._X = Xmat;
|
|
1290
|
+
this._y = y;
|
|
1291
|
+
const n = Xmat.length;
|
|
1292
|
+
const p = Xmat[0].length;
|
|
1293
|
+
const { Xstd, xMeans, xStds, yMean } = this.standardize(Xmat, y);
|
|
1294
|
+
const yCentered = y.map((yi) => yi - yMean);
|
|
1295
|
+
const beta = new Float64Array(p);
|
|
1296
|
+
const residual = new Float64Array(yCentered);
|
|
1297
|
+
const colNormsSq = new Float64Array(p);
|
|
1298
|
+
for (let j = 0;j < p; j++) {
|
|
1299
|
+
let sum = 0;
|
|
1300
|
+
for (let i = 0;i < n; i++) {
|
|
1301
|
+
sum += Xstd[i][j] * Xstd[i][j];
|
|
1302
|
+
}
|
|
1303
|
+
colNormsSq[j] = sum;
|
|
1304
|
+
}
|
|
1305
|
+
for (let iter = 0;iter < this._maxIterations; iter++) {
|
|
1306
|
+
let maxChange = 0;
|
|
1307
|
+
for (let j = 0;j < p; j++) {
|
|
1308
|
+
const oldBeta = beta[j];
|
|
1309
|
+
let rho = 0;
|
|
1310
|
+
for (let i = 0;i < n; i++) {
|
|
1311
|
+
rho += Xstd[i][j] * (residual[i] + oldBeta * Xstd[i][j]);
|
|
1312
|
+
}
|
|
1313
|
+
beta[j] = this.coordinateUpdate(rho, colNormsSq[j], n, j);
|
|
1314
|
+
const change = beta[j] - oldBeta;
|
|
1315
|
+
if (change !== 0) {
|
|
1316
|
+
for (let i = 0;i < n; i++) {
|
|
1317
|
+
residual[i] -= change * Xstd[i][j];
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
maxChange = Math.max(maxChange, Math.abs(change));
|
|
1321
|
+
}
|
|
1322
|
+
if (maxChange < this._tolerance)
|
|
1323
|
+
break;
|
|
1324
|
+
}
|
|
1325
|
+
this._coefficients = Array.from(beta).map((bj, j) => {
|
|
1326
|
+
const std = xStds[j];
|
|
1327
|
+
return std > 0.000000000000001 ? bj / std : 0;
|
|
1328
|
+
});
|
|
1329
|
+
this._intercept = this._fitIntercept ? yMean - this._coefficients.reduce((sum, bj, j) => sum + bj * xMeans[j], 0) : 0;
|
|
1330
|
+
this._yHat = this.predict(Xmat);
|
|
1331
|
+
this._fitted = true;
|
|
1332
|
+
return this;
|
|
1333
|
+
}
|
|
1334
|
+
predict(X) {
|
|
1335
|
+
const Xmat = this.normalizeInput(X);
|
|
1336
|
+
return Xmat.map((row) => {
|
|
1337
|
+
let sum = this._intercept;
|
|
1338
|
+
for (let j = 0;j < this._coefficients.length; j++) {
|
|
1339
|
+
sum += row[j] * this._coefficients[j];
|
|
1340
|
+
}
|
|
1341
|
+
return sum;
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
coordinateUpdate(rho, colNormSq, n, _j) {
|
|
1345
|
+
return this.softThreshold(rho, n * this._alpha) / colNormSq;
|
|
1346
|
+
}
|
|
1347
|
+
softThreshold(rho, lambda) {
|
|
1348
|
+
if (rho < -lambda)
|
|
1349
|
+
return rho + lambda;
|
|
1350
|
+
if (rho > lambda)
|
|
1351
|
+
return rho - lambda;
|
|
1352
|
+
return 0;
|
|
1353
|
+
}
|
|
1354
|
+
standardize(X, y) {
|
|
1355
|
+
const n = X.length;
|
|
1356
|
+
const p = X[0].length;
|
|
1357
|
+
const xMeans = [];
|
|
1358
|
+
const xStds = [];
|
|
1359
|
+
for (let j = 0;j < p; j++) {
|
|
1360
|
+
let sum = 0;
|
|
1361
|
+
for (let i = 0;i < n; i++)
|
|
1362
|
+
sum += X[i][j];
|
|
1363
|
+
xMeans.push(sum / n);
|
|
1364
|
+
}
|
|
1365
|
+
for (let j = 0;j < p; j++) {
|
|
1366
|
+
let sum = 0;
|
|
1367
|
+
for (let i = 0;i < n; i++) {
|
|
1368
|
+
const diff = X[i][j] - xMeans[j];
|
|
1369
|
+
sum += diff * diff;
|
|
1370
|
+
}
|
|
1371
|
+
xStds.push(Math.sqrt(sum / n));
|
|
1372
|
+
}
|
|
1373
|
+
const Xstd = X.map((row) => row.map((val, j) => {
|
|
1374
|
+
const std = xStds[j];
|
|
1375
|
+
return std > 0.000000000000001 ? (val - xMeans[j]) / std : 0;
|
|
1376
|
+
}));
|
|
1377
|
+
let yMean = 0;
|
|
1378
|
+
for (let i = 0;i < n; i++)
|
|
1379
|
+
yMean += y[i];
|
|
1380
|
+
yMean /= n;
|
|
1381
|
+
return { Xstd, xMeans, xStds, yMean };
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// src/models/elastic-net.ts
|
|
1386
|
+
class ElasticNet extends LassoRegression {
|
|
1387
|
+
_l1Ratio;
|
|
1388
|
+
constructor(options = {}) {
|
|
1389
|
+
super({
|
|
1390
|
+
fitIntercept: options.fitIntercept,
|
|
1391
|
+
alpha: options.alpha,
|
|
1392
|
+
maxIterations: options.maxIterations,
|
|
1393
|
+
tolerance: options.tolerance
|
|
1394
|
+
});
|
|
1395
|
+
this._l1Ratio = options.l1Ratio ?? 0.5;
|
|
1396
|
+
}
|
|
1397
|
+
coordinateUpdate(rho, colNormSq, n, _j) {
|
|
1398
|
+
const l1Penalty = n * this._alpha * this._l1Ratio;
|
|
1399
|
+
const l2Penalty = n * this._alpha * (1 - this._l1Ratio);
|
|
1400
|
+
return this.softThreshold(rho, l1Penalty) / (colNormSq + l2Penalty);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
// src/models/knn.ts
|
|
1404
|
+
class KNearestNeighbors {
|
|
1405
|
+
_k;
|
|
1406
|
+
_distance;
|
|
1407
|
+
_mode;
|
|
1408
|
+
_fitted = false;
|
|
1409
|
+
_X = [];
|
|
1410
|
+
_y = [];
|
|
1411
|
+
constructor(options = {}) {
|
|
1412
|
+
this._k = options.k ?? 5;
|
|
1413
|
+
this._distance = options.distance ?? "euclidean";
|
|
1414
|
+
this._mode = options.mode ?? "classification";
|
|
1415
|
+
}
|
|
1416
|
+
fit(X, y) {
|
|
1417
|
+
const Xmat = this.normalizeInput(X);
|
|
1418
|
+
if (Xmat.length !== y.length) {
|
|
1419
|
+
throw new Error(`X has ${Xmat.length} rows but y has ${y.length} elements`);
|
|
1420
|
+
}
|
|
1421
|
+
if (Xmat.length < this._k) {
|
|
1422
|
+
throw new Error(`Need at least k=${this._k} samples, got ${Xmat.length}`);
|
|
1423
|
+
}
|
|
1424
|
+
this._X = Xmat;
|
|
1425
|
+
this._y = y;
|
|
1426
|
+
this._fitted = true;
|
|
1427
|
+
return this;
|
|
1428
|
+
}
|
|
1429
|
+
predict(X) {
|
|
1430
|
+
if (!this._fitted)
|
|
1431
|
+
throw new Error("Model has not been fitted. Call fit() first.");
|
|
1432
|
+
const Xmat = this.normalizeInput(X);
|
|
1433
|
+
return Xmat.map((row) => this.predictOne(row));
|
|
1434
|
+
}
|
|
1435
|
+
neighbors(point) {
|
|
1436
|
+
if (!this._fitted)
|
|
1437
|
+
throw new Error("Model has not been fitted. Call fit() first.");
|
|
1438
|
+
return this.findNeighbors(point).map((n) => n.index);
|
|
1439
|
+
}
|
|
1440
|
+
predictOne(point) {
|
|
1441
|
+
const nearest = this.findNeighbors(point);
|
|
1442
|
+
if (this._mode === "regression") {
|
|
1443
|
+
let sum = 0;
|
|
1444
|
+
for (const n of nearest)
|
|
1445
|
+
sum += this._y[n.index];
|
|
1446
|
+
return sum / nearest.length;
|
|
1447
|
+
}
|
|
1448
|
+
const votes = new Map;
|
|
1449
|
+
for (const n of nearest) {
|
|
1450
|
+
const label = this._y[n.index];
|
|
1451
|
+
votes.set(label, (votes.get(label) ?? 0) + 1);
|
|
1452
|
+
}
|
|
1453
|
+
let bestLabel = 0;
|
|
1454
|
+
let bestCount = 0;
|
|
1455
|
+
for (const [label, count] of votes) {
|
|
1456
|
+
if (count > bestCount) {
|
|
1457
|
+
bestCount = count;
|
|
1458
|
+
bestLabel = label;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
return bestLabel;
|
|
1462
|
+
}
|
|
1463
|
+
findNeighbors(point) {
|
|
1464
|
+
const distances = [];
|
|
1465
|
+
for (let i = 0;i < this._X.length; i++) {
|
|
1466
|
+
const d = this.computeDistance(point, this._X[i]);
|
|
1467
|
+
distances.push({ index: i, distance: d });
|
|
1468
|
+
}
|
|
1469
|
+
distances.sort((a, b) => a.distance - b.distance);
|
|
1470
|
+
return distances.slice(0, this._k);
|
|
1471
|
+
}
|
|
1472
|
+
computeDistance(a, b) {
|
|
1473
|
+
let sum = 0;
|
|
1474
|
+
for (let i = 0;i < a.length; i++) {
|
|
1475
|
+
const diff = a[i] - b[i];
|
|
1476
|
+
if (this._distance === "manhattan") {
|
|
1477
|
+
sum += Math.abs(diff);
|
|
1478
|
+
} else {
|
|
1479
|
+
sum += diff * diff;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
return this._distance === "manhattan" ? sum : Math.sqrt(sum);
|
|
1483
|
+
}
|
|
1484
|
+
normalizeInput(X) {
|
|
1485
|
+
if (X.length === 0)
|
|
1486
|
+
throw new Error("Input data cannot be empty");
|
|
1487
|
+
if (typeof X[0] === "number") {
|
|
1488
|
+
return X.map((v) => [v]);
|
|
1489
|
+
}
|
|
1490
|
+
return X;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
// src/models/logistic-regression.ts
|
|
1494
|
+
class LogisticRegression {
|
|
1495
|
+
_coefficients = [];
|
|
1496
|
+
_intercept = 0;
|
|
1497
|
+
_fitted = false;
|
|
1498
|
+
_fitIntercept;
|
|
1499
|
+
_maxIterations;
|
|
1500
|
+
_tolerance;
|
|
1501
|
+
_y = [];
|
|
1502
|
+
_probabilities = [];
|
|
1503
|
+
constructor(options = {}) {
|
|
1504
|
+
this._fitIntercept = options.fitIntercept ?? true;
|
|
1505
|
+
this._maxIterations = options.maxIterations ?? 100;
|
|
1506
|
+
this._tolerance = options.tolerance ?? 0.000001;
|
|
1507
|
+
}
|
|
1508
|
+
get coefficients() {
|
|
1509
|
+
if (!this._fitted)
|
|
1510
|
+
throw new Error("Model has not been fitted. Call fit() first.");
|
|
1511
|
+
return this._coefficients;
|
|
1512
|
+
}
|
|
1513
|
+
get intercept() {
|
|
1514
|
+
if (!this._fitted)
|
|
1515
|
+
throw new Error("Model has not been fitted. Call fit() first.");
|
|
1516
|
+
return this._intercept;
|
|
1517
|
+
}
|
|
1518
|
+
sigmoid(z) {
|
|
1519
|
+
if (z >= 0)
|
|
1520
|
+
return 1 / (1 + Math.exp(-z));
|
|
1521
|
+
const expZ = Math.exp(z);
|
|
1522
|
+
return expZ / (1 + expZ);
|
|
1523
|
+
}
|
|
1524
|
+
fit(X, y) {
|
|
1525
|
+
const Xmat = this.normalizeInput(X);
|
|
1526
|
+
if (Xmat.length !== y.length) {
|
|
1527
|
+
throw new Error(`X has ${Xmat.length} rows but y has ${y.length} elements`);
|
|
1528
|
+
}
|
|
1529
|
+
for (const yi of y) {
|
|
1530
|
+
if (yi !== 0 && yi !== 1) {
|
|
1531
|
+
throw new Error("Logistic regression requires binary y (0 or 1)");
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
this._X = Xmat;
|
|
1535
|
+
this._y = y;
|
|
1536
|
+
const n = Xmat.length;
|
|
1537
|
+
const Xdesign = this._fitIntercept ? this.addInterceptColumn(Xmat) : Xmat;
|
|
1538
|
+
const k = Xdesign[0].length;
|
|
1539
|
+
const beta = new Float64Array(k);
|
|
1540
|
+
for (let iter = 0;iter < this._maxIterations; iter++) {
|
|
1541
|
+
const eta = new Float64Array(n);
|
|
1542
|
+
const p = new Float64Array(n);
|
|
1543
|
+
for (let i = 0;i < n; i++) {
|
|
1544
|
+
let sum = 0;
|
|
1545
|
+
for (let j = 0;j < k; j++) {
|
|
1546
|
+
sum += Xdesign[i][j] * beta[j];
|
|
1547
|
+
}
|
|
1548
|
+
eta[i] = sum;
|
|
1549
|
+
p[i] = this.sigmoid(sum);
|
|
1550
|
+
}
|
|
1551
|
+
const sqrtW = new Float64Array(n);
|
|
1552
|
+
const z = new Float64Array(n);
|
|
1553
|
+
for (let i = 0;i < n; i++) {
|
|
1554
|
+
const pi = Math.max(0.000000000000001, Math.min(1 - 0.000000000000001, p[i]));
|
|
1555
|
+
const wi = pi * (1 - pi);
|
|
1556
|
+
sqrtW[i] = Math.sqrt(Math.max(wi, 0.0000000001));
|
|
1557
|
+
z[i] = eta[i] + (y[i] - pi) / wi;
|
|
1558
|
+
}
|
|
1559
|
+
const XW = Matrix.zeros(n, k);
|
|
1560
|
+
const zW = Matrix.zeros(n, 1);
|
|
1561
|
+
for (let i = 0;i < n; i++) {
|
|
1562
|
+
for (let j = 0;j < k; j++) {
|
|
1563
|
+
XW.set(i, j, Xdesign[i][j] * sqrtW[i]);
|
|
1564
|
+
}
|
|
1565
|
+
zW.set(i, 0, z[i] * sqrtW[i]);
|
|
1566
|
+
}
|
|
1567
|
+
const { Q, R } = qrDecomposition(XW);
|
|
1568
|
+
const betaNew = solveQR(Q, R, zW);
|
|
1569
|
+
let maxChange = 0;
|
|
1570
|
+
for (let j = 0;j < k; j++) {
|
|
1571
|
+
maxChange = Math.max(maxChange, Math.abs(betaNew.get(j, 0) - beta[j]));
|
|
1572
|
+
beta[j] = betaNew.get(j, 0);
|
|
1573
|
+
}
|
|
1574
|
+
if (maxChange < this._tolerance)
|
|
1575
|
+
break;
|
|
1576
|
+
}
|
|
1577
|
+
const betaArray = Array.from(beta);
|
|
1578
|
+
if (this._fitIntercept) {
|
|
1579
|
+
this._intercept = betaArray[0];
|
|
1580
|
+
this._coefficients = betaArray.slice(1);
|
|
1581
|
+
} else {
|
|
1582
|
+
this._intercept = 0;
|
|
1583
|
+
this._coefficients = betaArray;
|
|
1584
|
+
}
|
|
1585
|
+
this._probabilities = this.predictProbability(Xmat);
|
|
1586
|
+
this._fitted = true;
|
|
1587
|
+
return this;
|
|
1588
|
+
}
|
|
1589
|
+
predict(X) {
|
|
1590
|
+
return this.predictProbability(X).map((p) => p >= 0.5 ? 1 : 0);
|
|
1591
|
+
}
|
|
1592
|
+
predictProbability(X) {
|
|
1593
|
+
const Xmat = this.normalizeInput(X);
|
|
1594
|
+
return Xmat.map((row) => {
|
|
1595
|
+
let sum = this._intercept;
|
|
1596
|
+
for (let j = 0;j < this._coefficients.length; j++) {
|
|
1597
|
+
sum += row[j] * this._coefficients[j];
|
|
1598
|
+
}
|
|
1599
|
+
return this.sigmoid(sum);
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
statistics() {
|
|
1603
|
+
if (!this._fitted)
|
|
1604
|
+
throw new Error("Model has not been fitted. Call fit() first.");
|
|
1605
|
+
const n = this._y.length;
|
|
1606
|
+
const predicted = this._probabilities.map((p) => p >= 0.5 ? 1 : 0);
|
|
1607
|
+
let tp = 0;
|
|
1608
|
+
let tn = 0;
|
|
1609
|
+
let fp = 0;
|
|
1610
|
+
let fn = 0;
|
|
1611
|
+
for (let i = 0;i < n; i++) {
|
|
1612
|
+
if (this._y[i] === 1 && predicted[i] === 1)
|
|
1613
|
+
tp++;
|
|
1614
|
+
else if (this._y[i] === 0 && predicted[i] === 0)
|
|
1615
|
+
tn++;
|
|
1616
|
+
else if (this._y[i] === 0 && predicted[i] === 1)
|
|
1617
|
+
fp++;
|
|
1618
|
+
else
|
|
1619
|
+
fn++;
|
|
1620
|
+
}
|
|
1621
|
+
const accuracy = (tp + tn) / n;
|
|
1622
|
+
const precision = tp + fp > 0 ? tp / (tp + fp) : 0;
|
|
1623
|
+
const recall = tp + fn > 0 ? tp / (tp + fn) : 0;
|
|
1624
|
+
const f1Score = precision + recall > 0 ? 2 * precision * recall / (precision + recall) : 0;
|
|
1625
|
+
let logLik = 0;
|
|
1626
|
+
for (let i = 0;i < n; i++) {
|
|
1627
|
+
const pi = Math.max(0.000000000000001, Math.min(1 - 0.000000000000001, this._probabilities[i]));
|
|
1628
|
+
logLik += this._y[i] * Math.log(pi) + (1 - this._y[i]) * Math.log(1 - pi);
|
|
1629
|
+
}
|
|
1630
|
+
let yMean = 0;
|
|
1631
|
+
for (const yi of this._y)
|
|
1632
|
+
yMean += yi;
|
|
1633
|
+
yMean /= n;
|
|
1634
|
+
const pBar = Math.max(0.000000000000001, Math.min(1 - 0.000000000000001, yMean));
|
|
1635
|
+
const nullLogLik = n * (pBar * Math.log(pBar) + (1 - pBar) * Math.log(1 - pBar));
|
|
1636
|
+
const pseudoRSquared = 1 - logLik / nullLogLik;
|
|
1637
|
+
const k = this._coefficients.length + (this._fitIntercept ? 1 : 0);
|
|
1638
|
+
const aic = -2 * logLik + 2 * k;
|
|
1639
|
+
const bic = -2 * logLik + Math.log(n) * k;
|
|
1640
|
+
return {
|
|
1641
|
+
accuracy,
|
|
1642
|
+
precision,
|
|
1643
|
+
recall,
|
|
1644
|
+
f1Score,
|
|
1645
|
+
confusionMatrix: {
|
|
1646
|
+
truePositives: tp,
|
|
1647
|
+
trueNegatives: tn,
|
|
1648
|
+
falsePositives: fp,
|
|
1649
|
+
falseNegatives: fn
|
|
1650
|
+
},
|
|
1651
|
+
pseudoRSquared,
|
|
1652
|
+
logLikelihood: logLik,
|
|
1653
|
+
aic,
|
|
1654
|
+
bic
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
normalizeInput(X) {
|
|
1658
|
+
if (X.length === 0)
|
|
1659
|
+
throw new Error("Input data cannot be empty");
|
|
1660
|
+
if (typeof X[0] === "number") {
|
|
1661
|
+
return X.map((v) => [v]);
|
|
1662
|
+
}
|
|
1663
|
+
return X;
|
|
1664
|
+
}
|
|
1665
|
+
addInterceptColumn(X) {
|
|
1666
|
+
return X.map((row) => [1, ...row]);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
// src/models/multiclass-logistic-regression.ts
|
|
1670
|
+
class MulticlassLogisticRegression {
|
|
1671
|
+
_weights = Matrix.zeros(0, 0);
|
|
1672
|
+
_fitted = false;
|
|
1673
|
+
_fitIntercept;
|
|
1674
|
+
_maxIterations;
|
|
1675
|
+
_tolerance;
|
|
1676
|
+
_learningRate;
|
|
1677
|
+
_nClasses = 0;
|
|
1678
|
+
_classes = [];
|
|
1679
|
+
_X = [];
|
|
1680
|
+
_y = [];
|
|
1681
|
+
constructor(options = {}) {
|
|
1682
|
+
this._fitIntercept = options.fitIntercept ?? true;
|
|
1683
|
+
this._maxIterations = options.maxIterations ?? 200;
|
|
1684
|
+
this._tolerance = options.tolerance ?? 0.000001;
|
|
1685
|
+
this._learningRate = options.learningRate ?? 0.1;
|
|
1686
|
+
}
|
|
1687
|
+
get weights() {
|
|
1688
|
+
if (!this._fitted)
|
|
1689
|
+
throw new Error("Model has not been fitted. Call fit() first.");
|
|
1690
|
+
return this._weights;
|
|
1691
|
+
}
|
|
1692
|
+
get classes() {
|
|
1693
|
+
if (!this._fitted)
|
|
1694
|
+
throw new Error("Model has not been fitted. Call fit() first.");
|
|
1695
|
+
return this._classes;
|
|
1696
|
+
}
|
|
1697
|
+
softmax(logits) {
|
|
1698
|
+
const max = Math.max(...logits);
|
|
1699
|
+
const exps = logits.map((l) => Math.exp(l - max));
|
|
1700
|
+
const sum = exps.reduce((a, b) => a + b, 0);
|
|
1701
|
+
return exps.map((e) => e / sum);
|
|
1702
|
+
}
|
|
1703
|
+
fit(X, y) {
|
|
1704
|
+
const Xmat = this.normalizeInput(X);
|
|
1705
|
+
if (Xmat.length !== y.length) {
|
|
1706
|
+
throw new Error(`X has ${Xmat.length} rows but y has ${y.length} elements`);
|
|
1707
|
+
}
|
|
1708
|
+
this._X = Xmat;
|
|
1709
|
+
this._y = y;
|
|
1710
|
+
this._classes = [...new Set(y)].sort((a, b) => a - b);
|
|
1711
|
+
this._nClasses = this._classes.length;
|
|
1712
|
+
for (const yi of y) {
|
|
1713
|
+
if (!this._classes.includes(yi)) {
|
|
1714
|
+
throw new Error(`Unexpected label ${yi}`);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
const n = Xmat.length;
|
|
1718
|
+
const Xdesign = this._fitIntercept ? Xmat.map((row) => [1, ...row]) : Xmat;
|
|
1719
|
+
const k = Xdesign[0].length;
|
|
1720
|
+
const K = this._nClasses;
|
|
1721
|
+
const Y = Matrix.zeros(n, K);
|
|
1722
|
+
for (let i = 0;i < n; i++) {
|
|
1723
|
+
const classIdx = this._classes.indexOf(y[i]);
|
|
1724
|
+
Y.set(i, classIdx, 1);
|
|
1725
|
+
}
|
|
1726
|
+
this._weights = Matrix.zeros(k, K);
|
|
1727
|
+
const XMat = Matrix.fromArray(Xdesign);
|
|
1728
|
+
for (let iter = 0;iter < this._maxIterations; iter++) {
|
|
1729
|
+
const scores = XMat.multiply(this._weights);
|
|
1730
|
+
const P = Matrix.zeros(n, K);
|
|
1731
|
+
for (let i = 0;i < n; i++) {
|
|
1732
|
+
const logits = [];
|
|
1733
|
+
for (let c = 0;c < K; c++)
|
|
1734
|
+
logits.push(scores.get(i, c));
|
|
1735
|
+
const probs = this.softmax(logits);
|
|
1736
|
+
for (let c = 0;c < K; c++)
|
|
1737
|
+
P.set(i, c, probs[c]);
|
|
1738
|
+
}
|
|
1739
|
+
const diff = P.subtract(Y);
|
|
1740
|
+
const grad = XMat.transpose().multiply(diff).scale(1 / n);
|
|
1741
|
+
let maxChange = 0;
|
|
1742
|
+
for (let i = 0;i < grad.data.length; i++) {
|
|
1743
|
+
const delta = this._learningRate * grad.data[i];
|
|
1744
|
+
this._weights.data[i] -= delta;
|
|
1745
|
+
maxChange = Math.max(maxChange, Math.abs(delta));
|
|
1746
|
+
}
|
|
1747
|
+
if (maxChange < this._tolerance)
|
|
1748
|
+
break;
|
|
1749
|
+
}
|
|
1750
|
+
this._fitted = true;
|
|
1751
|
+
return this;
|
|
1752
|
+
}
|
|
1753
|
+
predict(X) {
|
|
1754
|
+
const probs = this.predictProbability(X);
|
|
1755
|
+
return probs.map((row) => {
|
|
1756
|
+
let maxIdx = 0;
|
|
1757
|
+
let maxVal = row[0];
|
|
1758
|
+
for (let c = 1;c < row.length; c++) {
|
|
1759
|
+
if (row[c] > maxVal) {
|
|
1760
|
+
maxVal = row[c];
|
|
1761
|
+
maxIdx = c;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
return this._classes[maxIdx];
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
predictProbability(X) {
|
|
1768
|
+
if (!this._fitted)
|
|
1769
|
+
throw new Error("Model has not been fitted. Call fit() first.");
|
|
1770
|
+
const Xmat = this.normalizeInput(X);
|
|
1771
|
+
const Xdesign = this._fitIntercept ? Xmat.map((row) => [1, ...row]) : Xmat;
|
|
1772
|
+
const XMat = Matrix.fromArray(Xdesign);
|
|
1773
|
+
const scores = XMat.multiply(this._weights);
|
|
1774
|
+
const K = this._nClasses;
|
|
1775
|
+
const result = [];
|
|
1776
|
+
for (let i = 0;i < Xmat.length; i++) {
|
|
1777
|
+
const logits = [];
|
|
1778
|
+
for (let c = 0;c < K; c++)
|
|
1779
|
+
logits.push(scores.get(i, c));
|
|
1780
|
+
result.push(this.softmax(logits));
|
|
1781
|
+
}
|
|
1782
|
+
return result;
|
|
1783
|
+
}
|
|
1784
|
+
statistics() {
|
|
1785
|
+
if (!this._fitted)
|
|
1786
|
+
throw new Error("Model has not been fitted. Call fit() first.");
|
|
1787
|
+
const predicted = this.predict(this._X);
|
|
1788
|
+
const n = this._y.length;
|
|
1789
|
+
const K = this._nClasses;
|
|
1790
|
+
let correct = 0;
|
|
1791
|
+
for (let i = 0;i < n; i++) {
|
|
1792
|
+
if (predicted[i] === this._y[i])
|
|
1793
|
+
correct++;
|
|
1794
|
+
}
|
|
1795
|
+
const precision = [];
|
|
1796
|
+
const recall = [];
|
|
1797
|
+
for (let c = 0;c < K; c++) {
|
|
1798
|
+
const cls = this._classes[c];
|
|
1799
|
+
let tp = 0;
|
|
1800
|
+
let fp = 0;
|
|
1801
|
+
let fn = 0;
|
|
1802
|
+
for (let i = 0;i < n; i++) {
|
|
1803
|
+
if (predicted[i] === cls && this._y[i] === cls)
|
|
1804
|
+
tp++;
|
|
1805
|
+
else if (predicted[i] === cls && this._y[i] !== cls)
|
|
1806
|
+
fp++;
|
|
1807
|
+
else if (predicted[i] !== cls && this._y[i] === cls)
|
|
1808
|
+
fn++;
|
|
1809
|
+
}
|
|
1810
|
+
precision.push(tp + fp > 0 ? tp / (tp + fp) : 0);
|
|
1811
|
+
recall.push(tp + fn > 0 ? tp / (tp + fn) : 0);
|
|
1812
|
+
}
|
|
1813
|
+
const probs = this.predictProbability(this._X);
|
|
1814
|
+
let logLik = 0;
|
|
1815
|
+
for (let i = 0;i < n; i++) {
|
|
1816
|
+
const classIdx = this._classes.indexOf(this._y[i]);
|
|
1817
|
+
logLik += Math.log(Math.max(probs[i][classIdx], 0.000000000000001));
|
|
1818
|
+
}
|
|
1819
|
+
return {
|
|
1820
|
+
accuracy: correct / n,
|
|
1821
|
+
precision,
|
|
1822
|
+
recall,
|
|
1823
|
+
nClasses: K,
|
|
1824
|
+
logLikelihood: logLik
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
normalizeInput(X) {
|
|
1828
|
+
if (X.length === 0)
|
|
1829
|
+
throw new Error("Input data cannot be empty");
|
|
1830
|
+
if (typeof X[0] === "number") {
|
|
1831
|
+
return X.map((v) => [v]);
|
|
1832
|
+
}
|
|
1833
|
+
return X;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
// src/models/neural-network.ts
|
|
1837
|
+
class NeuralNetwork {
|
|
1838
|
+
_layers = [];
|
|
1839
|
+
_learningRate;
|
|
1840
|
+
_epochs;
|
|
1841
|
+
_task;
|
|
1842
|
+
_fitted = false;
|
|
1843
|
+
_outputSize = 0;
|
|
1844
|
+
_classes = [];
|
|
1845
|
+
constructor(options) {
|
|
1846
|
+
this._learningRate = options.learningRate ?? 0.01;
|
|
1847
|
+
this._epochs = options.epochs ?? 100;
|
|
1848
|
+
this._task = options.task ?? "regression";
|
|
1849
|
+
this._layerConfigs = options.layers;
|
|
1850
|
+
}
|
|
1851
|
+
_layerConfigs;
|
|
1852
|
+
initializeLayers(inputSize, outputSize) {
|
|
1853
|
+
this._layers = [];
|
|
1854
|
+
let prevSize = inputSize;
|
|
1855
|
+
for (const config of this._layerConfigs) {
|
|
1856
|
+
this._layers.push(this.createLayer(prevSize, config.units, config.activation ?? "relu"));
|
|
1857
|
+
prevSize = config.units;
|
|
1858
|
+
}
|
|
1859
|
+
const outputActivation = this._task === "classification" && outputSize > 1 ? "softmax" : "linear";
|
|
1860
|
+
this._layers.push(this.createLayer(prevSize, outputSize, outputActivation));
|
|
1861
|
+
}
|
|
1862
|
+
createLayer(inputSize, units, activation) {
|
|
1863
|
+
const scale = Math.sqrt(6 / (inputSize + units));
|
|
1864
|
+
const weights = new Float64Array(inputSize * units);
|
|
1865
|
+
for (let i = 0;i < weights.length; i++) {
|
|
1866
|
+
weights[i] = (Math.random() * 2 - 1) * scale;
|
|
1867
|
+
}
|
|
1868
|
+
return {
|
|
1869
|
+
weights,
|
|
1870
|
+
biases: new Float64Array(units),
|
|
1871
|
+
inputSize,
|
|
1872
|
+
units,
|
|
1873
|
+
activation
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
fit(X, y) {
|
|
1877
|
+
const Xmat = this.normalizeInput(X);
|
|
1878
|
+
if (Xmat.length !== y.length) {
|
|
1879
|
+
throw new Error(`X has ${Xmat.length} rows but y has ${y.length} elements`);
|
|
1880
|
+
}
|
|
1881
|
+
const inputSize = Xmat[0].length;
|
|
1882
|
+
const n = Xmat.length;
|
|
1883
|
+
if (this._task === "classification") {
|
|
1884
|
+
this._classes = [...new Set(y)].sort((a, b) => a - b);
|
|
1885
|
+
this._outputSize = this._classes.length;
|
|
1886
|
+
} else {
|
|
1887
|
+
this._outputSize = 1;
|
|
1888
|
+
}
|
|
1889
|
+
this.initializeLayers(inputSize, this._outputSize);
|
|
1890
|
+
for (let epoch = 0;epoch < this._epochs; epoch++) {
|
|
1891
|
+
for (let i = 0;i < n; i++) {
|
|
1892
|
+
const input = Xmat[i];
|
|
1893
|
+
const activations = this.forward(input);
|
|
1894
|
+
const outputLayer = activations[activations.length - 1];
|
|
1895
|
+
const target = this.encodeTarget(y[i]);
|
|
1896
|
+
const outputError = new Float64Array(this._outputSize);
|
|
1897
|
+
for (let j = 0;j < this._outputSize; j++) {
|
|
1898
|
+
outputError[j] = outputLayer[j] - target[j];
|
|
1899
|
+
}
|
|
1900
|
+
this.backward(activations, outputError);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
this._fitted = true;
|
|
1904
|
+
return this;
|
|
1905
|
+
}
|
|
1906
|
+
predict(X) {
|
|
1907
|
+
if (!this._fitted)
|
|
1908
|
+
throw new Error("Model has not been fitted. Call fit() first.");
|
|
1909
|
+
const Xmat = this.normalizeInput(X);
|
|
1910
|
+
return Xmat.map((row) => {
|
|
1911
|
+
const activations = this.forward(row);
|
|
1912
|
+
const output = activations[activations.length - 1];
|
|
1913
|
+
if (this._task === "classification") {
|
|
1914
|
+
let maxIdx = 0;
|
|
1915
|
+
let maxVal = output[0];
|
|
1916
|
+
for (let c = 1;c < output.length; c++) {
|
|
1917
|
+
if (output[c] > maxVal) {
|
|
1918
|
+
maxVal = output[c];
|
|
1919
|
+
maxIdx = c;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
return this._classes[maxIdx];
|
|
1923
|
+
}
|
|
1924
|
+
return output[0];
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
predictRaw(X) {
|
|
1928
|
+
if (!this._fitted)
|
|
1929
|
+
throw new Error("Model has not been fitted. Call fit() first.");
|
|
1930
|
+
const Xmat = this.normalizeInput(X);
|
|
1931
|
+
return Xmat.map((row) => {
|
|
1932
|
+
const activations = this.forward(row);
|
|
1933
|
+
return Array.from(activations[activations.length - 1]);
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
forward(input) {
|
|
1937
|
+
const activations = [new Float64Array(input)];
|
|
1938
|
+
let current = new Float64Array(input);
|
|
1939
|
+
for (const layer of this._layers) {
|
|
1940
|
+
const z = new Float64Array(layer.units);
|
|
1941
|
+
for (let j = 0;j < layer.units; j++) {
|
|
1942
|
+
let sum = layer.biases[j];
|
|
1943
|
+
for (let i = 0;i < layer.inputSize; i++) {
|
|
1944
|
+
sum += current[i] * layer.weights[i * layer.units + j];
|
|
1945
|
+
}
|
|
1946
|
+
z[j] = sum;
|
|
1947
|
+
}
|
|
1948
|
+
current = this.activate(z, layer.activation);
|
|
1949
|
+
activations.push(current);
|
|
1950
|
+
}
|
|
1951
|
+
return activations;
|
|
1952
|
+
}
|
|
1953
|
+
backward(activations, outputError) {
|
|
1954
|
+
const deltas = new Array(this._layers.length);
|
|
1955
|
+
deltas[this._layers.length - 1] = outputError;
|
|
1956
|
+
for (let l = this._layers.length - 2;l >= 0; l--) {
|
|
1957
|
+
const layer = this._layers[l];
|
|
1958
|
+
const output = activations[l + 1];
|
|
1959
|
+
const nextLayer = this._layers[l + 1];
|
|
1960
|
+
const nextDelta = deltas[l + 1];
|
|
1961
|
+
const delta = new Float64Array(layer.units);
|
|
1962
|
+
for (let j = 0;j < layer.units; j++) {
|
|
1963
|
+
let sum = 0;
|
|
1964
|
+
for (let k = 0;k < nextLayer.units; k++) {
|
|
1965
|
+
sum += nextDelta[k] * nextLayer.weights[j * nextLayer.units + k];
|
|
1966
|
+
}
|
|
1967
|
+
delta[j] = sum * this.activationDerivative(output[j], layer.activation);
|
|
1968
|
+
}
|
|
1969
|
+
deltas[l] = delta;
|
|
1970
|
+
}
|
|
1971
|
+
const lr = this._learningRate;
|
|
1972
|
+
for (let l = 0;l < this._layers.length; l++) {
|
|
1973
|
+
const layer = this._layers[l];
|
|
1974
|
+
const input = activations[l];
|
|
1975
|
+
const delta = deltas[l];
|
|
1976
|
+
for (let j = 0;j < layer.units; j++) {
|
|
1977
|
+
for (let i = 0;i < layer.inputSize; i++) {
|
|
1978
|
+
layer.weights[i * layer.units + j] -= lr * delta[j] * input[i];
|
|
1979
|
+
}
|
|
1980
|
+
layer.biases[j] -= lr * delta[j];
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
activate(z, fn) {
|
|
1985
|
+
const result = new Float64Array(z.length);
|
|
1986
|
+
switch (fn) {
|
|
1987
|
+
case "relu":
|
|
1988
|
+
for (let i = 0;i < z.length; i++)
|
|
1989
|
+
result[i] = Math.max(0, z[i]);
|
|
1990
|
+
break;
|
|
1991
|
+
case "sigmoid":
|
|
1992
|
+
for (let i = 0;i < z.length; i++)
|
|
1993
|
+
result[i] = this.sigmoid(z[i]);
|
|
1994
|
+
break;
|
|
1995
|
+
case "tanh":
|
|
1996
|
+
for (let i = 0;i < z.length; i++)
|
|
1997
|
+
result[i] = Math.tanh(z[i]);
|
|
1998
|
+
break;
|
|
1999
|
+
case "linear":
|
|
2000
|
+
result.set(z);
|
|
2001
|
+
break;
|
|
2002
|
+
case "softmax": {
|
|
2003
|
+
const max = Math.max(...z);
|
|
2004
|
+
let sum = 0;
|
|
2005
|
+
for (let i = 0;i < z.length; i++) {
|
|
2006
|
+
result[i] = Math.exp(z[i] - max);
|
|
2007
|
+
sum += result[i];
|
|
2008
|
+
}
|
|
2009
|
+
for (let i = 0;i < z.length; i++)
|
|
2010
|
+
result[i] /= sum;
|
|
2011
|
+
break;
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
return result;
|
|
2015
|
+
}
|
|
2016
|
+
activationDerivative(output, fn) {
|
|
2017
|
+
switch (fn) {
|
|
2018
|
+
case "relu":
|
|
2019
|
+
return output > 0 ? 1 : 0;
|
|
2020
|
+
case "sigmoid":
|
|
2021
|
+
return output * (1 - output);
|
|
2022
|
+
case "tanh":
|
|
2023
|
+
return 1 - output * output;
|
|
2024
|
+
case "linear":
|
|
2025
|
+
return 1;
|
|
2026
|
+
case "softmax":
|
|
2027
|
+
return output * (1 - output);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
sigmoid(z) {
|
|
2031
|
+
if (z >= 0)
|
|
2032
|
+
return 1 / (1 + Math.exp(-z));
|
|
2033
|
+
const expZ = Math.exp(z);
|
|
2034
|
+
return expZ / (1 + expZ);
|
|
2035
|
+
}
|
|
2036
|
+
encodeTarget(y) {
|
|
2037
|
+
if (this._task === "regression") {
|
|
2038
|
+
return new Float64Array([y]);
|
|
2039
|
+
}
|
|
2040
|
+
const encoded = new Float64Array(this._outputSize);
|
|
2041
|
+
const idx = this._classes.indexOf(y);
|
|
2042
|
+
if (idx >= 0)
|
|
2043
|
+
encoded[idx] = 1;
|
|
2044
|
+
return encoded;
|
|
2045
|
+
}
|
|
2046
|
+
normalizeInput(X) {
|
|
2047
|
+
if (X.length === 0)
|
|
2048
|
+
throw new Error("Input data cannot be empty");
|
|
2049
|
+
if (typeof X[0] === "number") {
|
|
2050
|
+
return X.map((v) => [v]);
|
|
2051
|
+
}
|
|
2052
|
+
return X;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
// src/models/polynomial-regression.ts
|
|
2056
|
+
class PolynomialRegression extends BaseRegression {
|
|
2057
|
+
_degree;
|
|
2058
|
+
_inner;
|
|
2059
|
+
constructor(options = {}) {
|
|
2060
|
+
super(options);
|
|
2061
|
+
this._degree = options.degree ?? 2;
|
|
2062
|
+
this._inner = new LinearRegression({ fitIntercept: this._fitIntercept });
|
|
2063
|
+
}
|
|
2064
|
+
expandFeatures(X) {
|
|
2065
|
+
return X.map((row) => {
|
|
2066
|
+
const x = row[0];
|
|
2067
|
+
const expanded = [];
|
|
2068
|
+
for (let d = 1;d <= this._degree; d++) {
|
|
2069
|
+
expanded.push(x ** d);
|
|
2070
|
+
}
|
|
2071
|
+
return expanded;
|
|
2072
|
+
});
|
|
2073
|
+
}
|
|
2074
|
+
fit(X, y) {
|
|
2075
|
+
const Xmat = this.normalizeInput(X);
|
|
2076
|
+
this.validateFitInput(Xmat, y);
|
|
2077
|
+
if (Xmat[0].length !== 1) {
|
|
2078
|
+
throw new Error("PolynomialRegression expects a single feature. Use LinearRegression with manual feature engineering for multiple features.");
|
|
2079
|
+
}
|
|
2080
|
+
this._X = Xmat;
|
|
2081
|
+
this._y = y;
|
|
2082
|
+
const Xexpanded = this.expandFeatures(Xmat);
|
|
2083
|
+
this._inner.fit(Xexpanded, y);
|
|
2084
|
+
this._coefficients = this._inner.coefficients;
|
|
2085
|
+
this._intercept = this._inner.intercept;
|
|
2086
|
+
this._yHat = this.predict(Xmat);
|
|
2087
|
+
this._fitted = true;
|
|
2088
|
+
return this;
|
|
2089
|
+
}
|
|
2090
|
+
predict(X) {
|
|
2091
|
+
const Xmat = this.normalizeInput(X);
|
|
2092
|
+
const Xexpanded = this.expandFeatures(Xmat);
|
|
2093
|
+
return this._inner.predict(Xexpanded);
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
// src/models/ridge-regression.ts
|
|
2097
|
+
class RidgeRegression extends BaseRegression {
|
|
2098
|
+
_alpha;
|
|
2099
|
+
constructor(options = {}) {
|
|
2100
|
+
super(options);
|
|
2101
|
+
this._alpha = options.alpha ?? 1;
|
|
2102
|
+
}
|
|
2103
|
+
fit(X, y) {
|
|
2104
|
+
const Xmat = this.normalizeInput(X);
|
|
2105
|
+
this.validateFitInput(Xmat, y);
|
|
2106
|
+
this._X = Xmat;
|
|
2107
|
+
this._y = y;
|
|
2108
|
+
const Xdesign = this._fitIntercept ? this.addInterceptColumn(Xmat) : Xmat;
|
|
2109
|
+
const A = Matrix.fromArray(Xdesign);
|
|
2110
|
+
const b = Matrix.columnVector(y);
|
|
2111
|
+
const XtX = A.transpose().multiply(A);
|
|
2112
|
+
const p = XtX.cols;
|
|
2113
|
+
const startIdx = this._fitIntercept ? 1 : 0;
|
|
2114
|
+
for (let i = startIdx;i < p; i++) {
|
|
2115
|
+
XtX.set(i, i, XtX.get(i, i) + this._alpha);
|
|
2116
|
+
}
|
|
2117
|
+
const Xty = A.transpose().multiply(b);
|
|
2118
|
+
const { L } = choleskyDecomposition(XtX);
|
|
2119
|
+
const beta = solveCholesky(L, Xty);
|
|
2120
|
+
const betaArray = beta.toFlatArray();
|
|
2121
|
+
if (this._fitIntercept) {
|
|
2122
|
+
this._intercept = betaArray[0];
|
|
2123
|
+
this._coefficients = betaArray.slice(1);
|
|
2124
|
+
} else {
|
|
2125
|
+
this._intercept = 0;
|
|
2126
|
+
this._coefficients = betaArray;
|
|
2127
|
+
}
|
|
2128
|
+
this._yHat = this.predict(Xmat);
|
|
2129
|
+
this._fitted = true;
|
|
2130
|
+
return this;
|
|
2131
|
+
}
|
|
2132
|
+
predict(X) {
|
|
2133
|
+
const Xmat = this.normalizeInput(X);
|
|
2134
|
+
return Xmat.map((row) => {
|
|
2135
|
+
let sum = this._intercept;
|
|
2136
|
+
for (let j = 0;j < this._coefficients.length; j++) {
|
|
2137
|
+
sum += row[j] * this._coefficients[j];
|
|
2138
|
+
}
|
|
2139
|
+
return sum;
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
// src/models/weighted-regression.ts
|
|
2144
|
+
class WeightedRegression extends BaseRegression {
|
|
2145
|
+
_weights;
|
|
2146
|
+
constructor(options = {}) {
|
|
2147
|
+
super(options);
|
|
2148
|
+
this._weights = options.weights;
|
|
2149
|
+
}
|
|
2150
|
+
fit(X, y, weights) {
|
|
2151
|
+
const Xmat = this.normalizeInput(X);
|
|
2152
|
+
this.validateFitInput(Xmat, y);
|
|
2153
|
+
const w = weights ?? this._weights;
|
|
2154
|
+
if (!w)
|
|
2155
|
+
throw new Error("Weights must be provided either in constructor or fit()");
|
|
2156
|
+
if (w.length !== y.length) {
|
|
2157
|
+
throw new Error(`Weights length (${w.length}) must match y length (${y.length})`);
|
|
2158
|
+
}
|
|
2159
|
+
this._X = Xmat;
|
|
2160
|
+
this._y = y;
|
|
2161
|
+
const Xdesign = this._fitIntercept ? this.addInterceptColumn(Xmat) : Xmat;
|
|
2162
|
+
const sqrtW = w.map((wi) => Math.sqrt(wi));
|
|
2163
|
+
const Xweighted = Xdesign.map((row, i) => row.map((val) => val * sqrtW[i]));
|
|
2164
|
+
const yWeighted = y.map((yi, i) => yi * sqrtW[i]);
|
|
2165
|
+
const A = Matrix.fromArray(Xweighted);
|
|
2166
|
+
const b = Matrix.columnVector(yWeighted);
|
|
2167
|
+
const { Q, R } = qrDecomposition(A);
|
|
2168
|
+
const beta = solveQR(Q, R, b);
|
|
2169
|
+
const betaArray = beta.toFlatArray();
|
|
2170
|
+
if (this._fitIntercept) {
|
|
2171
|
+
this._intercept = betaArray[0];
|
|
2172
|
+
this._coefficients = betaArray.slice(1);
|
|
2173
|
+
} else {
|
|
2174
|
+
this._intercept = 0;
|
|
2175
|
+
this._coefficients = betaArray;
|
|
2176
|
+
}
|
|
2177
|
+
this._yHat = this.predict(Xmat);
|
|
2178
|
+
this._fitted = true;
|
|
2179
|
+
return this;
|
|
2180
|
+
}
|
|
2181
|
+
predict(X) {
|
|
2182
|
+
const Xmat = this.normalizeInput(X);
|
|
2183
|
+
return Xmat.map((row) => {
|
|
2184
|
+
let sum = this._intercept;
|
|
2185
|
+
for (let j = 0;j < this._coefficients.length; j++) {
|
|
2186
|
+
sum += row[j] * this._coefficients[j];
|
|
2187
|
+
}
|
|
2188
|
+
return sum;
|
|
2189
|
+
});
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
// src/models/robust-regression.ts
|
|
2194
|
+
class RobustRegression extends BaseRegression {
|
|
2195
|
+
_method;
|
|
2196
|
+
_tuningConstant;
|
|
2197
|
+
_maxIterations;
|
|
2198
|
+
_tolerance;
|
|
2199
|
+
constructor(options = {}) {
|
|
2200
|
+
super(options);
|
|
2201
|
+
this._method = options.method ?? "huber";
|
|
2202
|
+
this._tuningConstant = options.tuningConstant ?? (this._method === "huber" ? 1.345 : 4.685);
|
|
2203
|
+
this._maxIterations = options.maxIterations ?? 50;
|
|
2204
|
+
this._tolerance = options.tolerance ?? 0.0001;
|
|
2205
|
+
}
|
|
2206
|
+
fit(X, y) {
|
|
2207
|
+
const Xmat = this.normalizeInput(X);
|
|
2208
|
+
this.validateFitInput(Xmat, y);
|
|
2209
|
+
this._X = Xmat;
|
|
2210
|
+
this._y = y;
|
|
2211
|
+
const ols = new LinearRegression({ fitIntercept: this._fitIntercept });
|
|
2212
|
+
ols.fit(Xmat, y);
|
|
2213
|
+
this._coefficients = ols.coefficients;
|
|
2214
|
+
this._intercept = ols.intercept;
|
|
2215
|
+
for (let iter = 0;iter < this._maxIterations; iter++) {
|
|
2216
|
+
const prevCoeffs = [...this._coefficients];
|
|
2217
|
+
const prevIntercept = this._intercept;
|
|
2218
|
+
const yHat = this.predict(Xmat);
|
|
2219
|
+
const residuals = y.map((yi, i) => yi - yHat[i]);
|
|
2220
|
+
const absResiduals = residuals.map(Math.abs).sort((a, b) => a - b);
|
|
2221
|
+
const mad = median(absResiduals);
|
|
2222
|
+
const scale = Math.max(mad / 0.6745, 0.0000000001);
|
|
2223
|
+
const weights = residuals.map((r) => {
|
|
2224
|
+
const u = r / scale;
|
|
2225
|
+
return this._method === "huber" ? huberWeight(u, this._tuningConstant) : tukeyWeight(u, this._tuningConstant);
|
|
2226
|
+
});
|
|
2227
|
+
const wls = new WeightedRegression({ fitIntercept: this._fitIntercept });
|
|
2228
|
+
wls.fit(Xmat, y, weights);
|
|
2229
|
+
this._coefficients = wls.coefficients;
|
|
2230
|
+
this._intercept = wls.intercept;
|
|
2231
|
+
let maxChange = Math.abs(this._intercept - prevIntercept);
|
|
2232
|
+
for (let j = 0;j < this._coefficients.length; j++) {
|
|
2233
|
+
maxChange = Math.max(maxChange, Math.abs(this._coefficients[j] - prevCoeffs[j]));
|
|
2234
|
+
}
|
|
2235
|
+
if (maxChange < this._tolerance)
|
|
2236
|
+
break;
|
|
2237
|
+
}
|
|
2238
|
+
this._yHat = this.predict(Xmat);
|
|
2239
|
+
this._fitted = true;
|
|
2240
|
+
return this;
|
|
2241
|
+
}
|
|
2242
|
+
predict(X) {
|
|
2243
|
+
const Xmat = this.normalizeInput(X);
|
|
2244
|
+
return Xmat.map((row) => {
|
|
2245
|
+
let sum = this._intercept;
|
|
2246
|
+
for (let j = 0;j < this._coefficients.length; j++) {
|
|
2247
|
+
sum += row[j] * this._coefficients[j];
|
|
2248
|
+
}
|
|
2249
|
+
return sum;
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
function huberWeight(u, k) {
|
|
2254
|
+
return Math.abs(u) <= k ? 1 : k / Math.abs(u);
|
|
2255
|
+
}
|
|
2256
|
+
function tukeyWeight(u, k) {
|
|
2257
|
+
if (Math.abs(u) > k)
|
|
2258
|
+
return 0;
|
|
2259
|
+
const t = 1 - (u / k) ** 2;
|
|
2260
|
+
return t * t;
|
|
2261
|
+
}
|
|
2262
|
+
function median(sorted) {
|
|
2263
|
+
const n = sorted.length;
|
|
2264
|
+
if (n === 0)
|
|
2265
|
+
return 0;
|
|
2266
|
+
const mid = Math.floor(n / 2);
|
|
2267
|
+
return n % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
2268
|
+
}
|
|
2269
|
+
// src/predictions/bootstrap.ts
|
|
2270
|
+
function bootstrapCoefficients(X, y, nBootstrap = 1000, alpha = 0.05, fitIntercept = true) {
|
|
2271
|
+
const n = X.length;
|
|
2272
|
+
const p = X[0].length;
|
|
2273
|
+
const nParams = p + (fitIntercept ? 1 : 0);
|
|
2274
|
+
const allCoeffs = Array.from({ length: nParams }, () => []);
|
|
2275
|
+
for (let b = 0;b < nBootstrap; b++) {
|
|
2276
|
+
const indices = Array.from({ length: n }, () => Math.floor(Math.random() * n));
|
|
2277
|
+
const Xb = indices.map((i) => X[i]);
|
|
2278
|
+
const yb = indices.map((i) => y[i]);
|
|
2279
|
+
try {
|
|
2280
|
+
const model = new LinearRegression({ fitIntercept });
|
|
2281
|
+
model.fit(Xb, yb);
|
|
2282
|
+
const coeffs = fitIntercept ? [model.intercept, ...model.coefficients] : [...model.coefficients];
|
|
2283
|
+
for (let j = 0;j < nParams; j++) {
|
|
2284
|
+
allCoeffs[j].push(coeffs[j]);
|
|
2285
|
+
}
|
|
2286
|
+
} catch {}
|
|
2287
|
+
}
|
|
2288
|
+
const coefficients = [];
|
|
2289
|
+
const confidenceIntervals = [];
|
|
2290
|
+
const standardErrors = [];
|
|
2291
|
+
for (let j = 0;j < nParams; j++) {
|
|
2292
|
+
const samples = allCoeffs[j];
|
|
2293
|
+
if (samples.length === 0) {
|
|
2294
|
+
coefficients.push(0);
|
|
2295
|
+
confidenceIntervals.push([0, 0]);
|
|
2296
|
+
standardErrors.push(0);
|
|
2297
|
+
continue;
|
|
2298
|
+
}
|
|
2299
|
+
const mean = samples.reduce((a, b) => a + b, 0) / samples.length;
|
|
2300
|
+
coefficients.push(mean);
|
|
2301
|
+
const variance = samples.reduce((sum, x) => sum + (x - mean) ** 2, 0) / (samples.length - 1);
|
|
2302
|
+
standardErrors.push(Math.sqrt(variance));
|
|
2303
|
+
const sorted = [...samples].sort((a, b) => a - b);
|
|
2304
|
+
const loIdx = Math.floor(alpha / 2 * sorted.length);
|
|
2305
|
+
const hiIdx = Math.floor((1 - alpha / 2) * sorted.length);
|
|
2306
|
+
confidenceIntervals.push([sorted[loIdx], sorted[Math.min(hiIdx, sorted.length - 1)]]);
|
|
2307
|
+
}
|
|
2308
|
+
return { coefficients, confidenceIntervals, standardErrors };
|
|
2309
|
+
}
|
|
2310
|
+
// src/predictions/intervals.ts
|
|
2311
|
+
init_distributions();
|
|
2312
|
+
function confidenceInterval(X, y, yHat, newX, newYHat, fitIntercept = true, alpha = 0.05) {
|
|
2313
|
+
const { s, XtXinv, df } = computeBase(X, y, yHat, fitIntercept);
|
|
2314
|
+
const tCrit = tInverseCDF(1 - alpha / 2, df);
|
|
2315
|
+
return newX.map((row, idx) => {
|
|
2316
|
+
const x0 = fitIntercept ? [1, ...row] : row;
|
|
2317
|
+
const x0vec = Matrix.columnVector(x0);
|
|
2318
|
+
const se = s * Math.sqrt(quadForm(XtXinv, x0vec));
|
|
2319
|
+
const predicted = newYHat[idx];
|
|
2320
|
+
return {
|
|
2321
|
+
predicted,
|
|
2322
|
+
lower: predicted - tCrit * se,
|
|
2323
|
+
upper: predicted + tCrit * se
|
|
2324
|
+
};
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2327
|
+
function predictionInterval(X, y, yHat, newX, newYHat, fitIntercept = true, alpha = 0.05) {
|
|
2328
|
+
const { s, XtXinv, df } = computeBase(X, y, yHat, fitIntercept);
|
|
2329
|
+
const tCrit = tInverseCDF(1 - alpha / 2, df);
|
|
2330
|
+
return newX.map((row, idx) => {
|
|
2331
|
+
const x0 = fitIntercept ? [1, ...row] : row;
|
|
2332
|
+
const x0vec = Matrix.columnVector(x0);
|
|
2333
|
+
const se = s * Math.sqrt(1 + quadForm(XtXinv, x0vec));
|
|
2334
|
+
const predicted = newYHat[idx];
|
|
2335
|
+
return {
|
|
2336
|
+
predicted,
|
|
2337
|
+
lower: predicted - tCrit * se,
|
|
2338
|
+
upper: predicted + tCrit * se
|
|
2339
|
+
};
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
function computeBase(X, y, yHat, fitIntercept) {
|
|
2343
|
+
const n = y.length;
|
|
2344
|
+
const Xdesign = fitIntercept ? X.map((row) => [1, ...row]) : X;
|
|
2345
|
+
const A = Matrix.fromArray(Xdesign);
|
|
2346
|
+
const k = A.cols;
|
|
2347
|
+
const df = n - k;
|
|
2348
|
+
let rss = 0;
|
|
2349
|
+
for (let i = 0;i < n; i++) {
|
|
2350
|
+
const r = y[i] - yHat[i];
|
|
2351
|
+
rss += r * r;
|
|
2352
|
+
}
|
|
2353
|
+
const s = Math.sqrt(rss / df);
|
|
2354
|
+
const { R } = qrDecomposition(A);
|
|
2355
|
+
const Rsq = R.submatrix(0, k, 0, k);
|
|
2356
|
+
const Rinv = Matrix.zeros(k, k);
|
|
2357
|
+
for (let j = 0;j < k; j++) {
|
|
2358
|
+
const ej = Matrix.zeros(k, 1);
|
|
2359
|
+
ej.set(j, 0, 1);
|
|
2360
|
+
const col = backSubstitution(Rsq, ej);
|
|
2361
|
+
for (let i = 0;i < k; i++) {
|
|
2362
|
+
Rinv.set(i, j, col.get(i, 0));
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
const XtXinv = Rinv.multiply(Rinv.transpose());
|
|
2366
|
+
return { s, XtXinv, k, df };
|
|
2367
|
+
}
|
|
2368
|
+
function quadForm(M, v) {
|
|
2369
|
+
const Mv = M.multiply(v);
|
|
2370
|
+
return v.dot(Mv);
|
|
2371
|
+
}
|
|
2372
|
+
// src/preprocessing/encoding.ts
|
|
2373
|
+
function oneHotEncode(column, categories, dropFirst = false) {
|
|
2374
|
+
const cats = categories ?? [...new Set(column)].sort();
|
|
2375
|
+
const startIdx = dropFirst ? 1 : 0;
|
|
2376
|
+
const resultCats = cats.slice(startIdx);
|
|
2377
|
+
return column.map((val) => resultCats.map((cat) => val === cat ? 1 : 0));
|
|
2378
|
+
}
|
|
2379
|
+
// src/preprocessing/features.ts
|
|
2380
|
+
function polynomialFeatures(X, degree) {
|
|
2381
|
+
return X.map((row) => {
|
|
2382
|
+
const expanded = [];
|
|
2383
|
+
for (const x of row) {
|
|
2384
|
+
for (let d = 1;d <= degree; d++) {
|
|
2385
|
+
expanded.push(x ** d);
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
return expanded;
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
function interactionFeatures(X, pairs) {
|
|
2392
|
+
const p = X[0].length;
|
|
2393
|
+
const allPairs = pairs ?? (() => {
|
|
2394
|
+
const result = [];
|
|
2395
|
+
for (let i = 0;i < p; i++) {
|
|
2396
|
+
for (let j = i + 1;j < p; j++) {
|
|
2397
|
+
result.push([i, j]);
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
return result;
|
|
2401
|
+
})();
|
|
2402
|
+
return X.map((row) => allPairs.map(([i, j]) => row[i] * row[j]));
|
|
2403
|
+
}
|
|
2404
|
+
// src/preprocessing/missing.ts
|
|
2405
|
+
function dropMissing(X, y) {
|
|
2406
|
+
const validIndices = [];
|
|
2407
|
+
for (let i = 0;i < X.length; i++) {
|
|
2408
|
+
const rowValid = X[i].every((v) => v != null && !Number.isNaN(v));
|
|
2409
|
+
const yValid = y ? y[i] != null && !Number.isNaN(y[i]) : true;
|
|
2410
|
+
if (rowValid && yValid)
|
|
2411
|
+
validIndices.push(i);
|
|
2412
|
+
}
|
|
2413
|
+
return {
|
|
2414
|
+
X: validIndices.map((i) => X[i]),
|
|
2415
|
+
y: y ? validIndices.map((i) => y[i]) : undefined
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
2418
|
+
function imputeMean(X) {
|
|
2419
|
+
const n = X.length;
|
|
2420
|
+
const p = X[0].length;
|
|
2421
|
+
const means = [];
|
|
2422
|
+
for (let j = 0;j < p; j++) {
|
|
2423
|
+
let sum = 0;
|
|
2424
|
+
let count = 0;
|
|
2425
|
+
for (let i = 0;i < n; i++) {
|
|
2426
|
+
const val = X[i][j];
|
|
2427
|
+
if (!Number.isNaN(val)) {
|
|
2428
|
+
sum += val;
|
|
2429
|
+
count++;
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
means.push(count > 0 ? sum / count : 0);
|
|
2433
|
+
}
|
|
2434
|
+
return X.map((row) => row.map((val, j) => Number.isNaN(val) ? means[j] : val));
|
|
2435
|
+
}
|
|
2436
|
+
function imputeMedian(X) {
|
|
2437
|
+
const n = X.length;
|
|
2438
|
+
const p = X[0].length;
|
|
2439
|
+
const medians = [];
|
|
2440
|
+
for (let j = 0;j < p; j++) {
|
|
2441
|
+
const values = [];
|
|
2442
|
+
for (let i = 0;i < n; i++) {
|
|
2443
|
+
const val = X[i][j];
|
|
2444
|
+
if (!Number.isNaN(val))
|
|
2445
|
+
values.push(val);
|
|
2446
|
+
}
|
|
2447
|
+
values.sort((a, b) => a - b);
|
|
2448
|
+
const mid = Math.floor(values.length / 2);
|
|
2449
|
+
medians.push(values.length === 0 ? 0 : values.length % 2 === 0 ? (values[mid - 1] + values[mid]) / 2 : values[mid]);
|
|
2450
|
+
}
|
|
2451
|
+
return X.map((row) => row.map((val, j) => Number.isNaN(val) ? medians[j] : val));
|
|
2452
|
+
}
|
|
2453
|
+
// src/preprocessing/scaling.ts
|
|
2454
|
+
function standardize(X) {
|
|
2455
|
+
const n = X.length;
|
|
2456
|
+
const p = X[0].length;
|
|
2457
|
+
const means = [];
|
|
2458
|
+
const stds = [];
|
|
2459
|
+
for (let j = 0;j < p; j++) {
|
|
2460
|
+
let sum = 0;
|
|
2461
|
+
for (let i = 0;i < n; i++)
|
|
2462
|
+
sum += X[i][j];
|
|
2463
|
+
const mean = sum / n;
|
|
2464
|
+
means.push(mean);
|
|
2465
|
+
let sqSum = 0;
|
|
2466
|
+
for (let i = 0;i < n; i++)
|
|
2467
|
+
sqSum += (X[i][j] - mean) ** 2;
|
|
2468
|
+
stds.push(Math.sqrt(sqSum / n));
|
|
2469
|
+
}
|
|
2470
|
+
const transformed = X.map((row) => row.map((val, j) => {
|
|
2471
|
+
const std = stds[j];
|
|
2472
|
+
return std > 0.000000000000001 ? (val - means[j]) / std : 0;
|
|
2473
|
+
}));
|
|
2474
|
+
return { transformed, means, stds };
|
|
2475
|
+
}
|
|
2476
|
+
function unstandardize(X, params) {
|
|
2477
|
+
return X.map((row) => row.map((val, j) => val * params.stds[j] + params.means[j]));
|
|
2478
|
+
}
|
|
2479
|
+
function normalize(X) {
|
|
2480
|
+
const n = X.length;
|
|
2481
|
+
const p = X[0].length;
|
|
2482
|
+
const mins = [];
|
|
2483
|
+
const maxs = [];
|
|
2484
|
+
for (let j = 0;j < p; j++) {
|
|
2485
|
+
let min = Infinity;
|
|
2486
|
+
let max = -Infinity;
|
|
2487
|
+
for (let i = 0;i < n; i++) {
|
|
2488
|
+
const val = X[i][j];
|
|
2489
|
+
if (val < min)
|
|
2490
|
+
min = val;
|
|
2491
|
+
if (val > max)
|
|
2492
|
+
max = val;
|
|
2493
|
+
}
|
|
2494
|
+
mins.push(min);
|
|
2495
|
+
maxs.push(max);
|
|
2496
|
+
}
|
|
2497
|
+
const transformed = X.map((row) => row.map((val, j) => {
|
|
2498
|
+
const range = maxs[j] - mins[j];
|
|
2499
|
+
return range > 0.000000000000001 ? (val - mins[j]) / range : 0;
|
|
2500
|
+
}));
|
|
2501
|
+
return { transformed, mins, maxs };
|
|
2502
|
+
}
|
|
2503
|
+
function unnormalize(X, params) {
|
|
2504
|
+
return X.map((row) => row.map((val, j) => val * (params.maxs[j] - params.mins[j]) + params.mins[j]));
|
|
2505
|
+
}
|