qualia_core_db/solvers/statistics/hypothesis/
t_tests.rs1use super::super::descriptive::{mean, variance};
5use super::super::distributions::students_t;
6
7#[derive(Debug, Clone, Copy, PartialEq)]
9pub struct TTest {
10 pub t_statistic: f64,
11 pub p_value: f64,
13 pub degrees_of_freedom: u32,
14 pub confidence_interval: (f64, f64),
17}
18
19pub fn one_sample_t(values: &[f64], mu: f64) -> Option<TTest> {
21 let n = values.len();
22 if n < 2 {
23 return None;
24 }
25 let m = mean(values)?;
26 let var = variance(values, true)?;
27 let df = (n - 1) as f64;
28 let std_error = (var / n as f64).sqrt();
29 if std_error == 0.0 {
30 let t = if m == mu {
32 0.0
33 } else {
34 f64::INFINITY.copysign(m - mu)
35 };
36 return Some(TTest {
37 t_statistic: t,
38 p_value: if m == mu { 1.0 } else { 0.0 },
39 degrees_of_freedom: (n - 1) as u32,
40 confidence_interval: (m, m),
41 });
42 }
43 let t = (m - mu) / std_error;
44 let p = students_t::two_sided_p(t, df);
45 let t_crit = students_t::quantile(0.975, df);
46 let margin = t_crit * std_error;
47 Some(TTest {
48 t_statistic: t,
49 p_value: p,
50 degrees_of_freedom: (n - 1) as u32,
51 confidence_interval: (m - margin, m + margin),
52 })
53}
54
55pub fn paired_t(a: &[f64], b: &[f64]) -> Option<TTest> {
58 if a.len() != b.len() || a.len() < 2 {
59 return None;
60 }
61 let diffs: Vec<f64> = a.iter().zip(b.iter()).map(|(x, y)| x - y).collect();
62 one_sample_t(&diffs, 0.0)
63}
64
65#[derive(Debug, Clone, Copy, PartialEq)]
67pub struct TwoSampleTTest {
68 pub t_statistic: f64,
69 pub p_value: f64,
70 pub degrees_of_freedom: f64,
71 pub mean_difference: f64,
73 pub confidence_interval: (f64, f64),
75}
76
77pub fn two_sample_t(a: &[f64], b: &[f64], equal_var: bool) -> Option<TwoSampleTTest> {
83 let (na, nb) = (a.len(), b.len());
84 if na < 2 || nb < 2 {
85 return None;
86 }
87 let (ma, mb) = (mean(a)?, mean(b)?);
88 let (va, vb) = (variance(a, true)?, variance(b, true)?);
89 let (na_f, nb_f) = (na as f64, nb as f64);
90 let diff = ma - mb;
91
92 let (se, df) = if equal_var {
93 let sp2 = ((na_f - 1.0) * va + (nb_f - 1.0) * vb) / (na_f + nb_f - 2.0);
95 let se = (sp2 * (1.0 / na_f + 1.0 / nb_f)).sqrt();
96 (se, na_f + nb_f - 2.0)
97 } else {
98 let se2 = va / na_f + vb / nb_f;
100 let se = se2.sqrt();
101 let df =
102 se2 * se2 / ((va / na_f).powi(2) / (na_f - 1.0) + (vb / nb_f).powi(2) / (nb_f - 1.0));
103 (se, df)
104 };
105
106 if se == 0.0 {
107 return Some(TwoSampleTTest {
108 t_statistic: if diff == 0.0 {
109 0.0
110 } else {
111 f64::INFINITY.copysign(diff)
112 },
113 p_value: if diff == 0.0 { 1.0 } else { 0.0 },
114 degrees_of_freedom: df,
115 mean_difference: diff,
116 confidence_interval: (diff, diff),
117 });
118 }
119 let t = diff / se;
120 let p = students_t::two_sided_p(t, df);
121 let t_crit = students_t::quantile(0.975, df);
122 let margin = t_crit * se;
123 Some(TwoSampleTTest {
124 t_statistic: t,
125 p_value: p,
126 degrees_of_freedom: df,
127 mean_difference: diff,
128 confidence_interval: (diff - margin, diff + margin),
129 })
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn one_sample_real_p_value_not_a_threshold() {
138 let v = [5.1, 4.9, 5.6, 5.2, 5.8, 5.3, 4.7, 5.5];
140 let r = one_sample_t(&v, 5.0).unwrap();
141 assert_eq!(r.degrees_of_freedom, 7);
142 assert!(r.p_value > 0.0 && r.p_value < 1.0);
144 assert_ne!(r.p_value, 0.05);
145 assert_ne!(r.p_value, 0.1);
146 let m = v.iter().sum::<f64>() / v.len() as f64;
148 assert!(r.confidence_interval.0 < m && r.confidence_interval.1 > m);
149 }
150
151 #[test]
152 fn one_sample_matches_known_statistic() {
153 let r = one_sample_t(&[1.0, 2.0, 3.0, 4.0, 5.0], 3.0).unwrap();
155 assert!(r.t_statistic.abs() < 1e-12);
156 assert!((r.p_value - 1.0).abs() < 1e-9);
157 let r2 = one_sample_t(&[1.0, 2.0, 3.0, 4.0, 5.0], 0.0).unwrap();
159 assert!((r2.t_statistic - 4.242_640_687).abs() < 1e-6);
160 assert!((r2.p_value - 0.013_31).abs() < 1e-4);
161 }
162
163 #[test]
164 fn paired_is_one_sample_of_differences() {
165 let before = [10.0, 12.0, 9.0, 11.0, 13.0];
166 let after = [11.0, 14.0, 10.0, 12.0, 15.0];
167 let r = paired_t(&before, &after).unwrap();
168 assert!(r.t_statistic < 0.0);
170 assert!(r.p_value < 0.05);
171 assert_eq!(paired_t(&before, &after[..4]), None); }
173
174 #[test]
175 fn welch_vs_pooled_two_sample() {
176 let a = [20.0, 22.0, 19.0, 24.0, 25.0, 21.0];
177 let b = [28.0, 31.0, 26.0, 30.0, 29.0, 27.0];
178 let welch = two_sample_t(&a, &b, false).unwrap();
179 let pooled = two_sample_t(&a, &b, true).unwrap();
180 assert!(welch.mean_difference < 0.0);
182 assert!(welch.p_value < 0.01 && pooled.p_value < 0.01);
183 assert!(welch.degrees_of_freedom <= 10.0 + 1e-9);
185 assert!((pooled.degrees_of_freedom - 10.0).abs() < 1e-9);
186 assert!(welch.confidence_interval.1 < 0.0);
188 }
189
190 #[test]
191 fn identical_groups_are_not_significant() {
192 let a = [1.0, 2.0, 3.0, 4.0, 5.0];
193 let r = two_sample_t(&a, &a, false).unwrap();
194 assert!(r.t_statistic.abs() < 1e-9);
195 assert!((r.p_value - 1.0).abs() < 1e-9);
196 }
197}