1use crate::error::FdarError;
7use crate::slice_maybe_parallel;
8#[cfg(feature = "parallel")]
9use rayon::iter::ParallelIterator;
10
11fn gaussian_kernel(u: f64) -> f64 {
13 (-0.5 * u * u).exp() / (2.0 * std::f64::consts::PI).sqrt()
14}
15
16fn epanechnikov_kernel(u: f64) -> f64 {
18 if u.abs() <= 1.0 {
19 0.75 * (1.0 - u * u)
20 } else {
21 0.0
22 }
23}
24
25fn tricube_kernel(u: f64) -> f64 {
27 let abs_u = u.abs();
28 if abs_u < 1.0 {
29 (1.0 - abs_u.powi(3)).powi(3)
30 } else {
31 0.0
32 }
33}
34
35fn get_kernel(kernel_type: &str) -> fn(f64) -> f64 {
37 match kernel_type.to_lowercase().as_str() {
38 "epanechnikov" | "epan" => epanechnikov_kernel,
39 "tricube" | "tri-cube" => tricube_kernel,
40 _ => gaussian_kernel,
41 }
42}
43
44pub fn nadaraya_watson(
73 x: &[f64],
74 y: &[f64],
75 x_new: &[f64],
76 bandwidth: f64,
77 kernel: &str,
78) -> Result<Vec<f64>, FdarError> {
79 let n = x.len();
80 if n == 0 {
81 return Err(FdarError::InvalidDimension {
82 parameter: "x",
83 expected: "non-empty slice".to_string(),
84 actual: "empty".to_string(),
85 });
86 }
87 if y.len() != n {
88 return Err(FdarError::InvalidDimension {
89 parameter: "y",
90 expected: format!("length {n} (matching x)"),
91 actual: format!("length {}", y.len()),
92 });
93 }
94 if x_new.is_empty() {
95 return Err(FdarError::InvalidDimension {
96 parameter: "x_new",
97 expected: "non-empty slice".to_string(),
98 actual: "empty".to_string(),
99 });
100 }
101 if bandwidth <= 0.0 {
102 return Err(FdarError::InvalidParameter {
103 parameter: "bandwidth",
104 message: format!("must be positive, got {bandwidth}"),
105 });
106 }
107
108 let kernel_fn = get_kernel(kernel);
109
110 Ok(slice_maybe_parallel!(x_new)
111 .map(|&x0| {
112 let mut num = 0.0;
113 let mut denom = 0.0;
114
115 for i in 0..n {
116 let u = (x[i] - x0) / bandwidth;
117 let w = kernel_fn(u);
118 num += w * y[i];
119 denom += w;
120 }
121
122 if denom > 1e-10 {
123 num / denom
124 } else {
125 0.0
126 }
127 })
128 .collect())
129}
130
131pub fn local_linear(
161 x: &[f64],
162 y: &[f64],
163 x_new: &[f64],
164 bandwidth: f64,
165 kernel: &str,
166) -> Result<Vec<f64>, FdarError> {
167 let n = x.len();
168 if n == 0 {
169 return Err(FdarError::InvalidDimension {
170 parameter: "x",
171 expected: "non-empty slice".to_string(),
172 actual: "empty".to_string(),
173 });
174 }
175 if y.len() != n {
176 return Err(FdarError::InvalidDimension {
177 parameter: "y",
178 expected: format!("length {n} (matching x)"),
179 actual: format!("length {}", y.len()),
180 });
181 }
182 if x_new.is_empty() {
183 return Err(FdarError::InvalidDimension {
184 parameter: "x_new",
185 expected: "non-empty slice".to_string(),
186 actual: "empty".to_string(),
187 });
188 }
189 if bandwidth <= 0.0 {
190 return Err(FdarError::InvalidParameter {
191 parameter: "bandwidth",
192 message: format!("must be positive, got {bandwidth}"),
193 });
194 }
195
196 let kernel_fn = get_kernel(kernel);
197
198 Ok(slice_maybe_parallel!(x_new)
199 .map(|&x0| {
200 let mut s0 = 0.0;
202 let mut s1 = 0.0;
203 let mut s2 = 0.0;
204 let mut t0 = 0.0;
205 let mut t1 = 0.0;
206
207 for i in 0..n {
208 let u = (x[i] - x0) / bandwidth;
209 let w = kernel_fn(u);
210 let d = x[i] - x0;
211
212 s0 += w;
213 s1 += w * d;
214 s2 += w * d * d;
215 t0 += w * y[i];
216 t1 += w * y[i] * d;
217 }
218
219 let det = s0 * s2 - s1 * s1;
221 if det.abs() > 1e-10 {
222 (s2 * t0 - s1 * t1) / det
223 } else if s0 > 1e-10 {
224 t0 / s0
225 } else {
226 0.0
227 }
228 })
229 .collect())
230}
231
232fn accumulate_weighted_normal_equations(
234 x: &[f64],
235 y: &[f64],
236 x0: f64,
237 bandwidth: f64,
238 p: usize,
239 kernel_fn: impl Fn(f64) -> f64,
240) -> (Vec<f64>, Vec<f64>) {
241 let n = x.len();
242 let mut xtx = vec![0.0; p * p];
243 let mut xty = vec![0.0; p];
244
245 for i in 0..n {
246 let u = (x[i] - x0) / bandwidth;
247 let w = kernel_fn(u);
248 let d = x[i] - x0;
249
250 for j in 0..p {
251 let w_dj = w * d.powi(j as i32);
252 for k in 0..p {
253 xtx[j * p + k] += w_dj * d.powi(k as i32);
254 }
255 xty[j] += w_dj * y[i];
256 }
257 }
258
259 (xtx, xty)
260}
261
262fn find_pivot(a: &[f64], p: usize, col: usize) -> usize {
267 let mut max_idx = col;
268 for j in (col + 1)..p {
269 if a[j * p + col].abs() > a[max_idx * p + col].abs() {
270 max_idx = j;
271 }
272 }
273 max_idx
274}
275
276fn swap_rows(a: &mut [f64], b: &mut [f64], p: usize, row_a: usize, row_b: usize) {
278 for k in 0..p {
279 a.swap(row_a * p + k, row_b * p + k);
280 }
281 b.swap(row_a, row_b);
282}
283
284fn eliminate_below(a: &mut [f64], b: &mut [f64], p: usize, pivot_row: usize) {
286 let pivot = a[pivot_row * p + pivot_row];
287 for j in (pivot_row + 1)..p {
288 let factor = a[j * p + pivot_row] / pivot;
289 for k in pivot_row..p {
290 a[j * p + k] -= factor * a[pivot_row * p + k];
291 }
292 b[j] -= factor * b[pivot_row];
293 }
294}
295
296fn forward_eliminate(a: &mut [f64], b: &mut [f64], p: usize) {
297 for i in 0..p {
298 let max_idx = find_pivot(a, p, i);
299 if max_idx != i {
300 swap_rows(a, b, p, i, max_idx);
301 }
302
303 if a[i * p + i].abs() < 1e-10 {
304 continue;
305 }
306
307 eliminate_below(a, b, p, i);
308 }
309}
310
311fn back_substitute(a: &[f64], b: &[f64], p: usize) -> Vec<f64> {
313 let mut coefs = vec![0.0; p];
314 for i in (0..p).rev() {
315 let mut sum = b[i];
316 for j in (i + 1)..p {
317 sum -= a[i * p + j] * coefs[j];
318 }
319 if a[i * p + i].abs() > 1e-10 {
320 coefs[i] = sum / a[i * p + i];
321 }
322 }
323 coefs
324}
325
326fn solve_gaussian(a: &mut [f64], b: &mut [f64], p: usize) -> Vec<f64> {
327 forward_eliminate(a, b, p);
328 back_substitute(a, b, p)
329}
330
331pub fn solve_gaussian_pub(a: &mut [f64], b: &mut [f64], p: usize) -> Vec<f64> {
337 solve_gaussian(a, b, p)
338}
339
340pub fn local_polynomial(
358 x: &[f64],
359 y: &[f64],
360 x_new: &[f64],
361 bandwidth: f64,
362 degree: usize,
363 kernel: &str,
364) -> Result<Vec<f64>, FdarError> {
365 let n = x.len();
366 if n == 0 {
367 return Err(FdarError::InvalidDimension {
368 parameter: "x",
369 expected: "non-empty slice".to_string(),
370 actual: "empty".to_string(),
371 });
372 }
373 if y.len() != n {
374 return Err(FdarError::InvalidDimension {
375 parameter: "y",
376 expected: format!("length {n} (matching x)"),
377 actual: format!("length {}", y.len()),
378 });
379 }
380 if x_new.is_empty() {
381 return Err(FdarError::InvalidDimension {
382 parameter: "x_new",
383 expected: "non-empty slice".to_string(),
384 actual: "empty".to_string(),
385 });
386 }
387 if bandwidth <= 0.0 {
388 return Err(FdarError::InvalidParameter {
389 parameter: "bandwidth",
390 message: format!("must be positive, got {bandwidth}"),
391 });
392 }
393 if degree == 0 {
394 return nadaraya_watson(x, y, x_new, bandwidth, kernel);
395 }
396
397 if degree == 1 {
398 return local_linear(x, y, x_new, bandwidth, kernel);
399 }
400
401 let kernel_fn = get_kernel(kernel);
402 let p = degree + 1; Ok(slice_maybe_parallel!(x_new)
405 .map(|&x0| {
406 let (mut xtx, mut xty) =
407 accumulate_weighted_normal_equations(x, y, x0, bandwidth, p, kernel_fn);
408 let coefs = solve_gaussian(&mut xtx, &mut xty, p);
409 coefs[0]
410 })
411 .collect())
412}
413
414pub fn knn_smoother(x: &[f64], y: &[f64], x_new: &[f64], k: usize) -> Result<Vec<f64>, FdarError> {
430 let n = x.len();
431 if n == 0 {
432 return Err(FdarError::InvalidDimension {
433 parameter: "x",
434 expected: "non-empty slice".to_string(),
435 actual: "empty".to_string(),
436 });
437 }
438 if y.len() != n {
439 return Err(FdarError::InvalidDimension {
440 parameter: "y",
441 expected: format!("length {n} (matching x)"),
442 actual: format!("length {}", y.len()),
443 });
444 }
445 if x_new.is_empty() {
446 return Err(FdarError::InvalidDimension {
447 parameter: "x_new",
448 expected: "non-empty slice".to_string(),
449 actual: "empty".to_string(),
450 });
451 }
452 if k == 0 {
453 return Err(FdarError::InvalidParameter {
454 parameter: "k",
455 message: "must be at least 1".to_string(),
456 });
457 }
458
459 let k = k.min(n);
460
461 Ok(slice_maybe_parallel!(x_new)
462 .map(|&x0| {
463 let mut distances: Vec<(usize, f64)> = x
465 .iter()
466 .enumerate()
467 .map(|(i, &xi)| (i, (xi - x0).abs()))
468 .collect();
469
470 distances.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
472
473 let sum: f64 = distances.iter().take(k).map(|(i, _)| y[*i]).sum();
475 sum / k as f64
476 })
477 .collect())
478}
479
480pub fn smoothing_matrix_nw(x: &[f64], bandwidth: f64, kernel: &str) -> Result<Vec<f64>, FdarError> {
488 let n = x.len();
489 if n == 0 {
490 return Err(FdarError::InvalidDimension {
491 parameter: "x",
492 expected: "non-empty slice".to_string(),
493 actual: "empty".to_string(),
494 });
495 }
496 if bandwidth <= 0.0 {
497 return Err(FdarError::InvalidParameter {
498 parameter: "bandwidth",
499 message: format!("must be positive, got {bandwidth}"),
500 });
501 }
502
503 let kernel_fn = get_kernel(kernel);
504 let mut s = vec![0.0; n * n];
505
506 for i in 0..n {
507 let mut row_sum = 0.0;
508 for j in 0..n {
509 let u = (x[j] - x[i]) / bandwidth;
510 s[i + j * n] = kernel_fn(u);
511 row_sum += s[i + j * n];
512 }
513 if row_sum > 1e-10 {
514 for j in 0..n {
515 s[i + j * n] /= row_sum;
516 }
517 }
518 }
519
520 Ok(s)
521}
522
523#[derive(Debug, Clone, Copy, PartialEq)]
530#[non_exhaustive]
531pub enum CvCriterion {
532 Cv,
534 Gcv,
536 Aic,
540}
541
542#[derive(Debug, Clone, PartialEq)]
544pub struct OptimBandwidthResult {
545 pub h_opt: f64,
547 pub criterion: CvCriterion,
549 pub value: f64,
551}
552
553pub fn cv_smoother(x: &[f64], y: &[f64], bandwidth: f64, kernel: &str) -> f64 {
567 let n = x.len();
568 if n < 2 || y.len() != n || bandwidth <= 0.0 {
569 return f64::INFINITY;
570 }
571
572 let mut s = match smoothing_matrix_nw(x, bandwidth, kernel) {
574 Ok(s) => s,
575 Err(_) => return f64::INFINITY,
576 };
577
578 for i in 0..n {
580 s[i + i * n] = 0.0;
581 }
582
583 for i in 0..n {
585 let row_sum: f64 = (0..n).map(|j| s[i + j * n]).sum();
586 if row_sum > 1e-10 {
587 for j in 0..n {
588 s[i + j * n] /= row_sum;
589 }
590 }
591 }
592
593 let mut mse = 0.0;
595 for i in 0..n {
596 let y_hat: f64 = (0..n).map(|j| s[i + j * n] * y[j]).sum();
597 let resid = y[i] - y_hat;
598 mse += resid * resid;
599 }
600 mse / n as f64
601}
602
603pub fn gcv_smoother(x: &[f64], y: &[f64], bandwidth: f64, kernel: &str) -> f64 {
616 let n = x.len();
617 if n < 2 || y.len() != n || bandwidth <= 0.0 {
618 return f64::INFINITY;
619 }
620
621 let s = match smoothing_matrix_nw(x, bandwidth, kernel) {
622 Ok(s) => s,
623 Err(_) => return f64::INFINITY,
624 };
625
626 let mut rss = 0.0;
628 for i in 0..n {
629 let y_hat: f64 = (0..n).map(|j| s[i + j * n] * y[j]).sum();
630 let resid = y[i] - y_hat;
631 rss += resid * resid;
632 }
633
634 let trace_s: f64 = (0..n).map(|i| s[i + i * n]).sum();
636
637 let denom = 1.0 - trace_s / n as f64;
638 if denom.abs() < 1e-10 {
639 f64::INFINITY
640 } else {
641 (rss / n as f64) / (denom * denom)
642 }
643}
644
645pub fn aic_smoother(x: &[f64], y: &[f64], bandwidth: f64, kernel: &str) -> f64 {
664 let n = x.len();
665 if n < 2 || y.len() != n || bandwidth <= 0.0 {
666 return f64::INFINITY;
667 }
668
669 let s = match smoothing_matrix_nw(x, bandwidth, kernel) {
670 Ok(s) => s,
671 Err(_) => return f64::INFINITY,
672 };
673
674 let mut rss = 0.0;
676 for i in 0..n {
677 let y_hat: f64 = (0..n).map(|j| s[i + j * n] * y[j]).sum();
678 let resid = y[i] - y_hat;
679 rss += resid * resid;
680 }
681
682 let trace_s: f64 = (0..n).map(|i| s[i + i * n]).sum();
684
685 let n_f = n as f64;
686 n_f * (rss / n_f).max(1e-300).ln() + 2.0 * trace_s
688}
689
690pub fn optim_bandwidth(
716 x: &[f64],
717 y: &[f64],
718 h_range: Option<(f64, f64)>,
719 criterion: CvCriterion,
720 kernel: &str,
721 n_grid: usize,
722) -> OptimBandwidthResult {
723 let n = x.len();
724 let n_grid = n_grid.max(2);
725
726 let (h_min, h_max) = match h_range {
728 Some((lo, hi)) if lo > 0.0 && hi > lo => (lo, hi),
729 _ => {
730 let x_min = x.iter().copied().fold(f64::INFINITY, f64::min);
731 let x_max = x.iter().copied().fold(f64::NEG_INFINITY, f64::max);
732 let h_default = (x_max - x_min) / (n as f64).powf(0.2);
733 let h_default = h_default.max(1e-10);
734 (h_default / 5.0, h_default * 5.0)
735 }
736 };
737
738 let score_fn = match criterion {
739 CvCriterion::Cv => cv_smoother,
740 CvCriterion::Gcv => gcv_smoother,
741 CvCriterion::Aic => aic_smoother,
742 };
743
744 let mut best_h = h_min;
745 let mut best_score = f64::INFINITY;
746
747 for i in 0..n_grid {
748 let h = h_min + (h_max - h_min) * i as f64 / (n_grid - 1) as f64;
749 let score = score_fn(x, y, h, kernel);
750 if score < best_score {
751 best_score = score;
752 best_h = h;
753 }
754 }
755
756 OptimBandwidthResult {
757 h_opt: best_h,
758 criterion,
759 value: best_score,
760 }
761}
762
763#[derive(Debug, Clone, PartialEq)]
767pub struct KnnCvResult {
768 pub optimal_k: usize,
770 pub cv_errors: Vec<f64>,
772}
773
774pub fn knn_gcv(x: &[f64], y: &[f64], max_k: usize) -> KnnCvResult {
784 let n = x.len();
785 let max_k = max_k.min(n.saturating_sub(1)).max(1);
786
787 let mut sorted_neighbors: Vec<Vec<(usize, f64)>> = Vec::with_capacity(n);
789 for i in 0..n {
790 let mut dists: Vec<(usize, f64)> = (0..n)
791 .filter(|&j| j != i)
792 .map(|j| (j, (x[j] - x[i]).abs()))
793 .collect();
794 dists.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
795 sorted_neighbors.push(dists);
796 }
797
798 let mut cv_errors = Vec::with_capacity(max_k);
799
800 for k in 1..=max_k {
801 let mut mse = 0.0;
802 for i in 0..n {
803 let neighbors = &sorted_neighbors[i];
804 let d_k = if k <= neighbors.len() {
806 neighbors[k - 1].1
807 } else {
808 neighbors.last().map_or(1.0, |x| x.1)
809 };
810 let d_k1 = if k < neighbors.len() {
811 neighbors[k].1
812 } else {
813 d_k * 2.0
814 };
815 let h = (d_k + d_k1) / 2.0;
816 let h = h.max(1e-10);
817
818 let mut num = 0.0;
820 let mut den = 0.0;
821 for &(j, dist) in neighbors.iter().take(k) {
822 let u = dist / h;
823 let w = epanechnikov_kernel(u);
824 num += w * y[j];
825 den += w;
826 }
827 let y_hat = if den > 1e-10 { num / den } else { y[i] };
828 mse += (y[i] - y_hat).powi(2);
829 }
830 cv_errors.push(mse / n as f64);
831 }
832
833 let optimal_k = cv_errors
834 .iter()
835 .enumerate()
836 .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
837 .map_or(1, |(i, _)| i + 1);
838
839 KnnCvResult {
840 optimal_k,
841 cv_errors,
842 }
843}
844
845pub fn knn_lcv(x: &[f64], y: &[f64], max_k: usize) -> Vec<usize> {
858 let n = x.len();
859 let max_k = max_k.min(n.saturating_sub(1)).max(1);
860
861 let mut per_obs_k = vec![1usize; n];
862
863 for i in 0..n {
864 let mut neighbors: Vec<(usize, f64)> = (0..n)
866 .filter(|&j| j != i)
867 .map(|j| (j, (x[j] - x[i]).abs()))
868 .collect();
869 neighbors.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
870
871 let mut best_k = 1;
872 let mut best_err = f64::INFINITY;
873
874 for k in 1..=max_k {
875 let sum: f64 = neighbors.iter().take(k).map(|&(j, _)| y[j]).sum();
877 let y_hat = sum / k as f64;
878 let err = (y[i] - y_hat).abs();
879 if err < best_err {
880 best_err = err;
881 best_k = k;
882 }
883 }
884 per_obs_k[i] = best_k;
885 }
886
887 per_obs_k
888}
889
890#[cfg(test)]
891mod tests {
892 use super::*;
893 use crate::test_helpers::uniform_grid;
894
895 #[test]
898 fn test_nw_constant_data() {
899 let x = uniform_grid(20);
900 let y: Vec<f64> = vec![5.0; 20];
901
902 let y_smooth = nadaraya_watson(&x, &y, &x, 0.1, "gaussian").unwrap();
903
904 for &yi in &y_smooth {
906 assert!(
907 (yi - 5.0).abs() < 0.1,
908 "Constant data should remain constant"
909 );
910 }
911 }
912
913 #[test]
914 fn test_nw_linear_data() {
915 let x = uniform_grid(50);
916 let y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi + 1.0).collect();
917
918 let y_smooth = nadaraya_watson(&x, &y, &x, 0.2, "gaussian").unwrap();
919
920 for i in 10..40 {
922 let expected = 2.0 * x[i] + 1.0;
923 assert!(
924 (y_smooth[i] - expected).abs() < 0.2,
925 "Linear trend should be approximately preserved"
926 );
927 }
928 }
929
930 #[test]
931 fn test_nw_gaussian_vs_epanechnikov() {
932 let x = uniform_grid(30);
933 let y: Vec<f64> = x
934 .iter()
935 .map(|&xi| (2.0 * std::f64::consts::PI * xi).sin())
936 .collect();
937
938 let y_gauss = nadaraya_watson(&x, &y, &x, 0.1, "gaussian").unwrap();
939 let y_epan = nadaraya_watson(&x, &y, &x, 0.1, "epanechnikov").unwrap();
940
941 assert_eq!(y_gauss.len(), 30);
943 assert_eq!(y_epan.len(), 30);
944
945 let diff: f64 = y_gauss
947 .iter()
948 .zip(&y_epan)
949 .map(|(a, b)| (a - b).abs())
950 .sum();
951 assert!(
952 diff > 0.0,
953 "Different kernels should give different results"
954 );
955 }
956
957 #[test]
958 fn test_nw_invalid_input() {
959 assert!(nadaraya_watson(&[], &[], &[0.5], 0.1, "gaussian").is_err());
961
962 assert!(nadaraya_watson(&[0.0, 1.0], &[1.0], &[0.5], 0.1, "gaussian").is_err());
964
965 assert!(nadaraya_watson(&[0.0, 1.0], &[1.0, 2.0], &[0.5], 0.0, "gaussian").is_err());
967
968 assert!(nadaraya_watson(&[0.0, 1.0], &[1.0, 2.0], &[], 0.1, "gaussian").is_err());
970 }
971
972 #[test]
975 fn test_ll_constant_data() {
976 let x = uniform_grid(20);
977 let y: Vec<f64> = vec![3.0; 20];
978
979 let y_smooth = local_linear(&x, &y, &x, 0.15, "gaussian").unwrap();
980
981 for &yi in &y_smooth {
982 assert!((yi - 3.0).abs() < 0.1, "Constant should remain constant");
983 }
984 }
985
986 #[test]
987 fn test_ll_linear_data_exact() {
988 let x = uniform_grid(30);
989 let y: Vec<f64> = x.iter().map(|&xi| 3.0 * xi + 2.0).collect();
990
991 let y_smooth = local_linear(&x, &y, &x, 0.2, "gaussian").unwrap();
992
993 for i in 5..25 {
995 let expected = 3.0 * x[i] + 2.0;
996 assert!(
997 (y_smooth[i] - expected).abs() < 0.1,
998 "Local linear should fit linear data well"
999 );
1000 }
1001 }
1002
1003 #[test]
1004 fn test_ll_invalid_input() {
1005 assert!(local_linear(&[], &[], &[0.5], 0.1, "gaussian").is_err());
1006
1007 assert!(local_linear(&[0.0, 1.0], &[1.0, 2.0], &[0.5], -0.1, "gaussian").is_err());
1008 }
1009
1010 #[test]
1013 fn test_lp_degree1_equals_local_linear() {
1014 let x = uniform_grid(25);
1015 let y: Vec<f64> = x.iter().map(|&xi| xi * xi).collect();
1016
1017 let y_ll = local_linear(&x, &y, &x, 0.15, "gaussian").unwrap();
1018 let y_lp = local_polynomial(&x, &y, &x, 0.15, 1, "gaussian").unwrap();
1019
1020 for i in 0..25 {
1021 assert!(
1022 (y_ll[i] - y_lp[i]).abs() < 1e-10,
1023 "Degree 1 should equal local linear"
1024 );
1025 }
1026 }
1027
1028 #[test]
1029 fn test_lp_quadratic_data() {
1030 let x = uniform_grid(40);
1031 let y: Vec<f64> = x.iter().map(|&xi| xi * xi).collect();
1032
1033 let y_smooth = local_polynomial(&x, &y, &x, 0.15, 2, "gaussian").unwrap();
1034
1035 for i in 8..32 {
1037 let expected = x[i] * x[i];
1038 assert!(
1039 (y_smooth[i] - expected).abs() < 0.1,
1040 "Local quadratic should fit quadratic data"
1041 );
1042 }
1043 }
1044
1045 #[test]
1046 fn test_lp_invalid_input() {
1047 let result =
1049 local_polynomial(&[0.0, 1.0], &[1.0, 2.0], &[0.5], 0.1, 0, "gaussian").unwrap();
1050 let nw = nadaraya_watson(&[0.0, 1.0], &[1.0, 2.0], &[0.5], 0.1, "gaussian").unwrap();
1051 assert_eq!(result, nw);
1052
1053 assert!(local_polynomial(&[], &[], &[0.5], 0.1, 2, "gaussian").is_err());
1055 }
1056
1057 #[test]
1060 fn test_knn_k1_nearest() {
1061 let x = vec![0.0, 0.5, 1.0];
1062 let y = vec![1.0, 2.0, 3.0];
1063
1064 let result = knn_smoother(&x, &y, &[0.1, 0.6, 0.9], 1).unwrap();
1065
1066 assert!((result[0] - 1.0).abs() < 1e-10, "0.1 nearest to 0.0 -> 1.0");
1068 assert!((result[1] - 2.0).abs() < 1e-10, "0.6 nearest to 0.5 -> 2.0");
1069 assert!((result[2] - 3.0).abs() < 1e-10, "0.9 nearest to 1.0 -> 3.0");
1070 }
1071
1072 #[test]
1073 fn test_knn_k_equals_n_is_mean() {
1074 let x = vec![0.0, 0.25, 0.5, 0.75, 1.0];
1075 let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1076 let expected_mean = 3.0;
1077
1078 let result = knn_smoother(&x, &y, &[0.5], 5).unwrap();
1079
1080 assert!(
1081 (result[0] - expected_mean).abs() < 1e-10,
1082 "k=n should return mean"
1083 );
1084 }
1085
1086 #[test]
1087 fn test_knn_invalid_input() {
1088 assert!(knn_smoother(&[], &[], &[0.5], 3).is_err());
1089
1090 assert!(knn_smoother(&[0.0, 1.0], &[1.0, 2.0], &[0.5], 0).is_err());
1091 }
1092
1093 #[test]
1096 fn test_smoothing_matrix_row_stochastic() {
1097 let x = uniform_grid(10);
1098 let s = smoothing_matrix_nw(&x, 0.2, "gaussian").unwrap();
1099
1100 assert_eq!(s.len(), 100);
1101
1102 for i in 0..10 {
1104 let row_sum: f64 = (0..10).map(|j| s[i + j * 10]).sum();
1105 assert!(
1106 (row_sum - 1.0).abs() < 1e-10,
1107 "Row {} should sum to 1, got {}",
1108 i,
1109 row_sum
1110 );
1111 }
1112 }
1113
1114 #[test]
1115 fn test_smoothing_matrix_invalid_input() {
1116 assert!(smoothing_matrix_nw(&[], 0.1, "gaussian").is_err());
1117
1118 assert!(smoothing_matrix_nw(&[0.0, 1.0], 0.0, "gaussian").is_err());
1119 }
1120
1121 #[test]
1122 fn test_nan_nw_no_panic() {
1123 let x = vec![0.0, 0.25, 0.5, 0.75, 1.0];
1124 let mut y = vec![0.0, 1.0, 2.0, 1.0, 0.0];
1125 y[2] = f64::NAN;
1126 let result = nadaraya_watson(&x, &y, &x, 0.3, "gaussian").unwrap();
1127 assert_eq!(result.len(), x.len());
1128 }
1130
1131 #[test]
1132 fn test_n1_smoother() {
1133 let x = vec![0.5];
1135 let y = vec![3.0];
1136 let x_new = vec![0.5];
1137 let result = nadaraya_watson(&x, &y, &x_new, 0.3, "gaussian").unwrap();
1138 assert_eq!(result.len(), 1);
1139 assert!(
1140 (result[0] - 3.0).abs() < 1e-6,
1141 "Single point smoother should return the value"
1142 );
1143 }
1144
1145 #[test]
1146 fn test_duplicate_x_smoother() {
1147 let x = vec![0.0, 0.0, 0.5, 1.0, 1.0];
1149 let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1150 let x_new = vec![0.0, 0.5, 1.0];
1151 let result = nadaraya_watson(&x, &y, &x_new, 0.3, "gaussian").unwrap();
1152 assert_eq!(result.len(), 3);
1153 for v in &result {
1154 assert!(v.is_finite());
1155 }
1156 }
1157
1158 #[test]
1161 fn test_cv_smoother_linear_data() {
1162 let x = uniform_grid(30);
1163 let y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi + 1.0).collect();
1164 let cv = cv_smoother(&x, &y, 0.2, "gaussian");
1165 assert!(cv.is_finite());
1166 assert!(cv >= 0.0);
1167 assert!(cv < 1.0, "CV error for smooth linear data should be small");
1168 }
1169
1170 #[test]
1171 fn test_cv_smoother_invalid() {
1172 assert_eq!(cv_smoother(&[], &[], 0.1, "gaussian"), f64::INFINITY);
1173 assert_eq!(
1174 cv_smoother(&[0.0, 1.0], &[1.0, 2.0], -0.1, "gaussian"),
1175 f64::INFINITY
1176 );
1177 }
1178
1179 #[test]
1180 fn test_gcv_smoother_linear_data() {
1181 let x = uniform_grid(30);
1182 let y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi + 1.0).collect();
1183 let gcv = gcv_smoother(&x, &y, 0.2, "gaussian");
1184 assert!(gcv.is_finite());
1185 assert!(gcv >= 0.0);
1186 }
1187
1188 #[test]
1189 fn test_gcv_smoother_invalid() {
1190 assert_eq!(gcv_smoother(&[], &[], 0.1, "gaussian"), f64::INFINITY);
1191 }
1192
1193 #[test]
1194 fn test_optim_bandwidth_returns_valid() {
1195 let x = uniform_grid(30);
1196 let y: Vec<f64> = x
1197 .iter()
1198 .map(|&xi| (2.0 * std::f64::consts::PI * xi).sin())
1199 .collect();
1200
1201 let result = optim_bandwidth(&x, &y, None, CvCriterion::Gcv, "gaussian", 20);
1202 assert!(result.h_opt > 0.0);
1203 assert!(result.value.is_finite());
1204 assert_eq!(result.criterion, CvCriterion::Gcv);
1205 }
1206
1207 #[test]
1208 fn test_optim_bandwidth_cv_vs_gcv() {
1209 let x = uniform_grid(25);
1210 let y: Vec<f64> = x.iter().map(|&xi| xi * xi).collect();
1211
1212 let cv_result = optim_bandwidth(&x, &y, None, CvCriterion::Cv, "gaussian", 20);
1213 let gcv_result = optim_bandwidth(&x, &y, None, CvCriterion::Gcv, "gaussian", 20);
1214
1215 assert!(cv_result.h_opt > 0.0);
1216 assert!(gcv_result.h_opt > 0.0);
1217 }
1218
1219 #[test]
1220 fn test_optim_bandwidth_custom_range() {
1221 let x = uniform_grid(20);
1222 let y: Vec<f64> = x.to_vec();
1223 let result = optim_bandwidth(&x, &y, Some((0.05, 0.5)), CvCriterion::Cv, "gaussian", 10);
1224 assert!(result.h_opt >= 0.05);
1225 assert!(result.h_opt <= 0.5);
1226 }
1227
1228 #[test]
1231 fn test_aic_smoother_matches_hand_computed() {
1232 let x = vec![0.0, 0.25, 0.5, 0.75, 1.0];
1235 let y = vec![0.1, 0.4, 0.35, 0.8, 1.2];
1236 let bandwidth = 0.3;
1237 let kernel = "gaussian";
1238
1239 let s = smoothing_matrix_nw(&x, bandwidth, kernel).unwrap();
1240 let n = x.len();
1241 let mut rss = 0.0;
1242 for i in 0..n {
1243 let y_hat: f64 = (0..n).map(|j| s[i + j * n] * y[j]).sum();
1244 let resid = y[i] - y_hat;
1245 rss += resid * resid;
1246 }
1247 let trace_s: f64 = (0..n).map(|i| s[i + i * n]).sum();
1248 let n_f = n as f64;
1249 let expected = n_f * (rss / n_f).max(1e-300).ln() + 2.0 * trace_s;
1250
1251 let got = aic_smoother(&x, &y, bandwidth, kernel);
1252 assert!(
1253 (got - expected).abs() < 1e-12,
1254 "got={got}, expected={expected}"
1255 );
1256 }
1257
1258 #[test]
1259 fn test_aic_smoother_invalid_inputs() {
1260 assert_eq!(aic_smoother(&[0.0], &[1.0], 0.3, "gaussian"), f64::INFINITY);
1262 assert_eq!(
1263 aic_smoother(&[0.0, 1.0], &[1.0], 0.3, "gaussian"),
1264 f64::INFINITY
1265 );
1266 assert_eq!(
1267 aic_smoother(&[0.0, 1.0], &[1.0, 2.0], 0.0, "gaussian"),
1268 f64::INFINITY
1269 );
1270 }
1271
1272 #[test]
1273 fn test_optim_bandwidth_aic_matches_brute_force_grid() {
1274 let x = uniform_grid(25);
1277 let y: Vec<f64> = x
1278 .iter()
1279 .map(|&xi| (2.0 * std::f64::consts::PI * xi).sin())
1280 .collect();
1281 let kernel = "gaussian";
1282 let n_grid = 20;
1283
1284 let n = x.len();
1286 let x_min = x.iter().copied().fold(f64::INFINITY, f64::min);
1287 let x_max = x.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1288 let h_default = ((x_max - x_min) / (n as f64).powf(0.2)).max(1e-10);
1289 let (h_min, h_max) = (h_default / 5.0, h_default * 5.0);
1290
1291 let mut best_h = h_min;
1292 let mut best_score = f64::INFINITY;
1293 for i in 0..n_grid {
1294 let h = h_min + (h_max - h_min) * i as f64 / (n_grid - 1) as f64;
1295 let score = aic_smoother(&x, &y, h, kernel);
1296 if score < best_score {
1297 best_score = score;
1298 best_h = h;
1299 }
1300 }
1301
1302 let result = optim_bandwidth(&x, &y, None, CvCriterion::Aic, kernel, n_grid);
1303 assert_eq!(result.criterion, CvCriterion::Aic);
1304 assert_eq!(result.h_opt, best_h);
1305 assert_eq!(result.value, best_score);
1306 }
1307
1308 #[test]
1309 fn test_optim_bandwidth_aic_diverges_from_gcv() {
1310 let n = 50usize;
1317 let x: Vec<f64> = (0..n).map(|i| i as f64 / (n as f64 - 1.0)).collect();
1318 let mut seed = 999u64;
1320 let mut lcg = || {
1321 seed = seed
1322 .wrapping_mul(6364136223846793005)
1323 .wrapping_add(1442695040888963407);
1324 ((seed >> 11) as f64) / ((1u64 << 53) as f64) - 0.5
1325 };
1326 let y: Vec<f64> = x
1327 .iter()
1328 .map(|&xi| (2.0 * std::f64::consts::PI * xi).sin() + 0.5 * lcg())
1329 .collect();
1330 let kernel = "gaussian";
1331 let range = Some((0.01, 0.5));
1332
1333 let aic = optim_bandwidth(&x, &y, range, CvCriterion::Aic, kernel, 60);
1334 let gcv = optim_bandwidth(&x, &y, range, CvCriterion::Gcv, kernel, 60);
1335 assert!(aic.h_opt.is_finite() && gcv.h_opt.is_finite());
1336 assert_ne!(
1337 aic.h_opt, gcv.h_opt,
1338 "AIC and GCV selected the same bandwidth; expected divergence"
1339 );
1340 assert!(
1342 aic.h_opt < gcv.h_opt,
1343 "expected AIC to pick a smaller bandwidth than GCV: aic={}, gcv={}",
1344 aic.h_opt,
1345 gcv.h_opt
1346 );
1347 }
1348
1349 #[test]
1350 fn test_gcv_cv_unchanged_by_aic_addition() {
1351 let x = uniform_grid(25);
1353 let y: Vec<f64> = x.iter().map(|&xi| xi * xi).collect();
1354 let kernel = "gaussian";
1355 let h = 0.2;
1356 let n = x.len();
1358 let s = smoothing_matrix_nw(&x, h, kernel).unwrap();
1359 let mut rss = 0.0;
1360 for i in 0..n {
1361 let y_hat: f64 = (0..n).map(|j| s[i + j * n] * y[j]).sum();
1362 rss += (y[i] - y_hat).powi(2);
1363 }
1364 let trace_s: f64 = (0..n).map(|i| s[i + i * n]).sum();
1365 let denom = 1.0 - trace_s / n as f64;
1366 let expected_gcv = (rss / n as f64) / (denom * denom);
1367 assert!((gcv_smoother(&x, &y, h, kernel) - expected_gcv).abs() < 1e-12);
1368 }
1369
1370 #[test]
1373 fn test_knn_gcv_returns_valid() {
1374 let x = uniform_grid(20);
1375 let y: Vec<f64> = x.iter().map(|&xi| xi * xi).collect();
1376
1377 let result = knn_gcv(&x, &y, 10);
1378 assert!(result.optimal_k >= 1);
1379 assert!(result.optimal_k <= 10);
1380 assert_eq!(result.cv_errors.len(), 10);
1381 for &e in &result.cv_errors {
1382 assert!(e.is_finite());
1383 assert!(e >= 0.0);
1384 }
1385 }
1386
1387 #[test]
1388 fn test_knn_gcv_constant_data() {
1389 let x = uniform_grid(15);
1390 let y = vec![5.0; 15];
1391 let result = knn_gcv(&x, &y, 5);
1392 for &e in &result.cv_errors {
1394 assert!(e < 0.01, "Constant data: CV error should be near zero");
1395 }
1396 }
1397
1398 #[test]
1399 fn test_knn_lcv_returns_valid() {
1400 let x = uniform_grid(15);
1401 let y: Vec<f64> = x.to_vec();
1402
1403 let result = knn_lcv(&x, &y, 5);
1404 assert_eq!(result.len(), 15);
1405 for &k in &result {
1406 assert!(k >= 1);
1407 assert!(k <= 5);
1408 }
1409 }
1410
1411 #[test]
1412 fn test_knn_lcv_constant_data() {
1413 let x = uniform_grid(10);
1414 let y = vec![3.0; 10];
1415 let result = knn_lcv(&x, &y, 5);
1416 assert_eq!(result.len(), 10);
1417 for &k in &result {
1420 assert!(k >= 1);
1421 }
1422 }
1423
1424 #[test]
1427 fn test_tricube_kernel_values() {
1428 let k0 = tricube_kernel(0.0);
1430 assert!((k0 - 1.0).abs() < 1e-10, "tricube(0) should be 1.0");
1431
1432 assert_eq!(tricube_kernel(1.0), 0.0, "tricube(1) should be 0");
1434 assert_eq!(tricube_kernel(-1.0), 0.0, "tricube(-1) should be 0");
1435 assert_eq!(tricube_kernel(2.0), 0.0, "tricube(2) should be 0");
1436
1437 let k05 = tricube_kernel(0.5);
1439 assert!(k05 > 0.0 && k05 < 1.0, "tricube(0.5) should be in (0, 1)");
1440 }
1441
1442 #[test]
1443 fn test_nw_tricube_constant_data() {
1444 let x = uniform_grid(20);
1445 let y = vec![5.0; 20];
1446
1447 let y_smooth = nadaraya_watson(&x, &y, &x, 0.2, "tricube").unwrap();
1448
1449 for &yi in &y_smooth {
1450 assert!(
1451 (yi - 5.0).abs() < 0.1,
1452 "Tricube NW: constant data should remain constant"
1453 );
1454 }
1455 }
1456
1457 #[test]
1458 fn test_nw_tricube_linear_data() {
1459 let x = uniform_grid(50);
1460 let y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi + 1.0).collect();
1461
1462 let y_smooth = nadaraya_watson(&x, &y, &x, 0.2, "tricube").unwrap();
1463
1464 for i in 10..40 {
1466 let expected = 2.0 * x[i] + 1.0;
1467 assert!(
1468 (y_smooth[i] - expected).abs() < 0.3,
1469 "Tricube NW: linear trend should be approximately preserved at i={i}"
1470 );
1471 }
1472 }
1473
1474 #[test]
1475 fn test_nw_tricube_vs_gaussian() {
1476 let x = uniform_grid(30);
1477 let y: Vec<f64> = x
1478 .iter()
1479 .map(|&xi| (2.0 * std::f64::consts::PI * xi).sin())
1480 .collect();
1481
1482 let y_gauss = nadaraya_watson(&x, &y, &x, 0.15, "gaussian").unwrap();
1483 let y_tri = nadaraya_watson(&x, &y, &x, 0.15, "tricube").unwrap();
1484
1485 assert_eq!(y_gauss.len(), y_tri.len());
1486
1487 assert!(y_tri.iter().all(|v| v.is_finite()));
1489
1490 let diff: f64 = y_gauss.iter().zip(&y_tri).map(|(a, b)| (a - b).abs()).sum();
1492 assert!(
1493 diff > 0.0,
1494 "Gaussian and tricube kernels should give different results"
1495 );
1496 }
1497
1498 #[test]
1499 fn test_nw_tricube_vs_epanechnikov() {
1500 let x = uniform_grid(30);
1501 let y: Vec<f64> = x
1502 .iter()
1503 .map(|&xi| (2.0 * std::f64::consts::PI * xi).sin())
1504 .collect();
1505
1506 let y_epan = nadaraya_watson(&x, &y, &x, 0.15, "epanechnikov").unwrap();
1507 let y_tri = nadaraya_watson(&x, &y, &x, 0.15, "tricube").unwrap();
1508
1509 assert!(y_epan.iter().all(|v| v.is_finite()));
1511 assert!(y_tri.iter().all(|v| v.is_finite()));
1512
1513 let diff: f64 = y_epan.iter().zip(&y_tri).map(|(a, b)| (a - b).abs()).sum();
1515 assert!(
1516 diff > 0.0,
1517 "Epanechnikov and tricube should give different results"
1518 );
1519 }
1520
1521 #[test]
1522 fn test_ll_tricube_constant_data() {
1523 let x = uniform_grid(20);
1524 let y = vec![3.0; 20];
1525
1526 let y_smooth = local_linear(&x, &y, &x, 0.2, "tricube").unwrap();
1527
1528 for &yi in &y_smooth {
1529 assert!(
1530 (yi - 3.0).abs() < 0.1,
1531 "Tricube LL: constant should remain constant"
1532 );
1533 }
1534 }
1535
1536 #[test]
1537 fn test_ll_tricube_linear_data() {
1538 let x = uniform_grid(30);
1539 let y: Vec<f64> = x.iter().map(|&xi| 3.0 * xi + 2.0).collect();
1540
1541 let y_smooth = local_linear(&x, &y, &x, 0.2, "tricube").unwrap();
1542
1543 for i in 5..25 {
1545 let expected = 3.0 * x[i] + 2.0;
1546 assert!(
1547 (y_smooth[i] - expected).abs() < 0.2,
1548 "Tricube LL: should fit linear data well at i={i}"
1549 );
1550 }
1551 }
1552
1553 #[test]
1554 fn test_ll_tricube_vs_gaussian() {
1555 let x = uniform_grid(30);
1556 let y: Vec<f64> = x.iter().map(|&xi| xi * xi).collect();
1557
1558 let y_gauss = local_linear(&x, &y, &x, 0.15, "gaussian").unwrap();
1559 let y_tri = local_linear(&x, &y, &x, 0.15, "tricube").unwrap();
1560
1561 assert_eq!(y_gauss.len(), y_tri.len());
1562 assert!(y_tri.iter().all(|v| v.is_finite()));
1563
1564 let diff: f64 = y_gauss.iter().zip(&y_tri).map(|(a, b)| (a - b).abs()).sum();
1565 assert!(
1566 diff > 0.0,
1567 "Gaussian and tricube local linear should differ"
1568 );
1569 }
1570
1571 #[test]
1572 fn test_lp_tricube_quadratic() {
1573 let x = uniform_grid(40);
1574 let y: Vec<f64> = x.iter().map(|&xi| xi * xi).collect();
1575
1576 let y_smooth = local_polynomial(&x, &y, &x, 0.15, 2, "tricube").unwrap();
1577
1578 for i in 8..32 {
1580 let expected = x[i] * x[i];
1581 assert!(
1582 (y_smooth[i] - expected).abs() < 0.15,
1583 "Tricube LP: should fit quadratic data at i={i}"
1584 );
1585 }
1586 }
1587
1588 #[test]
1589 fn test_get_kernel_tricube_aliases() {
1590 let k1 = get_kernel("tricube");
1592 let k2 = get_kernel("tri-cube");
1593
1594 let test_val = 0.5;
1595 assert!(
1596 (k1(test_val) - k2(test_val)).abs() < 1e-15,
1597 "Both tricube aliases should give the same result"
1598 );
1599 }
1600
1601 #[test]
1602 fn test_smoothing_matrix_tricube() {
1603 let x = uniform_grid(10);
1604 let s = smoothing_matrix_nw(&x, 0.2, "tricube").unwrap();
1605
1606 assert_eq!(s.len(), 100);
1607
1608 for i in 0..10 {
1610 let row_sum: f64 = (0..10).map(|j| s[i + j * 10]).sum();
1611 assert!(
1612 (row_sum - 1.0).abs() < 1e-10,
1613 "Tricube: row {} should sum to 1, got {}",
1614 i,
1615 row_sum
1616 );
1617 }
1618 }
1619
1620 #[test]
1621 fn test_cv_smoother_tricube() {
1622 let x = uniform_grid(30);
1623 let y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi + 1.0).collect();
1624 let cv = cv_smoother(&x, &y, 0.2, "tricube");
1625 assert!(cv.is_finite());
1626 assert!(cv >= 0.0);
1627 }
1628
1629 #[test]
1630 fn test_optim_bandwidth_tricube() {
1631 let x = uniform_grid(25);
1632 let y: Vec<f64> = x
1633 .iter()
1634 .map(|&xi| (2.0 * std::f64::consts::PI * xi).sin())
1635 .collect();
1636
1637 let result = optim_bandwidth(&x, &y, None, CvCriterion::Gcv, "tricube", 20);
1638 assert!(result.h_opt > 0.0);
1639 assert!(result.value.is_finite());
1640 }
1641}