@visactor/vutils 1.0.25 → 1.0.27
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/cjs/common/index.d.ts +1 -0
- package/cjs/common/index.js +2 -1
- package/cjs/common/index.js.map +1 -1
- package/cjs/common/kde.d.ts +4 -0
- package/cjs/common/kde.js +25 -14
- package/cjs/common/kde.js.map +1 -1
- package/cjs/common/regression-bootstrap.d.ts +14 -0
- package/cjs/common/regression-bootstrap.js +37 -0
- package/cjs/common/regression-bootstrap.js.map +1 -0
- package/cjs/common/regression-linear.d.ts +1 -0
- package/cjs/common/regression-linear.js +11 -29
- package/cjs/common/regression-linear.js.map +1 -1
- package/cjs/common/regression-lowess.d.ts +9 -8
- package/cjs/common/regression-lowess.js +109 -130
- package/cjs/common/regression-lowess.js.map +1 -1
- package/cjs/common/regression-polynomial.d.ts +1 -0
- package/cjs/common/regression-polynomial.js +11 -92
- package/cjs/common/regression-polynomial.js.map +1 -1
- package/cjs/common/regression-solver.d.ts +26 -0
- package/cjs/common/regression-solver.js +180 -0
- package/cjs/common/regression-solver.js.map +1 -0
- package/dist/index.js +536 -378
- package/dist/index.min.js +16 -16
- package/es/common/index.d.ts +1 -0
- package/es/common/index.js +2 -0
- package/es/common/index.js.map +1 -1
- package/es/common/kde.d.ts +4 -0
- package/es/common/kde.js +29 -14
- package/es/common/kde.js.map +1 -1
- package/es/common/regression-bootstrap.d.ts +14 -0
- package/es/common/regression-bootstrap.js +31 -0
- package/es/common/regression-bootstrap.js.map +1 -0
- package/es/common/regression-linear.d.ts +1 -0
- package/es/common/regression-linear.js +9 -27
- package/es/common/regression-linear.js.map +1 -1
- package/es/common/regression-lowess.d.ts +9 -8
- package/es/common/regression-lowess.js +106 -130
- package/es/common/regression-lowess.js.map +1 -1
- package/es/common/regression-polynomial.d.ts +1 -0
- package/es/common/regression-polynomial.js +9 -86
- package/es/common/regression-polynomial.js.map +1 -1
- package/es/common/regression-solver.d.ts +26 -0
- package/es/common/regression-solver.js +168 -0
- package/es/common/regression-solver.js.map +1 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -2236,6 +2236,213 @@
|
|
|
2236
2236
|
return [min, max];
|
|
2237
2237
|
};
|
|
2238
2238
|
|
|
2239
|
+
function regressionPoints(data, x, y) {
|
|
2240
|
+
const points = [];
|
|
2241
|
+
data.forEach(d => {
|
|
2242
|
+
const a = x(d);
|
|
2243
|
+
const b = y(d);
|
|
2244
|
+
// 保留既有 accessor 对缺失观测的排除行为;无穷大不是可拟合观测。
|
|
2245
|
+
if (a != null && b != null && Number.isFinite(+a) && Number.isFinite(+b)) {
|
|
2246
|
+
points.push({ x: +a, y: +b });
|
|
2247
|
+
}
|
|
2248
|
+
});
|
|
2249
|
+
return points;
|
|
2250
|
+
}
|
|
2251
|
+
function normalizeRegression(value, center, scale) {
|
|
2252
|
+
const delta = value - center;
|
|
2253
|
+
return Number.isFinite(delta) ? delta / scale : value / scale - center / scale;
|
|
2254
|
+
}
|
|
2255
|
+
function regressionBasis(points) {
|
|
2256
|
+
let xMin = Infinity;
|
|
2257
|
+
let xMax = -Infinity;
|
|
2258
|
+
let yMin = Infinity;
|
|
2259
|
+
let yMax = -Infinity;
|
|
2260
|
+
for (const p of points) {
|
|
2261
|
+
xMin = Math.min(xMin, p.x);
|
|
2262
|
+
xMax = Math.max(xMax, p.x);
|
|
2263
|
+
yMin = Math.min(yMin, p.y);
|
|
2264
|
+
yMax = Math.max(yMax, p.y);
|
|
2265
|
+
}
|
|
2266
|
+
if (!points.length) {
|
|
2267
|
+
return { xCenter: 0, xScale: 1, yCenter: 0, yScale: 1 };
|
|
2268
|
+
}
|
|
2269
|
+
const xCenter = xMin / 2 + xMax / 2;
|
|
2270
|
+
const yCenter = yMin / 2 + yMax / 2;
|
|
2271
|
+
return {
|
|
2272
|
+
xCenter,
|
|
2273
|
+
yCenter,
|
|
2274
|
+
xScale: Math.max(Math.abs(xMin - xCenter), Math.abs(xMax - xCenter)) || 1,
|
|
2275
|
+
yScale: Math.max(Math.abs(yMin - yCenter), Math.abs(yMax - yCenter)) || 1
|
|
2276
|
+
};
|
|
2277
|
+
}
|
|
2278
|
+
/** 秩不足时对 QR 的小型 R 做单边 Jacobi SVD,获得明确的最小范数解。 */
|
|
2279
|
+
function minimumNorm(r, rhs) {
|
|
2280
|
+
const p = rhs.length;
|
|
2281
|
+
const a = r.map(row => row.slice());
|
|
2282
|
+
const v = Array.from({ length: p }, (_, i) => Array.from({ length: p }, (_, j) => +(i === j)));
|
|
2283
|
+
for (let sweep = 0; sweep < 50; sweep++) {
|
|
2284
|
+
let changed = false;
|
|
2285
|
+
for (let j = 0; j < p - 1; j++) {
|
|
2286
|
+
for (let k = j + 1; k < p; k++) {
|
|
2287
|
+
let aa = 0;
|
|
2288
|
+
let bb = 0;
|
|
2289
|
+
let ab = 0;
|
|
2290
|
+
for (let i = 0; i < p; i++) {
|
|
2291
|
+
aa += a[i][j] ** 2;
|
|
2292
|
+
bb += a[i][k] ** 2;
|
|
2293
|
+
ab += a[i][j] * a[i][k];
|
|
2294
|
+
}
|
|
2295
|
+
if (Math.abs(ab) <= Number.EPSILON * 8 * Math.sqrt(aa * bb)) {
|
|
2296
|
+
continue;
|
|
2297
|
+
}
|
|
2298
|
+
changed = true;
|
|
2299
|
+
const tau = (bb - aa) / (2 * ab);
|
|
2300
|
+
const t = (tau >= 0 ? 1 : -1) / (Math.abs(tau) + Math.hypot(1, tau));
|
|
2301
|
+
const c = 1 / Math.hypot(1, t);
|
|
2302
|
+
const s = c * t;
|
|
2303
|
+
for (let i = 0; i < p; i++) {
|
|
2304
|
+
const aj = a[i][j];
|
|
2305
|
+
const ak = a[i][k];
|
|
2306
|
+
const vj = v[i][j];
|
|
2307
|
+
const vk = v[i][k];
|
|
2308
|
+
a[i][j] = c * aj - s * ak;
|
|
2309
|
+
a[i][k] = s * aj + c * ak;
|
|
2310
|
+
v[i][j] = c * vj - s * vk;
|
|
2311
|
+
v[i][k] = s * vj + c * vk;
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
if (!changed) {
|
|
2316
|
+
break;
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
const norms = Array.from({ length: p }, (_, j) => Math.hypot(...a.map(row => row[j])));
|
|
2320
|
+
const tolerance = Math.max(...norms) * Number.EPSILON * p * 64;
|
|
2321
|
+
const coef = new Array(p).fill(0);
|
|
2322
|
+
let rank = 0;
|
|
2323
|
+
for (let j = 0; j < p; j++) {
|
|
2324
|
+
if (norms[j] <= tolerance) {
|
|
2325
|
+
continue;
|
|
2326
|
+
}
|
|
2327
|
+
rank++;
|
|
2328
|
+
let projection = 0;
|
|
2329
|
+
for (let i = 0; i < p; i++) {
|
|
2330
|
+
projection += a[i][j] * rhs[i];
|
|
2331
|
+
}
|
|
2332
|
+
projection /= norms[j] ** 2;
|
|
2333
|
+
for (let i = 0; i < p; i++) {
|
|
2334
|
+
coef[i] += v[i][j] * projection;
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
return { coef, rank };
|
|
2338
|
+
}
|
|
2339
|
+
function fitRegression(points, degree, basis = regressionBasis(points)) {
|
|
2340
|
+
const p = degree + 1;
|
|
2341
|
+
if (degree === 1) {
|
|
2342
|
+
let mx = 0;
|
|
2343
|
+
let my = 0;
|
|
2344
|
+
let n = 0;
|
|
2345
|
+
for (const row of points) {
|
|
2346
|
+
n++;
|
|
2347
|
+
mx += (normalizeRegression(row.x, basis.xCenter, basis.xScale) - mx) / n;
|
|
2348
|
+
my += (normalizeRegression(row.y, basis.yCenter, basis.yScale) - my) / n;
|
|
2349
|
+
}
|
|
2350
|
+
let sxx = 0;
|
|
2351
|
+
let sxy = 0;
|
|
2352
|
+
for (const row of points) {
|
|
2353
|
+
const dx = normalizeRegression(row.x, basis.xCenter, basis.xScale) - mx;
|
|
2354
|
+
sxx += dx * dx;
|
|
2355
|
+
sxy += dx * (normalizeRegression(row.y, basis.yCenter, basis.yScale) - my);
|
|
2356
|
+
}
|
|
2357
|
+
const b = sxx === 0 ? 0 : sxy / sxx;
|
|
2358
|
+
const coef = sxx === 0 ? [my / (1 + mx * mx), (mx * my) / (1 + mx * mx)] : [my - b * mx, b];
|
|
2359
|
+
return { coef, rank: n === 0 ? 0 : sxx === 0 ? 1 : 2, basis };
|
|
2360
|
+
}
|
|
2361
|
+
const r = Array.from({ length: p }, () => new Array(p).fill(0));
|
|
2362
|
+
const rhs = new Array(p).fill(0);
|
|
2363
|
+
const row = new Array(p);
|
|
2364
|
+
for (const point of points) {
|
|
2365
|
+
const z = normalizeRegression(point.x, basis.xCenter, basis.xScale);
|
|
2366
|
+
row[0] = 1;
|
|
2367
|
+
for (let j = 1; j < p; j++) {
|
|
2368
|
+
row[j] = row[j - 1] * z;
|
|
2369
|
+
}
|
|
2370
|
+
let value = normalizeRegression(point.y, basis.yCenter, basis.yScale);
|
|
2371
|
+
// 增量 Givens QR 仅保留 p×p 的 R,bootstrap 不分配 N×p 矩阵。
|
|
2372
|
+
for (let j = 0; j < p; j++) {
|
|
2373
|
+
const norm = Math.hypot(r[j][j], row[j]);
|
|
2374
|
+
if (norm === 0) {
|
|
2375
|
+
continue;
|
|
2376
|
+
}
|
|
2377
|
+
const c = r[j][j] / norm;
|
|
2378
|
+
const s = row[j] / norm;
|
|
2379
|
+
r[j][j] = norm;
|
|
2380
|
+
for (let k = j + 1; k < p; k++) {
|
|
2381
|
+
const previous = r[j][k];
|
|
2382
|
+
r[j][k] = c * previous + s * row[k];
|
|
2383
|
+
row[k] = -s * previous + c * row[k];
|
|
2384
|
+
}
|
|
2385
|
+
const previous = rhs[j];
|
|
2386
|
+
rhs[j] = c * previous + s * value;
|
|
2387
|
+
value = -s * previous + c * value;
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
const tolerance = Math.max(...r.map((row, i) => Math.abs(row[i]))) * Number.EPSILON * p * 64;
|
|
2391
|
+
if (r.some((row, i) => Math.abs(row[i]) <= tolerance)) {
|
|
2392
|
+
return Object.assign(Object.assign({}, minimumNorm(r, rhs)), { basis });
|
|
2393
|
+
}
|
|
2394
|
+
const coef = new Array(p);
|
|
2395
|
+
for (let i = p - 1; i >= 0; i--) {
|
|
2396
|
+
let value = rhs[i];
|
|
2397
|
+
for (let j = i + 1; j < p; j++) {
|
|
2398
|
+
value -= r[i][j] * coef[j];
|
|
2399
|
+
}
|
|
2400
|
+
coef[i] = value / r[i][i];
|
|
2401
|
+
}
|
|
2402
|
+
return { coef, rank: p, basis };
|
|
2403
|
+
}
|
|
2404
|
+
function regressionPrediction(model, x) {
|
|
2405
|
+
const z = normalizeRegression(x, model.basis.xCenter, model.basis.xScale);
|
|
2406
|
+
let value = 0;
|
|
2407
|
+
for (let j = model.coef.length - 1; j >= 0; j--) {
|
|
2408
|
+
value = value * z + model.coef[j];
|
|
2409
|
+
}
|
|
2410
|
+
return model.basis.yCenter + model.basis.yScale * value;
|
|
2411
|
+
}
|
|
2412
|
+
/** 保留公开的升幂 coef;预测始终使用稳定基底。 */
|
|
2413
|
+
function regressionCoefficients(model) {
|
|
2414
|
+
let coef = [model.coef[model.coef.length - 1]];
|
|
2415
|
+
const a = -model.basis.xCenter / model.basis.xScale;
|
|
2416
|
+
const b = 1 / model.basis.xScale;
|
|
2417
|
+
for (let j = model.coef.length - 2; j >= 0; j--) {
|
|
2418
|
+
const next = new Array(coef.length + 1).fill(0);
|
|
2419
|
+
coef.forEach((c, i) => {
|
|
2420
|
+
next[i] += a * c;
|
|
2421
|
+
next[i + 1] += b * c;
|
|
2422
|
+
});
|
|
2423
|
+
next[0] += model.coef[j];
|
|
2424
|
+
coef = next;
|
|
2425
|
+
}
|
|
2426
|
+
coef = coef.map(c => c * model.basis.yScale);
|
|
2427
|
+
coef[0] += model.basis.yCenter;
|
|
2428
|
+
if (coef.some(c => !Number.isFinite(c))) {
|
|
2429
|
+
throw new RangeError('Regression coefficients are not finite');
|
|
2430
|
+
}
|
|
2431
|
+
return coef;
|
|
2432
|
+
}
|
|
2433
|
+
function regressionGrid(min, max, count, predict) {
|
|
2434
|
+
if (!Number.isInteger(count) || count <= 0 || !Number.isFinite(min) || !Number.isFinite(max)) {
|
|
2435
|
+
return [];
|
|
2436
|
+
}
|
|
2437
|
+
const width = max - min;
|
|
2438
|
+
const step = count === 1 ? 0 : width / (count - 1);
|
|
2439
|
+
return Array.from({ length: count }, (_, i) => {
|
|
2440
|
+
const t = count === 1 ? 0 : i / (count - 1);
|
|
2441
|
+
const x = i === 0 ? min : i === count - 1 ? max : Number.isFinite(width) ? min + i * step : (1 - t) * min + t * max;
|
|
2442
|
+
return { x, y: predict(x) };
|
|
2443
|
+
});
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2239
2446
|
/** Compute a two-sided normal critical value without subtracting a small tail probability from one. */
|
|
2240
2447
|
function confidenceCriticalValue(alpha) {
|
|
2241
2448
|
if (!Number.isFinite(alpha) || alpha < 0 || alpha > 1) {
|
|
@@ -2347,39 +2554,19 @@
|
|
|
2347
2554
|
function regressionLinear(data, x = d => d.x, y = d => d.y, options) {
|
|
2348
2555
|
var _a;
|
|
2349
2556
|
const alpha = (_a = options === null || options === void 0 ? void 0 : options.alpha) !== null && _a !== void 0 ? _a : 0.05;
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2557
|
+
const points = regressionPoints(data, x, y);
|
|
2558
|
+
const model = fitRegression(points, 1);
|
|
2559
|
+
const coef = regressionCoefficients(model);
|
|
2560
|
+
const a = coef[0];
|
|
2561
|
+
const b = coef[1];
|
|
2562
|
+
const predict = (xx) => regressionPrediction(model, xx);
|
|
2563
|
+
const comps = computeLinearCIComponents(data, x, y, predict);
|
|
2353
2564
|
let meanY = 0;
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
visitPoints(data, x, y, (xi, yi) => {
|
|
2357
|
-
n++;
|
|
2358
|
-
meanX += (xi - meanX) / n;
|
|
2359
|
-
meanY += (yi - meanY) / n;
|
|
2360
|
-
meanXY += (xi * yi - meanXY) / n;
|
|
2361
|
-
meanX2 += (xi * xi - meanX2) / n;
|
|
2565
|
+
points.forEach((p, i) => {
|
|
2566
|
+
meanY += (p.y - meanY) / (i + 1);
|
|
2362
2567
|
});
|
|
2363
|
-
const { a, b } = ordinaryLeastSquares(meanX, meanY, meanXY, meanX2);
|
|
2364
|
-
const predict = (xx) => a + b * xx;
|
|
2365
|
-
const comps = computeLinearCIComponents(data, x, y, predict);
|
|
2366
2568
|
function evaluateGrid(N) {
|
|
2367
|
-
|
|
2368
|
-
if (comps.n === 0 || N <= 0) {
|
|
2369
|
-
return out;
|
|
2370
|
-
}
|
|
2371
|
-
if (comps.min === comps.max) {
|
|
2372
|
-
for (let i = 0; i < N; i++) {
|
|
2373
|
-
out.push({ x: comps.min, y: predict(comps.min) });
|
|
2374
|
-
}
|
|
2375
|
-
return out;
|
|
2376
|
-
}
|
|
2377
|
-
const step = (comps.max - comps.min) / (N - 1);
|
|
2378
|
-
for (let i = 0; i < N; i++) {
|
|
2379
|
-
const px = i === N - 1 ? comps.max : comps.min + step * i;
|
|
2380
|
-
out.push({ x: px, y: predict(px) });
|
|
2381
|
-
}
|
|
2382
|
-
return out;
|
|
2569
|
+
return regressionGrid(comps.min, comps.max, N, predict);
|
|
2383
2570
|
}
|
|
2384
2571
|
function confidenceInterval(N = 50) {
|
|
2385
2572
|
const out = [];
|
|
@@ -2424,6 +2611,7 @@
|
|
|
2424
2611
|
}
|
|
2425
2612
|
return {
|
|
2426
2613
|
coef: { a, b },
|
|
2614
|
+
rank: model.rank,
|
|
2427
2615
|
predict,
|
|
2428
2616
|
rSquared: rSquared(data, x, y, meanY, predict),
|
|
2429
2617
|
evaluateGrid,
|
|
@@ -2589,222 +2777,226 @@
|
|
|
2589
2777
|
};
|
|
2590
2778
|
}
|
|
2591
2779
|
|
|
2592
|
-
function tricube(u) {
|
|
2593
|
-
const uu = Math.abs(u);
|
|
2594
|
-
if (uu >= 1) {
|
|
2595
|
-
return 0;
|
|
2596
|
-
}
|
|
2597
|
-
const t = 1 - uu * uu * uu;
|
|
2598
|
-
return t * t * t;
|
|
2599
|
-
}
|
|
2600
2780
|
/**
|
|
2601
|
-
*
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
const n = sortedData.length;
|
|
2605
|
-
if (n <= maxSamples) {
|
|
2606
|
-
return sortedData;
|
|
2607
|
-
}
|
|
2608
|
-
const sampled = [];
|
|
2609
|
-
// More aggressive sampling - use exact step size
|
|
2610
|
-
const step = n / maxSamples;
|
|
2611
|
-
for (let i = 0; i < maxSamples; i++) {
|
|
2612
|
-
const idx = Math.min(Math.floor(i * step), n - 1);
|
|
2613
|
-
sampled.push(sortedData[idx]);
|
|
2614
|
-
}
|
|
2615
|
-
return sampled;
|
|
2616
|
-
}
|
|
2617
|
-
/**
|
|
2618
|
-
* Simple lowess implementation (univariate x)
|
|
2619
|
-
* options:
|
|
2620
|
-
* - span: fraction of points used in local regression (0,1]
|
|
2621
|
-
* - degree: 0 (constant) or 1 (linear)
|
|
2622
|
-
* - iterations: number of robustifying iterations
|
|
2623
|
-
* - alpha: confidence level for CI
|
|
2624
|
-
* - maxSamples: maximum number of points to use (default: 1000 for fast processing)
|
|
2781
|
+
* 局部线性 LOWESS。默认全量观测、span=2/3、三次稳健重加权。
|
|
2782
|
+
* delta 显式插值加速;maxSamples 仅保留显式选择的历史抽样用途。
|
|
2783
|
+
* predict 与 evaluateGrid 查询构建时固定的同一模型,不触发再拟合。
|
|
2625
2784
|
*/
|
|
2626
2785
|
function regressionLowess(data, x = d => d.x, y = d => d.y, options = {}) {
|
|
2627
|
-
var _a;
|
|
2628
|
-
const span = options.span
|
|
2629
|
-
const degree = options.degree
|
|
2630
|
-
const
|
|
2631
|
-
const
|
|
2632
|
-
const
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
}
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2786
|
+
var _a, _b, _c, _d, _e;
|
|
2787
|
+
const span = (_a = options.span) !== null && _a !== void 0 ? _a : 2 / 3;
|
|
2788
|
+
const degree = (_b = options.degree) !== null && _b !== void 0 ? _b : 1;
|
|
2789
|
+
const iterations = (_c = options.iterations) !== null && _c !== void 0 ? _c : 3;
|
|
2790
|
+
const delta = (_d = options.delta) !== null && _d !== void 0 ? _d : 0;
|
|
2791
|
+
const alpha = (_e = options.alpha) !== null && _e !== void 0 ? _e : 0.05;
|
|
2792
|
+
if (!(span > 0 && span <= 1) ||
|
|
2793
|
+
!Number.isInteger(iterations) ||
|
|
2794
|
+
iterations < 0 ||
|
|
2795
|
+
!Number.isFinite(delta) ||
|
|
2796
|
+
delta < 0 ||
|
|
2797
|
+
(degree !== 0 && degree !== 1) ||
|
|
2798
|
+
(options.maxSamples != null && (!Number.isInteger(options.maxSamples) || options.maxSamples < 2))) {
|
|
2799
|
+
throw new RangeError('Invalid LOWESS span, degree, iterations, delta or maxSamples');
|
|
2800
|
+
}
|
|
2801
|
+
let pts = regressionPoints(data, x, y).sort((a, b) => a.x - b.x || a.y - b.y);
|
|
2802
|
+
if (options.maxSamples != null && pts.length > options.maxSamples) {
|
|
2803
|
+
const full = pts;
|
|
2804
|
+
const count = options.maxSamples;
|
|
2805
|
+
pts = Array.from({ length: count }, (_, i) => full[Math.floor((i * (full.length - 1)) / (count - 1))]);
|
|
2806
|
+
}
|
|
2807
|
+
const n = pts.length;
|
|
2808
|
+
const ptsX = pts.map(p => p.x);
|
|
2809
|
+
const ptsY = pts.map(p => p.y);
|
|
2810
|
+
const basis = regressionBasis(pts);
|
|
2811
|
+
const normalizedX = ptsX.map(v => normalizeRegression(v, basis.xCenter, basis.xScale));
|
|
2812
|
+
const normalizedY = ptsY.map(v => normalizeRegression(v, basis.yCenter, basis.yScale));
|
|
2813
|
+
const count = Math.min(n, Math.max(2, Math.floor(span * n + 1e-10)));
|
|
2814
|
+
let robustWeights;
|
|
2815
|
+
function exact(x0) {
|
|
2816
|
+
if (!n) {
|
|
2653
2817
|
return 0;
|
|
2654
2818
|
}
|
|
2655
|
-
|
|
2819
|
+
const query = normalizeRegression(x0, basis.xCenter, basis.xScale);
|
|
2820
|
+
// 找到连续的最近邻窗口;相等距离选择较小的 x(ties 的 y 已排序)。
|
|
2656
2821
|
let left = 0;
|
|
2657
|
-
let right = n;
|
|
2822
|
+
let right = n - count;
|
|
2658
2823
|
while (left < right) {
|
|
2659
|
-
const mid = (left + right)
|
|
2660
|
-
|
|
2824
|
+
const mid = (left + right) >>> 1;
|
|
2825
|
+
const lowerDistance = x0 - ptsX[mid];
|
|
2826
|
+
const upperDistance = ptsX[mid + count] - x0;
|
|
2827
|
+
if (Number.isFinite(lowerDistance) && Number.isFinite(upperDistance)
|
|
2828
|
+
? lowerDistance > upperDistance
|
|
2829
|
+
: query - normalizedX[mid] > normalizedX[mid + count] - query) {
|
|
2661
2830
|
left = mid + 1;
|
|
2662
2831
|
}
|
|
2663
2832
|
else {
|
|
2664
2833
|
right = mid;
|
|
2665
2834
|
}
|
|
2666
2835
|
}
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
if (lo >= 0 && (hi >= n || x0 - ptsX[lo] <= ptsX[hi] - x0)) {
|
|
2673
|
-
lo--;
|
|
2836
|
+
let start = left;
|
|
2837
|
+
let end = left + count;
|
|
2838
|
+
if (ptsX[start] === ptsX[end - 1]) {
|
|
2839
|
+
while (start > 0 && ptsX[start - 1] === ptsX[start]) {
|
|
2840
|
+
start--;
|
|
2674
2841
|
}
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
}
|
|
2678
|
-
}
|
|
2679
|
-
const actualStart = lo + 1;
|
|
2680
|
-
const end = hi;
|
|
2681
|
-
// Find max distance and compute weights in single pass
|
|
2682
|
-
let maxDist = 0;
|
|
2683
|
-
const windowSize = end - actualStart;
|
|
2684
|
-
const distances = new Array(windowSize);
|
|
2685
|
-
for (let i = actualStart; i < end; i++) {
|
|
2686
|
-
const dist = Math.abs(ptsX[i] - x0);
|
|
2687
|
-
distances[i - actualStart] = dist;
|
|
2688
|
-
if (dist > maxDist) {
|
|
2689
|
-
maxDist = dist;
|
|
2690
|
-
}
|
|
2691
|
-
}
|
|
2692
|
-
// Compute weights using pre-calculated distances
|
|
2693
|
-
let sumw = 0;
|
|
2694
|
-
const w = new Array(windowSize);
|
|
2695
|
-
for (let i = 0; i < windowSize; i++) {
|
|
2696
|
-
const u = maxDist === 0 ? 0 : distances[i] / maxDist;
|
|
2697
|
-
let wi = tricube(u);
|
|
2698
|
-
if (robustWeights && robustWeights[actualStart + i] != null) {
|
|
2699
|
-
wi *= robustWeights[actualStart + i];
|
|
2700
|
-
}
|
|
2701
|
-
w[i] = wi;
|
|
2702
|
-
sumw += wi;
|
|
2703
|
-
}
|
|
2704
|
-
if (sumw === 0) {
|
|
2705
|
-
// Fall back to the nearest y, preserving input order for equal distances.
|
|
2706
|
-
let nearestIdx = 0;
|
|
2707
|
-
let nearestDistance = Math.abs(ptsX[0] - x0);
|
|
2708
|
-
for (let i = 1; i < n; i++) {
|
|
2709
|
-
const distance = Math.abs(ptsX[i] - x0);
|
|
2710
|
-
if (distance < nearestDistance ||
|
|
2711
|
-
(distance === nearestDistance && sampledPoints[i].index < sampledPoints[nearestIdx].index)) {
|
|
2712
|
-
nearestIdx = i;
|
|
2713
|
-
nearestDistance = distance;
|
|
2714
|
-
}
|
|
2842
|
+
while (end < n && ptsX[end] === ptsX[start]) {
|
|
2843
|
+
end++;
|
|
2715
2844
|
}
|
|
2716
|
-
return ptsY[nearestIdx];
|
|
2717
2845
|
}
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2846
|
+
const rawRadius = Math.max(Math.abs(ptsX[start] - x0), Math.abs(ptsX[end - 1] - x0));
|
|
2847
|
+
const scaled = !Number.isFinite(rawRadius);
|
|
2848
|
+
const radius = scaled
|
|
2849
|
+
? Math.max(Math.abs(normalizedX[start] - query), Math.abs(normalizedX[end - 1] - query))
|
|
2850
|
+
: rawRadius;
|
|
2851
|
+
// 局部响应坐标不能由远处、未参与该邻域的观测决定精度。
|
|
2852
|
+
let yMin = Infinity;
|
|
2853
|
+
let yMax = -Infinity;
|
|
2854
|
+
for (let i = start; i < end; i++) {
|
|
2855
|
+
yMin = Math.min(yMin, ptsY[i]);
|
|
2856
|
+
yMax = Math.max(yMax, ptsY[i]);
|
|
2724
2857
|
}
|
|
2725
|
-
|
|
2858
|
+
const yCenter = yMin / 2 + yMax / 2;
|
|
2859
|
+
const yScale = Math.max(Math.abs(yMin - yCenter), Math.abs(yMax - yCenter)) || 1;
|
|
2726
2860
|
let sw = 0;
|
|
2727
2861
|
let sx = 0;
|
|
2728
2862
|
let sy = 0;
|
|
2729
2863
|
let sxx = 0;
|
|
2730
2864
|
let sxy = 0;
|
|
2731
|
-
for (let i =
|
|
2732
|
-
const
|
|
2733
|
-
const
|
|
2734
|
-
const
|
|
2735
|
-
const
|
|
2736
|
-
sw +=
|
|
2737
|
-
sx +=
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2865
|
+
for (let i = start; i < end; i++) {
|
|
2866
|
+
const z = radius === 0 ? 0 : (scaled ? normalizedX[i] - query : ptsX[i] - x0) / radius;
|
|
2867
|
+
const u = Math.abs(z);
|
|
2868
|
+
const t = u >= 1 ? 0 : 1 - u * u * u;
|
|
2869
|
+
const w = t * t * t * (robustWeights ? robustWeights[i] : 1);
|
|
2870
|
+
sw += w;
|
|
2871
|
+
sx += w * z;
|
|
2872
|
+
const response = normalizeRegression(ptsY[i], yCenter, yScale);
|
|
2873
|
+
sy += w * response;
|
|
2874
|
+
sxx += w * z * z;
|
|
2875
|
+
sxy += w * z * response;
|
|
2876
|
+
}
|
|
2877
|
+
if (sw === 0) {
|
|
2878
|
+
// 核边界零权重时使用最近有效观测的加权常数;等距离取均值。
|
|
2879
|
+
let distance = Infinity;
|
|
2880
|
+
let weight = 0;
|
|
2881
|
+
let value = 0;
|
|
2882
|
+
for (let i = 0; i < n; i++) {
|
|
2883
|
+
const w = robustWeights ? robustWeights[i] : 1;
|
|
2884
|
+
if (w === 0) {
|
|
2885
|
+
continue;
|
|
2886
|
+
}
|
|
2887
|
+
const d = Math.abs(scaled ? normalizedX[i] - query : ptsX[i] - x0);
|
|
2888
|
+
if (d < distance) {
|
|
2889
|
+
distance = d;
|
|
2890
|
+
weight = w;
|
|
2891
|
+
value = ptsY[i];
|
|
2892
|
+
}
|
|
2893
|
+
else if (d === distance) {
|
|
2894
|
+
value = value * (weight / (weight + w)) + ptsY[i] * (w / (weight + w));
|
|
2895
|
+
weight += w;
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
if (weight === 0) {
|
|
2899
|
+
throw new RangeError('LOWESS has no effective local observations');
|
|
2900
|
+
}
|
|
2901
|
+
return value;
|
|
2902
|
+
}
|
|
2903
|
+
const mx = sx / sw;
|
|
2904
|
+
const my = sy / sw;
|
|
2905
|
+
const variance = sxx / sw - mx * mx;
|
|
2906
|
+
const slope = degree === 0 || variance <= Number.EPSILON * 32 ? 0 : (sxy / sw - mx * my) / variance;
|
|
2907
|
+
return yCenter + yScale * (my - slope * mx);
|
|
2908
|
+
}
|
|
2909
|
+
// delta 不删除观测。只减少局部回归求值位置,全部残差参与重加权。
|
|
2910
|
+
const anchors = [];
|
|
2911
|
+
for (let i = 0; i < n;) {
|
|
2912
|
+
anchors.push(i);
|
|
2913
|
+
if (i === n - 1) {
|
|
2914
|
+
break;
|
|
2915
|
+
}
|
|
2916
|
+
let next = i + 1;
|
|
2917
|
+
if (delta > 0) {
|
|
2918
|
+
while (next + 1 < n && ptsX[next + 1] - ptsX[i] <= delta) {
|
|
2919
|
+
next++;
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
// 重复 x 一起使用同一预测;端点不能丢失。
|
|
2923
|
+
while (next + 1 < n && ptsX[next + 1] === ptsX[next]) {
|
|
2924
|
+
next++;
|
|
2925
|
+
}
|
|
2926
|
+
i = next;
|
|
2748
2927
|
}
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2928
|
+
let fits = new Array(n);
|
|
2929
|
+
function fitObservations() {
|
|
2930
|
+
const out = new Array(n);
|
|
2931
|
+
for (let a = 0; a < anchors.length; a++) {
|
|
2932
|
+
const index = anchors[a];
|
|
2933
|
+
out[index] = exact(ptsX[index]);
|
|
2934
|
+
if (a === 0) {
|
|
2935
|
+
continue;
|
|
2936
|
+
}
|
|
2937
|
+
const previous = anchors[a - 1];
|
|
2938
|
+
const rawWidth = ptsX[index] - ptsX[previous];
|
|
2939
|
+
const width = Number.isFinite(rawWidth) ? rawWidth : normalizedX[index] - normalizedX[previous];
|
|
2940
|
+
for (let i = previous + 1; i < index; i++) {
|
|
2941
|
+
const t = width === 0
|
|
2942
|
+
? 0
|
|
2943
|
+
: (Number.isFinite(rawWidth) ? ptsX[i] - ptsX[previous] : normalizedX[i] - normalizedX[previous]) / width;
|
|
2944
|
+
out[i] = out[previous] * (1 - t) + out[index] * t;
|
|
2755
2945
|
}
|
|
2756
|
-
return out;
|
|
2757
2946
|
}
|
|
2758
|
-
return
|
|
2947
|
+
return out;
|
|
2759
2948
|
}
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2949
|
+
for (let iteration = 0; iteration <= iterations; iteration++) {
|
|
2950
|
+
fits = fitObservations();
|
|
2951
|
+
if (iteration === iterations) {
|
|
2952
|
+
break;
|
|
2763
2953
|
}
|
|
2764
|
-
|
|
2765
|
-
|
|
2954
|
+
const residuals = fits.map((v, i) => {
|
|
2955
|
+
const residual = ptsY[i] - v;
|
|
2956
|
+
return Number.isFinite(residual)
|
|
2957
|
+
? Math.abs(residual / basis.yScale)
|
|
2958
|
+
: Math.abs(normalizedY[i] - normalizeRegression(v, basis.yCenter, basis.yScale));
|
|
2959
|
+
});
|
|
2960
|
+
const med = median(residuals.slice());
|
|
2961
|
+
const currentFits = fits;
|
|
2962
|
+
robustWeights = residuals.map((r, i) => {
|
|
2963
|
+
if (med === 0) {
|
|
2964
|
+
const tolerance = Number.EPSILON * 64 * (Math.max(Math.abs(ptsY[i]), Math.abs(currentFits[i])) / basis.yScale);
|
|
2965
|
+
return r <= tolerance ? 1 : 0;
|
|
2966
|
+
}
|
|
2967
|
+
const u = r / (6 * med);
|
|
2968
|
+
return u >= 1 ? 0 : (1 - u * u) ** 2;
|
|
2969
|
+
});
|
|
2970
|
+
}
|
|
2971
|
+
function predictSingle(x0) {
|
|
2972
|
+
if (delta === 0 || n < 2 || x0 < ptsX[0] || x0 > ptsX[n - 1]) {
|
|
2973
|
+
return exact(x0);
|
|
2766
2974
|
}
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
out[i] = { x: min, y: v };
|
|
2975
|
+
let lo = 0;
|
|
2976
|
+
let hi = n - 1;
|
|
2977
|
+
while (hi - lo > 1) {
|
|
2978
|
+
const mid = (lo + hi) >>> 1;
|
|
2979
|
+
if (ptsX[mid] <= x0) {
|
|
2980
|
+
lo = mid;
|
|
2774
2981
|
}
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
const step = (max - min) / (N - 1);
|
|
2778
|
-
// optionally add robust iterations
|
|
2779
|
-
let robustWeights;
|
|
2780
|
-
// Disable robustness iterations for large datasets to improve performance
|
|
2781
|
-
// Users can override by setting iterations explicitly
|
|
2782
|
-
const effectiveIterations = options.iterations != null ? iterations : n > 500 ? 0 : iterations;
|
|
2783
|
-
if (effectiveIterations > 0) {
|
|
2784
|
-
for (let iter = 0; iter < effectiveIterations; iter++) {
|
|
2785
|
-
// compute fits - pre-allocate arrays
|
|
2786
|
-
const fits = new Array(n);
|
|
2787
|
-
const res = new Array(n);
|
|
2788
|
-
for (let i = 0; i < n; i++) {
|
|
2789
|
-
fits[i] = predictSingle(ptsX[i], robustWeights);
|
|
2790
|
-
res[i] = Math.abs(ptsY[i] - fits[i]);
|
|
2791
|
-
}
|
|
2792
|
-
// median absolute deviation
|
|
2793
|
-
const sortedRes = res.slice().sort((a, b) => a - b);
|
|
2794
|
-
const med = sortedRes[Math.floor(n / 2)] || 0;
|
|
2795
|
-
robustWeights = new Array(n);
|
|
2796
|
-
for (let i = 0; i < n; i++) {
|
|
2797
|
-
const u = med === 0 ? 0 : res[i] / (6 * med);
|
|
2798
|
-
const w = Math.abs(u) >= 1 ? 0 : (1 - u * u) * (1 - u * u);
|
|
2799
|
-
robustWeights[i] = w;
|
|
2800
|
-
}
|
|
2982
|
+
else {
|
|
2983
|
+
hi = mid;
|
|
2801
2984
|
}
|
|
2802
2985
|
}
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
out[i] = { x: px, y: predictSingle(px, robustWeights) };
|
|
2986
|
+
if (ptsX[hi] === ptsX[lo]) {
|
|
2987
|
+
return fits[lo];
|
|
2806
2988
|
}
|
|
2807
|
-
|
|
2989
|
+
const width = ptsX[hi] - ptsX[lo];
|
|
2990
|
+
const t = Number.isFinite(width)
|
|
2991
|
+
? (x0 - ptsX[lo]) / width
|
|
2992
|
+
: (normalizeRegression(x0, basis.xCenter, basis.xScale) - normalizedX[lo]) / (normalizedX[hi] - normalizedX[lo]);
|
|
2993
|
+
return fits[lo] * (1 - t) + fits[hi] * t;
|
|
2994
|
+
}
|
|
2995
|
+
function predict(x0) {
|
|
2996
|
+
return Array.isArray(x0) ? x0.map(predictSingle) : predictSingle(x0);
|
|
2997
|
+
}
|
|
2998
|
+
function evaluateGrid(N) {
|
|
2999
|
+
return regressionGrid(ptsX[0], ptsX[n - 1], N, predictSingle);
|
|
2808
3000
|
}
|
|
2809
3001
|
function confidenceInterval(N = 50) {
|
|
2810
3002
|
const out = [];
|
|
@@ -2864,111 +3056,26 @@
|
|
|
2864
3056
|
predict,
|
|
2865
3057
|
evaluate: predict,
|
|
2866
3058
|
evaluateGrid,
|
|
2867
|
-
confidenceInterval
|
|
3059
|
+
confidenceInterval,
|
|
3060
|
+
// 节点值按 x 排序,重复观测仍保持其统计权重。
|
|
3061
|
+
fitted: pts.map((p, i) => ({ x: p.x, y: fits[i] }))
|
|
2868
3062
|
};
|
|
2869
3063
|
}
|
|
2870
3064
|
|
|
2871
|
-
function solveLinearSystem(A, b) {
|
|
2872
|
-
// Gaussian elimination with partial pivoting
|
|
2873
|
-
const n = b.length;
|
|
2874
|
-
// clone
|
|
2875
|
-
const M = new Array(n);
|
|
2876
|
-
for (let i = 0; i < n; i++) {
|
|
2877
|
-
M[i] = A[i].slice();
|
|
2878
|
-
M[i].push(b[i]);
|
|
2879
|
-
}
|
|
2880
|
-
for (let k = 0; k < n; k++) {
|
|
2881
|
-
// find pivot
|
|
2882
|
-
let maxRow = k;
|
|
2883
|
-
let maxVal = Math.abs(M[k][k]);
|
|
2884
|
-
for (let i = k + 1; i < n; i++) {
|
|
2885
|
-
const v = Math.abs(M[i][k]);
|
|
2886
|
-
if (v > maxVal) {
|
|
2887
|
-
maxVal = v;
|
|
2888
|
-
maxRow = i;
|
|
2889
|
-
}
|
|
2890
|
-
}
|
|
2891
|
-
if (maxRow !== k) {
|
|
2892
|
-
const tmp = M[k];
|
|
2893
|
-
M[k] = M[maxRow];
|
|
2894
|
-
M[maxRow] = tmp;
|
|
2895
|
-
}
|
|
2896
|
-
// singular check
|
|
2897
|
-
if (Math.abs(M[k][k]) < 1e-12) {
|
|
2898
|
-
// return least squares fallback zeros
|
|
2899
|
-
const res = new Array(n).fill(0);
|
|
2900
|
-
return res;
|
|
2901
|
-
}
|
|
2902
|
-
// normalize row
|
|
2903
|
-
for (let j = k + 1; j <= n; j++) {
|
|
2904
|
-
M[k][j] = M[k][j] / M[k][k];
|
|
2905
|
-
}
|
|
2906
|
-
M[k][k] = 1;
|
|
2907
|
-
// eliminate
|
|
2908
|
-
for (let i = 0; i < n; i++) {
|
|
2909
|
-
if (i === k) {
|
|
2910
|
-
continue;
|
|
2911
|
-
}
|
|
2912
|
-
const factor = M[i][k];
|
|
2913
|
-
if (factor === 0) {
|
|
2914
|
-
continue;
|
|
2915
|
-
}
|
|
2916
|
-
for (let j = k + 1; j <= n; j++) {
|
|
2917
|
-
M[i][j] -= factor * M[k][j];
|
|
2918
|
-
}
|
|
2919
|
-
M[i][k] = 0;
|
|
2920
|
-
}
|
|
2921
|
-
}
|
|
2922
|
-
const x = new Array(n);
|
|
2923
|
-
for (let i = 0; i < n; i++) {
|
|
2924
|
-
x[i] = M[i][n];
|
|
2925
|
-
}
|
|
2926
|
-
return x;
|
|
2927
|
-
}
|
|
2928
3065
|
function regressionPolynomial(data, x = d => d.x, y = d => d.y, options = {}) {
|
|
2929
3066
|
var _a, _b;
|
|
2930
|
-
|
|
2931
|
-
if (degree < 0) {
|
|
2932
|
-
degree
|
|
3067
|
+
const degree = (_a = options.degree) !== null && _a !== void 0 ? _a : 0;
|
|
3068
|
+
if (!Number.isInteger(degree) || degree < 0) {
|
|
3069
|
+
throw new RangeError('Polynomial degree must be an nonnegative integer');
|
|
2933
3070
|
}
|
|
2934
3071
|
const alpha = (_b = options.alpha) !== null && _b !== void 0 ? _b : 0.05;
|
|
2935
|
-
const
|
|
2936
|
-
const
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
for (let k = 0; k < sums.length; k++) {
|
|
2940
|
-
sums[k] += xp;
|
|
2941
|
-
xp *= dx;
|
|
2942
|
-
}
|
|
2943
|
-
});
|
|
2944
|
-
// build normal matrix
|
|
2945
|
-
const A = new Array(m);
|
|
2946
|
-
for (let i = 0; i < m; i++) {
|
|
2947
|
-
A[i] = new Array(m).fill(0);
|
|
2948
|
-
for (let j = 0; j < m; j++) {
|
|
2949
|
-
A[i][j] = sums[i + j];
|
|
2950
|
-
}
|
|
2951
|
-
}
|
|
2952
|
-
const B = new Array(m).fill(0);
|
|
2953
|
-
visitPoints(data, x, y, (dx, dy) => {
|
|
2954
|
-
let xp = 1;
|
|
2955
|
-
for (let k = 0; k < m; k++) {
|
|
2956
|
-
B[k] += dy * xp;
|
|
2957
|
-
xp *= dx;
|
|
2958
|
-
}
|
|
2959
|
-
});
|
|
2960
|
-
const coef = solveLinearSystem(A, B);
|
|
2961
|
-
const predict = (xx) => {
|
|
2962
|
-
let xp = 1;
|
|
2963
|
-
let v = 0;
|
|
2964
|
-
for (let k = 0; k < coef.length; k++) {
|
|
2965
|
-
v += coef[k] * xp;
|
|
2966
|
-
xp *= xx;
|
|
2967
|
-
}
|
|
2968
|
-
return v;
|
|
2969
|
-
};
|
|
3072
|
+
const points = regressionPoints(data, x, y);
|
|
3073
|
+
const model = fitRegression(points, degree);
|
|
3074
|
+
const coef = regressionCoefficients(model);
|
|
3075
|
+
const predict = (xx) => regressionPrediction(model, xx);
|
|
2970
3076
|
return {
|
|
2971
3077
|
degree,
|
|
3078
|
+
rank: model.rank,
|
|
2972
3079
|
coef,
|
|
2973
3080
|
predict,
|
|
2974
3081
|
rSquared: rSquared(data, x, y, (() => {
|
|
@@ -2982,39 +3089,13 @@
|
|
|
2982
3089
|
return cnt === 0 ? 0 : sum / cnt;
|
|
2983
3090
|
})(), predict),
|
|
2984
3091
|
evaluateGrid(N) {
|
|
2985
|
-
const out = [];
|
|
2986
|
-
if (N <= 0) {
|
|
2987
|
-
return out;
|
|
2988
|
-
}
|
|
2989
|
-
// compute range
|
|
2990
3092
|
let min = Infinity;
|
|
2991
3093
|
let max = -Infinity;
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
min = dx;
|
|
2996
|
-
}
|
|
2997
|
-
if (dx > max) {
|
|
2998
|
-
max = dx;
|
|
2999
|
-
}
|
|
3000
|
-
}
|
|
3094
|
+
points.forEach(p => {
|
|
3095
|
+
min = Math.min(min, p.x);
|
|
3096
|
+
max = Math.max(max, p.x);
|
|
3001
3097
|
});
|
|
3002
|
-
|
|
3003
|
-
return out;
|
|
3004
|
-
}
|
|
3005
|
-
if (min === max) {
|
|
3006
|
-
const v = predict(min);
|
|
3007
|
-
for (let i = 0; i < N; i++) {
|
|
3008
|
-
out.push({ x: min, y: v });
|
|
3009
|
-
}
|
|
3010
|
-
return out;
|
|
3011
|
-
}
|
|
3012
|
-
const step = (max - min) / (N - 1);
|
|
3013
|
-
for (let i = 0; i < N; i++) {
|
|
3014
|
-
const px = i === N - 1 ? max : min + step * i;
|
|
3015
|
-
out.push({ x: px, y: predict(px) });
|
|
3016
|
-
}
|
|
3017
|
-
return out;
|
|
3098
|
+
return regressionGrid(min, max, N, predict);
|
|
3018
3099
|
},
|
|
3019
3100
|
confidenceInterval(N = 50) {
|
|
3020
3101
|
const out = [];
|
|
@@ -3064,6 +3145,33 @@
|
|
|
3064
3145
|
};
|
|
3065
3146
|
}
|
|
3066
3147
|
|
|
3148
|
+
/** 有限数值的均值;空输入没有均值,不修改输入。 */
|
|
3149
|
+
function mean(values) {
|
|
3150
|
+
if (!values.length) {
|
|
3151
|
+
return undefined;
|
|
3152
|
+
}
|
|
3153
|
+
let scale = 0;
|
|
3154
|
+
for (const value of values) {
|
|
3155
|
+
if (!Number.isFinite(value)) {
|
|
3156
|
+
throw new RangeError('mean requires finite numbers');
|
|
3157
|
+
}
|
|
3158
|
+
scale = Math.max(scale, Math.abs(value));
|
|
3159
|
+
}
|
|
3160
|
+
if (scale === 0) {
|
|
3161
|
+
return 0;
|
|
3162
|
+
}
|
|
3163
|
+
let sum = 0;
|
|
3164
|
+
let correction = 0;
|
|
3165
|
+
for (const value of values) {
|
|
3166
|
+
const next = value / scale;
|
|
3167
|
+
const total = sum + next;
|
|
3168
|
+
// Neumaier compensation retains small contributions even when larger values cancel.
|
|
3169
|
+
correction += Math.abs(sum) >= Math.abs(next) ? sum - total + next : next - total + sum;
|
|
3170
|
+
sum = total;
|
|
3171
|
+
}
|
|
3172
|
+
return ((sum + correction) / values.length) * scale;
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3067
3175
|
/**
|
|
3068
3176
|
* Kernel Density Estimation (KDE) utilities
|
|
3069
3177
|
*
|
|
@@ -3103,33 +3211,59 @@
|
|
|
3103
3211
|
/**
|
|
3104
3212
|
* Compute standard deviation for numeric array
|
|
3105
3213
|
*/
|
|
3106
|
-
function std(values) {
|
|
3214
|
+
function std(values, convention) {
|
|
3107
3215
|
const n = values.length;
|
|
3108
|
-
if (n
|
|
3216
|
+
if (n < 2) {
|
|
3109
3217
|
return 0;
|
|
3110
3218
|
}
|
|
3111
|
-
|
|
3219
|
+
const center = mean(values);
|
|
3220
|
+
const differences = [];
|
|
3221
|
+
let scale = 0;
|
|
3222
|
+
let sourceScale = 1;
|
|
3112
3223
|
for (let i = 0; i < n; i++) {
|
|
3113
|
-
|
|
3224
|
+
const difference = values[i] - center;
|
|
3225
|
+
differences.push(difference);
|
|
3226
|
+
scale = Math.max(scale, Math.abs(difference));
|
|
3227
|
+
}
|
|
3228
|
+
// Finite values of opposite signs can overflow subtraction although population deviation is representable.
|
|
3229
|
+
if (scale === Infinity) {
|
|
3230
|
+
sourceScale = Math.abs(center);
|
|
3231
|
+
for (const value of values) {
|
|
3232
|
+
sourceScale = Math.max(sourceScale, Math.abs(value));
|
|
3233
|
+
}
|
|
3234
|
+
scale = 0;
|
|
3235
|
+
for (let i = 0; i < n; i++) {
|
|
3236
|
+
differences[i] = values[i] / sourceScale - center / sourceScale;
|
|
3237
|
+
scale = Math.max(scale, Math.abs(differences[i]));
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
if (scale === 0) {
|
|
3241
|
+
return 0;
|
|
3114
3242
|
}
|
|
3115
|
-
mean /= n;
|
|
3116
|
-
let s = 0;
|
|
3117
3243
|
for (let i = 0; i < n; i++) {
|
|
3118
|
-
|
|
3119
|
-
s += d * d;
|
|
3244
|
+
differences[i] /= scale;
|
|
3120
3245
|
}
|
|
3121
|
-
|
|
3246
|
+
const correction = convention === 'population' ? (n - 1) / n : 1;
|
|
3247
|
+
return Math.sqrt(variance(differences) * correction) * scale * sourceScale;
|
|
3122
3248
|
}
|
|
3123
3249
|
/**
|
|
3124
3250
|
* Factory: create a KDE evaluator for given data and options.
|
|
3125
3251
|
* Usage: const model = kde(data, options); model.evaluate(x) or model.evaluate([x1,x2])
|
|
3126
3252
|
*/
|
|
3127
3253
|
function kde(data, options = {}) {
|
|
3254
|
+
const deviation = options.standardDeviation === undefined ? 'population' : options.standardDeviation;
|
|
3255
|
+
const adjustment = options.bandwidthAdjust === undefined ? 1 : options.bandwidthAdjust;
|
|
3256
|
+
if (deviation !== 'sample' && deviation !== 'population') {
|
|
3257
|
+
throw new RangeError('standardDeviation must be sample or population');
|
|
3258
|
+
}
|
|
3259
|
+
if (!Number.isFinite(adjustment) || adjustment <= 0) {
|
|
3260
|
+
throw new RangeError('bandwidthAdjust must be finite and positive');
|
|
3261
|
+
}
|
|
3128
3262
|
const n = data.length;
|
|
3129
3263
|
const kernel = options.kernel || gaussian;
|
|
3130
3264
|
let h = options.bandwidth;
|
|
3131
3265
|
if (!h || h <= 0) {
|
|
3132
|
-
const sd = std(data) || 0;
|
|
3266
|
+
const sd = std(data, deviation) || 0;
|
|
3133
3267
|
const method = options.bandwidthMethod || 'scott';
|
|
3134
3268
|
if (method === 'silverman') {
|
|
3135
3269
|
h = silverman(n, sd, 1);
|
|
@@ -3138,6 +3272,7 @@
|
|
|
3138
3272
|
h = scott(n, sd, 1);
|
|
3139
3273
|
}
|
|
3140
3274
|
}
|
|
3275
|
+
h *= adjustment;
|
|
3141
3276
|
// if still zero (constant data), evaluator returns zeros
|
|
3142
3277
|
if (!h || h <= 0) {
|
|
3143
3278
|
const zerosEval = (x) => {
|
|
@@ -3185,7 +3320,7 @@
|
|
|
3185
3320
|
evaluateGrid: zerosGrid
|
|
3186
3321
|
};
|
|
3187
3322
|
}
|
|
3188
|
-
const invNh = 1 /
|
|
3323
|
+
const invNh = 1 / n / h;
|
|
3189
3324
|
function evalPoint(x) {
|
|
3190
3325
|
let sum = 0;
|
|
3191
3326
|
for (let j = 0; j < n; j++) {
|
|
@@ -3442,33 +3577,6 @@
|
|
|
3442
3577
|
return method === 'kendall' ? kendall(x, y) : method === 'spearman' ? pearson(ranks(x), ranks(y)) : pearson(x, y);
|
|
3443
3578
|
}
|
|
3444
3579
|
|
|
3445
|
-
/** 有限数值的均值;空输入没有均值,不修改输入。 */
|
|
3446
|
-
function mean(values) {
|
|
3447
|
-
if (!values.length) {
|
|
3448
|
-
return undefined;
|
|
3449
|
-
}
|
|
3450
|
-
let scale = 0;
|
|
3451
|
-
for (const value of values) {
|
|
3452
|
-
if (!Number.isFinite(value)) {
|
|
3453
|
-
throw new RangeError('mean requires finite numbers');
|
|
3454
|
-
}
|
|
3455
|
-
scale = Math.max(scale, Math.abs(value));
|
|
3456
|
-
}
|
|
3457
|
-
if (scale === 0) {
|
|
3458
|
-
return 0;
|
|
3459
|
-
}
|
|
3460
|
-
let sum = 0;
|
|
3461
|
-
let correction = 0;
|
|
3462
|
-
for (const value of values) {
|
|
3463
|
-
const next = value / scale;
|
|
3464
|
-
const total = sum + next;
|
|
3465
|
-
// Neumaier compensation retains small contributions even when larger values cancel.
|
|
3466
|
-
correction += Math.abs(sum) >= Math.abs(next) ? sum - total + next : next - total + sum;
|
|
3467
|
-
sum = total;
|
|
3468
|
-
}
|
|
3469
|
-
return ((sum + correction) / values.length) * scale;
|
|
3470
|
-
}
|
|
3471
|
-
|
|
3472
3580
|
/** 有放回重采样;回调不得修改或持有复用的只读样本缓冲区。 */
|
|
3473
3581
|
function bootstrapEstimate(values, estimator, options) {
|
|
3474
3582
|
if (!values.length ||
|
|
@@ -3496,6 +3604,55 @@
|
|
|
3496
3604
|
return estimates;
|
|
3497
3605
|
}
|
|
3498
3606
|
|
|
3607
|
+
/** 成对 percentile bootstrap;每次重采样仅拟合一次,固定原始坐标基底。 */
|
|
3608
|
+
function regressionBootstrap(data, options) {
|
|
3609
|
+
var _a;
|
|
3610
|
+
const degree = options.method === 'linear' ? 1 : (_a = options.degree) !== null && _a !== void 0 ? _a : NaN;
|
|
3611
|
+
if ((options.method !== 'linear' && options.method !== 'polynomial') ||
|
|
3612
|
+
!Number.isInteger(degree) ||
|
|
3613
|
+
degree < 1 ||
|
|
3614
|
+
!data.length ||
|
|
3615
|
+
data.some(p => !Number.isFinite(p.x) || !Number.isFinite(p.y)) ||
|
|
3616
|
+
options.grid.some(x => !Number.isFinite(x)) ||
|
|
3617
|
+
!(options.level > 0 && options.level < 1) ||
|
|
3618
|
+
!Number.isInteger(options.resamples) ||
|
|
3619
|
+
options.resamples < 1 ||
|
|
3620
|
+
!Number.isInteger(options.seed) ||
|
|
3621
|
+
options.seed < 0 ||
|
|
3622
|
+
options.seed > 0xffffffff) {
|
|
3623
|
+
throw new RangeError('Invalid paired regression bootstrap inputs');
|
|
3624
|
+
}
|
|
3625
|
+
if (data.length <= degree + 1) {
|
|
3626
|
+
throw new RangeError('Regression bootstrap requires positive residual degrees of freedom');
|
|
3627
|
+
}
|
|
3628
|
+
const sorted = data.slice().sort((a, b) => a.x - b.x || a.y - b.y);
|
|
3629
|
+
const basis = regressionBasis(sorted);
|
|
3630
|
+
if (fitRegression(sorted, degree, basis).rank < degree + 1) {
|
|
3631
|
+
throw new RangeError('Original regression design is rank deficient');
|
|
3632
|
+
}
|
|
3633
|
+
const random = randomLCG(options.seed);
|
|
3634
|
+
const sample = new Array(sorted.length);
|
|
3635
|
+
const values = options.grid.map(() => new Array(options.resamples));
|
|
3636
|
+
for (let b = 0; b < options.resamples; b++) {
|
|
3637
|
+
for (let i = 0; i < sample.length; i++) {
|
|
3638
|
+
sample[i] = sorted[Math.floor(random() * sorted.length)];
|
|
3639
|
+
}
|
|
3640
|
+
const model = fitRegression(sample, degree, basis);
|
|
3641
|
+
for (let j = 0; j < options.grid.length; j++) {
|
|
3642
|
+
const value = regressionPrediction(model, options.grid[j]);
|
|
3643
|
+
if (!Number.isFinite(value)) {
|
|
3644
|
+
throw new RangeError('Regression bootstrap prediction is not finite');
|
|
3645
|
+
}
|
|
3646
|
+
values[j][b] = value;
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
const tail = (1 - options.level) / 2;
|
|
3650
|
+
return options.grid.map((x, j) => {
|
|
3651
|
+
values[j].sort((a, b) => a - b);
|
|
3652
|
+
return { x, lower: quantileSorted(values[j], tail), upper: quantileSorted(values[j], 1 - tail) };
|
|
3653
|
+
});
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3499
3656
|
class HashValue {
|
|
3500
3657
|
}
|
|
3501
3658
|
// 基于 array 和 object的 HashTable
|
|
@@ -8975,6 +9132,7 @@
|
|
|
8975
9132
|
exports.randomLCG = randomLCG;
|
|
8976
9133
|
exports.range = range;
|
|
8977
9134
|
exports.rectInsideAnotherRect = rectInsideAnotherRect;
|
|
9135
|
+
exports.regressionBootstrap = regressionBootstrap;
|
|
8978
9136
|
exports.regressionLinear = regressionLinear;
|
|
8979
9137
|
exports.regressionLogistic = regressionLogistic;
|
|
8980
9138
|
exports.regressionLowess = regressionLowess;
|