use statrs::function::erf::erfc;
use std::f64::consts::SQRT_2;
pub fn two_sided_p(modz: f64) -> f64 {
erfc(modz.abs() / SQRT_2)
}
pub fn benjamini_hochberg(pvals: &[f64], q: f64) -> Option<f64> {
let m = pvals.len();
if m == 0 {
return None;
}
let mut sorted = pvals.to_vec();
sorted.sort_by(f64::total_cmp);
let m_f = m as f64;
let mut cutoff = None;
for (i, &p) in sorted.iter().enumerate() {
let rank = (i + 1) as f64;
if p <= (rank / m_f) * q {
cutoff = Some(p);
}
}
cutoff
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn two_sided_p_known_values() {
assert_eq!(two_sided_p(0.0), 1.0);
assert_eq!(two_sided_p(2.5), two_sided_p(-2.5));
assert!((two_sided_p(1.959964) - 0.05).abs() < 1e-4);
assert!((two_sided_p(3.5) - 4.6520e-4).abs() < 1e-6);
assert!(two_sided_p(4.0) < two_sided_p(3.0));
}
#[test]
fn bh_empty_is_none() {
assert_eq!(benjamini_hochberg(&[], 0.05), None);
}
#[test]
fn bh_all_null_rejects_nothing() {
let p = [0.40, 0.55, 0.70, 0.85, 0.99];
assert_eq!(benjamini_hochberg(&p, 0.05), None);
}
#[test]
fn bh_rejects_the_clearly_significant_only() {
let p = [0.0001, 0.40, 0.55, 0.70, 0.99];
let cutoff = benjamini_hochberg(&p, 0.05).unwrap();
assert_eq!(cutoff, 0.0001);
let rejected = p.iter().filter(|&&x| x <= cutoff).count();
assert_eq!(rejected, 1);
}
#[test]
fn bh_step_up_rejects_a_run_not_just_the_smallest() {
let p = [0.40, 0.011, 0.30, 0.009, 0.012];
let cutoff = benjamini_hochberg(&p, 0.05).unwrap();
assert_eq!(cutoff, 0.012);
assert_eq!(p.iter().filter(|&&x| x <= cutoff).count(), 3);
}
#[test]
fn bh_is_order_independent() {
let a = [0.04, 0.011, 0.03, 0.009, 0.012];
let mut b = a;
b.reverse();
assert_eq!(benjamini_hochberg(&a, 0.05), benjamini_hochberg(&b, 0.05));
}
#[test]
fn bh_larger_q_rejects_at_least_as_much() {
let p = [0.001, 0.02, 0.03, 0.2, 0.5, 0.6];
let lo = benjamini_hochberg(&p, 0.01).map_or(0, |c| p.iter().filter(|&&x| x <= c).count());
let hi = benjamini_hochberg(&p, 0.10).map_or(0, |c| p.iter().filter(|&&x| x <= c).count());
assert!(
hi >= lo,
"a larger FDR level cannot reject fewer ({hi} >= {lo})"
);
}
}