1#[allow(clippy::too_many_arguments)]
21fn lowest(
22 x: &[f64],
23 y: &[f64],
24 n: usize,
25 xs: f64,
26 nleft: usize,
27 nright: usize,
28 w: &mut [f64],
29 userw: bool,
30 rw: &[f64],
31) -> (f64, bool) {
32 let range = x[n - 1] - x[0];
33 let h = (xs - x[nleft - 1]).max(x[nright - 1] - xs);
34 let h9 = 0.999 * h;
35 let h1 = 0.001 * h;
36
37 let mut a = 0.0;
39 let mut j = nleft; while j <= n {
41 w[j - 1] = 0.0;
42 let r = (x[j - 1] - xs).abs();
43 if r <= h9 {
44 if r <= h1 {
45 w[j - 1] = 1.0;
46 } else {
47 let t = 1.0 - (r / h).powi(3);
48 w[j - 1] = t * t * t; }
50 if userw {
51 w[j - 1] *= rw[j - 1];
52 }
53 a += w[j - 1];
54 } else if x[j - 1] > xs {
55 break;
56 }
57 j += 1;
58 }
59 let nrt = j - 1; if a <= 0.0 {
61 return (0.0, false);
62 }
63
64 for ww in w.iter_mut().take(nrt).skip(nleft - 1) {
66 *ww /= a;
67 }
68 if h > 0.0 {
69 let mut center = 0.0;
71 for jj in nleft..=nrt {
72 center += w[jj - 1] * x[jj - 1];
73 }
74 let mut b = xs - center;
75 let mut c = 0.0;
76 for jj in nleft..=nrt {
77 let d = x[jj - 1] - center;
78 c += w[jj - 1] * d * d;
79 }
80 if c.sqrt() > 0.001 * range {
81 b /= c;
83 for jj in nleft..=nrt {
84 w[jj - 1] *= b * (x[jj - 1] - center) + 1.0;
85 }
86 }
87 }
88 let mut ys = 0.0;
89 for jj in nleft..=nrt {
90 ys += w[jj - 1] * y[jj - 1];
91 }
92 (ys, true)
93}
94
95fn clowess(x: &[f64], y: &[f64], f: f64, nsteps: usize, delta: f64) -> Vec<f64> {
100 let n = x.len();
101 let mut ys = vec![0.0_f64; n];
102 if n < 2 {
103 if n == 1 {
104 ys[0] = y[0];
105 }
106 return ys;
107 }
108
109 let ns = (((f * n as f64) + 1e-7) as usize).clamp(2, n);
111
112 let mut rw = vec![0.0_f64; n]; let mut res = vec![0.0_f64; n]; let mut w = vec![0.0_f64; n]; let mut iter = 1usize;
117 while iter <= nsteps + 1 {
118 let mut nleft = 1usize; let mut nright = ns;
120 let mut last = 0usize; let mut i = 1usize; loop {
124 if nright < n {
125 let d1 = x[i - 1] - x[nleft - 1];
127 let d2 = x[nright] - x[i - 1]; if d1 > d2 {
129 nleft += 1;
130 nright += 1;
131 continue;
132 }
133 }
134
135 let (yi, ok) = lowest(x, y, n, x[i - 1], nleft, nright, &mut w, iter > 1, &rw);
136 ys[i - 1] = if ok { yi } else { y[i - 1] };
137
138 if last < i - 1 {
140 let denom = x[i - 1] - x[last - 1];
141 for j in (last + 1)..i {
142 let alpha = (x[j - 1] - x[last - 1]) / denom;
143 ys[j - 1] = alpha * ys[i - 1] + (1.0 - alpha) * ys[last - 1];
144 }
145 }
146 last = i;
147
148 let cut = x[last - 1] + delta;
151 i = last + 1;
152 while i <= n {
153 if x[i - 1] > cut {
154 break;
155 }
156 if x[i - 1] == x[last - 1] {
157 ys[i - 1] = ys[last - 1];
158 last = i;
159 }
160 i += 1;
161 }
162 i = (last + 1).max(i - 1);
163 if last >= n {
164 break;
165 }
166 }
167
168 for k in 0..n {
169 res[k] = y[k] - ys[k];
170 }
171 let sc: f64 = res.iter().map(|v| v.abs()).sum::<f64>() / n as f64;
172
173 if iter > nsteps {
175 break;
176 }
177 for k in 0..n {
178 rw[k] = res[k].abs();
179 }
180 let m1 = n / 2;
188 let cmad = {
189 rw.select_nth_unstable_by(m1, |a, b| a.partial_cmp(b).unwrap());
190 if n % 2 == 0 {
191 let lo = rw[..m1].iter().copied().fold(f64::NEG_INFINITY, f64::max);
192 3.0 * (rw[m1] + lo)
193 } else {
194 6.0 * rw[m1]
195 }
196 };
197 if cmad < 1e-7 * sc {
198 break; }
200 let c9 = 0.999 * cmad;
201 let c1 = 0.001 * cmad;
202 for k in 0..n {
203 let r = res[k].abs();
204 rw[k] = if r <= c1 {
205 1.0
206 } else if r <= c9 {
207 let t = 1.0 - (r / cmad).powi(2);
208 t * t } else {
210 0.0
211 };
212 }
213 iter += 1;
214 }
215 ys
216}
217
218pub(crate) fn loess_fit_unweighted(
224 y: &[f64],
225 x: &[f64],
226 span: f64,
227 iterations: usize,
228) -> (Vec<f64>, Vec<f64>) {
229 let n = y.len();
230 let mut fitted = vec![f64::NAN; n];
231 let mut residuals = vec![f64::NAN; n];
232
233 let obs: Vec<usize> = (0..n)
234 .filter(|&k| y[k].is_finite() && x[k].is_finite())
235 .collect();
236 let nobs = obs.len();
237 if nobs == 0 {
238 return (fitted, residuals);
239 }
240 if span < 1.0 / nobs as f64 {
241 for &k in &obs {
242 fitted[k] = y[k];
243 residuals[k] = 0.0;
244 }
245 return (fitted, residuals);
246 }
247
248 let mut order = obs.clone();
251 order.sort_by(|&a, &b| x[a].partial_cmp(&x[b]).unwrap());
252 let xs: Vec<f64> = order.iter().map(|&k| x[k]).collect();
253 let ys: Vec<f64> = order.iter().map(|&k| y[k]).collect();
254 let delta = 0.01 * (xs[nobs - 1] - xs[0]);
255
256 let fit = clowess(&xs, &ys, span, iterations.saturating_sub(1), delta);
257 for (j, &k) in order.iter().enumerate() {
258 fitted[k] = fit[j];
259 residuals[k] = y[k] - fit[j];
260 }
261 (fitted, residuals)
262}
263
264fn find_seeds(x: &[f64], delta: f64) -> Vec<usize> {
268 let npts = x.len();
269 let mut indices = vec![0usize];
270 let mut last_pt = 0usize;
271 for pt in 1..npts.saturating_sub(1) {
272 if x[pt] - x[last_pt] > delta {
273 indices.push(pt);
274 last_pt = pt;
275 }
276 }
277 indices.push(npts - 1);
278 indices
279}
280
281fn find_limits(
287 indices: &[usize],
288 x: &[f64],
289 w: &[f64],
290 spanweight: f64,
291) -> (Vec<usize>, Vec<usize>, Vec<f64>) {
292 let npts = x.len();
293 let num = indices.len();
294 let mut start = vec![0usize; num];
295 let mut end = vec![0usize; num];
296 let mut dist = vec![0.0f64; num];
297
298 for (curx, &curpt) in indices.iter().enumerate() {
299 let mut left = curpt;
300 let mut right = curpt;
301 let mut curw = w[curpt];
302 let mut at_end = curpt == npts - 1;
303 let mut at_start = curpt == 0;
304 let mut mdist = 0.0f64;
305
306 while curw < spanweight && (!at_end || !at_start) {
307 if at_end {
308 left -= 1;
309 curw += w[left];
310 if left == 0 {
311 at_start = true;
312 }
313 let ldist = x[curpt] - x[left];
314 if mdist < ldist {
315 mdist = ldist;
316 }
317 } else if at_start {
318 right += 1;
319 curw += w[right];
320 if right == npts - 1 {
321 at_end = true;
322 }
323 let rdist = x[right] - x[curpt];
324 if mdist < rdist {
325 mdist = rdist;
326 }
327 } else {
328 let ldist = x[curpt] - x[left - 1];
329 let rdist = x[right + 1] - x[curpt];
330 if ldist < rdist {
331 left -= 1;
332 curw += w[left];
333 if left == 0 {
334 at_start = true;
335 }
336 if mdist < ldist {
337 mdist = ldist;
338 }
339 } else {
340 right += 1;
341 curw += w[right];
342 if right == npts - 1 {
343 at_end = true;
344 }
345 if mdist < rdist {
346 mdist = rdist;
347 }
348 }
349 }
350 }
351
352 while left > 0 && x[left] == x[left - 1] {
354 left -= 1;
355 }
356 while right < npts - 1 && x[right] == x[right + 1] {
357 right += 1;
358 }
359
360 start[curx] = left;
361 end[curx] = right;
362 dist[curx] = mdist;
363 }
364 (start, end, dist)
365}
366
367#[allow(clippy::too_many_arguments)]
373fn weighted_local_fit(
374 x: &[f64],
375 y: &[f64],
376 w: &[f64],
377 rw: &[f64],
378 curpt: usize,
379 left: usize,
380 right: usize,
381 dist: f64,
382 work: &mut [f64],
383) -> f64 {
384 if dist < 1e-7 {
385 let mut ymean = 0.0;
386 let mut allweight = 0.0;
387 for pt in left..=right {
388 work[pt] = w[pt] * rw[pt];
389 ymean += y[pt] * work[pt];
390 allweight += work[pt];
391 }
392 return ymean / allweight;
393 }
394 let mut xmean = 0.0;
395 let mut ymean = 0.0;
396 let mut allweight = 0.0;
397 for pt in left..=right {
398 let tri = (1.0 - ((x[curpt] - x[pt]).abs() / dist).powi(3)).powi(3);
399 work[pt] = tri * w[pt] * rw[pt];
400 xmean += work[pt] * x[pt];
401 ymean += work[pt] * y[pt];
402 allweight += work[pt];
403 }
404 xmean /= allweight;
405 ymean /= allweight;
406
407 let mut var = 0.0;
408 let mut covar = 0.0;
409 for pt in left..=right {
410 let temp = x[pt] - xmean;
411 var += temp * temp * work[pt];
412 covar += temp * (y[pt] - ymean) * work[pt];
413 }
414 if var < 1e-7 {
415 return ymean;
416 }
417 let slope = covar / var;
418 let intercept = ymean - slope * xmean;
419 slope * x[curpt] + intercept
420}
421
422fn weighted_lowess_core(
428 x: &[f64],
429 y: &[f64],
430 w: &[f64],
431 span: f64,
432 iterations: usize,
433 delta: f64,
434) -> (Vec<f64>, Vec<f64>) {
435 let n = x.len();
436 let totalweight: f64 = w.iter().sum();
437 let spanweight = totalweight * span;
438 let subrange = (x[n - 1] - x[0]) / n as f64;
439
440 let seeds = find_seeds(x, delta);
441 let (frame_start, frame_end, max_dist) = find_limits(&seeds, x, w, spanweight);
442
443 let mut fit = vec![0.0f64; n];
444 let mut rob = vec![1.0f64; n];
445 let mut work = vec![0.0f64; n];
446 let mut absres = vec![0.0f64; n];
448 let mut ror = vec![0usize; n];
449 let mut sorted = vec![0.0f64; n];
450
451 for _ in 0..iterations.max(1) {
452 fit[0] = weighted_local_fit(
454 x,
455 y,
456 w,
457 &rob,
458 0,
459 frame_start[0],
460 frame_end[0],
461 max_dist[0],
462 &mut work,
463 );
464 let mut last_pt = 0usize;
465 for cur_seed in 1..seeds.len() {
466 let pt = seeds[cur_seed];
467 fit[pt] = weighted_local_fit(
468 x,
469 y,
470 w,
471 &rob,
472 pt,
473 frame_start[cur_seed],
474 frame_end[cur_seed],
475 max_dist[cur_seed],
476 &mut work,
477 );
478 if pt - last_pt > 1 {
479 let current = x[pt] - x[last_pt];
480 if current > 1e-7 * subrange {
481 let slope = (fit[pt] - fit[last_pt]) / current;
482 let intercept = fit[pt] - slope * x[pt];
483 for subpt in (last_pt + 1)..pt {
484 fit[subpt] = slope * x[subpt] + intercept;
485 }
486 } else {
487 let endave = 0.5 * (fit[pt] + fit[last_pt]);
488 for f in fit.iter_mut().take(pt).skip(last_pt + 1) {
489 *f = endave;
490 }
491 }
492 }
493 last_pt = pt;
494 }
495
496 let mut resid_scale = 0.0;
498 for pt in 0..n {
499 absres[pt] = (y[pt] - fit[pt]).abs();
500 resid_scale += absres[pt];
501 }
502 resid_scale /= n as f64;
503
504 for (i, r) in ror.iter_mut().enumerate() {
505 *r = i;
506 }
507 ror.sort_by(|&a, &b| absres[a].partial_cmp(&absres[b]).unwrap());
508 for pt in 0..n {
509 sorted[pt] = absres[ror[pt]];
510 }
511
512 let halfweight = totalweight / 2.0;
513 let mut current = 0.0;
514 let mut cmad = 0.0;
515 for pt in 0..n {
516 current += w[ror[pt]];
517 if current == halfweight {
518 let next = if pt + 1 < n {
519 sorted[pt + 1]
520 } else {
521 sorted[pt]
522 };
523 cmad = 3.0 * (sorted[pt] + next);
524 break;
525 } else if current > halfweight {
526 cmad = 6.0 * sorted[pt];
527 break;
528 }
529 }
530
531 if cmad <= 1e-7 * resid_scale {
532 break;
533 }
534
535 for pt in 0..n {
536 rob[ror[pt]] = if sorted[pt] < cmad {
537 let t = 1.0 - (sorted[pt] / cmad).powi(2);
538 t * t
539 } else {
540 0.0
541 };
542 }
543 }
544 (fit, rob)
545}
546
547pub struct WeightedLowess {
550 pub fitted: Vec<f64>,
551 pub residuals: Vec<f64>,
552 pub weights: Vec<f64>,
555 pub delta: f64,
556}
557
558pub fn weighted_lowess(
566 x: &[f64],
567 y: &[f64],
568 weights: Option<&[f64]>,
569 span: f64,
570 iterations: usize,
571 npts: usize,
572 delta: Option<f64>,
573) -> WeightedLowess {
574 let n = x.len();
575 assert_eq!(y.len(), n, "x and y should have same length");
576 let w_in: Vec<f64> = match weights {
577 Some(wv) => {
578 assert_eq!(wv.len(), n, "weights should have same length as x and y");
579 wv.to_vec()
580 }
581 None => vec![1.0; n],
582 };
583
584 let mut o: Vec<usize> = (0..n).collect();
586 o.sort_by(|&a, &b| x[a].partial_cmp(&x[b]).unwrap());
587 let xs: Vec<f64> = o.iter().map(|&k| x[k]).collect();
588 let ys: Vec<f64> = o.iter().map(|&k| y[k]).collect();
589 let ws: Vec<f64> = o.iter().map(|&k| w_in[k]).collect();
590
591 let delta = match delta {
593 Some(d) => d,
594 None if npts >= n => 0.0,
595 None => {
596 let mut dx: Vec<f64> = (0..n - 1).map(|i| xs[i + 1] - xs[i]).collect();
597 dx.sort_by(|a, b| a.partial_cmp(b).unwrap());
598 let mut cum = 0.0;
599 let cumrange: Vec<f64> = dx
600 .iter()
601 .map(|&d| {
602 cum += d;
603 cum
604 })
605 .collect();
606 let mut best = f64::INFINITY;
607 for k in 0..npts {
608 let cand = cumrange[n - 2 - k] / (npts - k) as f64;
609 if cand < best {
610 best = cand;
611 }
612 }
613 best
614 }
615 };
616
617 let (fit_sorted, rob_sorted) = weighted_lowess_core(&xs, &ys, &ws, span, iterations, delta);
618
619 let mut fitted = vec![0.0f64; n];
621 let mut robweights = vec![0.0f64; n];
622 for (j, &k) in o.iter().enumerate() {
623 fitted[k] = fit_sorted[j];
624 robweights[k] = rob_sorted[j];
625 }
626 let residuals: Vec<f64> = (0..n).map(|i| y[i] - fitted[i]).collect();
627
628 WeightedLowess {
629 fitted,
630 residuals,
631 weights: robweights,
632 delta,
633 }
634}
635
636pub struct LoessFit {
639 pub fitted: Vec<f64>,
640 pub residuals: Vec<f64>,
641}
642
643#[allow(clippy::too_many_arguments)]
654pub fn loess_fit(
655 y: &[f64],
656 x: &[f64],
657 weights: Option<&[f64]>,
658 span: f64,
659 iterations: usize,
660 min_weight: f64,
661 max_weight: f64,
662 equal_weights_as_null: bool,
663) -> LoessFit {
664 let n = y.len();
665 assert_eq!(x.len(), n, "y and x have different lengths");
666 let mut fitted = vec![f64::NAN; n];
667 let mut residuals = vec![f64::NAN; n];
668
669 let obs: Vec<usize> = (0..n)
670 .filter(|&k| y[k].is_finite() && x[k].is_finite())
671 .collect();
672 let nobs = obs.len();
673 if nobs == 0 {
674 return LoessFit { fitted, residuals };
675 }
676
677 if span < 1.0 / nobs as f64 {
679 for &k in &obs {
680 fitted[k] = y[k];
681 residuals[k] = 0.0;
682 }
683 return LoessFit { fitted, residuals };
684 }
685
686 let min_weight = min_weight.max(0.0);
687 let xobs: Vec<f64> = obs.iter().map(|&k| x[k]).collect();
688 let yobs: Vec<f64> = obs.iter().map(|&k| y[k]).collect();
689
690 let wobs: Option<Vec<f64>> = match weights {
694 None => None,
695 Some(wv) => {
696 assert_eq!(wv.len(), n, "y and weights have different lengths");
697 let wo: Vec<f64> = obs
698 .iter()
699 .map(|&k| {
700 let w = if wv[k].is_nan() { 0.0 } else { wv[k] };
701 w.max(min_weight).min(max_weight)
702 })
703 .collect();
704 if equal_weights_as_null {
705 let lo = wo.iter().copied().fold(f64::INFINITY, f64::min);
706 let hi = wo.iter().copied().fold(f64::NEG_INFINITY, f64::max);
707 if hi - lo < 1e-15 {
708 None
709 } else {
710 Some(wo)
711 }
712 } else {
713 Some(wo)
714 }
715 }
716 };
717
718 let Some(wobs) = wobs else {
719 let (f, _) = loess_fit_unweighted(&yobs, &xobs, span, iterations);
720 for (j, &k) in obs.iter().enumerate() {
721 fitted[k] = f[j];
722 residuals[k] = y[k] - f[j];
723 }
724 return LoessFit { fitted, residuals };
725 };
726
727 let nwobs = if min_weight > 0.0 {
729 nobs
730 } else {
731 wobs.iter().filter(|&&w| w > 0.0).count()
732 };
733
734 if (nwobs as f64) < 4.0 + 1.0 / span {
737 if nwobs == 1 {
738 let kpos = wobs.iter().position(|&w| w > 0.0).unwrap();
739 let val = yobs[kpos];
740 for (j, &k) in obs.iter().enumerate() {
741 fitted[k] = val;
742 residuals[k] = yobs[j] - val;
743 }
744 } else {
745 let (slope, intercept) = weighted_line_fit(&xobs, &yobs, &wobs);
746 for (j, &k) in obs.iter().enumerate() {
747 let f = intercept + slope * xobs[j];
748 fitted[k] = f;
749 residuals[k] = yobs[j] - f;
750 }
751 }
752 return LoessFit { fitted, residuals };
753 }
754
755 let wl = weighted_lowess(&xobs, &yobs, Some(&wobs), span, iterations, 200, None);
756 for (j, &k) in obs.iter().enumerate() {
757 fitted[k] = wl.fitted[j];
758 residuals[k] = y[k] - wl.fitted[j];
759 }
760 LoessFit { fitted, residuals }
761}
762
763fn weighted_line_fit(x: &[f64], y: &[f64], w: &[f64]) -> (f64, f64) {
767 let mut sw = 0.0;
768 let mut swx = 0.0;
769 let mut swy = 0.0;
770 let mut swxx = 0.0;
771 let mut swxy = 0.0;
772 for ((&xi, &yi), &wi) in x.iter().zip(y.iter()).zip(w.iter()) {
773 sw += wi;
774 swx += wi * xi;
775 swy += wi * yi;
776 swxx += wi * xi * xi;
777 swxy += wi * xi * yi;
778 }
779 let denom = sw * swxx - swx * swx;
780 let slope = (sw * swxy - swx * swy) / denom;
781 let intercept = (swy - slope * swx) / sw;
782 (slope, intercept)
783}
784
785fn lowess_xy(x: &[f64], y: &[f64], f: f64, iter: usize) -> (Vec<f64>, Vec<f64>) {
790 let n = x.len();
791 if n == 0 {
792 return (Vec::new(), Vec::new());
793 }
794 let mut order: Vec<usize> = (0..n).collect();
795 order.sort_by(|&a, &b| x[a].partial_cmp(&x[b]).unwrap());
796 let xs: Vec<f64> = order.iter().map(|&k| x[k]).collect();
797 let ys: Vec<f64> = order.iter().map(|&k| y[k]).collect();
798 let delta = 0.01 * (xs[n - 1] - xs[0]);
799 let fit = clowess(&xs, &ys, f, iter, delta);
800 (xs, fit)
801}
802
803fn collapse_ties(xs: &[f64], ys: &[f64]) -> (Vec<f64>, Vec<f64>) {
806 let mut gx = Vec::new();
807 let mut gy = Vec::new();
808 let mut i = 0usize;
809 while i < xs.len() {
810 let mut j = i;
811 let mut sum = 0.0;
812 while j < xs.len() && xs[j] == xs[i] {
813 sum += ys[j];
814 j += 1;
815 }
816 gx.push(xs[i]);
817 gy.push(sum / (j - i) as f64);
818 i = j;
819 }
820 (gx, gy)
821}
822
823pub(crate) fn approx_rule2(gx: &[f64], gy: &[f64], xout: f64) -> f64 {
826 let n = gx.len();
827 if n == 1 {
828 return gy[0];
829 }
830 if xout <= gx[0] {
831 return gy[0];
832 }
833 if xout >= gx[n - 1] {
834 return gy[n - 1];
835 }
836 let i = gx.partition_point(|&v| v <= xout); let (x0, x1) = (gx[i - 1], gx[i]);
838 let (y0, y1) = (gy[i - 1], gy[i]);
839 y0 + (y1 - y0) * (xout - x0) / (x1 - x0)
840}
841
842pub(crate) struct LowessInterpolator {
846 gx: Vec<f64>,
847 gy: Vec<f64>,
848}
849
850impl LowessInterpolator {
851 pub(crate) fn fit(x: &[f64], y: &[f64], span: f64, iter: usize) -> Self {
852 let (xs, ys) = lowess_xy(x, y, span, iter);
853 let (gx, gy) = collapse_ties(&xs, &ys);
854 Self { gx, gy }
855 }
856
857 pub(crate) fn eval(&self, xout: f64) -> f64 {
858 approx_rule2(&self.gx, &self.gy, xout)
859 }
860}
861
862#[cfg(test)]
863mod tests {
864 use super::*;
865
866 fn rel(a: f64, b: f64) -> f64 {
867 ((a - b) / b).abs()
868 }
869
870 #[test]
875 fn lowess_matches_r_reference() {
876 let x: Vec<f64> = (1..=20).map(|i| i as f64).collect();
877 let y: Vec<f64> = (1..=20)
878 .map(|i| {
879 let xi = i as f64;
880 (xi / 3.0).sin() + ((i % 4) as f64) / 10.0
881 })
882 .collect();
883 let (fitted, residuals) = loess_fit_unweighted(&y, &x, 0.4, 4);
885
886 let r_fitted = [
889 0.596_248_207_878_296,
890 0.733_833_572_576_796,
891 0.865_358_555_762_325,
892 0.994_091_762_946_332,
893 1.012_360_782_819_75,
894 0.898_791_455_642_421,
895 0.731_283_317_640_518,
896 0.529_277_971_327_881,
897 0.269_444_204_960_129,
898 -0.012_657_116_933_792_8,
899 -0.266_008_842_117_912,
900 -0.457_558_931_089_973,
901 -0.613_194_376_883_428,
902 -0.715_648_998_499_32,
903 -0.676_645_471_029_55,
904 -0.521_458_054_754_246,
905 -0.319_116_552_594_766,
906 -0.077_757_590_217_849,
907 0.171_815_169_826_499,
908 0.431_111_024_786_567,
909 ];
910 for (a, b) in fitted.iter().zip(r_fitted.iter()) {
911 assert!(rel(*a, *b) < 1e-9, "fitted {a} vs {b}");
912 }
913 for k in 0..x.len() {
915 assert!((residuals[k] - (y[k] - fitted[k])).abs() < 1e-15);
916 }
917 }
918
919 #[test]
921 fn lowess_tiny_span_is_identity() {
922 let x: Vec<f64> = (1..=10).map(|i| i as f64).collect();
923 let y: Vec<f64> = (1..=10).map(|i| (i * i) as f64).collect();
924 let (fitted, residuals) = loess_fit_unweighted(&y, &x, 0.05, 4);
925 for k in 0..x.len() {
926 assert_eq!(fitted[k], y[k]);
927 assert_eq!(residuals[k], 0.0);
928 }
929 }
930
931 fn close(a: f64, b: f64) {
934 if b == 0.0 {
935 assert!(a.abs() < 1e-12, "{a} vs 0");
936 } else {
937 assert!(((a - b) / b).abs() < 1e-9, "{a} vs {b}");
938 }
939 }
940
941 fn case_ab_data() -> (Vec<f64>, Vec<f64>, Vec<f64>) {
943 let x = vec![
944 0.45749338542921825,
945 0.36490716953341845,
946 2.1216858507761938,
947 2.231_517_906_338_681,
948 2.4560694953036157,
949 3.003_471_605_493_78,
950 3.2937879157555763,
951 3.619468372607308,
952 4.1084805104211775,
953 4.5866320499433755,
954 5.444_886_515_827_218,
955 5.775_470_171_559_732,
956 5.940_419_082_916_579,
957 6.149_600_128_386_905,
958 6.600_257_345_624_926,
959 6.604648996606679,
960 7.030_477_401_104_289,
961 7.093_654_185_978_542,
962 7.141587042811965,
963 7.333_683_544_946_952,
964 8.254_754_236_391_259,
965 8.694_301_976_611_142,
966 9.375_836_948_985_027,
967 9.532_155_904_709_773,
968 ];
969 let y = vec![
970 0.612_545_426_481_486_9,
971 0.650_401_760_370_632,
972 1.1542440348430094,
973 1.5486987991164907,
974 4.466_390_679_222_614,
975 1.1903665393098684,
976 0.37266251854917354,
977 0.534_045_647_135_347_6,
978 0.18834499131155025,
979 0.464_472_256_763_577_8,
980 1.0037463888693132,
981 1.1052425234922978,
982 1.3880139159873457,
983 1.4149161956436145,
984 2.061_812_038_829_897,
985 2.2424560756224436,
986 2.9043783394714633,
987 0.073_326_112_417_279_27,
988 3.0490864839234257,
989 2.8575387507063152,
990 3.430_262_298_625_616,
991 3.501_476_667_740_035,
992 3.0964170542301663,
993 2.666902407201408,
994 ];
995 let w = vec![
996 0.915_514_786_448_329_7,
997 0.45188319422304635,
998 0.34237423320300875,
999 1.1903987498488278,
1000 0.669_161_357_125_267_5,
1001 1.6571537321433425,
1002 1.1805854137521237,
1003 1.054087870148942,
1004 1.3946744401939213,
1005 0.36591726122424006,
1006 1.3710096625145525,
1007 0.863_262_355_374_172_3,
1008 0.642_421_815_311_536_2,
1009 0.737_486_680_131_405_7,
1010 1.2065324763767422,
1011 1.064640044188127,
1012 1.0586248501669615,
1013 1.8839623192790895,
1014 1.046_319_722_617_045,
1015 1.4207539540715515,
1016 1.8820373674388975,
1017 0.693_199_012_149_125_4,
1018 1.9052787743508817,
1019 0.763_943_711_994_215_8,
1020 ];
1021 (x, y, w)
1022 }
1023
1024 #[test]
1027 fn weighted_lowess_case_a() {
1028 let (x, y, w) = case_ab_data();
1029 let out = weighted_lowess(&x, &y, Some(&w), 0.3, 4, 200, None);
1030 assert_eq!(out.delta, 0.0);
1031
1032 let r_fitted = [
1033 0.704_865_579_518_969_9,
1034 0.675_018_400_307_334_6,
1035 1.3600628188780086,
1036 1.4805324495189862,
1037 1.3743558087666423,
1038 1.0285760664659196,
1039 0.852_100_138_874_763_8,
1040 0.617_496_271_481_045_2,
1041 0.513_999_292_241_682_6,
1042 0.610_689_342_452_828_6,
1043 0.988_412_214_777_719_5,
1044 1.2375884034126585,
1045 1.3951903052897316,
1046 1.5905658893023542,
1047 2.152258891028989,
1048 2.1589538558884804,
1049 2.6882075718940692,
1050 2.754_372_887_365_016,
1051 2.804_782_373_798_898,
1052 2.972630547835498,
1053 3.396_904_942_104_693,
1054 3.2853367132950937,
1055 3.0552758361559125,
1056 2.996_975_009_544_398,
1057 ];
1058 let r_rweights = [
1059 0.964_573_093_916_010_6,
1060 0.9974600786593063,
1061 0.830_223_645_617_545_5,
1062 0.980_606_432_031_510_2,
1063 0.0,
1064 0.8932277961273466,
1065 0.26829417855553755,
1066 0.971_005_534_450_722_9,
1067 0.604_671_627_514_471_7,
1068 0.912_342_577_459_221_6,
1069 0.9990140555563487,
1070 0.927_887_678_710_836_4,
1071 0.999_784_013_165_360_9,
1072 0.874_786_293_731_308_1,
1073 0.965_983_909_872_752_1,
1074 0.970_969_934_423_463_4,
1075 0.813_613_521_418_203_6,
1076 0.0,
1077 0.765_342_631_054_660_8,
1078 0.945_216_049_382_716,
1079 0.995_338_614_565_773_8,
1080 0.813_663_916_297_942_5,
1081 0.992_913_666_264_513_6,
1082 0.595_259_735_008_007_2,
1083 ];
1084 for (a, b) in out.fitted.iter().zip(r_fitted.iter()) {
1085 close(*a, *b);
1086 }
1087 for (a, b) in out.weights.iter().zip(r_rweights.iter()) {
1088 close(*a, *b);
1089 }
1090 for ((r, yy), f) in out.residuals.iter().zip(y.iter()).zip(out.fitted.iter()) {
1091 assert!((r - (yy - f)).abs() < 1e-15);
1092 }
1093 }
1094
1095 #[test]
1098 fn weighted_lowess_case_b() {
1099 let (x, y, w) = case_ab_data();
1100 let out = weighted_lowess(&x, &y, Some(&w), 0.5, 1, 200, None);
1101 assert_eq!(out.delta, 0.0);
1102
1103 let r_fitted = [
1104 1.3164091907640776,
1105 1.3175754346917248,
1106 1.2097472947654593,
1107 1.2023153256037815,
1108 1.1902171010985594,
1109 1.1769344746702706,
1110 1.1383757235477594,
1111 1.004_305_077_879_562,
1112 0.796_818_060_092_517,
1113 0.774_791_789_454_579_5,
1114 1.031_357_331_214_907,
1115 1.2516144937670477,
1116 1.358_007_678_833_491,
1117 1.4738918429020829,
1118 1.7425837851859414,
1119 1.7453266670230196,
1120 2.0349113652577646,
1121 2.099_264_238_862_616,
1122 2.148618243528349,
1123 2.340_243_909_841_483,
1124 3.010_161_361_558_707,
1125 3.056683040538843,
1126 3.158_235_211_076_587,
1127 3.1633262263048074,
1128 ];
1129 let r_rweights = [
1130 0.879_412_603_471_390_4,
1131 0.8913033071647305,
1132 0.999_226_241_529_259_7,
1133 0.9700854954413779,
1134 0.0,
1135 0.999_954_675_492_639_4,
1136 0.858_129_979_744_656_7,
1137 0.945_216_049_382_716,
1138 0.909_151_582_734_925_9,
1139 0.975_954_372_722_726_4,
1140 0.999_808_488_820_543_1,
1141 0.994_624_937_295_328_6,
1142 0.999_773_821_675_273_3,
1143 0.999_126_419_320_665,
1144 0.974_562_958_604_831_9,
1145 0.9388779867911321,
1146 0.819_102_172_057_449_2,
1147 0.2346871608581525,
1148 0.806_674_164_798_635_2,
1149 0.933_905_106_727_050_2,
1150 0.956_155_094_856_453_8,
1151 0.950_916_050_426_533_5,
1152 0.999_040_200_929_231_7,
1153 0.939_048_642_187_021_6,
1154 ];
1155 for (a, b) in out.fitted.iter().zip(r_fitted.iter()) {
1156 close(*a, *b);
1157 }
1158 for (a, b) in out.weights.iter().zip(r_rweights.iter()) {
1159 close(*a, *b);
1160 }
1161 }
1162
1163 #[test]
1166 fn weighted_lowess_case_c() {
1167 let x = [
1168 0.531_976_991_333_067_4,
1169 0.683_757_108_636_200_4,
1170 0.789_968_837_052_583_7,
1171 0.801_603_901_199_996_5,
1172 1.5255942661315203,
1173 1.581659372895956,
1174 2.577_251_400_798_559,
1175 2.7313429210335016,
1176 3.1198115088045597,
1177 3.2957231905311346,
1178 3.5041399160400033,
1179 3.535_446_380_265_057,
1180 3.562_620_780_430_734,
1181 3.573_531_210_422_516,
1182 3.904986004345119,
1183 3.969_677_845_016_122,
1184 4.026_793_888_770_044,
1185 4.0656226221472025,
1186 4.074_543_830_938_637,
1187 4.320_158_297_196_031,
1188 4.373_944_881_372_154,
1189 4.3781809182837605,
1190 4.381_559_635_512_531,
1191 4.541_048_249_229_789,
1192 4.595673899166286,
1193 5.085_642_351_768_911,
1194 5.320_935_468_189_418,
1195 5.745_922_313_071_787,
1196 5.820684814825654,
1197 6.121_302_433_311_939,
1198 6.595_797_222_107_649,
1199 7.196_558_876_894_414,
1200 7.365_691_293_962_3,
1201 7.962_349_629_960_954,
1202 8.258_966_878_056_526,
1203 8.290_081_201_121_211,
1204 8.838_997_278_362_513,
1205 8.898_926_260_881_126,
1206 9.147_843_788_377_94,
1207 9.250_376_787_967_98,
1208 9.582185884937644,
1209 10.625_021_490_268_41,
1210 10.671597444452345,
1211 11.63403379265219,
1212 11.695629926398396,
1213 11.993930744938552,
1214 12.457542419433594,
1215 12.604068955406547,
1216 12.657269365154207,
1217 12.804370601661503,
1218 12.825214397162199,
1219 12.832566713914275,
1220 12.845537355169654,
1221 13.152610226534307,
1222 13.243088563904166,
1223 13.403024673461914,
1224 13.606816763058305,
1225 13.800439438782632,
1226 13.916632984764874,
1227 14.362269355915487,
1228 14.631125540472567,
1229 15.467019253410399,
1230 15.646071173250675,
1231 16.216_562_888_585_03,
1232 16.22252113185823,
1233 16.337_592_676_281_93,
1234 16.472833515144885,
1235 16.546852341853082,
1236 16.616179938428104,
1237 16.9334597280249,
1238 17.528_903_256_170_45,
1239 17.619_530_349_038_54,
1240 17.659138469025493,
1241 17.849509133957326,
1242 17.9549042833969,
1243 18.515_662_457_793_95,
1244 19.050154094584286,
1245 19.228_869_844_228_03,
1246 19.516776781529188,
1247 19.939716095104814,
1248 ];
1249 let y = [
1250 1.293428527408967,
1251 1.1000429951508104,
1252 0.985_676_209_295_923_4,
1253 1.078416645203697,
1254 1.0384497202430822,
1255 0.911_168_961_849_530_4,
1256 0.814_110_599_798_517_6,
1257 0.30870061707136665,
1258 0.360_630_501_091_990_5,
1259 2.653220107028643,
1260 0.035616391685330595,
1261 0.3396670541930909,
1262 0.1502442458597838,
1263 0.47763647905409656,
1264 0.094_209_347_547_079_92,
1265 0.10090502868850283,
1266 -0.012099541381990271,
1267 -0.018_456_884_031_812_94,
1268 0.027497693822005634,
1269 -0.139_844_621_530_572_6,
1270 0.012543821944120875,
1271 -0.003_995_997_315_695_388,
1272 -0.130_293_614_459_443_5,
1273 -0.46252856466785347,
1274 -0.19381646487771756,
1275 -0.35165581858660344,
1276 -0.320_150_797_023_739_7,
1277 -0.5361856364472154,
1278 -0.45587942674406273,
1279 -0.24861363699527775,
1280 -0.292_622_685_302_052_5,
1281 -0.28060451696229294,
1282 -0.11620781461766616,
1283 0.11487078623846304,
1284 0.26052335888772676,
1285 0.20269409789109122,
1286 0.431_968_733_064_165_2,
1287 0.276_224_130_937_391_6,
1288 0.47591651163197063,
1289 0.823_716_212_214_928_2,
1290 1.0238334201209505,
1291 1.7712082466756467,
1292 1.6076809524574995,
1293 2.032_190_109_082_845,
1294 1.7614737823535958,
1295 2.3284019188313416,
1296 2.4427408893291362,
1297 2.2803873792713096,
1298 2.3519177007491914,
1299 2.193_670_558_739_165,
1300 2.1263994596978226,
1301 2.3645007168762517,
1302 2.5249861145768056,
1303 2.122_272_855_834_001,
1304 0.299_203_443_460_228,
1305 2.172805960509645,
1306 2.3805956106200714,
1307 2.2635884407253357,
1308 2.1413513061524423,
1309 2.2424209481860458,
1310 1.8287138511852497,
1311 1.661_710_250_360_172,
1312 1.456_810_520_104_257,
1313 1.3638716209786697,
1314 1.3174847840211241,
1315 1.1294361324150146,
1316 1.6406879454416847,
1317 1.227_972_499_725_56,
1318 1.333044215401372,
1319 1.0429699942281332,
1320 0.830_352_777_457_079_7,
1321 0.594_504_552_950_756,
1322 0.864_889_768_902_684_3,
1323 1.0988525623839671,
1324 1.122_004_167_081_219,
1325 0.837_027_039_099_292_9,
1326 0.840_324_931_775_807_9,
1327 1.0662167330324928,
1328 0.7984426688798818,
1329 0.886_983_471_386_884_5,
1330 ];
1331 let w = [
1332 1.0889637747313827,
1333 1.4202703633345664,
1334 1.275_927_386_712_283,
1335 0.988_120_193_593_204,
1336 0.700_306_445_360_183_7,
1337 0.700_581_608_805_805_4,
1338 0.591_909_030_452_370_6,
1339 0.729152005398646,
1340 0.791_078_662_732_616_1,
1341 1.0620378700550646,
1342 1.2971524118911475,
1343 1.3690105613786727,
1344 0.929_895_969_340_577_7,
1345 0.623_054_652_707_651_3,
1346 1.2314715515822172,
1347 1.3028102195821702,
1348 1.3870370583608747,
1349 1.3259274356532842,
1350 1.3139970258343965,
1351 0.940_836_715_046_316_4,
1352 0.608_983_992_366_120_2,
1353 1.3987310056108981,
1354 0.797_507_378_039_881_6,
1355 0.788_213_320_076_465_6,
1356 0.765_858_371_742_069_7,
1357 0.582_622_988_615_185,
1358 0.907_211_071_578_785_8,
1359 1.3830148903653026,
1360 1.1364444366190583,
1361 1.4955437039025128,
1362 1.3725970827508718,
1363 1.314765295246616,
1364 1.106_141_550_699_249,
1365 0.869_393_355_911_597_6,
1366 0.928_705_062_018_707_4,
1367 0.695_141_761_098_057,
1368 0.962_294_715_689_495_2,
1369 0.610_878_398_176_282_6,
1370 1.4070586825255305,
1371 0.691_825_973_335_653_5,
1372 1.4032431468367577,
1373 1.4545864365063608,
1374 0.595_691_040_623_933_1,
1375 0.695_055_700_605_735_2,
1376 1.334_562_980_569_899,
1377 1.337_477_844_208_479,
1378 0.969_861_233_141_273_3,
1379 0.6534878071397543,
1380 1.439_056_930_365_041,
1381 1.483_104_110_462_591,
1382 1.316_310_929_134_488,
1383 0.9697276595979929,
1384 0.913_404_341_088_607_9,
1385 1.1443348934408277,
1386 0.999_848_530_394_956_5,
1387 1.0565558585803956,
1388 0.955_150_888_534_262_8,
1389 0.8411982893012464,
1390 1.2106719859875739,
1391 1.0034676706418395,
1392 0.781_219_038_413_837_6,
1393 0.896_646_452_136_337_8,
1394 1.325254688039422,
1395 0.828_120_636_753_737_9,
1396 1.2752281511202455,
1397 1.378995991544798,
1398 1.4765544817782938,
1399 0.808_399_976_463_988_4,
1400 1.0114127546548843,
1401 0.702_006_856_212_392_4,
1402 1.4021367505192757,
1403 1.4634487400762737,
1404 0.856_994_667_090_475_6,
1405 0.596_906_685_270_369,
1406 1.4358097377698869,
1407 1.249910962767899,
1408 1.3429545727558434,
1409 1.313_349_277_479_574,
1410 0.515_121_111_180_633_3,
1411 0.502_998_038_660_734_9,
1412 ];
1413 let out = weighted_lowess(&x, &y, Some(&w), 0.3, 4, 30, None);
1414 assert!(
1415 rel(out.delta, 0.593_369_432_979_009_3) < 1e-12,
1416 "delta {}",
1417 out.delta
1418 );
1419
1420 let r_fitted = [
1421 1.1884821422349683,
1422 1.1401746805937334,
1423 1.1063703912870086,
1424 1.1026672684736833,
1425 0.872_240_929_345_854_8,
1426 0.855_951_874_762_393_3,
1427 0.5666943700277064,
1428 0.510_552_240_462_768_4,
1429 0.369_016_523_550_517_2,
1430 0.304_924_376_153_817_2,
1431 0.227_306_278_971_063_6,
1432 0.215_647_194_621_015_9,
1433 0.20552696445334262,
1434 0.20146372618232533,
1435 0.078_024_062_961_633_33,
1436 0.057_933_325_377_075_74,
1437 0.040_195_331_914_130_16,
1438 0.028_136_656_740_725_74,
1439 0.025_366_080_684_971_15,
1440 -0.050_912_098_098_715,
1441 -0.067_616_092_559_898_98,
1442 -0.068_931_638_772_633_97,
1443 -0.069_980_935_260_592_24,
1444 -0.11951181663114796,
1445 -0.1295277526928138,
1446 -0.21936636695564127,
1447 -0.262_508_751_761_269_3,
1448 -0.261_180_486_021_105_6,
1449 -0.260_946_821_212_595_9,
1450 -0.26000726252193695,
1451 -0.167_592_176_158_451_8,
1452 -0.050_584_694_681_363_06,
1453 0.00809634544820792,
1454 0.21510888843295684,
1455 0.33873746515295533,
1456 0.351_705_758_469_647,
1457 0.580_491_217_858_042_1,
1458 0.6082580657896921,
1459 0.723_588_826_350_821_9,
1460 0.771_095_359_716_515_1,
1461 0.924_832_205_407_716,
1462 1.4089433518016659,
1463 1.4327444779344392,
1464 1.9245662627937583,
1465 1.940_370_302_694_94,
1466 2.0169068927703515,
1467 2.135_858_147_609_854,
1468 2.1450068314468513,
1469 2.1483285077940617,
1470 2.157_513_074_233_857,
1471 2.1588144992196634,
1472 2.159_273_556_147_39,
1473 2.160083404651763,
1474 2.1792561271649924,
1475 2.1677064401616297,
1476 2.1472903708612345,
1477 2.1212760240350104,
1478 2.0965598171906232,
1479 2.065_629_550_176_702,
1480 1.9470028929533285,
1481 1.875434429262878,
1482 1.6244677094067717,
1483 1.5664639693873488,
1484 1.3816535905374199,
1485 1.3801654339673561,
1486 1.3514246681057358,
1487 1.3176463321656406,
1488 1.299_159_070_363_2,
1489 1.2818435104574863,
1490 1.2025983400628886,
1491 1.1018567306289988,
1492 1.0873066282823167,
1493 1.0809475784515046,
1494 1.0503837308105375,
1495 1.0334626295961167,
1496 0.943_433_383_693_995,
1497 0.861_571_735_904_992_1,
1498 0.834_199_996_583_610_7,
1499 0.792_602_471_709_358_3,
1500 0.731_495_126_462_035_3,
1501 ];
1502 for (a, b) in out.fitted.iter().zip(r_fitted.iter()) {
1503 close(*a, *b);
1504 }
1505 let sum: f64 = out.fitted.iter().sum();
1506 assert!(rel(sum, 73.941_169_255_579_42) < 1e-9, "sum {sum}");
1507 }
1508
1509 #[test]
1513 fn weighted_lowess_case_d_unsorted() {
1514 let x = [
1515 2.4769279826432467,
1516 6.676_589_386_537_671,
1517 4.878_524_420_782_924,
1518 5.919_815_640_896_559,
1519 5.197_473_073_378_205,
1520 7.844762085005641,
1521 4.218_711_268_156_767,
1522 3.7431930266320705,
1523 2.252_489_222_213_626,
1524 2.2300906758755445,
1525 7.2444714065641165,
1526 6.980_926_280_841_231,
1527 0.829_540_582_373_738_3,
1528 0.291_089_225_560_426_7,
1529 1.8692466896027327,
1530 ];
1531 let y = [
1532 3.1588662152772407,
1533 5.265_610_750_585_987,
1534 4.479_077_038_757_614,
1535 4.999_896_114_465_269,
1536 4.528_259_545_777_066,
1537 5.9505529060869105,
1538 3.9898370326344743,
1539 3.837_755_420_730_662,
1540 2.957781766496248,
1541 3.0561989716819555,
1542 5.562_200_231_289_714,
1543 5.482_773_635_299_048,
1544 2.5100552062553017,
1545 2.106_936_485_716_76,
1546 2.938_948_707_868_278,
1547 ];
1548 let w = [
1549 1.3902532287407665,
1550 0.648_387_671_448_290_3,
1551 0.560_372_849_553_823_4,
1552 1.3949049064423888,
1553 0.839_275_473_868_474_2,
1554 1.0821476810146122,
1555 1.1763596059288828,
1556 0.8876171682029963,
1557 1.2749323339201508,
1558 0.48667634087614714,
1559 0.887_075_331_481_173_7,
1560 1.6853833251167087,
1561 1.1941875480115414,
1562 1.34794178516604,
1563 1.1733048296067863,
1564 ];
1565 let out = weighted_lowess(&x, &y, Some(&w), 0.4, 3, 200, None);
1566 assert_eq!(out.delta, 0.0);
1567
1568 let r_fitted = [
1569 3.1610682329090687,
1570 5.328_131_047_276_528,
1571 4.401525118617398,
1572 4.953068229283593,
1573 4.574_573_132_447_472,
1574 5.921_881_766_698_609,
1575 4.045_695_517_076_606,
1576 3.8024012703068983,
1577 3.060_630_080_369_762,
1578 3.0500104995362616,
1579 5.6113672538311326,
1580 5.477_972_596_741_132,
1581 2.4294025367732424,
1582 2.142_734_808_562_499,
1583 2.907_051_913_114_72,
1584 ];
1585 let r_residuals = [
1586 -0.002_202_017_631_828_035,
1587 -0.062_520_296_690_541_6,
1588 0.077_551_920_140_216_17,
1589 0.04682788518167591,
1590 -0.046_313_586_670_406_08,
1591 0.028671139388301015,
1592 -0.055858484442131484,
1593 0.035_354_150_423_763_55,
1594 -0.10284831387351412,
1595 0.006_188_472_145_693_957,
1596 -0.049_167_022_541_418_25,
1597 0.004_801_038_557_915_582,
1598 0.080_652_669_482_059_29,
1599 -0.03579832284573925,
1600 0.031_896_794_753_558_22,
1601 ];
1602 for (a, b) in out.fitted.iter().zip(r_fitted.iter()) {
1603 close(*a, *b);
1604 }
1605 for (a, b) in out.residuals.iter().zip(r_residuals.iter()) {
1606 assert!((a - b).abs() < 1e-9, "{a} vs {b}");
1607 }
1608 }
1609
1610 fn close_vec(got: &[f64], want: &[f64]) {
1618 assert_eq!(got.len(), want.len(), "length mismatch");
1619 for (a, b) in got.iter().zip(want.iter()) {
1620 if b.is_nan() {
1621 assert!(a.is_nan(), "expected NA, got {a}");
1622 } else {
1623 close(*a, *b);
1624 }
1625 }
1626 }
1627
1628 fn check_residuals_are_y_minus_fitted(out: &LoessFit, y: &[f64]) {
1632 for ((r, yy), f) in out.residuals.iter().zip(y.iter()).zip(out.fitted.iter()) {
1633 let expected = yy - f;
1634 if expected.is_nan() {
1635 assert!(r.is_nan(), "expected NA residual, got {r}");
1636 } else {
1637 assert!((r - expected).abs() < 1e-12, "{r} vs {expected}");
1638 }
1639 }
1640 }
1641
1642 #[test]
1646 fn loess_fit_l1_weighted_with_na() {
1647 let nan = f64::NAN;
1648 let x = [
1649 0.005_183_129_105_716_944,
1650 0.14047908363863826,
1651 0.16214577946811914,
1652 0.578_589_488_286_525,
1653 0.619_884_438_347_071_4,
1654 0.646_897_766_273_468_7,
1655 0.864_958_912_134_170_5,
1656 1.0355600714683533,
1657 1.2321620131842792,
1658 1.250_832_553_487_271,
1659 1.576_602_489_221_841,
1660 1.7511292267590761,
1661 2.024_130_343_925_208,
1662 2.0371259469538927,
1663 nan,
1664 2.7687330706976354,
1665 2.7724979422055185,
1666 2.899_750_091_601_163,
1667 3.276793584227562,
1668 3.3061099448241293,
1669 3.5034838342107832,
1670 3.637_880_280_148_238,
1671 3.981_494_423_933_327,
1672 3.991_432_855_837_047,
1673 4.117_796_053_178_608,
1674 4.407_502_717_804_164,
1675 4.801_340_533_886_105,
1676 4.817_655_358_929_187,
1677 4.830_177_505_500_615,
1678 5.0491762650199234,
1679 5.1060837297700346,
1680 5.7368572149425745,
1681 6.420_319_871_976_972,
1682 6.8036738759838045,
1683 7.339_874_927_420_169,
1684 8.126_682_038_418_949,
1685 8.510_418_669_320_643,
1686 8.806_991_728_488_356,
1687 9.071_829_735_767_096,
1688 9.548_492_254_689_336,
1689 ];
1690 let y = [
1691 -0.13028379272342372,
1692 0.16494167307661872,
1693 0.10534440906646064,
1694 0.7330729492059922,
1695 0.719_552_107_645_758_6,
1696 0.7335251729085972,
1697 3.896_540_372_057_267,
1698 0.9141200377945875,
1699 1.1454302634920055,
1700 1.0026956063690007,
1701 1.0944468337004631,
1702 1.1463798418508317,
1703 1.4395428355871485,
1704 0.985_150_160_028_773_8,
1705 0.887_350_237_670_555_2,
1706 1.0149620403673598,
1707 0.878_060_307_279_870_1,
1708 1.1285529306977484,
1709 0.398_293_292_094_334_6,
1710 0.42789453241135494,
1711 0.019349953543560616,
1712 0.255_488_015_807_366_5,
1713 0.230_069_869_085_910_2,
1714 -0.12743829720812616,
1715 0.17319277852114137,
1716 -0.14115542663394637,
1717 -0.473_134_585_337_793_2,
1718 nan,
1719 0.11773586387606913,
1720 0.10998459160176483,
1721 0.255_692_811_653_590_8,
1722 0.581_819_342_086_163_4,
1723 1.2570686237489412,
1724 1.9579856745571356,
1725 2.370_545_808_258_101,
1726 2.6969087938082077,
1727 2.462_926_449_216_594,
1728 2.4283890102220855,
1729 2.457_605_861_523_145,
1730 1.7983325143797855,
1731 ];
1732 let w = [
1733 0.636_493_015_708_401_8,
1734 0.791_828_367_765_992_9,
1735 1.9835953457048163,
1736 0.589_909_437_927_417_5,
1737 1.0680007479852065,
1738 0.42237004640046505,
1739 0.34347869313787666,
1740 1.7559089590795338,
1741 1.4984269458800554,
1742 1.372_503_425_157_629,
1743 1.9231961047044024,
1744 1.4591313774231822,
1745 0.664_618_161_669_932_3,
1746 1.1917643264634534,
1747 0.5399193662917241,
1748 1.1196884553181008,
1749 0.761_779_755_516_909,
1750 1.5054568398045376,
1751 1.6169831238687038,
1752 1.988671691226773,
1753 1.4473249244736508,
1754 1.2495072918944061,
1755 1.0664991330821068,
1756 1.4697452861350029,
1757 0.678_594_810_375_943_8,
1758 1.1991494379937648,
1759 1.3034791655139997,
1760 0.862_886_963_831_260_8,
1761 1.7764983414905147,
1762 0.817_129_175_551_235_5,
1763 1.2557559457607568,
1764 1.599_651_281_326_078,
1765 0.717_321_668_099_612,
1766 1.6017108282074333,
1767 1.4470027274917812,
1768 1.3925923728384078,
1769 0.542_866_303_306_073,
1770 0.613_774_243_392_981_6,
1771 1.4151965341996402,
1772 1.2633167420513927,
1773 ];
1774 let out = loess_fit(&y, &x, Some(&w), 0.3, 4, 1e-5, 1e5, true);
1775 let r_fitted = [
1776 0.0020154160943804374,
1777 0.131_892_620_268_470_8,
1778 0.15244602671480564,
1779 0.537_033_400_554_882_2,
1780 0.5749773054397006,
1781 0.599_992_286_147_306_4,
1782 0.820_962_205_024_607_4,
1783 0.922_188_649_092_287,
1784 0.999_131_539_751_303_6,
1785 1.0042360193467927,
1786 1.084424219541861,
1787 1.105226618879442,
1788 1.0789358441377175,
1789 1.0790114350851858,
1790 nan,
1791 0.858_709_557_143_218_8,
1792 0.856_107_782_666_883_5,
1793 0.8347718825091448,
1794 0.505_231_802_313_234_7,
1795 0.47837184475249783,
1796 0.295_909_335_228_325_1,
1797 0.20354399729824024,
1798 0.054_388_087_732_097_47,
1799 0.050_612_262_098_252_09,
1800 0.016304620106176948,
1801 0.041_451_857_739_025_66,
1802 0.10396584158117128,
1803 nan,
1804 0.1157231185509553,
1805 0.20740861707140956,
1806 0.23703826228746294,
1807 0.760_303_516_697_105_9,
1808 1.419_606_559_087_084,
1809 1.7266927821338722,
1810 2.0077981459480405,
1811 2.3279293123460407,
1812 2.475_834_436_511_091,
1813 2.5825278346690603,
1814 2.671_725_980_932_141,
1815 2.819_077_988_722_33,
1816 ];
1817 close_vec(&out.fitted, &r_fitted);
1818 check_residuals_are_y_minus_fitted(&out, &y);
1819 close(out.residuals[6], 3.075_578_167_032_66);
1821 }
1822
1823 #[test]
1826 fn loess_fit_l2_wls_branch() {
1827 let x = [
1828 1.5213841770309955,
1829 2.3736945423297584,
1830 2.6032693102024496,
1831 3.6165727430488914,
1832 4.216_155_161_848_292,
1833 4.967628859449178,
1834 ];
1835 let y = [
1836 2.1528132956168466,
1837 2.598_898_371_290_544,
1838 3.3797162340845226,
1839 3.511_792_998_604_96,
1840 3.9024791277875033,
1841 4.417381997642738,
1842 ];
1843 let w = [
1844 1.265_375_897_428_021,
1845 0.920_177_808_171_138_1,
1846 0.711_445_754_114_538_5,
1847 0.904_493_984_859_436_8,
1848 1.1457260956522077,
1849 1.515057343104854,
1850 ];
1851 let out = loess_fit(&y, &x, Some(&w), 0.3, 4, 1e-5, 1e5, true);
1852 let r_fitted = [
1853 2.221282436297829,
1854 2.765_360_343_792_229,
1855 2.911_910_901_433_168,
1856 3.558_759_788_700_577,
1857 3.941_507_155_238_185,
1858 4.421215315052069,
1859 ];
1860 close_vec(&out.fitted, &r_fitted);
1861 check_residuals_are_y_minus_fitted(&out, &y);
1862 }
1863
1864 #[test]
1867 fn loess_fit_l3_equal_weights_classic() {
1868 let x = [
1869 0.12413568049669266,
1870 0.943_929_295_986_890_8,
1871 1.0861205589026213,
1872 1.8004096411168575,
1873 2.0838854778558016,
1874 2.739089785143733,
1875 2.7455857265740633,
1876 3.097_543_876_618_147,
1877 3.157_202_487_811_446,
1878 3.4970000348985195,
1879 3.5675238333642483,
1880 3.869_830_997_660_756,
1881 4.138_796_932_995_319,
1882 5.527_887_191_623_449,
1883 6.197_990_983_724_594,
1884 6.255_103_558_301_926,
1885 6.7459734827280045,
1886 6.751_051_519_066_095,
1887 7.202_860_610_559_583,
1888 7.351_007_658_988_237,
1889 ];
1890 let y = [
1891 1.0298211176564178,
1892 0.676_610_752_805_095_6,
1893 0.855_910_305_007_615_9,
1894 -0.010864869491520768,
1895 -0.17715935228426227,
1896 -0.6484373184623734,
1897 -0.6695555621419208,
1898 -0.6411756350457184,
1899 -0.665_807_808_281_218_5,
1900 -0.677_004_584_491_505_2,
1901 -0.6202612231020217,
1902 -0.315_789_402_713_021_6,
1903 -0.020225008839295677,
1904 1.349_792_027_254_391,
1905 1.6439380305192044,
1906 1.6601868401446462,
1907 1.6584092984580048,
1908 1.8678128119816537,
1909 1.0507848512764517,
1910 1.087775447878236,
1911 ];
1912 let w = [2.0; 20];
1913 let out = loess_fit(&y, &x, Some(&w), 0.4, 4, 1e-5, 1e5, true);
1914 let r_fitted = [
1915 1.131_683_235_813_225,
1916 0.622_701_981_921_744,
1917 0.5340726402959507,
1918 0.0534601137616516,
1919 -0.19037724883930937,
1920 -0.581_662_324_318_019_5,
1921 -0.583_783_621_386_261_8,
1922 -0.646_813_245_039_454_5,
1923 -0.640_248_250_804_955_1,
1924 -0.569_616_038_400_150_6,
1925 -0.517_887_741_556_381_1,
1926 -0.30168620597235857,
1927 -0.094_559_894_415_800_07,
1928 1.262_038_052_285_814,
1929 1.4901527265313739,
1930 1.4785335054830242,
1931 1.3616342992329014,
1932 1.360_321_175_900_416,
1933 1.2346611633429285,
1934 1.1890588683115497,
1935 ];
1936 close_vec(&out.fitted, &r_fitted);
1937 check_residuals_are_y_minus_fitted(&out, &y);
1938 }
1939
1940 #[test]
1944 fn loess_fit_l4_clamped_weights() {
1945 let nan = f64::NAN;
1946 let x = [
1947 0.000_812_362_879_514_694_2,
1948 0.19315525190904737,
1949 0.341_314_423_363_655_8,
1950 0.550_810_409_244_149_9,
1951 0.691_795_941_442_251_2,
1952 0.860_569_709_446_281_2,
1953 1.6972281779162586,
1954 2.683_141_722_343_862,
1955 2.6952867256477475,
1956 3.0456017875112593,
1957 3.4599765236489475,
1958 4.460_506_583_098_322,
1959 ];
1960 let y = [
1961 0.18370482724257098,
1962 0.063_986_626_702_510_85,
1963 -0.14976651732047797,
1964 0.1083635469696469,
1965 0.418_286_167_062_277_7,
1966 0.365_527_766_084_957_4,
1967 0.7026405120499789,
1968 1.2006499392960333,
1969 1.2752066988779682,
1970 1.5496202606111031,
1971 2.0578605625298567,
1972 1.9503358137564917,
1973 ];
1974 let w = [
1975 1.4987861497793347,
1976 0.835_285_151_377_320_3,
1977 nan,
1978 0.979_024_216_765_537_9,
1979 1.0222810602281243,
1980 0.813_572_955_550_625_9,
1981 1.1607185169123113,
1982 1e-08,
1983 0.676_338_533_638_045_2,
1984 10000000.0,
1985 0.755_478_185_601_532_5,
1986 0.796_876_986_976_712_9,
1987 ];
1988 let out = loess_fit(&y, &x, Some(&w), 0.5, 3, 1e-5, 1e5, true);
1989 let r_fitted = [
1990 0.092_715_644_709_798_5,
1991 0.1571141562155525,
1992 0.20738759663288137,
1993 0.27930367717161875,
1994 0.328_256_797_435_878_9,
1995 0.38783005712437973,
1996 0.730_831_473_478_615_7,
1997 1.2752055964112556,
1998 1.2752055966734654,
1999 1.5496202606111031,
2000 2.0578605625298567,
2001 1.9503358137564917,
2002 ];
2003 close_vec(&out.fitted, &r_fitted);
2004 check_residuals_are_y_minus_fitted(&out, &y);
2005 close(out.residuals[9], 0.0);
2007 }
2008
2009 #[test]
2012 fn loess_fit_l5_tiny_span_identity() {
2013 let x: Vec<f64> = (1..=10).map(|i| i as f64).collect();
2014 let y: Vec<f64> = (1..=10).map(|i| (i as f64).powf(1.5)).collect();
2015 let w = vec![1.0; 10];
2016 let out = loess_fit(&y, &x, Some(&w), 0.05, 4, 1e-5, 1e5, true);
2017 for (f, yy) in out.fitted.iter().zip(y.iter()) {
2018 assert_eq!(f, yy);
2019 }
2020 for r in &out.residuals {
2021 assert_eq!(*r, 0.0);
2022 }
2023 }
2024}