pub fn benjamini_hochberg(p_values: &[f64], q: f64) -> Vec<bool> {
let m = p_values.len();
let mut rejected = vec![false; m];
if m == 0 || q <= 0.0 {
return rejected;
}
if let Some((order, k)) = bh_cutoff(p_values, q) {
for &orig in &order[..k] {
rejected[orig] = true;
}
}
rejected
}
#[derive(Clone, Debug, PartialEq)]
pub struct FdrVerdict {
pub q: f64,
pub n_tested: usize,
pub n_discoveries: usize,
pub rejected: Vec<bool>,
pub threshold: Option<f64>,
}
pub fn fdr_verdict(p_values: &[f64], q: f64) -> FdrVerdict {
let m = p_values.len();
let (rejected, threshold) = match (m, q) {
(0, _) => (Vec::new(), None),
_ if q <= 0.0 => (vec![false; m], None),
_ => match bh_cutoff(p_values, q) {
Some((order, k)) => {
let mut mask = vec![false; m];
for &orig in &order[..k] {
mask[orig] = true;
}
(mask, Some(p_values[order[k - 1]]))
}
None => (vec![false; m], None),
},
};
let n_discoveries = rejected.iter().filter(|&&r| r).count();
FdrVerdict {
q,
n_tested: m,
n_discoveries,
rejected,
threshold,
}
}
fn bh_cutoff(p_values: &[f64], q: f64) -> Option<(Vec<usize>, usize)> {
let m = p_values.len();
let mut order: Vec<usize> = (0..m).collect();
order.sort_by(|&a, &b| {
p_values[a]
.partial_cmp(&p_values[b])
.unwrap_or(std::cmp::Ordering::Equal)
.then(a.cmp(&b))
});
let mut k_star: Option<usize> = None;
for rank in 1..=m {
let p = p_values[order[rank - 1]];
if p <= (rank as f64 / m as f64) * q {
k_star = Some(rank);
}
}
k_star.map(|k| (order, k))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_vector_rejects_the_correct_count() {
let p = [
0.9, 0.009, 0.205, 0.0008, 0.58, 0.51, 0.35, 0.165, 0.396, 0.75,
];
let rej = benjamini_hochberg(&p, 0.05);
assert_eq!(rej.iter().filter(|&&r| r).count(), 2);
assert!(rej[3] && rej[1]);
assert!(rej
.iter()
.enumerate()
.all(|(i, &r)| r == (i == 1 || i == 3)));
}
#[test]
fn all_null_rejects_none() {
let p = [0.4, 0.6, 0.55, 0.9, 0.72, 0.83];
let rej = benjamini_hochberg(&p, 0.05);
assert!(rej.iter().all(|&r| !r));
let v = fdr_verdict(&p, 0.05);
assert_eq!(v.n_discoveries, 0);
assert_eq!(v.threshold, None);
}
#[test]
fn all_significant_rejects_all() {
let p = [0.001, 0.002, 0.0005, 0.003];
let rej = benjamini_hochberg(&p, 0.05);
assert!(rej.iter().all(|&r| r));
}
#[test]
fn is_less_conservative_than_bonferroni() {
let p = [0.001, 0.006, 0.012, 0.018, 0.9, 0.8, 0.7, 0.95, 0.6, 0.85];
let rej = benjamini_hochberg(&p, 0.05);
let bonferroni = 0.05 / p.len() as f64; let bonf_count = p.iter().filter(|&&x| x <= bonferroni).count();
let bh_count = rej.iter().filter(|&&r| r).count();
assert!(
bh_count > bonf_count,
"BH {bh_count} > Bonferroni {bonf_count}"
);
}
#[test]
fn verdict_reports_threshold_and_counts() {
let p = [
0.9, 0.009, 0.205, 0.0008, 0.58, 0.51, 0.35, 0.165, 0.396, 0.75,
];
let v = fdr_verdict(&p, 0.05);
assert_eq!(v.n_tested, 10);
assert_eq!(v.n_discoveries, 2);
assert_eq!(v.q, 0.05);
assert_eq!(v.threshold, Some(0.009));
assert_eq!(v.rejected.iter().filter(|&&r| r).count(), 2);
}
#[test]
fn empty_and_nonpositive_q_are_safe() {
assert!(benjamini_hochberg(&[], 0.05).is_empty());
assert!(benjamini_hochberg(&[0.001, 0.002], 0.0).iter().all(|&r| !r));
let v = fdr_verdict(&[], 0.05);
assert_eq!(v.n_tested, 0);
assert_eq!(v.n_discoveries, 0);
assert_eq!(v.threshold, None);
}
#[test]
fn deterministic_with_tied_pvalues() {
let p = [0.01, 0.01, 0.01, 0.9, 0.9];
let a = benjamini_hochberg(&p, 0.05);
let b = benjamini_hochberg(&p, 0.05);
assert_eq!(a, b);
}
}