use super::dist::chi_square_sf;
use super::TestResult;
use crate::error::FdarError;
use crate::matrix::FdMatrix;
use crate::regression::fdata_to_pc_1d;
use crate::spm::stats::hotelling_t2;
fn mean_scores(scores: &FdMatrix) -> Vec<f64> {
let (n, ncomp) = scores.shape();
let mut mean = vec![0.0; ncomp];
for k in 0..ncomp {
let mut s = 0.0;
for i in 0..n {
s += scores[(i, k)];
}
mean[k] = s / n as f64;
}
mean
}
pub fn two_sample_mean_test(
data_a: &FdMatrix,
data_b: &FdMatrix,
argvals: &[f64],
ncomp: usize,
) -> Result<TestResult, FdarError> {
let (n_a, m_a) = data_a.shape();
let (n_b, m_b) = data_b.shape();
if m_a == 0 || m_b == 0 {
return Err(FdarError::InvalidDimension {
parameter: "data",
expected: "at least 1 column (grid points)".to_string(),
actual: format!("data_a has {m_a} columns, data_b has {m_b} columns"),
});
}
if m_a != m_b {
return Err(FdarError::InvalidDimension {
parameter: "data_b",
expected: format!("{m_a} columns (matching data_a)"),
actual: format!("{m_b} columns"),
});
}
if argvals.len() != m_a {
return Err(FdarError::InvalidDimension {
parameter: "argvals",
expected: format!("{m_a} elements (matching data columns)"),
actual: format!("{} elements", argvals.len()),
});
}
if n_a < 2 || n_b < 2 {
return Err(FdarError::InvalidDimension {
parameter: "data",
expected: "at least 2 rows per sample".to_string(),
actual: format!("data_a has {n_a} rows, data_b has {n_b} rows"),
});
}
if ncomp < 1 {
return Err(FdarError::InvalidParameter {
parameter: "ncomp",
message: format!("ncomp must be >= 1, got {ncomp}"),
});
}
let n_pooled = n_a + n_b;
let m = m_a;
let mut pooled = FdMatrix::zeros(n_pooled, m);
for j in 0..m {
for i in 0..n_a {
pooled[(i, j)] = data_a[(i, j)];
}
for i in 0..n_b {
pooled[(n_a + i, j)] = data_b[(i, j)];
}
}
let fpca = fdata_to_pc_1d(&pooled, ncomp, argvals)?;
let eff_ncomp = fpca.singular_values.len();
let scores_a = fpca.project(data_a)?;
let scores_b = fpca.project(data_b)?;
let mean_a = mean_scores(&scores_a);
let mean_b = mean_scores(&scores_b);
let scale = ((n_a as f64) * (n_b as f64) / (n_pooled as f64)).sqrt();
let diff: Vec<f64> = (0..eff_ncomp)
.map(|k| scale * (mean_a[k] - mean_b[k]))
.collect();
let diff_row = FdMatrix::from_column_major(diff, 1, eff_ncomp)?;
let eigenvalues: Vec<f64> = fpca
.singular_values
.iter()
.map(|&sv| (sv * sv / (n_pooled as f64 - 1.0)).max(1e-15))
.collect();
let t2 = hotelling_t2(&diff_row, &eigenvalues)?[0];
let p_value = chi_square_sf(t2, eff_ncomp);
Ok(TestResult {
statistic: t2,
p_value,
n_perm: 0,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::uniform_grid;
fn make_sample(n: usize, argvals: &[f64], shift: f64, seed: u64) -> FdMatrix {
let m = argvals.len();
let mut mat = FdMatrix::zeros(n, m);
let mut state = seed.wrapping_mul(2_654_435_761).wrapping_add(1);
for i in 0..n {
for (j, &t) in argvals.iter().enumerate() {
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
let noise = ((state >> 33) as f64 / (1u64 << 31) as f64) - 1.0;
mat[(i, j)] = (2.0 * std::f64::consts::PI * t).sin() + 0.2 * noise + shift;
}
}
mat
}
#[test]
fn chi_square_sf_sane() {
assert!((chi_square_sf(3.8415, 1) - 0.05).abs() < 1e-3);
assert!((chi_square_sf(0.0, 3) - 1.0).abs() < 1e-12);
assert!((chi_square_sf(5.9915, 2) - 0.05).abs() < 1e-3);
assert!(chi_square_sf(1.0, 3) > chi_square_sf(5.0, 3));
}
#[test]
fn mean_test_differ_rejects() {
let argvals = uniform_grid(30);
let a = make_sample(30, &argvals, 0.0, 101);
let b = make_sample(30, &argvals, 2.0, 102); let res = two_sample_mean_test(&a, &b, &argvals, 3).unwrap();
assert!(
res.p_value < 0.05,
"differing means should reject, got p={}",
res.p_value
);
}
#[test]
fn mean_test_coincide_fails_to_reject() {
let argvals = uniform_grid(30);
let a = make_sample(30, &argvals, 0.0, 201);
let b = make_sample(30, &argvals, 0.0, 202); let res = two_sample_mean_test(&a, &b, &argvals, 3).unwrap();
assert!(
res.p_value > 0.05,
"coinciding means should not reject, got p={}",
res.p_value
);
}
#[test]
fn mean_test_invalid_input() {
let argvals = uniform_grid(20);
let a = make_sample(10, &argvals, 0.0, 5);
let b = make_sample(10, &argvals, 0.0, 6);
assert!(matches!(
two_sample_mean_test(&a, &b, &argvals, 0),
Err(FdarError::InvalidParameter { .. })
));
let argvals_b = uniform_grid(15);
let b2 = make_sample(10, &argvals_b, 0.0, 7);
assert!(matches!(
two_sample_mean_test(&a, &b2, &argvals, 3),
Err(FdarError::InvalidDimension { .. })
));
let a_small = make_sample(1, &argvals, 0.0, 8);
assert!(matches!(
two_sample_mean_test(&a_small, &b, &argvals, 3),
Err(FdarError::InvalidDimension { .. })
));
}
}