@visactor/vutils 1.0.25 → 1.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/REGRESSION.md +36 -0
  2. package/cjs/common/ascending.js +2 -1
  3. package/cjs/common/deviation.js +1 -2
  4. package/cjs/common/getType.js +2 -1
  5. package/cjs/common/index.d.ts +1 -0
  6. package/cjs/common/index.js +2 -1
  7. package/cjs/common/index.js.map +1 -1
  8. package/cjs/common/isFunction.js +1 -2
  9. package/cjs/common/isObject.js +2 -1
  10. package/cjs/common/isPrototype.js +1 -1
  11. package/cjs/common/isRegExp.js +1 -1
  12. package/cjs/common/isShallowEqual.js +1 -1
  13. package/cjs/common/isString.js +1 -1
  14. package/cjs/common/isType.js +1 -1
  15. package/cjs/common/regression-bootstrap.d.ts +14 -0
  16. package/cjs/common/regression-bootstrap.js +37 -0
  17. package/cjs/common/regression-bootstrap.js.map +1 -0
  18. package/cjs/common/regression-linear.d.ts +1 -0
  19. package/cjs/common/regression-linear.js +11 -29
  20. package/cjs/common/regression-linear.js.map +1 -1
  21. package/cjs/common/regression-lowess.d.ts +9 -8
  22. package/cjs/common/regression-lowess.js +109 -130
  23. package/cjs/common/regression-lowess.js.map +1 -1
  24. package/cjs/common/regression-polynomial.d.ts +1 -0
  25. package/cjs/common/regression-polynomial.js +11 -92
  26. package/cjs/common/regression-polynomial.js.map +1 -1
  27. package/cjs/common/regression-solver.d.ts +26 -0
  28. package/cjs/common/regression-solver.js +180 -0
  29. package/cjs/common/regression-solver.js.map +1 -0
  30. package/cjs/data-structure/bounds.js +1 -1
  31. package/cjs/data-structure/hashTable.js +1 -2
  32. package/cjs/data-structure/index.js +1 -1
  33. package/cjs/data-structure/matrix.js +1 -1
  34. package/cjs/data-structure/point.js +1 -1
  35. package/dist/index.js +471 -340
  36. package/dist/index.min.js +16 -16
  37. package/es/common/ascending.js +2 -1
  38. package/es/common/deviation.js +1 -2
  39. package/es/common/getType.js +2 -1
  40. package/es/common/index.d.ts +1 -0
  41. package/es/common/index.js +2 -0
  42. package/es/common/index.js.map +1 -1
  43. package/es/common/isFunction.js +1 -2
  44. package/es/common/isObject.js +2 -1
  45. package/es/common/isPrototype.js +1 -1
  46. package/es/common/isRegExp.js +1 -1
  47. package/es/common/isShallowEqual.js +1 -1
  48. package/es/common/isString.js +1 -1
  49. package/es/common/isType.js +1 -1
  50. package/es/common/regression-bootstrap.d.ts +14 -0
  51. package/es/common/regression-bootstrap.js +31 -0
  52. package/es/common/regression-bootstrap.js.map +1 -0
  53. package/es/common/regression-linear.d.ts +1 -0
  54. package/es/common/regression-linear.js +9 -27
  55. package/es/common/regression-linear.js.map +1 -1
  56. package/es/common/regression-lowess.d.ts +9 -8
  57. package/es/common/regression-lowess.js +106 -130
  58. package/es/common/regression-lowess.js.map +1 -1
  59. package/es/common/regression-polynomial.d.ts +1 -0
  60. package/es/common/regression-polynomial.js +9 -86
  61. package/es/common/regression-polynomial.js.map +1 -1
  62. package/es/common/regression-solver.d.ts +26 -0
  63. package/es/common/regression-solver.js +168 -0
  64. package/es/common/regression-solver.js.map +1 -0
  65. package/es/data-structure/bounds.js +1 -1
  66. package/es/data-structure/hashTable.js +1 -2
  67. package/es/data-structure/index.js +1 -1
  68. package/es/data-structure/matrix.js +1 -1
  69. package/es/data-structure/point.js +1 -1
  70. package/package.json +5 -4
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
- // accumulate online means (sufficient statistics)
2351
- let n = 0;
2352
- let meanX = 0;
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
- let meanXY = 0;
2355
- let meanX2 = 0;
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
- const out = [];
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
- * Stratified sampling to reduce data size while preserving distribution
2602
- */
2603
- function stratifiedSample(sortedData, maxSamples) {
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 || 0.3;
2629
- const degree = options.degree === 0 ? 0 : 1;
2630
- const alpha = (_a = options.alpha) !== null && _a !== void 0 ? _a : 0.05;
2631
- const iterations = options.iterations == null ? 2 : options.iterations;
2632
- const maxSamples = options.maxSamples || 1000;
2633
- // Collect and sort data by x
2634
- const rawPoints = [];
2635
- visitPoints(data, x, y, (dx, dy, index) => {
2636
- rawPoints.push({ x: dx, y: dy, index });
2637
- });
2638
- rawPoints.sort((a, b) => a.x - b.x);
2639
- // Apply sampling if needed
2640
- const sampledPoints = stratifiedSample(rawPoints, maxSamples);
2641
- const n = sampledPoints.length;
2642
- const ptsX = new Array(n);
2643
- const ptsY = new Array(n);
2644
- for (let i = 0; i < n; i++) {
2645
- ptsX[i] = sampledPoints[i].x;
2646
- ptsY[i] = sampledPoints[i].y;
2647
- }
2648
- /**
2649
- * Optimized predictSingle using binary search on pre-sorted data
2650
- */
2651
- function predictSingle(x0, robustWeights) {
2652
- if (n === 0) {
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
- // Binary search to find insertion point
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) >> 1;
2660
- if (ptsX[mid] < x0) {
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
- const m = Math.min(n, Math.max(2, Math.floor(span * n)));
2668
- // Expand by distance: equal numbers on either side need not be the nearest points.
2669
- let lo = left - 1;
2670
- let hi = left;
2671
- for (let count = 0; count < m; count++) {
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
- else {
2676
- hi++;
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
- if (degree === 0) {
2719
- let s = 0;
2720
- for (let i = 0; i < w.length; i++) {
2721
- s += w[i] * ptsY[actualStart + i];
2722
- }
2723
- return s / sumw;
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
- // weighted linear regression on local points
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 = actualStart; i < end; i++) {
2732
- const idx = i - actualStart;
2733
- const xi = ptsX[i];
2734
- const yi = ptsY[i];
2735
- const wi = w[idx];
2736
- sw += wi;
2737
- sx += wi * xi;
2738
- sy += wi * yi;
2739
- sxx += wi * xi * xi;
2740
- sxy += wi * xi * yi;
2741
- }
2742
- const meanX = sx / sw;
2743
- const meanY = sy / sw;
2744
- const denom = sxx - sx * meanX;
2745
- const slope = Math.abs(denom) < 1e-12 ? 0 : (sxy - sx * meanY) / denom;
2746
- const intercept = meanY - slope * meanX;
2747
- return intercept + slope * x0;
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
- function predict(x0) {
2750
- if (Array.isArray(x0)) {
2751
- const len = x0.length;
2752
- const out = new Array(len);
2753
- for (let i = 0; i < len; i++) {
2754
- out[i] = predictSingle(x0[i]);
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 predictSingle(x0);
2947
+ return out;
2759
2948
  }
2760
- function evaluateGrid(N) {
2761
- if (N <= 0) {
2762
- return [];
2949
+ for (let iteration = 0; iteration <= iterations; iteration++) {
2950
+ fits = fitObservations();
2951
+ if (iteration === iterations) {
2952
+ break;
2763
2953
  }
2764
- if (n === 0) {
2765
- return [];
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
- const out = new Array(N);
2768
- const min = ptsX[0];
2769
- const max = ptsX[n - 1];
2770
- if (min === max) {
2771
- const v = predictSingle(min);
2772
- for (let i = 0; i < N; i++) {
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
- return out;
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
- for (let i = 0; i < N; i++) {
2804
- const px = i === N - 1 ? max : min + step * i;
2805
- out[i] = { x: px, y: predictSingle(px, robustWeights) };
2986
+ if (ptsX[hi] === ptsX[lo]) {
2987
+ return fits[lo];
2806
2988
  }
2807
- return out;
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
- let degree = (_a = options.degree) !== null && _a !== void 0 ? _a : 0;
2931
- if (degree < 0) {
2932
- degree = 0;
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 m = degree + 1;
2936
- const sums = new Array(2 * degree + 1).fill(0);
2937
- visitPoints(data, x, y, (dx, dy) => {
2938
- let xp = 1;
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
- visitPoints(data, x, y, dx => {
2993
- if (!isNil(dx)) {
2994
- if (dx < min) {
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
- if (min === Infinity || max === -Infinity) {
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 = [];
@@ -3496,6 +3577,55 @@
3496
3577
  return estimates;
3497
3578
  }
3498
3579
 
3580
+ /** 成对 percentile bootstrap;每次重采样仅拟合一次,固定原始坐标基底。 */
3581
+ function regressionBootstrap(data, options) {
3582
+ var _a;
3583
+ const degree = options.method === 'linear' ? 1 : (_a = options.degree) !== null && _a !== void 0 ? _a : NaN;
3584
+ if ((options.method !== 'linear' && options.method !== 'polynomial') ||
3585
+ !Number.isInteger(degree) ||
3586
+ degree < 1 ||
3587
+ !data.length ||
3588
+ data.some(p => !Number.isFinite(p.x) || !Number.isFinite(p.y)) ||
3589
+ options.grid.some(x => !Number.isFinite(x)) ||
3590
+ !(options.level > 0 && options.level < 1) ||
3591
+ !Number.isInteger(options.resamples) ||
3592
+ options.resamples < 1 ||
3593
+ !Number.isInteger(options.seed) ||
3594
+ options.seed < 0 ||
3595
+ options.seed > 0xffffffff) {
3596
+ throw new RangeError('Invalid paired regression bootstrap inputs');
3597
+ }
3598
+ if (data.length <= degree + 1) {
3599
+ throw new RangeError('Regression bootstrap requires positive residual degrees of freedom');
3600
+ }
3601
+ const sorted = data.slice().sort((a, b) => a.x - b.x || a.y - b.y);
3602
+ const basis = regressionBasis(sorted);
3603
+ if (fitRegression(sorted, degree, basis).rank < degree + 1) {
3604
+ throw new RangeError('Original regression design is rank deficient');
3605
+ }
3606
+ const random = randomLCG(options.seed);
3607
+ const sample = new Array(sorted.length);
3608
+ const values = options.grid.map(() => new Array(options.resamples));
3609
+ for (let b = 0; b < options.resamples; b++) {
3610
+ for (let i = 0; i < sample.length; i++) {
3611
+ sample[i] = sorted[Math.floor(random() * sorted.length)];
3612
+ }
3613
+ const model = fitRegression(sample, degree, basis);
3614
+ for (let j = 0; j < options.grid.length; j++) {
3615
+ const value = regressionPrediction(model, options.grid[j]);
3616
+ if (!Number.isFinite(value)) {
3617
+ throw new RangeError('Regression bootstrap prediction is not finite');
3618
+ }
3619
+ values[j][b] = value;
3620
+ }
3621
+ }
3622
+ const tail = (1 - options.level) / 2;
3623
+ return options.grid.map((x, j) => {
3624
+ values[j].sort((a, b) => a - b);
3625
+ return { x, lower: quantileSorted(values[j], tail), upper: quantileSorted(values[j], 1 - tail) };
3626
+ });
3627
+ }
3628
+
3499
3629
  class HashValue {
3500
3630
  }
3501
3631
  // 基于 array 和 object的 HashTable
@@ -8975,6 +9105,7 @@
8975
9105
  exports.randomLCG = randomLCG;
8976
9106
  exports.range = range;
8977
9107
  exports.rectInsideAnotherRect = rectInsideAnotherRect;
9108
+ exports.regressionBootstrap = regressionBootstrap;
8978
9109
  exports.regressionLinear = regressionLinear;
8979
9110
  exports.regressionLogistic = regressionLogistic;
8980
9111
  exports.regressionLowess = regressionLowess;