1use u_numflow::matrix::Matrix;
20use u_numflow::special;
21use u_numflow::stats;
22
23#[derive(Debug, Clone)]
25pub struct SimpleRegressionResult {
26 pub slope: f64,
28 pub intercept: f64,
30 pub r_squared: f64,
32 pub adjusted_r_squared: f64,
34 pub slope_se: f64,
36 pub intercept_se: f64,
38 pub slope_t: f64,
40 pub intercept_t: f64,
42 pub slope_p: f64,
44 pub intercept_p: f64,
46 pub residual_se: f64,
48 pub f_statistic: f64,
50 pub f_p_value: f64,
52 pub residuals: Vec<f64>,
54 pub fitted: Vec<f64>,
56 pub n: usize,
58}
59
60#[derive(Debug, Clone)]
62pub struct MultipleRegressionResult {
63 pub coefficients: Vec<f64>,
65 pub std_errors: Vec<f64>,
67 pub t_statistics: Vec<f64>,
69 pub p_values: Vec<f64>,
71 pub r_squared: f64,
73 pub adjusted_r_squared: f64,
75 pub f_statistic: f64,
77 pub f_p_value: f64,
79 pub residual_se: f64,
81 pub residuals: Vec<f64>,
83 pub fitted: Vec<f64>,
85 pub vif: Vec<f64>,
87 pub n: usize,
89 pub p: usize,
91}
92
93pub fn simple_linear_regression(x: &[f64], y: &[f64]) -> Option<SimpleRegressionResult> {
126 let n = x.len();
127 if n < 3 || n != y.len() {
128 return None;
129 }
130 if x.iter().any(|v| !v.is_finite()) || y.iter().any(|v| !v.is_finite()) {
131 return None;
132 }
133
134 let x_mean = stats::mean(x)?;
135 let y_mean = stats::mean(y)?;
136 let x_var = stats::variance(x)?;
137 let cov = stats::covariance(x, y)?;
138
139 if x_var < 1e-300 {
140 return None; }
142
143 let slope = cov / x_var;
144 let intercept = y_mean - slope * x_mean;
145
146 let fitted: Vec<f64> = x.iter().map(|&xi| intercept + slope * xi).collect();
148 let residuals: Vec<f64> = y
149 .iter()
150 .zip(fitted.iter())
151 .map(|(&yi, &fi)| yi - fi)
152 .collect();
153
154 let ss_res: f64 = residuals.iter().map(|r| r * r).sum();
156 let ss_tot: f64 = y.iter().map(|&yi| (yi - y_mean).powi(2)).sum();
157
158 let nf = n as f64;
159 let df_res = nf - 2.0;
160
161 let r_squared = if ss_tot > 1e-300 {
162 1.0 - ss_res / ss_tot
163 } else {
164 1.0
165 };
166 let adjusted_r_squared = 1.0 - (1.0 - r_squared) * (nf - 1.0) / df_res;
167
168 let mse = ss_res / df_res;
170 let residual_se = mse.sqrt();
171
172 let ss_x: f64 = x.iter().map(|&xi| (xi - x_mean).powi(2)).sum();
174 let slope_se = (mse / ss_x).sqrt();
175 let intercept_se = (mse * (1.0 / nf + x_mean * x_mean / ss_x)).sqrt();
176
177 let slope_t = if slope_se > 1e-300 {
179 slope / slope_se
180 } else {
181 f64::INFINITY
182 };
183 let intercept_t = if intercept_se > 1e-300 {
184 intercept / intercept_se
185 } else {
186 f64::INFINITY
187 };
188
189 let slope_p = 2.0 * special::t_distribution_sf(slope_t.abs(), df_res);
191 let intercept_p = 2.0 * special::t_distribution_sf(intercept_t.abs(), df_res);
192
193 let f_statistic = slope_t * slope_t;
195 let f_p_value = if f_statistic.is_infinite() {
196 0.0
197 } else {
198 special::f_distribution_sf(f_statistic, 1.0, df_res)
199 };
200
201 Some(SimpleRegressionResult {
202 slope,
203 intercept,
204 r_squared,
205 adjusted_r_squared,
206 slope_se,
207 intercept_se,
208 slope_t,
209 intercept_t,
210 slope_p,
211 intercept_p,
212 residual_se,
213 f_statistic,
214 f_p_value,
215 residuals,
216 fitted,
217 n,
218 })
219}
220
221pub fn multiple_linear_regression(
261 predictors: &[&[f64]],
262 y: &[f64],
263) -> Option<MultipleRegressionResult> {
264 let p = predictors.len(); let n = y.len();
266
267 if p == 0 || n < p + 2 {
268 return None;
269 }
270
271 for pred in predictors {
273 if pred.len() != n {
274 return None;
275 }
276 if pred.iter().any(|v| !v.is_finite()) {
277 return None;
278 }
279 }
280 if y.iter().any(|v| !v.is_finite()) {
281 return None;
282 }
283
284 let ncols = p + 1; let mut x_data = Vec::with_capacity(n * ncols);
288 for i in 0..n {
289 x_data.push(1.0); for pred in predictors {
291 x_data.push(pred[i]);
292 }
293 }
294 let x_mat = Matrix::new(n, ncols, x_data).ok()?;
295
296 let mut col_scale = vec![0.0_f64; ncols];
303 for (j, c) in col_scale.iter_mut().enumerate() {
304 let norm = (0..n).map(|i| x_mat.get(i, j).powi(2)).sum::<f64>().sqrt();
305 if !norm.is_finite() || norm == 0.0 {
306 return None;
307 }
308 *c = 1.0 / norm;
309 }
310 let mut scaled_data = Vec::with_capacity(n * ncols);
311 for i in 0..n {
312 for (j, c) in col_scale.iter().enumerate() {
313 scaled_data.push(x_mat.get(i, j) * c);
314 }
315 }
316 let x_scaled = Matrix::new(n, ncols, scaled_data).ok()?;
317
318 let xt = x_scaled.transpose();
320 let xtx = xt.mul_mat(&x_scaled).ok()?;
321
322 let xty = xt.mul_vec(y).ok()?;
324
325 let coefficients: Vec<f64> = xtx
327 .cholesky_solve(&xty)
328 .ok()?
329 .iter()
330 .zip(&col_scale)
331 .map(|(b, c)| b * c)
332 .collect();
333
334 let fitted = x_mat.mul_vec(&coefficients).ok()?;
336 let residuals: Vec<f64> = y
337 .iter()
338 .zip(fitted.iter())
339 .map(|(&yi, &fi)| yi - fi)
340 .collect();
341
342 let y_mean = stats::mean(y)?;
344 let ss_res: f64 = residuals.iter().map(|r| r * r).sum();
345 let ss_tot: f64 = y.iter().map(|&yi| (yi - y_mean).powi(2)).sum();
346
347 let nf = n as f64;
348 let pf = p as f64;
349 let df_res = nf - pf - 1.0;
350
351 let r_squared = if ss_tot > 1e-300 {
352 1.0 - ss_res / ss_tot
353 } else {
354 1.0
355 };
356 let adjusted_r_squared = 1.0 - (1.0 - r_squared) * (nf - 1.0) / df_res;
357
358 let mse = ss_res / df_res;
360 let residual_se = mse.sqrt();
361
362 let xtx_inv = xtx.inverse().ok()?;
364 let mut std_errors = Vec::with_capacity(ncols);
365 let mut t_statistics = Vec::with_capacity(ncols);
366 let mut p_values = Vec::with_capacity(ncols);
367
368 for (j, &coeff_j) in coefficients.iter().enumerate() {
369 let se = (xtx_inv.get(j, j) * col_scale[j] * col_scale[j] * mse).sqrt();
370 std_errors.push(se);
371 let t = if se > 1e-300 {
372 coeff_j / se
373 } else {
374 f64::INFINITY
375 };
376 t_statistics.push(t);
377 let pv = 2.0 * special::t_distribution_sf(t.abs(), df_res);
378 p_values.push(pv);
379 }
380
381 let ss_reg = ss_tot - ss_res;
383 let f_statistic = if pf > 0.0 && mse > 1e-300 {
384 (ss_reg / pf) / mse
385 } else {
386 0.0
387 };
388 let f_p_value = if f_statistic.is_infinite() || f_statistic.is_nan() {
389 0.0
390 } else {
391 special::f_distribution_sf(f_statistic, pf, df_res)
392 };
393
394 let vif = compute_vif(predictors);
397
398 Some(MultipleRegressionResult {
399 coefficients,
400 std_errors,
401 t_statistics,
402 p_values,
403 r_squared,
404 adjusted_r_squared,
405 f_statistic,
406 f_p_value,
407 residual_se,
408 residuals,
409 fitted,
410 vif,
411 n,
412 p,
413 })
414}
415
416fn compute_vif(predictors: &[&[f64]]) -> Vec<f64> {
418 let p = predictors.len();
419 if p < 2 {
420 return vec![1.0; p];
421 }
422
423 let mut vif = Vec::with_capacity(p);
424 for j in 0..p {
425 let y_j = predictors[j];
427 let others: Vec<&[f64]> = predictors
428 .iter()
429 .enumerate()
430 .filter(|&(i, _)| i != j)
431 .map(|(_, v)| *v)
432 .collect();
433
434 if let Some(result) = multiple_linear_regression(&others, y_j) {
435 let r2 = result.r_squared;
436 if r2 < 1.0 - 1e-15 {
437 vif.push(1.0 / (1.0 - r2));
438 } else {
439 vif.push(f64::INFINITY); }
441 } else {
442 vif.push(f64::NAN);
443 }
444 }
445 vif
446}
447
448pub fn predict_simple(model: &SimpleRegressionResult, x_new: &[f64]) -> Vec<f64> {
463 x_new
464 .iter()
465 .map(|&xi| model.intercept + model.slope * xi)
466 .collect()
467}
468
469pub fn predict_multiple(
478 model: &MultipleRegressionResult,
479 predictors_new: &[&[f64]],
480) -> Option<Vec<f64>> {
481 if predictors_new.len() != model.p {
482 return None;
483 }
484 let n = predictors_new.first()?.len();
485 for pred in predictors_new {
486 if pred.len() != n {
487 return None;
488 }
489 }
490
491 let mut result = Vec::with_capacity(n);
492 for i in 0..n {
493 let mut yi = model.coefficients[0]; for (j, pred) in predictors_new.iter().enumerate() {
495 yi += model.coefficients[j + 1] * pred[i];
496 }
497 result.push(yi);
498 }
499 Some(result)
500}
501
502pub fn vif(predictors: &[&[f64]]) -> Option<Vec<f64>> {
535 let p = predictors.len();
536 if p == 0 {
537 return None;
538 }
539 let n = predictors[0].len();
540 if !predictors.iter().all(|c| c.len() == n) {
541 return None;
542 }
543 if n <= p {
544 return None;
545 }
546 if predictors.iter().any(|c| c.iter().any(|v| !v.is_finite())) {
547 return None;
548 }
549 Some(compute_vif(predictors))
550}
551
552pub fn condition_number(predictors: &[&[f64]]) -> Option<f64> {
581 use u_numflow::matrix::Matrix;
582 let p = predictors.len();
583 if p == 0 {
584 return None;
585 }
586 let n = predictors[0].len();
587 if !predictors.iter().all(|c| c.len() == n) {
588 return None;
589 }
590 if n <= p {
591 return None;
592 }
593 if predictors.iter().any(|c| c.iter().any(|v| !v.is_finite())) {
594 return None;
595 }
596
597 let mut means = vec![0.0_f64; p];
599 for (j, col) in predictors.iter().enumerate() {
600 means[j] = col.iter().sum::<f64>() / n as f64;
601 }
602 let mut cov = vec![0.0_f64; p * p];
603 let denom = (n - 1) as f64;
604 for r in 0..p {
605 let col_r = predictors[r];
606 let mean_r = means[r];
607 for c in r..p {
608 let col_c = predictors[c];
609 let mean_c = means[c];
610 let sum: f64 = col_r
611 .iter()
612 .zip(col_c.iter())
613 .map(|(&a, &b)| (a - mean_r) * (b - mean_c))
614 .sum();
615 let v = sum / denom;
616 cov[r * p + c] = v;
617 cov[c * p + r] = v;
618 }
619 }
620
621 let mat = Matrix::new(p, p, cov).ok()?;
622 let (eigenvalues, _eigenvectors) = mat.eigen_symmetric().ok()?;
623 let lambda_max = eigenvalues
624 .iter()
625 .copied()
626 .fold(f64::NEG_INFINITY, f64::max);
627 let lambda_min = eigenvalues.iter().copied().fold(f64::INFINITY, f64::min);
628
629 if !lambda_max.is_finite() || lambda_max <= 0.0 {
630 return None;
631 }
632 if lambda_min <= lambda_max * p as f64 * f64::EPSILON {
637 return Some(f64::INFINITY);
638 }
639 Some(lambda_max / lambda_min)
640}
641
642#[cfg(test)]
643mod tests {
644 use super::*;
645
646 #[test]
651 fn simple_perfect_fit() {
652 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
653 let y = [3.0, 5.0, 7.0, 9.0, 11.0]; let r = simple_linear_regression(&x, &y).expect("should compute");
655 assert!((r.slope - 2.0).abs() < 1e-10);
656 assert!((r.intercept - 1.0).abs() < 1e-10);
657 assert!((r.r_squared - 1.0).abs() < 1e-10);
658 }
659
660 #[test]
661 fn simple_with_noise() {
662 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
663 let y = [2.1, 3.9, 6.1, 7.9, 10.1]; let r = simple_linear_regression(&x, &y).expect("should compute");
665 assert!((r.slope - 2.0).abs() < 0.1);
666 assert!(r.r_squared > 0.99);
667 assert_eq!(r.residuals.len(), 5);
668 assert_eq!(r.fitted.len(), 5);
669 }
670
671 #[test]
672 fn simple_residuals_sum_to_zero() {
673 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
674 let y = [2.1, 3.9, 6.1, 7.9, 10.1];
675 let r = simple_linear_regression(&x, &y).expect("should compute");
676 let sum: f64 = r.residuals.iter().sum();
677 assert!(sum.abs() < 1e-10, "residuals sum = {sum}");
678 }
679
680 #[test]
681 fn simple_f_equals_t_squared() {
682 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
683 let y = [2.1, 3.9, 6.1, 7.9, 10.1];
684 let r = simple_linear_regression(&x, &y).expect("should compute");
685 assert!(
686 (r.f_statistic - r.slope_t * r.slope_t).abs() < 1e-8,
687 "F = {}, t² = {}",
688 r.f_statistic,
689 r.slope_t * r.slope_t
690 );
691 }
692
693 #[test]
694 fn simple_significant_slope() {
695 let x: Vec<f64> = (0..20).map(|i| i as f64).collect();
696 let y: Vec<f64> = x.iter().map(|&xi| 3.0 + 2.0 * xi).collect();
697 let r = simple_linear_regression(&x, &y).expect("should compute");
698 assert!(r.slope_p < 1e-10, "slope p = {}", r.slope_p);
699 assert!(r.f_p_value < 1e-10, "F p = {}", r.f_p_value);
700 }
701
702 #[test]
703 fn simple_negative_slope() {
704 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
705 let y = [10.0, 8.0, 6.0, 4.0, 2.0]; let r = simple_linear_regression(&x, &y).expect("should compute");
707 assert!((r.slope + 2.0).abs() < 1e-10);
708 assert!((r.intercept - 12.0).abs() < 1e-10);
709 }
710
711 #[test]
712 fn simple_edge_cases() {
713 assert!(simple_linear_regression(&[1.0, 2.0], &[3.0, 4.0]).is_none()); assert!(simple_linear_regression(&[1.0, 2.0, 3.0], &[4.0, 5.0]).is_none()); assert!(simple_linear_regression(&[5.0, 5.0, 5.0], &[1.0, 2.0, 3.0]).is_none()); assert!(simple_linear_regression(&[1.0, f64::NAN, 3.0], &[4.0, 5.0, 6.0]).is_none());
717 }
718
719 #[test]
729 fn simple_numeric_reference_ols() {
730 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
731 let y = [2.0, 4.0, 5.0, 4.0, 5.0];
732 let r = simple_linear_regression(&x, &y).expect("should compute");
733
734 assert!(
735 (r.slope - 0.6).abs() < 1e-10,
736 "β̂₁ expected 0.6, got {}",
737 r.slope
738 );
739 assert!(
740 (r.intercept - 2.2).abs() < 1e-10,
741 "β̂₀ expected 2.2, got {}",
742 r.intercept
743 );
744 assert!(
745 (r.r_squared - 0.6).abs() < 1e-3,
746 "R² expected 0.6, got {}",
747 r.r_squared
748 );
749
750 let expected_fitted = [2.8, 3.4, 4.0, 4.6, 5.2];
753 for (i, (&fi, &ef)) in r.fitted.iter().zip(expected_fitted.iter()).enumerate() {
754 assert!(
755 (fi - ef).abs() < 1e-10,
756 "ŷ_{} expected {}, got {}",
757 i + 1,
758 ef,
759 fi
760 );
761 }
762 }
763
764 #[test]
765 fn simple_adjusted_r_squared() {
766 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
767 let y = [2.1, 3.9, 6.1, 7.9, 10.1];
768 let r = simple_linear_regression(&x, &y).expect("should compute");
769 assert!(r.adjusted_r_squared <= r.r_squared);
771 assert!(r.adjusted_r_squared > 0.98);
772 }
773
774 #[test]
779 fn multiple_perfect_fit() {
780 let x1 = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
782 let x2 = [2.0, 1.0, 3.0, 2.0, 4.0, 3.0, 5.0, 4.0, 6.0, 5.0];
783 let y: Vec<f64> = x1
784 .iter()
785 .zip(x2.iter())
786 .map(|(&a, &b)| 1.0 + 2.0 * a + 3.0 * b)
787 .collect();
788 let r = multiple_linear_regression(&[&x1, &x2], &y).expect("should compute");
789
790 assert!(
791 (r.coefficients[0] - 1.0).abs() < 1e-8,
792 "β₀ = {}",
793 r.coefficients[0]
794 );
795 assert!(
796 (r.coefficients[1] - 2.0).abs() < 1e-8,
797 "β₁ = {}",
798 r.coefficients[1]
799 );
800 assert!(
801 (r.coefficients[2] - 3.0).abs() < 1e-8,
802 "β₂ = {}",
803 r.coefficients[2]
804 );
805 assert!((r.r_squared - 1.0).abs() < 1e-8);
806 }
807
808 #[test]
809 fn multiple_with_noise() {
810 let x1 = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
811 let x2 = [2.0, 1.0, 3.0, 2.0, 4.0, 3.0, 5.0, 4.0];
812 let y = [5.1, 5.0, 9.2, 8.9, 13.1, 12.0, 17.2, 15.9];
813 let r = multiple_linear_regression(&[&x1, &x2], &y).expect("should compute");
814 assert!(r.r_squared > 0.95);
815 assert_eq!(r.coefficients.len(), 3);
816 assert_eq!(r.std_errors.len(), 3);
817 assert_eq!(r.residuals.len(), 8);
818 assert_eq!(r.vif.len(), 2);
819 }
820
821 #[test]
822 fn multiple_residuals_sum_to_zero() {
823 let x1 = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
824 let x2 = [2.0, 1.0, 3.0, 2.0, 4.0, 3.0, 5.0, 4.0];
825 let y = [5.1, 5.0, 9.2, 8.9, 13.1, 12.0, 17.2, 15.9];
826 let r = multiple_linear_regression(&[&x1, &x2], &y).expect("should compute");
827 let sum: f64 = r.residuals.iter().sum();
828 assert!(sum.abs() < 1e-8, "residuals sum = {sum}");
829 }
830
831 #[test]
832 fn multiple_single_predictor_matches_simple() {
833 let x = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
834 let y: Vec<f64> = x
835 .iter()
836 .map(|&xi| 3.0 + 2.5 * xi + 0.1 * (xi - 5.0))
837 .collect();
838
839 let simple = simple_linear_regression(&x, &y).expect("simple");
840 let multi = multiple_linear_regression(&[&x], &y).expect("multiple");
841
842 assert!(
843 (simple.slope - multi.coefficients[1]).abs() < 1e-8,
844 "slope: {} vs {}",
845 simple.slope,
846 multi.coefficients[1]
847 );
848 assert!(
849 (simple.intercept - multi.coefficients[0]).abs() < 1e-8,
850 "intercept: {} vs {}",
851 simple.intercept,
852 multi.coefficients[0]
853 );
854 assert!((simple.r_squared - multi.r_squared).abs() < 1e-8);
855 }
856
857 #[test]
858 fn multiple_edge_cases() {
859 let x1 = [1.0, 2.0];
860 let y = [3.0, 4.0];
861 assert!(multiple_linear_regression(&[&x1], &y).is_none()); let x2 = [1.0, 2.0, 3.0];
864 let y2 = [4.0, 5.0];
865 assert!(multiple_linear_regression(&[&x2], &y2).is_none()); }
867
868 #[test]
869 fn multiple_vif_independent_predictors() {
870 let x1 = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0];
872 let x2 = [0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
873 let y: Vec<f64> = x1
874 .iter()
875 .zip(x2.iter())
876 .map(|(&a, &b)| 1.0 + 2.0 * a + 3.0 * b)
877 .collect();
878 let r = multiple_linear_regression(&[&x1, &x2], &y).expect("should compute");
879 for (i, &v) in r.vif.iter().enumerate() {
880 assert!(
881 v < 2.0,
882 "VIF[{i}] = {v}, expected near 1.0 for independent predictors"
883 );
884 }
885 }
886
887 #[test]
888 fn multiple_vif_correlated_predictors() {
889 let x1: Vec<f64> = (0..20).map(|i| i as f64).collect();
891 let noise = [
893 0.1, -0.2, 0.3, -0.1, 0.2, -0.3, 0.1, -0.1, 0.2, -0.2, 0.3, -0.1, 0.1, -0.2, 0.3, -0.3,
894 0.1, -0.1, 0.2, -0.2,
895 ];
896 let x2: Vec<f64> = x1
897 .iter()
898 .zip(noise.iter())
899 .map(|(&v, &n)| v * 0.9 + 1.0 + n)
900 .collect();
901 let y: Vec<f64> = x1
902 .iter()
903 .zip(x2.iter())
904 .map(|(&a, &b)| 1.0 + a + b)
905 .collect();
906 let r = multiple_linear_regression(&[&x1, &x2], &y).expect("should compute");
907 assert!(
909 r.vif[0] > 5.0,
910 "VIF[0] = {}, expected > 5.0 for correlated predictors",
911 r.vif[0]
912 );
913 }
914
915 #[test]
920 fn predict_simple_basic() {
921 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
922 let y = [3.0, 5.0, 7.0, 9.0, 11.0]; let model = simple_linear_regression(&x, &y).expect("should compute");
924 let pred = predict_simple(&model, &[6.0, 7.0]);
925 assert!((pred[0] - 13.0).abs() < 1e-10);
926 assert!((pred[1] - 15.0).abs() < 1e-10);
927 }
928
929 #[test]
930 fn predict_multiple_basic() {
931 let x1 = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
932 let x2 = [2.0, 1.0, 3.0, 2.0, 4.0, 3.0, 5.0, 4.0, 6.0, 5.0];
933 let y: Vec<f64> = x1
934 .iter()
935 .zip(x2.iter())
936 .map(|(&a, &b)| 1.0 + 2.0 * a + 3.0 * b)
937 .collect();
938 let model = multiple_linear_regression(&[&x1, &x2], &y).expect("should compute");
939
940 let new_x1 = [11.0];
941 let new_x2 = [6.0];
942 let pred = predict_multiple(&model, &[&new_x1, &new_x2]).expect("should predict");
943 let expected = 1.0 + 2.0 * 11.0 + 3.0 * 6.0;
944 assert!(
945 (pred[0] - expected).abs() < 1e-6,
946 "pred = {}, expected = {}",
947 pred[0],
948 expected
949 );
950 }
951
952 #[test]
957 fn vif_pub_independent_predictors() {
958 let x1 = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
959 let x2 = [8.0, 1.0, 6.0, 3.0, 5.0, 7.0, 2.0, 4.0];
960 let v = vif(&[&x1, &x2]).expect("should compute");
961 for r in &v {
962 assert!(*r < 2.0, "VIF should be near 1.0 for independent, got {r}");
963 }
964 }
965
966 #[test]
967 fn vif_pub_collinear_predictors() {
968 let x1: Vec<f64> = (0..20).map(|i| i as f64).collect();
969 let x2: Vec<f64> = x1.iter().map(|&v| v * 2.0 + 0.001).collect();
970 let v = vif(&[&x1[..], &x2[..]]).expect("should compute");
971 assert!(
972 v[0] > 50.0,
973 "VIF should explode under near-collinearity, got {}",
974 v[0]
975 );
976 }
977
978 #[test]
979 fn vif_pub_rejects_short_input() {
980 let x = [1.0, 2.0];
981 assert!(vif(&[&x[..], &x[..]]).is_none());
982 }
983
984 #[test]
985 fn vif_pub_rejects_non_finite() {
986 let x1 = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
987 let x2 = [1.0, 2.0, f64::NAN, 4.0, 5.0, 6.0];
988 assert!(vif(&[&x1[..], &x2[..]]).is_none());
989 }
990
991 #[test]
992 fn condition_number_orthogonal() {
993 let x1 = [1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0];
994 let x2 = [1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0];
995 let c = condition_number(&[&x1[..], &x2[..]]).expect("should compute");
996 assert!(c < 5.0, "near-orthogonal: cond should be small, got {c}");
997 }
998
999 #[test]
1000 fn condition_number_collinear_explodes() {
1001 let x1: Vec<f64> = (0..20).map(|i| i as f64).collect();
1002 let x2: Vec<f64> = x1.iter().map(|&v| v * 2.0 + 1e-6).collect();
1003 let c = condition_number(&[&x1[..], &x2[..]]).expect("should compute");
1004 assert!(
1005 c > 1e5 || c.is_infinite(),
1006 "collinear: cond should be huge, got {c}"
1007 );
1008 }
1009
1010 #[test]
1011 fn condition_number_rejects_short_input() {
1012 let x = [1.0, 2.0];
1013 assert!(condition_number(&[&x[..], &x[..]]).is_none());
1014 }
1015
1016 #[test]
1017 fn predict_multiple_wrong_predictors() {
1018 let x1 = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
1019 let x2 = [2.0, 1.0, 3.0, 2.0, 4.0, 3.0, 5.0, 4.0, 6.0, 5.0];
1020 let y: Vec<f64> = x1.iter().zip(x2.iter()).map(|(&a, &b)| a + b).collect();
1021 let model = multiple_linear_regression(&[&x1, &x2], &y).expect("should compute");
1022
1023 assert!(predict_multiple(&model, &[&[1.0]]).is_none());
1025 }
1026}
1027
1028#[cfg(test)]
1029mod proptests {
1030 use super::*;
1031 use proptest::prelude::*;
1032
1033 proptest! {
1034 #[test]
1035 fn simple_r_squared_bounded(
1036 data in proptest::collection::vec(-1e3_f64..1e3, 5..=30)
1037 .prop_flat_map(|x| {
1038 let n = x.len();
1039 (Just(x), proptest::collection::vec(-1e3_f64..1e3, n..=n))
1040 })
1041 ) {
1042 let (x, y) = data;
1043 if let Some(r) = simple_linear_regression(&x, &y) {
1044 prop_assert!(r.r_squared >= -0.01 && r.r_squared <= 1.01,
1045 "R² = {}", r.r_squared);
1046 }
1047 }
1048
1049 #[test]
1050 fn simple_residuals_orthogonal_to_x(
1051 data in proptest::collection::vec(-1e3_f64..1e3, 5..=30)
1052 .prop_flat_map(|x| {
1053 let n = x.len();
1054 (Just(x), proptest::collection::vec(-1e3_f64..1e3, n..=n))
1055 })
1056 ) {
1057 let (x, y) = data;
1058 if let Some(r) = simple_linear_regression(&x, &y) {
1059 let dot: f64 = x.iter().zip(r.residuals.iter()).map(|(&xi, &ei)| xi * ei).sum();
1061 let norm = r.residuals.iter().map(|e| e * e).sum::<f64>().sqrt();
1062 let x_norm = x.iter().map(|xi| xi * xi).sum::<f64>().sqrt();
1063 if norm > 1e-10 && x_norm > 1e-10 {
1064 prop_assert!((dot / (norm * x_norm)).abs() < 1e-6,
1065 "residuals not orthogonal to x: dot={dot}");
1066 }
1067 }
1068 }
1069
1070 #[test]
1071 fn multiple_r_squared_bounded(
1072 x1 in proptest::collection::vec(-1e3_f64..1e3, 8..=20),
1073 x2_seed in proptest::collection::vec(-1e3_f64..1e3, 8..=20),
1074 y_seed in proptest::collection::vec(-1e3_f64..1e3, 8..=20),
1075 ) {
1076 let n = x1.len().min(x2_seed.len()).min(y_seed.len());
1077 let x2 = &x2_seed[..n];
1078 let y = &y_seed[..n];
1079 let x1 = &x1[..n];
1080 if let Some(r) = multiple_linear_regression(&[x1, x2], y) {
1081 prop_assert!(r.r_squared >= -0.01 && r.r_squared <= 1.01,
1082 "R² = {}", r.r_squared);
1083 }
1084 }
1085 }
1086}
1087
1088#[cfg(test)]
1092mod scipy_tail_reference {
1093 use super::*;
1094
1095 fn close(got: f64, want: f64, what: &str) {
1096 assert!(
1097 (got - want).abs() <= 1e-9 * want.abs(),
1098 "{what}: got {got:e}, SciPy {want:e}"
1099 );
1100 }
1101
1102 #[test]
1103 fn simple_regression_p_values_keep_their_tail() {
1104 let x: Vec<f64> = (1..=20).map(f64::from).collect();
1105 let y = [
1106 5.3999999999999995,
1107 7.0,
1108 9.4,
1109 11.8,
1110 13.2,
1111 15.8,
1112 17.6,
1113 19.8,
1114 22.250000000000004,
1115 23.75,
1116 26.25,
1117 28.150000000000002,
1118 30.55,
1119 32.050000000000004,
1120 34.55,
1121 36.7,
1122 38.550000000000004,
1123 41.1,
1124 42.699999999999996,
1125 45.0,
1126 ];
1127 let r = simple_linear_regression(&x, &y).expect("valid");
1128 close(r.slope_p, 8.203356920391111e-33, "slope p");
1129 close(r.intercept_p, 1.5689270135401997e-16, "intercept p");
1130 close(r.f_p_value, 8.203356920397853e-33, "F p");
1131 }
1132 #[test]
1137 fn multiple_regression_and_conditioning_do_not_depend_on_the_unit() {
1138 let n = 24;
1139 let x1: Vec<f64> = (0..n)
1140 .map(|i| (i as f64 * 0.7).sin() * 3.0 + i as f64 * 0.1)
1141 .collect();
1142 let x2: Vec<f64> = (0..n)
1143 .map(|i| (i as f64 * 1.3).cos() + 0.5 * x1[i])
1144 .collect();
1145 let y: Vec<f64> = (0..n)
1146 .map(|i| 1.0 + 2.0 * x1[i] - 0.5 * x2[i] + (i as f64 * 2.1).sin() * 0.3)
1147 .collect();
1148 let base = multiple_linear_regression(&[&x1, &x2], &y).expect("valid");
1149 let cond = condition_number(&[&x1, &x2]).expect("valid");
1150 for factor in [1e-9, 1e6] {
1151 let s1: Vec<f64> = x1.iter().map(|v| v * factor).collect();
1152 let s2: Vec<f64> = x2.iter().map(|v| v * factor).collect();
1153 let r = multiple_linear_regression(&[&s1, &s2], &y)
1154 .unwrap_or_else(|| panic!("a fit in units of {factor:e} was refused"));
1155 close(r.r_squared, base.r_squared, "R2");
1156 close(r.f_p_value, base.f_p_value, "F p");
1157 close(r.coefficients[0], base.coefficients[0], "intercept");
1158 for j in 0..2 {
1159 close(
1160 r.coefficients[j + 1] * factor,
1161 base.coefficients[j + 1],
1162 "slope",
1163 );
1164 close(r.p_values[j + 1], base.p_values[j + 1], "slope p");
1165 close(r.vif[j], base.vif[j], "vif");
1166 }
1167 close(
1168 condition_number(&[&s1, &s2]).expect("valid"),
1169 cond,
1170 "condition number",
1171 );
1172 }
1173 }
1174}