use faer::{Mat, linalg::solvers::DenseSolveCore};
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use rayon::prelude::*;
use statrs::distribution::FisherSnedecor;
use statrs::distribution::{Continuous, ContinuousCDF, Normal};
use statrs::function::gamma::ln_gamma;
use crate::core::math::vector_helpers::*;
use crate::prelude::*;
pub type EffectSizeRes<T> = (Vec<T>, Vec<T>);
pub fn hedge_g_effect<T>(
mean_a: &[T],
mean_b: &[T],
std_a: &[T],
std_b: &[T],
n_a: usize,
n_b: usize,
small_sample_correction: bool,
) -> EffectSizeRes<T>
where
T: BixverseFloat,
{
assert_same_len!(mean_a, mean_b, std_a, std_b);
let n_a_t = T::from_usize(n_a).unwrap();
let n_b_t = T::from_usize(n_b).unwrap();
let total_n = T::from_usize(n_a + n_b).unwrap();
let two = T::from_usize(2).unwrap();
let three = T::from_usize(3).unwrap();
let (effect_sizes, standard_errors): (Vec<T>, Vec<T>) = mean_a
.par_iter()
.zip(mean_b.par_iter())
.zip(std_a.par_iter())
.zip(std_b.par_iter())
.map(|(((mean_a, mean_b), std_a), std_b)| {
let pooled_sd = (((n_a_t - T::one()) * std_a.powi(2)
+ (n_b_t - T::one()) * std_b.powi(2))
/ (total_n - two))
.sqrt();
let mut effect_size = (*mean_a - *mean_b) / pooled_sd;
if small_sample_correction {
let correction_factor = ((total_n - three)
/ (total_n - T::from_f64(2.25).unwrap()))
* ((total_n - two) / total_n).sqrt();
effect_size = correction_factor * effect_size;
}
let standard_error =
((total_n / (n_a_t * n_b_t)) + (effect_size.powi(2) / (two * total_n))).sqrt();
(effect_size, standard_error)
})
.unzip();
(effect_sizes, standard_errors)
}
#[derive(Clone, Debug, Default)]
pub enum TestAlternative {
#[default]
TwoSided,
Greater,
Less,
}
pub fn get_test_alternative(s: &str) -> Option<TestAlternative> {
match s.to_lowercase().as_str() {
"twosided" => Some(TestAlternative::TwoSided),
"greater" => Some(TestAlternative::Greater),
"less" => Some(TestAlternative::Less),
_ => None,
}
}
pub fn z_scores_to_pval<T>(z_scores: &[T], test_alternative: &str) -> Vec<T>
where
T: BixverseFloat,
{
let test_alternative = get_test_alternative(test_alternative).unwrap_or_default();
let normal = Normal::new(0.0, 1.0).unwrap();
let one = T::one();
let two = T::from_usize(2).unwrap();
let six = T::from_usize(6).unwrap();
z_scores
.iter()
.map(|&z| match test_alternative {
TestAlternative::TwoSided => {
let abs_z = z.abs();
if abs_z > six {
let abs_z_f64 = abs_z.to_f64().unwrap();
let pdf = T::from_f64(normal.pdf(abs_z_f64)).unwrap();
let p = pdf / abs_z * (one - one / (abs_z * abs_z));
two * p
} else {
let abs_z_f64 = abs_z.to_f64().unwrap();
let cdf = T::from_f64(normal.cdf(abs_z_f64)).unwrap();
two * (one - cdf)
}
}
TestAlternative::Greater => {
if z > six {
let z_f64 = z.to_f64().unwrap();
let pdf = T::from_f64(normal.pdf(z_f64)).unwrap();
pdf / z * (one - one / (z * z))
} else {
let z_f64 = z.to_f64().unwrap();
let cdf = T::from_f64(normal.cdf(z_f64)).unwrap();
one - cdf
}
}
TestAlternative::Less => {
let neg_six = -six;
if z < neg_six {
let abs_z = z.abs();
let abs_z_f64 = abs_z.to_f64().unwrap();
let pdf = T::from_f64(normal.pdf(abs_z_f64)).unwrap();
pdf / abs_z * (one - one / (abs_z * abs_z))
} else {
let z_f64 = z.to_f64().unwrap();
T::from_f64(normal.cdf(z_f64)).unwrap()
}
}
})
.collect()
}
pub fn hypergeom_pval<T>(q: usize, m: usize, n: usize, k: usize) -> T
where
T: BixverseFloat,
{
if q == 0 {
return T::one();
}
let population = m + n;
let (n_f, m_f, k_f) = (
T::from_usize(n).unwrap(),
T::from_usize(m).unwrap(),
T::from_usize(k).unwrap(),
);
let population_f = T::from_usize(population).unwrap();
let upper = k.min(m);
let mut log_probs = Vec::new();
for i in (q + 1)..=upper {
let i_f = T::from_usize(i).unwrap();
let log_pmf_f64 = ln_gamma(m_f.to_f64().unwrap() + 1.0)
- ln_gamma(i_f.to_f64().unwrap() + 1.0)
- ln_gamma((m_f - i_f).to_f64().unwrap() + 1.0)
+ ln_gamma(n_f.to_f64().unwrap() + 1.0)
- ln_gamma((k_f - i_f).to_f64().unwrap() + 1.0)
- ln_gamma((n_f - (k_f - i_f)).to_f64().unwrap() + 1.0)
- (ln_gamma(population_f.to_f64().unwrap() + 1.0)
- ln_gamma(k_f.to_f64().unwrap() + 1.0)
- ln_gamma((population_f - k_f).to_f64().unwrap() + 1.0));
log_probs.push(T::from_f64(log_pmf_f64).unwrap());
}
if log_probs.is_empty() {
return T::zero();
}
let max_log_prob = log_probs
.iter()
.cloned()
.fold(T::neg_infinity(), |a, b| a.max(b));
let mut sum = T::zero();
for log_p in log_probs {
sum += (log_p - max_log_prob).exp();
}
sum * max_log_prob.exp()
}
pub fn calc_fdr<T>(pvals: &[T]) -> Vec<T>
where
T: BixverseFloat,
{
let n = pvals.len();
let n_t = T::from_usize(n).unwrap();
let one = T::one();
let mut indexed_pval: Vec<(usize, T)> =
pvals.par_iter().enumerate().map(|(i, &x)| (i, x)).collect();
indexed_pval
.sort_unstable_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let adj_pvals_tmp: Vec<T> = indexed_pval
.par_iter()
.enumerate()
.map(|(i, (_, p))| {
let i_t = T::from_usize(i + 1).unwrap();
(n_t / i_t) * *p
})
.collect();
let mut current_min = adj_pvals_tmp[n - 1].min(one);
let mut monotonic_adj = vec![current_min; n];
for i in (0..n - 1).rev() {
current_min = current_min.min(adj_pvals_tmp[i]).min(one);
monotonic_adj[i] = current_min;
}
let mut adj_pvals = vec![T::zero(); n];
for (i, &(original_idx, _)) in indexed_pval.iter().enumerate() {
adj_pvals[original_idx] = monotonic_adj[i];
}
adj_pvals
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct ManovaResult<T>
where
T: BixverseFloat,
{
pub sscp_between: Mat<T>,
pub sscp_within: Mat<T>,
pub sscp_total: Mat<T>,
pub df_between: usize,
pub df_within: usize,
pub df_total: usize,
pub n_vars: usize,
pub group_means: Vec<Vec<T>>,
pub overall_mean: Vec<T>,
}
#[allow(dead_code)]
impl<T> ManovaResult<T>
where
T: BixverseFloat + std::iter::Sum,
{
fn rank1_eigenvalue(&self) -> T {
debug_assert_eq!(
self.df_between, 1,
"rank1_eigenvalue assumes two-group MANOVA (df_between == 1)"
);
let e_inv = self.sscp_within.partial_piv_lu().inverse();
let prod = &e_inv * &self.sscp_between;
let trace: T = prod.diagonal().column_vector().iter().copied().sum::<T>();
trace.max(T::zero())
}
pub fn wilks_lambda(&self) -> T {
let lambda = self.rank1_eigenvalue();
T::one() / (T::one() + lambda)
}
pub fn pillai_trace(&self) -> T {
let lambda = self.rank1_eigenvalue();
lambda / (T::one() + lambda)
}
fn two_group_f_test(&self) -> (T, T) {
let lambda = self.rank1_eigenvalue();
let p = T::from_usize(self.n_vars).unwrap();
let n = T::from_usize(self.df_within + self.df_between + 1).unwrap();
let df2 = n - p - T::one();
if !lambda.is_finite() || df2 <= T::zero() {
return (T::nan(), T::one());
}
let f_stat = (df2 / p) * lambda;
let df1_f64 = p.to_f64().unwrap();
let df2_f64 = df2.to_f64().unwrap();
let f_f64 = f_stat.to_f64().unwrap();
if !f_f64.is_finite() || f_f64 < 0.0 {
return (f_stat, T::one());
}
let f_dist = match FisherSnedecor::new(df1_f64, df2_f64) {
Ok(d) => d,
Err(_) => return (f_stat, T::one()),
};
let p_value = T::from_f64(1.0 - f_dist.cdf(f_f64)).unwrap();
(f_stat, p_value)
}
pub fn wilks_f_test(&self) -> (T, T) {
self.two_group_f_test()
}
pub fn pillai_f_test(&self) -> (T, T) {
self.two_group_f_test()
}
}
#[derive(Debug)]
#[allow(dead_code)]
pub struct ManovaSummary<T>
where
T: BixverseFloat,
{
pub wilks_lambda: T,
pub pillai_trace: T,
pub df_between: usize,
pub df_within: usize,
pub f_stat_wilk: T,
pub p_val_wilk: T,
pub f_stat_pillai: T,
pub p_val_pillai: T,
}
impl<T> ManovaSummary<T>
where
T: BixverseFloat + std::iter::Sum,
{
pub fn from_manova_res(res: &ManovaResult<T>) -> Self {
let (f_stat_wilk, p_val_wilk) = res.wilks_f_test();
let (f_stat_pillai, p_val_pillai) = res.pillai_f_test();
ManovaSummary {
wilks_lambda: res.wilks_lambda(),
pillai_trace: res.pillai_trace(),
df_between: res.df_between,
df_within: res.df_within,
f_stat_wilk,
p_val_wilk,
f_stat_pillai,
p_val_pillai,
}
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct AnovaSummary<T>
where
T: BixverseFloat,
{
pub variable_index: usize,
pub ss_between: T,
pub ss_within: T,
pub ms_between: T,
pub ms_within: T,
pub f_stat: T,
pub p_val: T,
}
pub fn summary_aov<T>(res: &ManovaResult<T>) -> Vec<AnovaSummary<T>>
where
T: BixverseFloat,
{
let mut aov_res = Vec::with_capacity(res.n_vars);
let df_between_t = T::from_usize(res.df_between).unwrap();
let df_within_t = T::from_usize(res.df_within).unwrap();
let f_dist = FisherSnedecor::new(
df_between_t.to_f64().unwrap(),
df_within_t.to_f64().unwrap(),
)
.ok();
for var_idx in 0..res.n_vars {
let ss_between = res.sscp_between[(var_idx, var_idx)];
let ss_within = res.sscp_within[(var_idx, var_idx)];
let ms_between = ss_between / df_between_t;
let ms_within = ss_within / df_within_t;
let (f_stat, p_val) = if ms_within <= T::zero() {
(T::nan(), T::one())
} else {
let f = ms_between / ms_within;
let f_f64 = f.to_f64().unwrap();
let pv = match (&f_dist, f_f64.is_finite() && f_f64 >= 0.0) {
(Some(d), true) => T::from_f64(1.0 - d.cdf(f_f64)).unwrap(),
_ => T::one(),
};
(f, pv)
};
aov_res.push(AnovaSummary {
variable_index: var_idx,
ss_between,
ss_within,
ms_between,
ms_within,
f_stat,
p_val,
});
}
aov_res
}
pub fn trigamma<T: BixverseFloat>(x: T) -> T {
let mut x = x;
let mut result = T::zero();
if x <= T::from_f64(5.0).unwrap() {
while x < T::from_f64(5.0).unwrap() {
result += T::one() / (x * x);
x += T::one();
}
}
let xx = x * x;
result += T::one() / x
+ T::one() / (T::from_f64(2.0).unwrap() * xx)
+ T::one() / (T::from_f64(6.0).unwrap() * xx * x);
let xxx = xx * x;
result += -T::one() / (T::from_f64(30.0).unwrap() * xxx * x)
+ T::one() / (T::from_f64(42.0).unwrap() * xxx * xx * x)
- T::one() / (T::from_f64(30.0).unwrap() * xxx * xxx * x);
result
}
pub fn logit<T>(p: T) -> T
where
T: BixverseFloat,
{
(p / (T::one() - p)).ln()
}
pub fn inv_logit<T>(q: T) -> T
where
T: BixverseFloat,
{
q.exp() / (T::one() + q.exp())
}
pub fn calculate_critval<T: BixverseFloat>(
values: &[T],
sample_size: usize,
alpha: &T,
seed: usize,
) -> T {
let mut rng = StdRng::seed_from_u64(seed as u64);
let mut random_sample: Vec<T> = (0..sample_size)
.map(|_| {
let index = rng.random_range(0..values.len());
values[index]
})
.collect();
random_sample.sort_by(|a, b| b.partial_cmp(a).unwrap());
let index = (*alpha * T::from_usize(random_sample.len()).unwrap())
.ceil()
.to_usize()
.unwrap();
random_sample[index + 1]
}
#[derive(Clone, Debug, Default)]
pub enum OutlierDirection {
#[default]
Both,
Below,
Above,
}
pub fn parse_outlier_type(s: &str) -> Option<OutlierDirection> {
match s.to_lowercase().as_str() {
"below" => Some(OutlierDirection::Below),
"above" => Some(OutlierDirection::Above),
"twosided" => Some(OutlierDirection::Both),
_ => None,
}
}
pub fn mad_outlier<T>(x: &[T], threshold: T, direction: OutlierDirection) -> (Vec<bool>, T)
where
T: BixverseFloat,
{
let median_val = match median(x) {
Some(m) => m,
None => return (vec![], T::zero()),
};
let mad_val = match mad(x, None) {
Some(m) => m,
None => return (vec![], T::zero()),
};
let margin = threshold * mad_val;
let res = x
.iter()
.map(|&v| match direction {
OutlierDirection::Below => v < median_val - margin,
OutlierDirection::Above => v > median_val + margin,
OutlierDirection::Both => (v - median_val).abs() > margin,
})
.collect::<Vec<bool>>();
(res, margin)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_logit_and_inv_logit() {
let p: f64 = 0.5;
let log_odds: f64 = logit(p);
assert!((log_odds - 0.0).abs() < 1e-6);
let recovered_p = inv_logit(log_odds);
assert!((recovered_p - 0.5).abs() < 1e-6);
}
#[test]
fn test_z_scores_to_pval() {
let z_scores: Vec<f64> = vec![0.0, 1.95996398454];
let pvals_two_sided = z_scores_to_pval(&z_scores, "twosided");
assert!((pvals_two_sided[0] - 1.0).abs() < 1e-6);
assert!((pvals_two_sided[1] - 0.05).abs() < 1e-5);
}
#[test]
fn test_calc_fdr() {
let pvals: Vec<f64> = vec![0.01, 0.04, 0.03];
let fdr = calc_fdr(&pvals);
assert!((fdr[0] - 0.03).abs() < 1e-6);
assert!((fdr[1] - 0.04).abs() < 1e-6);
assert!((fdr[2] - 0.04).abs() < 1e-6);
}
#[test]
fn test_mad_outlier() {
let vec: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 100.0];
let (outliers, margin) = mad_outlier(&vec, 3.0, OutlierDirection::Both);
assert!((margin - 3.0).abs() < 1e-6);
assert_eq!(outliers, vec![false, false, false, false, true]); }
#[test]
fn test_parse_helpers() {
assert!(matches!(
get_test_alternative("twosided"),
Some(TestAlternative::TwoSided)
));
assert!(matches!(
get_test_alternative("GREATER"),
Some(TestAlternative::Greater)
));
assert!(matches!(
parse_outlier_type("below"),
Some(OutlierDirection::Below)
));
assert!(matches!(
parse_outlier_type("Twosided"),
Some(OutlierDirection::Both)
));
}
#[test]
fn test_manova_two_group_analytic() {
let mat = Mat::from_fn(4, 2, |i, j| match (i, j) {
(0, 0) => 1.0f64,
(0, 1) => 4.0,
(1, 0) => 2.0,
(1, 1) => 1.0,
(2, 0) => 5.0,
(2, 1) => 7.0,
(3, 0) => 6.0,
(3, 1) => 3.0,
_ => unreachable!(),
});
let res = crate::enrichment::mitch::manova_mitch(mat.as_ref(), &[2, 3]);
let wilks = res.wilks_lambda();
let pillai = res.pillai_trace();
let recovered_lambda = (1.0 - wilks) / wilks;
assert!((recovered_lambda - 1105.0).abs() < 1e-6);
assert!((wilks + pillai - 1.0).abs() < 1e-12);
assert!((wilks - 1.0 / 1106.0).abs() < 1e-9);
assert!((pillai - 1105.0 / 1106.0).abs() < 1e-9);
let (f_w, _) = res.wilks_f_test();
let (f_p, p_p) = res.pillai_f_test();
assert!((f_w - 552.5).abs() < 1e-6);
assert!((f_p - 552.5).abs() < 1e-6);
assert!((0.0..=1.0).contains(&p_p));
}
#[test]
fn test_manova_no_overflow_large_p() {
let p = 80;
let n_total = 2949usize;
let sigma = (n_total as f64).powi(3) / 12.0;
let sscp_within = Mat::from_fn(p, p, |i, j| if i == j { sigma } else { sigma * 0.3 });
let sscp_between = Mat::from_fn(
p,
p,
|i, j| {
if i == j { sigma * 1.5e-3 } else { sigma * 5e-4 }
},
);
let sscp_total = &sscp_within + &sscp_between;
let res = ManovaResult::<f64> {
sscp_between,
sscp_within,
sscp_total,
df_between: 1,
df_within: n_total - 2,
df_total: n_total - 1,
n_vars: p,
group_means: vec![vec![0.0; p], vec![0.0; p]],
overall_mean: vec![0.0; p],
};
let wilks = res.wilks_lambda();
let pillai = res.pillai_trace();
let (f_w, p_w) = res.wilks_f_test();
let (f_p, p_p) = res.pillai_f_test();
assert!(wilks.is_finite() && wilks > 0.0 && wilks <= 1.0);
assert!((0.0..1.0).contains(&pillai) && pillai.is_finite());
assert!((wilks + pillai - 1.0).abs() < 1e-9);
assert!(f_w.is_finite() && f_w >= 0.0);
assert!(f_p.is_finite() && f_p >= 0.0);
assert!((0.0..=1.0).contains(&p_w));
assert!((0.0..=1.0).contains(&p_p));
assert!((f_w - f_p).abs() < 1e-9);
assert!((p_w - p_p).abs() < 1e-9);
}
#[test]
fn test_summary_aov_constant_column() {
let p = 2;
let sscp_within = Mat::from_fn(p, p, |i, j| {
if i == 0 && j == 0 {
0.0
} else if i == j {
10.0
} else {
0.0
}
});
let sscp_between = Mat::from_fn(p, p, |i, j| if i == j { 5.0 } else { 0.0 });
let sscp_total = &sscp_within + &sscp_between;
let res = ManovaResult::<f64> {
sscp_between,
sscp_within,
sscp_total,
df_between: 1,
df_within: 10,
df_total: 11,
n_vars: p,
group_means: vec![vec![0.0; p], vec![0.0; p]],
overall_mean: vec![0.0; p],
};
let aov = summary_aov(&res);
assert_eq!(aov.len(), 2);
assert!(aov[0].f_stat.is_nan());
assert!((aov[0].p_val - 1.0).abs() < 1e-12);
assert!(aov[1].f_stat.is_finite() && aov[1].f_stat > 0.0);
assert!((0.0..=1.0).contains(&aov[1].p_val));
}
}