use faer::{Mat, MatRef};
use rayon::prelude::*;
use crate::core::math::stats::*;
use crate::core::math::{matrix_helpers::*, vector_helpers::*};
use crate::prelude::*;
pub type MitchPathways = (Vec<String>, Vec<Vec<usize>>);
#[derive(Clone, Debug)]
pub struct MitchResult<'a, T> {
pub pathway_name: &'a str,
pub pathway_size: usize,
pub manova_pval: T,
pub scores: Vec<T>,
pub anova_pvals: Vec<T>,
pub s_dist: T,
pub mysd: T,
}
#[derive(Clone, Debug)]
pub struct MitchTotals<T: BixverseFloat> {
overall_mean: Vec<T>,
sscp_total: Mat<T>,
}
impl<T: BixverseFloat> MitchTotals<T> {
pub fn new(x: MatRef<T>) -> Self {
let x_centered = scale_matrix_col(&x.as_ref(), false);
Self {
overall_mean: col_means(x),
sscp_total: x_centered.transpose() * &x_centered,
}
}
}
pub fn manova_mitch<T: BixverseFloat>(x: MatRef<T>, group1_indices: &[usize]) -> ManovaResult<T> {
manova_mitch_cached(x, &MitchTotals::new(x), group1_indices)
}
pub fn manova_mitch_cached<T: BixverseFloat>(
x: MatRef<T>,
totals: &MitchTotals<T>,
group1_indices: &[usize],
) -> ManovaResult<T> {
let (n, p) = x.shape();
assert!(
group1_indices.iter().all(|&idx| idx < n),
"All indices must be less than the number of rows"
);
let mut is_group1 = vec![false; n];
for &idx in group1_indices {
is_group1[idx] = true;
}
let group0_indices: Vec<usize> = (0..n).filter(|&i| !is_group1[i]).collect();
let n1 = group1_indices.len();
let n0 = group0_indices.len();
let mut x0 = Mat::zeros(n0, p);
let mut x1 = Mat::zeros(n1, p);
for (new_i, &orig_i) in group0_indices.iter().enumerate() {
for j in 0..p {
x0[(new_i, j)] = x[(orig_i, j)];
}
}
for (new_i, &orig_i) in group1_indices.iter().enumerate() {
for j in 0..p {
x1[(new_i, j)] = x[(orig_i, j)];
}
}
let mean_0 = col_means(x0.as_ref());
let mean_1 = col_means(x1.as_ref());
let x0_centered = scale_matrix_col(&x0.as_ref(), false);
let x1_centered = scale_matrix_col(&x1.as_ref(), false);
let sscp_within =
x0_centered.transpose() * &x0_centered + x1_centered.transpose() * &x1_centered;
let sscp_total = totals.sscp_total.clone();
let sscp_between = &sscp_total - &sscp_within;
ManovaResult {
sscp_between,
sscp_within,
sscp_total,
df_between: 1,
df_within: n - 2,
df_total: n - 1,
n_vars: p,
group_means: vec![mean_0, mean_1],
overall_mean: totals.overall_mean.clone(),
}
}
pub fn mitch_rank<T: BixverseFloat>(mat: &MatRef<T>) -> Mat<T> {
let mut ranked_mat = Mat::zeros(mat.nrows(), mat.ncols());
ranked_mat
.par_col_iter_mut()
.enumerate()
.for_each(|(col_idx, mut col)| {
let original_col: Vec<T> = mat.col(col_idx).iter().copied().collect();
let mut zeros = T::zero();
let mut neg = T::zero();
for x in &original_col {
if *x == T::zero() {
zeros += T::one();
} else if *x < T::zero() {
neg += T::one();
}
}
let adj = neg + (zeros / T::from_f64(2.0).unwrap());
let ranks = rank_vector(&original_col);
let ranks = ranks.iter().map(|x| *x - adj).collect::<Vec<T>>();
for (row_idx, &rank) in ranks.iter().enumerate() {
col[row_idx] = rank;
}
});
ranked_mat
}
pub fn process_mitch_pathway<'a, T>(
ranked_mat: MatRef<T>,
pathway_name: &'a str,
pathway_indices: &[usize],
) -> MitchResult<'a, T>
where
T: BixverseFloat + std::iter::Sum,
{
process_mitch_pathway_cached(
ranked_mat,
&MitchTotals::new(ranked_mat),
pathway_name,
pathway_indices,
)
}
pub fn process_mitch_pathway_cached<'a, T>(
ranked_mat: MatRef<T>,
totals: &MitchTotals<T>,
pathway_name: &'a str,
pathway_indices: &[usize],
) -> MitchResult<'a, T>
where
T: BixverseFloat + std::iter::Sum,
{
let nrow = T::from_usize(ranked_mat.nrows()).unwrap();
let manova_res: ManovaResult<T> = manova_mitch_cached(ranked_mat, totals, pathway_indices);
let sum_manova: ManovaSummary<T> = ManovaSummary::from_manova_res(&manova_res);
let sum_anova = summary_aov(&manova_res);
let p_manova = sum_manova.p_val_pillai;
let mut p_aovs = Vec::with_capacity(sum_anova.len());
for sum in sum_anova {
p_aovs.push(sum.p_val);
}
let scores = &manova_res.group_means[0]
.iter()
.zip(&manova_res.group_means[1])
.map(|(mean_0, mean_1)| (T::from_f64(2.0).unwrap() * (*mean_1 - *mean_0)) / nrow)
.collect::<Vec<T>>();
let sd_scores = standard_deviation(scores);
let hypotenuse = scores
.iter()
.map(|x| x.powi(2))
.fold(T::zero(), |acc, x| acc + x)
.sqrt();
MitchResult {
pathway_name,
pathway_size: pathway_indices.len(),
manova_pval: p_manova,
scores: scores.clone(),
anova_pvals: p_aovs,
s_dist: hypotenuse,
mysd: sd_scores,
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn mat_from_cols(cols: &[Vec<f64>]) -> Mat<f64> {
Mat::from_fn(cols[0].len(), cols.len(), |i, j| cols[j][i])
}
#[test]
fn mitch_rank_subtracts_the_negative_and_zero_offset() {
let mat = mat_from_cols(&[vec![-2.0, -1.0, 0.0, 1.0, 2.0]]);
let ranked = mitch_rank(&mat.as_ref());
let expected = [-1.5, -0.5, 0.5, 1.5, 2.5];
for (i, &want) in expected.iter().enumerate() {
assert_relative_eq!(ranked[(i, 0)], want, epsilon = 1e-12);
}
}
#[test]
fn mitch_rank_averages_tied_ranks() {
let mat = mat_from_cols(&[vec![1.0, 1.0, -1.0, 0.0, 0.0]]);
let ranked = mitch_rank(&mat.as_ref());
let expected = [2.5, 2.5, -1.0, 0.5, 0.5];
for (i, &want) in expected.iter().enumerate() {
assert_relative_eq!(ranked[(i, 0)], want, epsilon = 1e-12);
}
}
#[test]
fn process_mitch_pathway_scores_dist_and_sd_are_hand_computable() {
let ranked = mat_from_cols(&[
vec![3.0, 1.0, -1.0, -3.0, 2.0, -2.0],
vec![1.0, 2.0, 3.0, -1.0, -2.0, -3.0],
]);
let res = process_mitch_pathway(ranked.as_ref(), "pathway_a", &[0, 1]);
assert_eq!(res.pathway_name, "pathway_a");
assert_eq!(res.pathway_size, 2);
assert_eq!(res.scores.len(), 2);
assert_relative_eq!(res.scores[0], 1.0, epsilon = 1e-12);
assert_relative_eq!(res.scores[1], 0.75, epsilon = 1e-12);
assert_relative_eq!(res.s_dist, 1.25, epsilon = 1e-12);
assert_relative_eq!(res.mysd, 0.03125_f64.sqrt(), epsilon = 1e-12);
assert_eq!(res.anova_pvals.len(), 2);
assert!(res.anova_pvals.iter().all(|p| (0.0..=1.0).contains(p)));
assert!((0.0..=1.0).contains(&res.manova_pval));
}
#[test]
fn process_mitch_pathway_scores_flip_sign_with_the_complement() {
let ranked = mat_from_cols(&[
vec![3.0, 1.0, -1.0, -3.0, 2.0, -2.0],
vec![1.0, 2.0, 3.0, -1.0, -2.0, -3.0],
]);
let a = process_mitch_pathway(ranked.as_ref(), "a", &[0, 1]);
let b = process_mitch_pathway(ranked.as_ref(), "b", &[2, 3, 4, 5]);
assert_relative_eq!(a.scores[0], -b.scores[0], epsilon = 1e-12);
assert_relative_eq!(a.scores[1], -b.scores[1], epsilon = 1e-12);
assert_relative_eq!(a.s_dist, b.s_dist, epsilon = 1e-12);
assert_relative_eq!(a.mysd, b.mysd, epsilon = 1e-12);
assert_eq!(b.pathway_size, 4);
}
}