qualia_core_db/solvers/statistics/
regression.rs1use super::descriptive::{covariance, mean, variance};
11use super::distributions::students_t;
12
13#[derive(Debug, Clone, Copy, PartialEq)]
15pub struct LinearRegression {
16 pub slope: f64,
17 pub intercept: f64,
18 pub r_squared: f64,
20 pub residual_std_error: f64,
22 pub slope_std_error: f64,
23 pub slope_t: f64,
24 pub slope_p_value: f64,
26 pub intercept_std_error: f64,
27 pub intercept_p_value: f64,
28 pub n: usize,
29}
30
31pub fn simple_linear_regression(x: &[f64], y: &[f64]) -> Option<LinearRegression> {
35 let n = x.len();
36 if n != y.len() || n < 3 {
37 return None;
38 }
39 let mx = mean(x)?;
40 let my = mean(y)?;
41 let var_x = variance(x, false)?; if var_x <= 0.0 {
43 return None;
44 }
45 let cov_xy = covariance(x, y, false)?;
46 let slope = cov_xy / var_x;
47 let intercept = my - slope * mx;
48
49 let mut ss_tot = 0.0; let mut ss_res = 0.0; let mut sxx = 0.0; for i in 0..n {
54 let yhat = intercept + slope * x[i];
55 ss_tot += (y[i] - my).powi(2);
56 ss_res += (y[i] - yhat).powi(2);
57 sxx += (x[i] - mx).powi(2);
58 }
59
60 let df = (n - 2) as f64;
61 let r_squared = if ss_tot > 0.0 {
62 1.0 - ss_res / ss_tot
63 } else {
64 1.0
65 };
66 let s2 = ss_res / df; let residual_std_error = s2.sqrt();
68
69 let slope_std_error = (s2 / sxx).sqrt();
70 let (slope_t, slope_p_value) = if slope_std_error > 0.0 {
71 let t = slope / slope_std_error;
72 (t, students_t::two_sided_p(t, df))
73 } else {
74 (f64::INFINITY.copysign(slope), 0.0)
76 };
77
78 let intercept_std_error = (s2 * (1.0 / n as f64 + mx * mx / sxx)).sqrt();
79 let intercept_p_value = if intercept_std_error > 0.0 {
80 students_t::two_sided_p(intercept / intercept_std_error, df)
81 } else {
82 0.0
83 };
84
85 Some(LinearRegression {
86 slope,
87 intercept,
88 r_squared,
89 residual_std_error,
90 slope_std_error,
91 slope_t,
92 slope_p_value,
93 intercept_std_error,
94 intercept_p_value,
95 n,
96 })
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn exact_line_is_recovered() {
105 let x = [0.0, 1.0, 2.0, 3.0, 4.0];
107 let y = [3.0, 5.0, 7.0, 9.0, 11.0];
108 let r = simple_linear_regression(&x, &y).unwrap();
109 assert!((r.slope - 2.0).abs() < 1e-9);
110 assert!((r.intercept - 3.0).abs() < 1e-9);
111 assert!((r.r_squared - 1.0).abs() < 1e-12);
112 assert!(r.residual_std_error < 1e-9);
113 assert_eq!(r.slope_p_value, 0.0); }
115
116 #[test]
117 fn noisy_trend_matches_known_fit() {
118 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
120 let y = [2.1, 3.9, 6.1, 7.9, 10.2];
121 let r = simple_linear_regression(&x, &y).unwrap();
122 assert!((r.slope - 2.0).abs() < 0.05, "slope={}", r.slope);
124 assert!(r.intercept.abs() < 0.2, "intercept={}", r.intercept);
125 assert!(r.r_squared > 0.99, "r2={}", r.r_squared);
126 assert!(r.slope_p_value < 1e-4, "strong trend must be significant");
127 }
128
129 #[test]
130 fn no_relationship_is_not_significant() {
131 let x = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
132 let y = [5.0, 4.0, 6.0, 5.0, 6.0, 4.0]; let r = simple_linear_regression(&x, &y).unwrap();
134 assert!(
135 r.slope_p_value > 0.2,
136 "no real trend: p={}",
137 r.slope_p_value
138 );
139 assert!(r.r_squared < 0.3);
140 }
141
142 #[test]
143 fn guards_degenerate_input() {
144 assert!(simple_linear_regression(&[1.0, 2.0], &[1.0, 2.0]).is_none()); assert!(simple_linear_regression(&[2.0, 2.0, 2.0], &[1.0, 2.0, 3.0]).is_none()); assert!(simple_linear_regression(&[1.0, 2.0, 3.0], &[1.0, 2.0]).is_none());
147 }
149}