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 intercept_slope_covariance: f64,
60 pub n: usize,
62}
63
64#[derive(Debug, Clone)]
66pub struct MultipleRegressionResult {
67 pub coefficients: Vec<f64>,
69 pub std_errors: Vec<f64>,
71 pub t_statistics: Vec<f64>,
73 pub p_values: Vec<f64>,
75 pub r_squared: f64,
77 pub adjusted_r_squared: f64,
79 pub f_statistic: f64,
81 pub f_p_value: f64,
83 pub residual_se: f64,
85 pub residuals: Vec<f64>,
87 pub fitted: Vec<f64>,
89 pub vif: Vec<f64>,
91 pub covariance: Vec<f64>,
95 pub n: usize,
97 pub p: usize,
99}
100
101pub fn simple_linear_regression(x: &[f64], y: &[f64]) -> Option<SimpleRegressionResult> {
134 let n = x.len();
135 if n < 3 || n != y.len() {
136 return None;
137 }
138 if x.iter().any(|v| !v.is_finite()) || y.iter().any(|v| !v.is_finite()) {
139 return None;
140 }
141
142 let x_mean = stats::mean(x)?;
143 let y_mean = stats::mean(y)?;
144 let x_var = stats::variance(x)?;
145 let cov = stats::covariance(x, y)?;
146
147 if x_var < 1e-300 {
148 return None; }
150
151 let slope = cov / x_var;
152 let intercept = y_mean - slope * x_mean;
153
154 let fitted: Vec<f64> = x.iter().map(|&xi| intercept + slope * xi).collect();
156 let residuals: Vec<f64> = y
157 .iter()
158 .zip(fitted.iter())
159 .map(|(&yi, &fi)| yi - fi)
160 .collect();
161
162 let ss_res: f64 = residuals.iter().map(|r| r * r).sum();
164 let ss_tot: f64 = y.iter().map(|&yi| (yi - y_mean).powi(2)).sum();
165
166 let nf = n as f64;
167 let df_res = nf - 2.0;
168
169 let r_squared = if ss_tot > 1e-300 {
170 1.0 - ss_res / ss_tot
171 } else {
172 1.0
173 };
174 let adjusted_r_squared = 1.0 - (1.0 - r_squared) * (nf - 1.0) / df_res;
175
176 let mse = ss_res / df_res;
178 let residual_se = mse.sqrt();
179
180 let ss_x: f64 = x.iter().map(|&xi| (xi - x_mean).powi(2)).sum();
182 let slope_se = (mse / ss_x).sqrt();
183 let intercept_se = (mse * (1.0 / nf + x_mean * x_mean / ss_x)).sqrt();
184 let intercept_slope_covariance = -x_mean * mse / ss_x;
185
186 let slope_t = if slope_se > 1e-300 {
188 slope / slope_se
189 } else {
190 f64::INFINITY
191 };
192 let intercept_t = if intercept_se > 1e-300 {
193 intercept / intercept_se
194 } else {
195 f64::INFINITY
196 };
197
198 let slope_p = 2.0 * special::t_distribution_sf(slope_t.abs(), df_res);
200 let intercept_p = 2.0 * special::t_distribution_sf(intercept_t.abs(), df_res);
201
202 let f_statistic = slope_t * slope_t;
204 let f_p_value = if f_statistic.is_infinite() {
205 0.0
206 } else {
207 special::f_distribution_sf(f_statistic, 1.0, df_res)
208 };
209
210 Some(SimpleRegressionResult {
211 slope,
212 intercept,
213 r_squared,
214 adjusted_r_squared,
215 slope_se,
216 intercept_se,
217 slope_t,
218 intercept_t,
219 slope_p,
220 intercept_p,
221 residual_se,
222 f_statistic,
223 f_p_value,
224 residuals,
225 fitted,
226 intercept_slope_covariance,
227 n,
228 })
229}
230
231pub fn multiple_linear_regression(
271 predictors: &[&[f64]],
272 y: &[f64],
273) -> Option<MultipleRegressionResult> {
274 let p = predictors.len(); let n = y.len();
276
277 if p == 0 || n < p + 2 {
278 return None;
279 }
280
281 for pred in predictors {
283 if pred.len() != n {
284 return None;
285 }
286 if pred.iter().any(|v| !v.is_finite()) {
287 return None;
288 }
289 }
290 if y.iter().any(|v| !v.is_finite()) {
291 return None;
292 }
293
294 let ncols = p + 1; let mut x_data = Vec::with_capacity(n * ncols);
298 for i in 0..n {
299 x_data.push(1.0); for pred in predictors {
301 x_data.push(pred[i]);
302 }
303 }
304 let x_mat = Matrix::new(n, ncols, x_data).ok()?;
305
306 let mut col_scale = vec![0.0_f64; ncols];
313 for (j, c) in col_scale.iter_mut().enumerate() {
314 let norm = (0..n).map(|i| x_mat.get(i, j).powi(2)).sum::<f64>().sqrt();
315 if !norm.is_finite() || norm == 0.0 {
316 return None;
317 }
318 *c = 1.0 / norm;
319 }
320 let mut scaled_data = Vec::with_capacity(n * ncols);
321 for i in 0..n {
322 for (j, c) in col_scale.iter().enumerate() {
323 scaled_data.push(x_mat.get(i, j) * c);
324 }
325 }
326 let x_scaled = Matrix::new(n, ncols, scaled_data).ok()?;
327
328 let xt = x_scaled.transpose();
330 let xtx = xt.mul_mat(&x_scaled).ok()?;
331
332 let xty = xt.mul_vec(y).ok()?;
334
335 let coefficients: Vec<f64> = xtx
337 .cholesky_solve(&xty)
338 .ok()?
339 .iter()
340 .zip(&col_scale)
341 .map(|(b, c)| b * c)
342 .collect();
343
344 let fitted = x_mat.mul_vec(&coefficients).ok()?;
346 let residuals: Vec<f64> = y
347 .iter()
348 .zip(fitted.iter())
349 .map(|(&yi, &fi)| yi - fi)
350 .collect();
351
352 let y_mean = stats::mean(y)?;
354 let ss_res: f64 = residuals.iter().map(|r| r * r).sum();
355 let ss_tot: f64 = y.iter().map(|&yi| (yi - y_mean).powi(2)).sum();
356
357 let nf = n as f64;
358 let pf = p as f64;
359 let df_res = nf - pf - 1.0;
360
361 let r_squared = if ss_tot > 1e-300 {
362 1.0 - ss_res / ss_tot
363 } else {
364 1.0
365 };
366 let adjusted_r_squared = 1.0 - (1.0 - r_squared) * (nf - 1.0) / df_res;
367
368 let mse = ss_res / df_res;
370 let residual_se = mse.sqrt();
371
372 let xtx_inv = xtx.inverse().ok()?;
374 let mut covariance = Vec::with_capacity(ncols * ncols);
375 for i in 0..ncols {
376 for j in 0..ncols {
377 covariance.push(xtx_inv.get(i, j) * col_scale[i] * col_scale[j] * mse);
378 }
379 }
380 let mut std_errors = Vec::with_capacity(ncols);
381 let mut t_statistics = Vec::with_capacity(ncols);
382 let mut p_values = Vec::with_capacity(ncols);
383
384 for (j, &coeff_j) in coefficients.iter().enumerate() {
385 let se = covariance[j * ncols + j].sqrt();
386 std_errors.push(se);
387 let t = if se > 1e-300 {
388 coeff_j / se
389 } else {
390 f64::INFINITY
391 };
392 t_statistics.push(t);
393 let pv = 2.0 * special::t_distribution_sf(t.abs(), df_res);
394 p_values.push(pv);
395 }
396
397 let ss_reg = ss_tot - ss_res;
399 let f_statistic = if pf > 0.0 && mse > 1e-300 {
400 (ss_reg / pf) / mse
401 } else {
402 0.0
403 };
404 let f_p_value = if f_statistic.is_infinite() || f_statistic.is_nan() {
405 0.0
406 } else {
407 special::f_distribution_sf(f_statistic, pf, df_res)
408 };
409
410 let vif = compute_vif(predictors);
413
414 Some(MultipleRegressionResult {
415 coefficients,
416 std_errors,
417 t_statistics,
418 p_values,
419 r_squared,
420 adjusted_r_squared,
421 f_statistic,
422 f_p_value,
423 residual_se,
424 residuals,
425 fitted,
426 vif,
427 covariance,
428 n,
429 p,
430 })
431}
432
433fn compute_vif(predictors: &[&[f64]]) -> Vec<f64> {
435 let p = predictors.len();
436 if p < 2 {
437 return vec![1.0; p];
438 }
439
440 let mut vif = Vec::with_capacity(p);
441 for j in 0..p {
442 let y_j = predictors[j];
444 let others: Vec<&[f64]> = predictors
445 .iter()
446 .enumerate()
447 .filter(|&(i, _)| i != j)
448 .map(|(_, v)| *v)
449 .collect();
450
451 if let Some(result) = multiple_linear_regression(&others, y_j) {
452 let r2 = result.r_squared;
453 if r2 < 1.0 - 1e-15 {
454 vif.push(1.0 / (1.0 - r2));
455 } else {
456 vif.push(f64::INFINITY); }
458 } else {
459 vif.push(f64::NAN);
460 }
461 }
462 vif
463}
464
465pub fn predict_simple(model: &SimpleRegressionResult, x_new: &[f64]) -> Vec<f64> {
480 x_new
481 .iter()
482 .map(|&xi| model.intercept + model.slope * xi)
483 .collect()
484}
485
486pub fn predict_multiple(
495 model: &MultipleRegressionResult,
496 predictors_new: &[&[f64]],
497) -> Option<Vec<f64>> {
498 if predictors_new.len() != model.p {
499 return None;
500 }
501 let n = predictors_new.first()?.len();
502 for pred in predictors_new {
503 if pred.len() != n {
504 return None;
505 }
506 }
507
508 let mut result = Vec::with_capacity(n);
509 for i in 0..n {
510 let mut yi = model.coefficients[0]; for (j, pred) in predictors_new.iter().enumerate() {
512 yi += model.coefficients[j + 1] * pred[i];
513 }
514 result.push(yi);
515 }
516 Some(result)
517}
518
519#[derive(Debug, Clone, Copy, PartialEq)]
521pub struct RegressionPrediction {
522 pub fit: f64,
524 pub se_fit: f64,
527 pub confidence_lower: f64,
530 pub confidence_upper: f64,
532 pub prediction_lower: f64,
535 pub prediction_upper: f64,
537}
538
539fn prediction_at(fit: f64, var_fit: f64, mse: f64, df: f64, level: f64) -> RegressionPrediction {
543 let t = special::t_distribution_quantile(0.5 + level / 2.0, df);
544 let se_fit = var_fit.max(0.0).sqrt();
547 let se_new = (mse + var_fit.max(0.0)).sqrt();
548 RegressionPrediction {
549 fit,
550 se_fit,
551 confidence_lower: fit - t * se_fit,
552 confidence_upper: fit + t * se_fit,
553 prediction_lower: fit - t * se_new,
554 prediction_upper: fit + t * se_new,
555 }
556}
557
558pub fn predict_simple_intervals(
588 model: &SimpleRegressionResult,
589 x_new: &[f64],
590 level: f64,
591) -> Option<Vec<RegressionPrediction>> {
592 if !(level > 0.0 && level < 1.0) || x_new.iter().any(|v| !v.is_finite()) {
593 return None;
594 }
595 let df = model.n as f64 - 2.0;
596 let mse = model.residual_se * model.residual_se;
597 Some(
598 x_new
599 .iter()
600 .map(|&x0| {
601 let var_fit = model.intercept_se * model.intercept_se
602 + x0 * x0 * model.slope_se * model.slope_se
603 + 2.0 * x0 * model.intercept_slope_covariance;
604 prediction_at(model.intercept + model.slope * x0, var_fit, mse, df, level)
605 })
606 .collect(),
607 )
608}
609
610pub fn predict_multiple_intervals(
643 model: &MultipleRegressionResult,
644 predictors_new: &[&[f64]],
645 level: f64,
646) -> Option<Vec<RegressionPrediction>> {
647 if !(level > 0.0 && level < 1.0) {
648 return None;
649 }
650 let fits = predict_multiple(model, predictors_new)?;
651 if predictors_new
652 .iter()
653 .any(|p| p.iter().any(|v| !v.is_finite()))
654 {
655 return None;
656 }
657 let k = model.p + 1;
658 let df = model.n as f64 - k as f64;
659 let mse = model.residual_se * model.residual_se;
660 Some(
661 fits.iter()
662 .enumerate()
663 .map(|(i, &fit)| {
664 let x0: Vec<f64> = std::iter::once(1.0)
665 .chain(predictors_new.iter().map(|p| p[i]))
666 .collect();
667 let var_fit: f64 = (0..k)
668 .map(|a| {
669 (0..k)
670 .map(|b| x0[a] * model.covariance[a * k + b] * x0[b])
671 .sum::<f64>()
672 })
673 .sum();
674 prediction_at(fit, var_fit, mse, df, level)
675 })
676 .collect(),
677 )
678}
679
680pub fn vif(predictors: &[&[f64]]) -> Option<Vec<f64>> {
713 let p = predictors.len();
714 if p == 0 {
715 return None;
716 }
717 let n = predictors[0].len();
718 if !predictors.iter().all(|c| c.len() == n) {
719 return None;
720 }
721 if n <= p {
722 return None;
723 }
724 if predictors.iter().any(|c| c.iter().any(|v| !v.is_finite())) {
725 return None;
726 }
727 Some(compute_vif(predictors))
728}
729
730pub fn condition_number(predictors: &[&[f64]]) -> Option<f64> {
759 use u_numflow::matrix::Matrix;
760 let p = predictors.len();
761 if p == 0 {
762 return None;
763 }
764 let n = predictors[0].len();
765 if !predictors.iter().all(|c| c.len() == n) {
766 return None;
767 }
768 if n <= p {
769 return None;
770 }
771 if predictors.iter().any(|c| c.iter().any(|v| !v.is_finite())) {
772 return None;
773 }
774
775 let mut means = vec![0.0_f64; p];
777 for (j, col) in predictors.iter().enumerate() {
778 means[j] = col.iter().sum::<f64>() / n as f64;
779 }
780 let mut cov = vec![0.0_f64; p * p];
781 let denom = (n - 1) as f64;
782 for r in 0..p {
783 let col_r = predictors[r];
784 let mean_r = means[r];
785 for c in r..p {
786 let col_c = predictors[c];
787 let mean_c = means[c];
788 let sum: f64 = col_r
789 .iter()
790 .zip(col_c.iter())
791 .map(|(&a, &b)| (a - mean_r) * (b - mean_c))
792 .sum();
793 let v = sum / denom;
794 cov[r * p + c] = v;
795 cov[c * p + r] = v;
796 }
797 }
798
799 let mat = Matrix::new(p, p, cov).ok()?;
800 let (eigenvalues, _eigenvectors) = mat.eigen_symmetric().ok()?;
801 let lambda_max = eigenvalues
802 .iter()
803 .copied()
804 .fold(f64::NEG_INFINITY, f64::max);
805 let lambda_min = eigenvalues.iter().copied().fold(f64::INFINITY, f64::min);
806
807 if !lambda_max.is_finite() || lambda_max <= 0.0 {
808 return None;
809 }
810 if lambda_min <= lambda_max * p as f64 * f64::EPSILON {
815 return Some(f64::INFINITY);
816 }
817 Some(lambda_max / lambda_min)
818}
819
820#[cfg(test)]
821mod tests {
822 use super::*;
823
824 #[test]
829 fn simple_perfect_fit() {
830 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
831 let y = [3.0, 5.0, 7.0, 9.0, 11.0]; let r = simple_linear_regression(&x, &y).expect("should compute");
833 assert!((r.slope - 2.0).abs() < 1e-10);
834 assert!((r.intercept - 1.0).abs() < 1e-10);
835 assert!((r.r_squared - 1.0).abs() < 1e-10);
836 }
837
838 #[test]
839 fn simple_with_noise() {
840 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
841 let y = [2.1, 3.9, 6.1, 7.9, 10.1]; let r = simple_linear_regression(&x, &y).expect("should compute");
843 assert!((r.slope - 2.0).abs() < 0.1);
844 assert!(r.r_squared > 0.99);
845 assert_eq!(r.residuals.len(), 5);
846 assert_eq!(r.fitted.len(), 5);
847 }
848
849 #[test]
850 fn simple_residuals_sum_to_zero() {
851 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
852 let y = [2.1, 3.9, 6.1, 7.9, 10.1];
853 let r = simple_linear_regression(&x, &y).expect("should compute");
854 let sum: f64 = r.residuals.iter().sum();
855 assert!(sum.abs() < 1e-10, "residuals sum = {sum}");
856 }
857
858 #[test]
859 fn simple_f_equals_t_squared() {
860 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
861 let y = [2.1, 3.9, 6.1, 7.9, 10.1];
862 let r = simple_linear_regression(&x, &y).expect("should compute");
863 assert!(
864 (r.f_statistic - r.slope_t * r.slope_t).abs() < 1e-8,
865 "F = {}, t² = {}",
866 r.f_statistic,
867 r.slope_t * r.slope_t
868 );
869 }
870
871 #[test]
872 fn simple_significant_slope() {
873 let x: Vec<f64> = (0..20).map(|i| i as f64).collect();
874 let y: Vec<f64> = x.iter().map(|&xi| 3.0 + 2.0 * xi).collect();
875 let r = simple_linear_regression(&x, &y).expect("should compute");
876 assert!(r.slope_p < 1e-10, "slope p = {}", r.slope_p);
877 assert!(r.f_p_value < 1e-10, "F p = {}", r.f_p_value);
878 }
879
880 #[test]
881 fn simple_negative_slope() {
882 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
883 let y = [10.0, 8.0, 6.0, 4.0, 2.0]; let r = simple_linear_regression(&x, &y).expect("should compute");
885 assert!((r.slope + 2.0).abs() < 1e-10);
886 assert!((r.intercept - 12.0).abs() < 1e-10);
887 }
888
889 #[test]
890 fn simple_edge_cases() {
891 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());
895 }
896
897 #[test]
907 fn simple_numeric_reference_ols() {
908 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
909 let y = [2.0, 4.0, 5.0, 4.0, 5.0];
910 let r = simple_linear_regression(&x, &y).expect("should compute");
911
912 assert!(
913 (r.slope - 0.6).abs() < 1e-10,
914 "β̂₁ expected 0.6, got {}",
915 r.slope
916 );
917 assert!(
918 (r.intercept - 2.2).abs() < 1e-10,
919 "β̂₀ expected 2.2, got {}",
920 r.intercept
921 );
922 assert!(
923 (r.r_squared - 0.6).abs() < 1e-3,
924 "R² expected 0.6, got {}",
925 r.r_squared
926 );
927
928 let expected_fitted = [2.8, 3.4, 4.0, 4.6, 5.2];
931 for (i, (&fi, &ef)) in r.fitted.iter().zip(expected_fitted.iter()).enumerate() {
932 assert!(
933 (fi - ef).abs() < 1e-10,
934 "ŷ_{} expected {}, got {}",
935 i + 1,
936 ef,
937 fi
938 );
939 }
940 }
941
942 #[test]
943 fn simple_adjusted_r_squared() {
944 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
945 let y = [2.1, 3.9, 6.1, 7.9, 10.1];
946 let r = simple_linear_regression(&x, &y).expect("should compute");
947 assert!(r.adjusted_r_squared <= r.r_squared);
949 assert!(r.adjusted_r_squared > 0.98);
950 }
951
952 #[test]
957 fn multiple_perfect_fit() {
958 let x1 = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
960 let x2 = [2.0, 1.0, 3.0, 2.0, 4.0, 3.0, 5.0, 4.0, 6.0, 5.0];
961 let y: Vec<f64> = x1
962 .iter()
963 .zip(x2.iter())
964 .map(|(&a, &b)| 1.0 + 2.0 * a + 3.0 * b)
965 .collect();
966 let r = multiple_linear_regression(&[&x1, &x2], &y).expect("should compute");
967
968 assert!(
969 (r.coefficients[0] - 1.0).abs() < 1e-8,
970 "β₀ = {}",
971 r.coefficients[0]
972 );
973 assert!(
974 (r.coefficients[1] - 2.0).abs() < 1e-8,
975 "β₁ = {}",
976 r.coefficients[1]
977 );
978 assert!(
979 (r.coefficients[2] - 3.0).abs() < 1e-8,
980 "β₂ = {}",
981 r.coefficients[2]
982 );
983 assert!((r.r_squared - 1.0).abs() < 1e-8);
984 }
985
986 #[test]
987 fn multiple_with_noise() {
988 let x1 = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
989 let x2 = [2.0, 1.0, 3.0, 2.0, 4.0, 3.0, 5.0, 4.0];
990 let y = [5.1, 5.0, 9.2, 8.9, 13.1, 12.0, 17.2, 15.9];
991 let r = multiple_linear_regression(&[&x1, &x2], &y).expect("should compute");
992 assert!(r.r_squared > 0.95);
993 assert_eq!(r.coefficients.len(), 3);
994 assert_eq!(r.std_errors.len(), 3);
995 assert_eq!(r.residuals.len(), 8);
996 assert_eq!(r.vif.len(), 2);
997 }
998
999 #[test]
1000 fn multiple_residuals_sum_to_zero() {
1001 let x1 = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
1002 let x2 = [2.0, 1.0, 3.0, 2.0, 4.0, 3.0, 5.0, 4.0];
1003 let y = [5.1, 5.0, 9.2, 8.9, 13.1, 12.0, 17.2, 15.9];
1004 let r = multiple_linear_regression(&[&x1, &x2], &y).expect("should compute");
1005 let sum: f64 = r.residuals.iter().sum();
1006 assert!(sum.abs() < 1e-8, "residuals sum = {sum}");
1007 }
1008
1009 #[test]
1010 fn multiple_single_predictor_matches_simple() {
1011 let x = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
1012 let y: Vec<f64> = x
1013 .iter()
1014 .map(|&xi| 3.0 + 2.5 * xi + 0.1 * (xi - 5.0))
1015 .collect();
1016
1017 let simple = simple_linear_regression(&x, &y).expect("simple");
1018 let multi = multiple_linear_regression(&[&x], &y).expect("multiple");
1019
1020 assert!(
1021 (simple.slope - multi.coefficients[1]).abs() < 1e-8,
1022 "slope: {} vs {}",
1023 simple.slope,
1024 multi.coefficients[1]
1025 );
1026 assert!(
1027 (simple.intercept - multi.coefficients[0]).abs() < 1e-8,
1028 "intercept: {} vs {}",
1029 simple.intercept,
1030 multi.coefficients[0]
1031 );
1032 assert!((simple.r_squared - multi.r_squared).abs() < 1e-8);
1033 }
1034
1035 #[test]
1036 fn multiple_edge_cases() {
1037 let x1 = [1.0, 2.0];
1038 let y = [3.0, 4.0];
1039 assert!(multiple_linear_regression(&[&x1], &y).is_none()); let x2 = [1.0, 2.0, 3.0];
1042 let y2 = [4.0, 5.0];
1043 assert!(multiple_linear_regression(&[&x2], &y2).is_none()); }
1045
1046 #[test]
1047 fn multiple_vif_independent_predictors() {
1048 let x1 = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0];
1050 let x2 = [0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
1051 let y: Vec<f64> = x1
1052 .iter()
1053 .zip(x2.iter())
1054 .map(|(&a, &b)| 1.0 + 2.0 * a + 3.0 * b)
1055 .collect();
1056 let r = multiple_linear_regression(&[&x1, &x2], &y).expect("should compute");
1057 for (i, &v) in r.vif.iter().enumerate() {
1058 assert!(
1059 v < 2.0,
1060 "VIF[{i}] = {v}, expected near 1.0 for independent predictors"
1061 );
1062 }
1063 }
1064
1065 #[test]
1066 fn multiple_vif_correlated_predictors() {
1067 let x1: Vec<f64> = (0..20).map(|i| i as f64).collect();
1069 let noise = [
1071 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,
1072 0.1, -0.1, 0.2, -0.2,
1073 ];
1074 let x2: Vec<f64> = x1
1075 .iter()
1076 .zip(noise.iter())
1077 .map(|(&v, &n)| v * 0.9 + 1.0 + n)
1078 .collect();
1079 let y: Vec<f64> = x1
1080 .iter()
1081 .zip(x2.iter())
1082 .map(|(&a, &b)| 1.0 + a + b)
1083 .collect();
1084 let r = multiple_linear_regression(&[&x1, &x2], &y).expect("should compute");
1085 assert!(
1087 r.vif[0] > 5.0,
1088 "VIF[0] = {}, expected > 5.0 for correlated predictors",
1089 r.vif[0]
1090 );
1091 }
1092
1093 #[test]
1098 fn predict_simple_basic() {
1099 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1100 let y = [3.0, 5.0, 7.0, 9.0, 11.0]; let model = simple_linear_regression(&x, &y).expect("should compute");
1102 let pred = predict_simple(&model, &[6.0, 7.0]);
1103 assert!((pred[0] - 13.0).abs() < 1e-10);
1104 assert!((pred[1] - 15.0).abs() < 1e-10);
1105 }
1106
1107 #[test]
1108 fn predict_multiple_basic() {
1109 let x1 = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
1110 let x2 = [2.0, 1.0, 3.0, 2.0, 4.0, 3.0, 5.0, 4.0, 6.0, 5.0];
1111 let y: Vec<f64> = x1
1112 .iter()
1113 .zip(x2.iter())
1114 .map(|(&a, &b)| 1.0 + 2.0 * a + 3.0 * b)
1115 .collect();
1116 let model = multiple_linear_regression(&[&x1, &x2], &y).expect("should compute");
1117
1118 let new_x1 = [11.0];
1119 let new_x2 = [6.0];
1120 let pred = predict_multiple(&model, &[&new_x1, &new_x2]).expect("should predict");
1121 let expected = 1.0 + 2.0 * 11.0 + 3.0 * 6.0;
1122 assert!(
1123 (pred[0] - expected).abs() < 1e-6,
1124 "pred = {}, expected = {}",
1125 pred[0],
1126 expected
1127 );
1128 }
1129
1130 #[test]
1135 fn vif_pub_independent_predictors() {
1136 let x1 = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
1137 let x2 = [8.0, 1.0, 6.0, 3.0, 5.0, 7.0, 2.0, 4.0];
1138 let v = vif(&[&x1, &x2]).expect("should compute");
1139 for r in &v {
1140 assert!(*r < 2.0, "VIF should be near 1.0 for independent, got {r}");
1141 }
1142 }
1143
1144 #[test]
1145 fn vif_pub_collinear_predictors() {
1146 let x1: Vec<f64> = (0..20).map(|i| i as f64).collect();
1147 let x2: Vec<f64> = x1.iter().map(|&v| v * 2.0 + 0.001).collect();
1148 let v = vif(&[&x1[..], &x2[..]]).expect("should compute");
1149 assert!(
1150 v[0] > 50.0,
1151 "VIF should explode under near-collinearity, got {}",
1152 v[0]
1153 );
1154 }
1155
1156 #[test]
1157 fn vif_pub_rejects_short_input() {
1158 let x = [1.0, 2.0];
1159 assert!(vif(&[&x[..], &x[..]]).is_none());
1160 }
1161
1162 #[test]
1163 fn vif_pub_rejects_non_finite() {
1164 let x1 = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1165 let x2 = [1.0, 2.0, f64::NAN, 4.0, 5.0, 6.0];
1166 assert!(vif(&[&x1[..], &x2[..]]).is_none());
1167 }
1168
1169 #[test]
1170 fn condition_number_orthogonal() {
1171 let x1 = [1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0];
1172 let x2 = [1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0];
1173 let c = condition_number(&[&x1[..], &x2[..]]).expect("should compute");
1174 assert!(c < 5.0, "near-orthogonal: cond should be small, got {c}");
1175 }
1176
1177 #[test]
1178 fn condition_number_collinear_explodes() {
1179 let x1: Vec<f64> = (0..20).map(|i| i as f64).collect();
1180 let x2: Vec<f64> = x1.iter().map(|&v| v * 2.0 + 1e-6).collect();
1181 let c = condition_number(&[&x1[..], &x2[..]]).expect("should compute");
1182 assert!(
1183 c > 1e5 || c.is_infinite(),
1184 "collinear: cond should be huge, got {c}"
1185 );
1186 }
1187
1188 #[test]
1189 fn condition_number_rejects_short_input() {
1190 let x = [1.0, 2.0];
1191 assert!(condition_number(&[&x[..], &x[..]]).is_none());
1192 }
1193
1194 #[test]
1195 fn predict_multiple_wrong_predictors() {
1196 let x1 = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
1197 let x2 = [2.0, 1.0, 3.0, 2.0, 4.0, 3.0, 5.0, 4.0, 6.0, 5.0];
1198 let y: Vec<f64> = x1.iter().zip(x2.iter()).map(|(&a, &b)| a + b).collect();
1199 let model = multiple_linear_regression(&[&x1, &x2], &y).expect("should compute");
1200
1201 assert!(predict_multiple(&model, &[&[1.0]]).is_none());
1203 }
1204}
1205
1206#[cfg(test)]
1207mod proptests {
1208 use super::*;
1209 use proptest::prelude::*;
1210
1211 proptest! {
1212 #[test]
1213 fn simple_r_squared_bounded(
1214 data in proptest::collection::vec(-1e3_f64..1e3, 5..=30)
1215 .prop_flat_map(|x| {
1216 let n = x.len();
1217 (Just(x), proptest::collection::vec(-1e3_f64..1e3, n..=n))
1218 })
1219 ) {
1220 let (x, y) = data;
1221 if let Some(r) = simple_linear_regression(&x, &y) {
1222 prop_assert!(r.r_squared >= -0.01 && r.r_squared <= 1.01,
1223 "R² = {}", r.r_squared);
1224 }
1225 }
1226
1227 #[test]
1228 fn simple_residuals_orthogonal_to_x(
1229 data in proptest::collection::vec(-1e3_f64..1e3, 5..=30)
1230 .prop_flat_map(|x| {
1231 let n = x.len();
1232 (Just(x), proptest::collection::vec(-1e3_f64..1e3, n..=n))
1233 })
1234 ) {
1235 let (x, y) = data;
1236 if let Some(r) = simple_linear_regression(&x, &y) {
1237 let dot: f64 = x.iter().zip(r.residuals.iter()).map(|(&xi, &ei)| xi * ei).sum();
1239 let norm = r.residuals.iter().map(|e| e * e).sum::<f64>().sqrt();
1240 let x_norm = x.iter().map(|xi| xi * xi).sum::<f64>().sqrt();
1241 if norm > 1e-10 && x_norm > 1e-10 {
1242 prop_assert!((dot / (norm * x_norm)).abs() < 1e-6,
1243 "residuals not orthogonal to x: dot={dot}");
1244 }
1245 }
1246 }
1247
1248 #[test]
1249 fn multiple_r_squared_bounded(
1250 x1 in proptest::collection::vec(-1e3_f64..1e3, 8..=20),
1251 x2_seed in proptest::collection::vec(-1e3_f64..1e3, 8..=20),
1252 y_seed in proptest::collection::vec(-1e3_f64..1e3, 8..=20),
1253 ) {
1254 let n = x1.len().min(x2_seed.len()).min(y_seed.len());
1255 let x2 = &x2_seed[..n];
1256 let y = &y_seed[..n];
1257 let x1 = &x1[..n];
1258 if let Some(r) = multiple_linear_regression(&[x1, x2], y) {
1259 prop_assert!(r.r_squared >= -0.01 && r.r_squared <= 1.01,
1260 "R² = {}", r.r_squared);
1261 }
1262 }
1263 }
1264}
1265
1266#[cfg(test)]
1270mod scipy_tail_reference {
1271 use super::*;
1272
1273 fn close(got: f64, want: f64, what: &str) {
1274 assert!(
1275 (got - want).abs() <= 1e-9 * want.abs(),
1276 "{what}: got {got:e}, SciPy {want:e}"
1277 );
1278 }
1279
1280 #[test]
1281 fn simple_regression_p_values_keep_their_tail() {
1282 let x: Vec<f64> = (1..=20).map(f64::from).collect();
1283 let y = [
1284 5.3999999999999995,
1285 7.0,
1286 9.4,
1287 11.8,
1288 13.2,
1289 15.8,
1290 17.6,
1291 19.8,
1292 22.250000000000004,
1293 23.75,
1294 26.25,
1295 28.150000000000002,
1296 30.55,
1297 32.050000000000004,
1298 34.55,
1299 36.7,
1300 38.550000000000004,
1301 41.1,
1302 42.699999999999996,
1303 45.0,
1304 ];
1305 let r = simple_linear_regression(&x, &y).expect("valid");
1306 close(r.slope_p, 8.203356920391111e-33, "slope p");
1307 close(r.intercept_p, 1.5689270135401997e-16, "intercept p");
1308 close(r.f_p_value, 8.203356920397853e-33, "F p");
1309 }
1310 #[test]
1315 fn multiple_regression_and_conditioning_do_not_depend_on_the_unit() {
1316 let n = 24;
1317 let x1: Vec<f64> = (0..n)
1318 .map(|i| (i as f64 * 0.7).sin() * 3.0 + i as f64 * 0.1)
1319 .collect();
1320 let x2: Vec<f64> = (0..n)
1321 .map(|i| (i as f64 * 1.3).cos() + 0.5 * x1[i])
1322 .collect();
1323 let y: Vec<f64> = (0..n)
1324 .map(|i| 1.0 + 2.0 * x1[i] - 0.5 * x2[i] + (i as f64 * 2.1).sin() * 0.3)
1325 .collect();
1326 let base = multiple_linear_regression(&[&x1, &x2], &y).expect("valid");
1327 let cond = condition_number(&[&x1, &x2]).expect("valid");
1328 for factor in [1e-9, 1e6] {
1329 let s1: Vec<f64> = x1.iter().map(|v| v * factor).collect();
1330 let s2: Vec<f64> = x2.iter().map(|v| v * factor).collect();
1331 let r = multiple_linear_regression(&[&s1, &s2], &y)
1332 .unwrap_or_else(|| panic!("a fit in units of {factor:e} was refused"));
1333 close(r.r_squared, base.r_squared, "R2");
1334 close(r.f_p_value, base.f_p_value, "F p");
1335 close(r.coefficients[0], base.coefficients[0], "intercept");
1336 for j in 0..2 {
1337 close(
1338 r.coefficients[j + 1] * factor,
1339 base.coefficients[j + 1],
1340 "slope",
1341 );
1342 close(r.p_values[j + 1], base.p_values[j + 1], "slope p");
1343 close(r.vif[j], base.vif[j], "vif");
1344 }
1345 close(
1346 condition_number(&[&s1, &s2]).expect("valid"),
1347 cond,
1348 "condition number",
1349 );
1350 }
1351 }
1352}
1353
1354#[cfg(test)]
1360#[allow(clippy::approx_constant)] mod statsmodels_prediction_reference {
1362 use super::*;
1363
1364 const SX: &[f64] = &[
1366 0.321, 3.382, 13.09, 0.433, 11.207, 1.243, 13.577, 12.607, 3.653, 3.965, 11.199, 16.295,
1367 ];
1368 const SY: &[f64] = &[
1369 1.864, 10.159, 26.906, 4.994, 22.783, 9.397, 26.273, 24.293, 9.363, 9.846, 23.056, 29.43,
1370 ];
1371 const SXN: &[f64] = &[-5.0, 0.0, 10.0, 31.5];
1372 const S_MEAN: &[f64] = &[
1373 -4.004473932920135,
1374 4.156565438454745,
1375 20.478644181204505,
1376 55.57111347811649,
1377 ];
1378 const S_MEAN_SE: &[f64] = &[
1379 1.0916157424918431,
1380 0.7485088578492345,
1381 0.4868022389382572,
1382 1.9451041579807988,
1383 ];
1384 const S_MEAN_CI_LOWER: &[f64] = &[
1385 -5.982985027235801,
1386 2.799922233522837,
1387 19.596334048631235,
1388 52.04568781195695,
1389 ];
1390 const S_MEAN_CI_UPPER: &[f64] = &[
1391 -2.0259628386044684,
1392 5.513208643386653,
1393 21.360954313777775,
1394 59.09653914427602,
1395 ];
1396 const S_OBS_CI_LOWER: &[f64] = &[
1397 -7.441261322990979,
1398 1.0360715352868972,
1399 17.533228661407467,
1400 51.06271824900933,
1401 ];
1402 const S_OBS_CI_UPPER: &[f64] = &[
1403 -0.5676865428492905,
1404 7.277059341622593,
1405 23.424059701001543,
1406 60.079508707223646,
1407 ];
1408 const MX1: &[f64] = &[
1409 8.195, 0.642, 0.336, 6.144, 0.722, 9.91, 3.078, 2.241, 6.569, 9.172, 5.051, 8.676, 4.217,
1410 2.188, 6.705,
1411 ];
1412 const MX2: &[f64] = &[
1413 0.949, -2.284, 1.833, -0.66, -4.189, -3.649, -0.08, 1.587, 3.075, 4.395, 2.358, -4.012,
1414 -0.279, 4.502, 3.984,
1415 ];
1416 const MX3: &[f64] = &[
1417 3826.5, 3834.4, 3708.9, 1362.4, 2662.0, 2973.7, 3001.6, 4864.0, 2948.0, 1882.4, 2032.4,
1418 4144.4, 2666.3, 2747.4, 2715.4,
1419 ];
1420 const MY: &[f64] = &[
1421 28.737, 14.267, 13.838, 17.156, 10.139, 31.8, 15.884, 19.999, 21.378, 20.225, 16.187,
1422 33.594, 18.621, 10.903, 19.907,
1423 ];
1424 const MN1: &[f64] = &[5.0, 12.0];
1425 const MN2: &[f64] = &[0.0, -6.0];
1426 const MN3: &[f64] = &[3000.0, 800.0];
1427 const M_MEAN: &[f64] = &[19.808760915895164, 28.616648153275186];
1428 const M_MEAN_SE: &[f64] = &[0.3007541072279165, 1.2540843118704124];
1429 const M_MEAN_CI_LOWER: &[f64] = &[19.14680558904991, 25.856427193344672];
1430 const M_MEAN_CI_UPPER: &[f64] = &[20.470716242740416, 31.3768691132057];
1431 const M_OBS_CI_LOWER: &[f64] = &[17.202883874579435, 24.878837290302403];
1432 const M_OBS_CI_UPPER: &[f64] = &[22.414637957210893, 32.354459016247965];
1433 const M_COV: &[f64] = &[
1434 1.7052113874211179,
1435 -0.07001041367117572,
1436 -0.03185830538231558,
1437 -0.0004156285010374516,
1438 -0.07001041367117572,
1439 0.009234030588502234,
1440 0.000334157657603741,
1441 8.061392712052953e-06,
1442 -0.03185830538231558,
1443 0.000334157657603741,
1444 0.0109414882400463,
1445 8.173035100524671e-06,
1446 -0.0004156285010374516,
1447 8.061392712052953e-06,
1448 8.173035100524671e-06,
1449 1.229360269395184e-07,
1450 ];
1451
1452 fn close(got: f64, want: f64, what: &str) {
1453 let tol = 1e-9 * want.abs().max(1.0);
1454 assert!((got - want).abs() <= tol, "{what}: {got} vs {want}");
1455 }
1456
1457 #[test]
1458 fn simple_intervals_match_statsmodels() {
1459 let model = simple_linear_regression(SX, SY).expect("fits");
1460 let p = predict_simple_intervals(&model, SXN, 0.90).expect("intervals");
1461 for (i, r) in p.iter().enumerate() {
1462 close(r.fit, S_MEAN[i], "fit");
1463 close(r.se_fit, S_MEAN_SE[i], "se_fit");
1464 close(r.confidence_lower, S_MEAN_CI_LOWER[i], "mean lower");
1465 close(r.confidence_upper, S_MEAN_CI_UPPER[i], "mean upper");
1466 close(r.prediction_lower, S_OBS_CI_LOWER[i], "obs lower");
1467 close(r.prediction_upper, S_OBS_CI_UPPER[i], "obs upper");
1468 }
1469 }
1470
1471 #[test]
1472 fn multiple_intervals_and_covariance_match_statsmodels() {
1473 let model = multiple_linear_regression(&[MX1, MX2, MX3], MY).expect("fits");
1474 for (got, want) in model.covariance.iter().zip(M_COV) {
1475 close(*got, *want, "covariance");
1476 }
1477 let p = predict_multiple_intervals(&model, &[MN1, MN2, MN3], 0.95).expect("intervals");
1478 for (i, r) in p.iter().enumerate() {
1479 close(r.fit, M_MEAN[i], "fit");
1480 close(r.se_fit, M_MEAN_SE[i], "se_fit");
1481 close(r.confidence_lower, M_MEAN_CI_LOWER[i], "mean lower");
1482 close(r.confidence_upper, M_MEAN_CI_UPPER[i], "mean upper");
1483 close(r.prediction_lower, M_OBS_CI_LOWER[i], "obs lower");
1484 close(r.prediction_upper, M_OBS_CI_UPPER[i], "obs upper");
1485 }
1486 }
1487}