loess.js 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. // Adapted from science.js by Jason Davies
  2. // License: https://github.com/jasondavies/science.js/blob/master/LICENSE
  3. // Source: https://github.com/jasondavies/science.js/blob/master/src/stats/loess.js
  4. // Adapted from vega-statistics by Jeffrey Heer
  5. // License: https://github.com/vega/vega/blob/f058b099decad9db78301405dd0d2e9d8ba3d51a/LICENSE
  6. // Source: https://github.com/vega/vega/blob/f21cb8792b4e0cbe2b1a3fd44b0f5db370dbaadb/packages/vega-statistics/src/regression/loess.js
  7. import { median } from "./utils/median";
  8. import { ols } from "./utils/ols";
  9. import { points } from "./utils/points";
  10. const maxiters = 2, epsilon = 1e-12;
  11. export default function() {
  12. let x = d => d[0],
  13. y = d => d[1],
  14. bandwidth = .3;
  15. function loess(data) {
  16. const [xv, yv, ux, uy] = points(data, x, y, true),
  17. n = xv.length,
  18. bw = Math.max(2, ~~(bandwidth * n)), // # nearest neighbors
  19. yhat = new Float64Array(n),
  20. residuals = new Float64Array(n),
  21. robustWeights = new Float64Array(n).fill(1);
  22. for (let iter = -1; ++iter <= maxiters; ) {
  23. const interval = [0, bw - 1];
  24. for (let i = 0; i < n; ++i) {
  25. const dx = xv[i],
  26. i0 = interval[0],
  27. i1 = interval[1],
  28. edge = (dx - xv[i0]) > (xv[i1] - dx) ? i0 : i1;
  29. let W = 0, X = 0, Y = 0, XY = 0, X2 = 0,
  30. denom = 1 / Math.abs(xv[edge] - dx || 1); // Avoid singularity
  31. for (let k = i0; k <= i1; ++k) {
  32. const xk = xv[k],
  33. yk = yv[k],
  34. w = tricube(Math.abs(dx - xk) * denom) * robustWeights[k],
  35. xkw = xk * w;
  36. W += w;
  37. X += xkw;
  38. Y += yk * w;
  39. XY += yk * xkw;
  40. X2 += xk * xkw;
  41. }
  42. // Linear regression fit
  43. const [a, b] = ols(X / W, Y / W, XY / W, X2 / W);
  44. yhat[i] = a + b * dx;
  45. residuals[i] = Math.abs(yv[i] - yhat[i]);
  46. updateInterval(xv, i + 1, interval);
  47. }
  48. if (iter === maxiters) {
  49. break;
  50. }
  51. const medianResidual = median(residuals);
  52. if (Math.abs(medianResidual) < epsilon) break;
  53. for (let i = 0, arg, w; i < n; ++i){
  54. arg = residuals[i] / (6 * medianResidual);
  55. // Default to epsilon (rather than zero) for large deviations
  56. // Keeping weights tiny but non-zero prevents singularites
  57. robustWeights[i] = (arg >= 1) ? epsilon : ((w = 1 - arg * arg) * w);
  58. }
  59. }
  60. return output(xv, yhat, ux, uy);
  61. }
  62. loess.bandwidth = function(bw) {
  63. return arguments.length ? (bandwidth = bw, loess) : bandwidth;
  64. };
  65. loess.x = function(fn) {
  66. return arguments.length ? (x = fn, loess) : x;
  67. };
  68. loess.y = function(fn) {
  69. return arguments.length ? (y = fn, loess) : y;
  70. };
  71. return loess;
  72. }
  73. // Weighting kernel for local regression
  74. function tricube(x) {
  75. return (x = 1 - x * x * x) * x * x;
  76. }
  77. // Advance sliding window interval of nearest neighbors
  78. function updateInterval(xv, i, interval) {
  79. let val = xv[i],
  80. left = interval[0],
  81. right = interval[1] + 1;
  82. if (right >= xv.length) return;
  83. // Step right if distance to new right edge is <= distance to old left edge
  84. // Step when distance is equal to ensure movement over duplicate x values
  85. while (i > left && (xv[right] - val) <= (val - xv[left])) {
  86. interval[0] = ++left;
  87. interval[1] = right;
  88. ++right;
  89. }
  90. }
  91. // Generate smoothed output points
  92. // Average points with repeated x values
  93. function output(xv, yhat, ux, uy) {
  94. const n = xv.length, out = [];
  95. let i = 0, cnt = 0, prev = [], v;
  96. for (; i<n; ++i) {
  97. v = xv[i] + ux;
  98. if (prev[0] === v) {
  99. // Average output values via online update
  100. prev[1] += (yhat[i] - prev[1]) / (++cnt);
  101. } else {
  102. // Add new output point
  103. cnt = 0;
  104. prev[1] += uy;
  105. prev = [v, yhat[i]];
  106. out.push(prev);
  107. }
  108. }
  109. prev[1] += uy;
  110. return out;
  111. }