1use super::dist::chi_square_sf;
10use super::TestResult;
11use crate::error::FdarError;
12use crate::matrix::FdMatrix;
13use crate::regression::fdata_to_pc_1d;
14use crate::spm::stats::hotelling_t2;
15
16fn mean_scores(scores: &FdMatrix) -> Vec<f64> {
18 let (n, ncomp) = scores.shape();
19 let mut mean = vec![0.0; ncomp];
20 for k in 0..ncomp {
21 let mut s = 0.0;
22 for i in 0..n {
23 s += scores[(i, k)];
24 }
25 mean[k] = s / n as f64;
26 }
27 mean
28}
29
30pub fn two_sample_mean_test(
60 data_a: &FdMatrix,
61 data_b: &FdMatrix,
62 argvals: &[f64],
63 ncomp: usize,
64) -> Result<TestResult, FdarError> {
65 let (n_a, m_a) = data_a.shape();
66 let (n_b, m_b) = data_b.shape();
67 if m_a == 0 || m_b == 0 {
68 return Err(FdarError::InvalidDimension {
69 parameter: "data",
70 expected: "at least 1 column (grid points)".to_string(),
71 actual: format!("data_a has {m_a} columns, data_b has {m_b} columns"),
72 });
73 }
74 if m_a != m_b {
75 return Err(FdarError::InvalidDimension {
76 parameter: "data_b",
77 expected: format!("{m_a} columns (matching data_a)"),
78 actual: format!("{m_b} columns"),
79 });
80 }
81 if argvals.len() != m_a {
82 return Err(FdarError::InvalidDimension {
83 parameter: "argvals",
84 expected: format!("{m_a} elements (matching data columns)"),
85 actual: format!("{} elements", argvals.len()),
86 });
87 }
88 if n_a < 2 || n_b < 2 {
89 return Err(FdarError::InvalidDimension {
90 parameter: "data",
91 expected: "at least 2 rows per sample".to_string(),
92 actual: format!("data_a has {n_a} rows, data_b has {n_b} rows"),
93 });
94 }
95 if ncomp < 1 {
96 return Err(FdarError::InvalidParameter {
97 parameter: "ncomp",
98 message: format!("ncomp must be >= 1, got {ncomp}"),
99 });
100 }
101
102 let n_pooled = n_a + n_b;
104 let m = m_a;
105 let mut pooled = FdMatrix::zeros(n_pooled, m);
106 for j in 0..m {
107 for i in 0..n_a {
108 pooled[(i, j)] = data_a[(i, j)];
109 }
110 for i in 0..n_b {
111 pooled[(n_a + i, j)] = data_b[(i, j)];
112 }
113 }
114
115 let fpca = fdata_to_pc_1d(&pooled, ncomp, argvals)?;
117 let eff_ncomp = fpca.singular_values.len();
119
120 let scores_a = fpca.project(data_a)?;
122 let scores_b = fpca.project(data_b)?;
123 let mean_a = mean_scores(&scores_a);
124 let mean_b = mean_scores(&scores_b);
125
126 let scale = ((n_a as f64) * (n_b as f64) / (n_pooled as f64)).sqrt();
130 let diff: Vec<f64> = (0..eff_ncomp)
131 .map(|k| scale * (mean_a[k] - mean_b[k]))
132 .collect();
133 let diff_row = FdMatrix::from_column_major(diff, 1, eff_ncomp)?;
134
135 let eigenvalues: Vec<f64> = fpca
137 .singular_values
138 .iter()
139 .map(|&sv| (sv * sv / (n_pooled as f64 - 1.0)).max(1e-15))
140 .collect();
141
142 let t2 = hotelling_t2(&diff_row, &eigenvalues)?[0];
143 let p_value = chi_square_sf(t2, eff_ncomp);
144
145 Ok(TestResult {
146 statistic: t2,
147 p_value,
148 n_perm: 0,
149 })
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use crate::test_helpers::uniform_grid;
156
157 fn make_sample(n: usize, argvals: &[f64], shift: f64, seed: u64) -> FdMatrix {
158 let m = argvals.len();
159 let mut mat = FdMatrix::zeros(n, m);
160 let mut state = seed.wrapping_mul(2_654_435_761).wrapping_add(1);
161 for i in 0..n {
162 for (j, &t) in argvals.iter().enumerate() {
163 state = state
164 .wrapping_mul(6_364_136_223_846_793_005)
165 .wrapping_add(1_442_695_040_888_963_407);
166 let noise = ((state >> 33) as f64 / (1u64 << 31) as f64) - 1.0;
167 mat[(i, j)] = (2.0 * std::f64::consts::PI * t).sin() + 0.2 * noise + shift;
168 }
169 }
170 mat
171 }
172
173 #[test]
174 fn chi_square_sf_sane() {
175 assert!((chi_square_sf(3.8415, 1) - 0.05).abs() < 1e-3);
177 assert!((chi_square_sf(0.0, 3) - 1.0).abs() < 1e-12);
178 assert!((chi_square_sf(5.9915, 2) - 0.05).abs() < 1e-3);
180 assert!(chi_square_sf(1.0, 3) > chi_square_sf(5.0, 3));
182 }
183
184 #[test]
185 fn mean_test_differ_rejects() {
186 let argvals = uniform_grid(30);
187 let a = make_sample(30, &argvals, 0.0, 101);
188 let b = make_sample(30, &argvals, 2.0, 102); let res = two_sample_mean_test(&a, &b, &argvals, 3).unwrap();
190 assert!(
191 res.p_value < 0.05,
192 "differing means should reject, got p={}",
193 res.p_value
194 );
195 }
196
197 #[test]
198 fn mean_test_coincide_fails_to_reject() {
199 let argvals = uniform_grid(30);
200 let a = make_sample(30, &argvals, 0.0, 201);
201 let b = make_sample(30, &argvals, 0.0, 202); let res = two_sample_mean_test(&a, &b, &argvals, 3).unwrap();
203 assert!(
204 res.p_value > 0.05,
205 "coinciding means should not reject, got p={}",
206 res.p_value
207 );
208 }
209
210 #[test]
211 fn mean_test_invalid_input() {
212 let argvals = uniform_grid(20);
213 let a = make_sample(10, &argvals, 0.0, 5);
214 let b = make_sample(10, &argvals, 0.0, 6);
216 assert!(matches!(
217 two_sample_mean_test(&a, &b, &argvals, 0),
218 Err(FdarError::InvalidParameter { .. })
219 ));
220 let argvals_b = uniform_grid(15);
222 let b2 = make_sample(10, &argvals_b, 0.0, 7);
223 assert!(matches!(
224 two_sample_mean_test(&a, &b2, &argvals, 3),
225 Err(FdarError::InvalidDimension { .. })
226 ));
227 let a_small = make_sample(1, &argvals, 0.0, 8);
229 assert!(matches!(
230 two_sample_mean_test(&a_small, &b, &argvals, 3),
231 Err(FdarError::InvalidDimension { .. })
232 ));
233 }
234}