use super::mean::{frechet_mean, frechet_variance};
use super::space::WassersteinDensitySpace;
use super::{FrechetAnovaResult, MetricSpace};
use crate::error::FdarError;
use crate::helpers::NUMERICAL_EPS;
use crate::inference::dist::chi_square_sf;
use crate::matrix::FdMatrix;
use rand::rngs::StdRng;
use rand::seq::SliceRandom;
use rand::SeedableRng;
fn compute_tn(
space: &WassersteinDensitySpace,
objects: &[Vec<f64>],
labels: &[usize],
k: usize,
) -> Result<(f64, f64, f64, Vec<f64>, f64), FdarError> {
let n = objects.len();
let mut groups: Vec<Vec<usize>> = vec![Vec::new(); k];
for (i, &g) in labels.iter().enumerate() {
groups[g].push(i);
}
let pooled_mean = frechet_mean(space, objects, None)?;
let pooled_var = frechet_variance(space, objects, &pooled_mean, None)?;
let mut group_vars = vec![0.0; k];
let mut sigma2 = vec![0.0; k];
let mut lambda = vec![0.0; k];
for (g, idx) in groups.iter().enumerate() {
let n_g = idx.len();
lambda[g] = n_g as f64 / n as f64;
let subset: Vec<Vec<f64>> = idx.iter().map(|&i| objects[i].clone()).collect();
let mu_g = frechet_mean(space, &subset, None)?;
let d2: Vec<f64> = subset
.iter()
.map(|o| space.distance(o, &mu_g).map(|d| d * d))
.collect::<Result<Vec<f64>, _>>()?;
let v_g = d2.iter().sum::<f64>() / n_g as f64;
let s2 = d2.iter().map(|&d| (d - v_g).powi(2)).sum::<f64>() / n_g as f64;
group_vars[g] = v_g;
sigma2[g] = s2.max(NUMERICAL_EPS);
}
let fn_stat = pooled_var - (0..k).map(|g| lambda[g] * group_vars[g]).sum::<f64>();
let mut un = 0.0;
for j in 0..k {
for l in (j + 1)..k {
un += (lambda[j] * lambda[l] / (sigma2[j] * sigma2[l]))
* (group_vars[j] - group_vars[l]).powi(2);
}
}
let denom_u: f64 = (0..k).map(|g| lambda[g] / sigma2[g]).sum();
let denom_f: f64 = (0..k).map(|g| lambda[g] * lambda[g] * sigma2[g]).sum();
let term_u = if denom_u > NUMERICAL_EPS {
n as f64 * un / denom_u
} else {
0.0
};
let term_f = if denom_f > NUMERICAL_EPS {
n as f64 * fn_stat * fn_stat / denom_f
} else {
0.0
};
let tn = term_u + term_f;
Ok((tn, fn_stat, un, group_vars, pooled_var))
}
#[must_use = "returns the Fréchet ANOVA result; examine the p-values"]
pub fn frechet_anova(
responses: &FdMatrix,
argvals: &[f64],
group_labels: &[usize],
n_perm: usize,
seed: u64,
) -> Result<FrechetAnovaResult, FdarError> {
let (n, m) = responses.shape();
if group_labels.len() != n {
return Err(FdarError::InvalidDimension {
parameter: "group_labels",
expected: format!("{n} labels (matching response rows)"),
actual: format!("{} labels", group_labels.len()),
});
}
if argvals.len() != m {
return Err(FdarError::InvalidDimension {
parameter: "argvals",
expected: format!("{m} elements (matching response columns)"),
actual: format!("{} elements", argvals.len()),
});
}
let k = group_labels.iter().copied().max().map_or(0, |mx| mx + 1);
let distinct: std::collections::BTreeSet<usize> = group_labels.iter().copied().collect();
if distinct.len() < 2 {
return Err(FdarError::InvalidParameter {
parameter: "group_labels",
message: "need at least 2 distinct groups for a Fréchet ANOVA".to_string(),
});
}
if distinct.len() != k || *distinct.iter().next().unwrap() != 0 {
return Err(FdarError::InvalidParameter {
parameter: "group_labels",
message: format!("group labels must be contiguous 0..{k}"),
});
}
let space = WassersteinDensitySpace::new(argvals.to_vec())?;
let objects: Vec<Vec<f64>> = (0..n).map(|i| responses.row(i)).collect();
let n_perm = if n_perm == 0 { 999 } else { n_perm };
let (tn_obs, fn_stat, un_stat, group_vars, pooled_var) =
compute_tn(&space, &objects, group_labels, k)?;
let p_asymptotic = chi_square_sf(tn_obs, k - 1);
let mut n_ge = 0usize;
for perm in 0..n_perm {
let mut rng = StdRng::seed_from_u64(seed.wrapping_add(perm as u64));
let mut perm_labels = group_labels.to_vec();
perm_labels.shuffle(&mut rng);
if let Ok((tn_perm, _, _, _, _)) = compute_tn(&space, &objects, &perm_labels, k) {
if tn_perm >= tn_obs {
n_ge += 1;
}
}
}
let p_permutation = (n_ge as f64 + 1.0) / (n_perm as f64 + 1.0);
Ok(FrechetAnovaResult {
statistic: tn_obs,
p_value_asymptotic: p_asymptotic,
p_value_permutation: p_permutation,
n_perm,
group_frechet_variances: group_vars,
pooled_frechet_variance: pooled_var,
fn_statistic: fn_stat,
un_statistic: un_stat,
group_labels: group_labels.to_vec(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::helpers::trapz;
fn uniform_grid(m: usize, lb: f64, ub: f64) -> Vec<f64> {
(0..m)
.map(|j| lb + (ub - lb) * j as f64 / (m - 1) as f64)
.collect()
}
fn gaussian(argvals: &[f64], mu: f64) -> Vec<f64> {
let raw: Vec<f64> = argvals
.iter()
.map(|&x| (-(x - mu).powi(2) / 2.0).exp())
.collect();
let integral = trapz(&raw, argvals);
raw.iter().map(|&d| d / integral).collect()
}
fn two_group(argvals: &[f64], n_each: usize, mu_a: f64, mu_b: f64) -> (FdMatrix, Vec<usize>) {
let m = argvals.len();
let mut resp = FdMatrix::zeros(2 * n_each, m);
let mut labels = vec![0usize; 2 * n_each];
for i in 0..n_each {
let a = gaussian(
argvals,
mu_a + 0.05 * ((i as f64) - n_each as f64 / 2.0) / n_each as f64,
);
let b = gaussian(
argvals,
mu_b + 0.05 * ((i as f64) - n_each as f64 / 2.0) / n_each as f64,
);
for j in 0..m {
resp[(i, j)] = a[j];
resp[(n_each + i, j)] = b[j];
}
labels[i] = 0;
labels[n_each + i] = 1;
}
(resp, labels)
}
#[test]
fn anova_flags_shifted_groups() {
let argvals = uniform_grid(81, -5.0, 5.0);
let (resp, labels) = two_group(&argvals, 12, -1.0, 1.0);
let res = frechet_anova(&resp, &argvals, &labels, 199, 42).unwrap();
assert!(
res.p_value_permutation < 0.05,
"perm p = {}",
res.p_value_permutation
);
}
#[test]
fn anova_ignores_homogeneous_sample() {
let argvals = uniform_grid(81, -5.0, 5.0);
let (resp, labels) = two_group(&argvals, 12, 0.0, 0.0);
let res = frechet_anova(&resp, &argvals, &labels, 199, 7).unwrap();
assert!(
res.p_value_permutation > 0.05,
"perm p = {}",
res.p_value_permutation
);
}
#[test]
fn anova_permutation_is_seed_reproducible() {
let argvals = uniform_grid(61, -5.0, 5.0);
let (resp, labels) = two_group(&argvals, 10, -0.7, 0.7);
let a = frechet_anova(&resp, &argvals, &labels, 99, 123).unwrap();
let b = frechet_anova(&resp, &argvals, &labels, 99, 123).unwrap();
assert_eq!(a.p_value_permutation, b.p_value_permutation);
assert_eq!(a.statistic, b.statistic);
}
#[test]
fn anova_rejects_too_few_groups() {
let argvals = uniform_grid(41, -5.0, 5.0);
let (resp, _labels) = two_group(&argvals, 5, 0.0, 0.0);
let labels = vec![0usize; resp.nrows()]; assert!(matches!(
frechet_anova(&resp, &argvals, &labels, 49, 1).unwrap_err(),
FdarError::InvalidParameter { parameter, .. } if parameter == "group_labels"
));
}
#[test]
fn anova_rejects_label_mismatch() {
let argvals = uniform_grid(41, -5.0, 5.0);
let (resp, _labels) = two_group(&argvals, 5, -1.0, 1.0);
let labels = vec![0usize, 1, 0]; assert!(matches!(
frechet_anova(&resp, &argvals, &labels, 49, 1).unwrap_err(),
FdarError::InvalidDimension { .. }
));
}
}