use anyhow::{Result, anyhow};
use single_utilities::traits::FloatOps;
use std::cmp::Ordering;
pub fn bonferroni_correction<T>(p_values: &[T]) -> Result<Vec<T>>
where
T: FloatOps,
{
let n = p_values.len();
if n == 0 {
return Err(anyhow!("Empty p-value array"));
}
for (i, &p) in p_values.iter().enumerate() {
if !(T::zero()..=T::one()).contains(&p) {
return Err(anyhow!("Invalid p-value at index {:?}: {:?}", i, p));
}
}
let n_t = T::from(n).unwrap();
let adjusted = p_values
.iter()
.map(|&p| num_traits::Float::min(p * n_t, T::one()))
.collect();
Ok(adjusted)
}
pub fn benjamini_hochberg_correction<T>(p_values: &[T]) -> Result<Vec<T>>
where
T: FloatOps,
{
let n = p_values.len();
if n == 0 {
return Err(anyhow::anyhow!("Empty p-value array"));
}
for (i, &p) in p_values.iter().enumerate() {
if !(T::zero()..=T::one()).contains(&p) {
return Err(anyhow::anyhow!("Invalid p-value at index {:?}: {:?}", i, p));
}
}
let n_f = T::from(n).unwrap();
let mut indices: Vec<usize> = (0..n).collect();
indices.sort_unstable_by(|&a, &b| {
p_values[a]
.partial_cmp(&p_values[b])
.unwrap_or(Ordering::Equal)
});
let mut adjusted = vec![T::zero(); n];
let mut running_min = T::one();
for i in (0..n).rev() {
let orig_idx = indices[i];
let rank = T::from(i + 1).unwrap();
let adjustment = num_traits::Float::min(p_values[orig_idx] * n_f / rank, T::one());
running_min = num_traits::Float::min(adjustment, running_min);
adjusted[orig_idx] = running_min;
}
Ok(adjusted)
}
pub fn benjamini_yekutieli_correction<T>(p_values: &[T]) -> Result<Vec<T>>
where
T: FloatOps,
{
let n = p_values.len();
if n == 0 {
return Err(anyhow!("Empty p-value array"));
}
let c_n: T = (1..=n).map(|i| T::one() / T::from(i).unwrap()).sum();
let mut indexed_p_values: Vec<(usize, T)> =
p_values.iter().enumerate().map(|(i, &p)| (i, p)).collect();
indexed_p_values.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
let mut adjusted_p_values = vec![T::zero(); n];
let mut current_min = T::one();
let n_f64 = T::from(n).unwrap();
for i in (0..n).rev() {
let (orig_idx, p_val) = indexed_p_values[i];
let rank = i + 1;
let adjustment =
num_traits::Float::min(p_val * c_n * n_f64 / T::from(rank).unwrap(), T::one());
current_min = num_traits::Float::min(adjustment, current_min);
adjusted_p_values[orig_idx] = current_min;
}
Ok(adjusted_p_values)
}
pub fn holm_bonferroni_correction<T>(p_values: &[T]) -> Result<Vec<T>>
where
T: FloatOps,
{
let n = p_values.len();
if n == 0 {
return Err(anyhow!("Empty p-value array"));
}
let zero = T::zero();
let one = T::one();
for (i, &p) in p_values.iter().enumerate() {
if p < zero || p > one {
return Err(anyhow!("Invalid p-value at index {}: {:?}", i, p));
}
}
let mut indexed_p_values: Vec<(usize, T)> =
p_values.iter().enumerate().map(|(i, &p)| (i, p)).collect();
indexed_p_values.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
let mut adjusted_p_values = vec![zero; n];
for (i, &(idx, p_val)) in indexed_p_values.iter().enumerate() {
let multiplier = T::from((n - i) as f64).unwrap_or(one);
let adjusted_p = p_val * multiplier;
adjusted_p_values[idx] = num_traits::Float::min(adjusted_p, one);
}
Ok(adjusted_p_values)
}
pub fn hochberg_correction<T>(p_values: &[T]) -> Result<Vec<T>>
where
T: FloatOps,
{
let n = p_values.len();
if n == 0 {
return Err(anyhow!("Empty p-value array"));
}
let zero = T::zero();
let one = T::one();
let mut indexed_p_values: Vec<(usize, T)> =
p_values.iter().enumerate().map(|(i, &p)| (i, p)).collect();
indexed_p_values.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(Ordering::Equal));
let mut adjusted_p_values = vec![zero; n];
adjusted_p_values[indexed_p_values[0].0] = indexed_p_values[0].1;
for i in 1..n {
let current_index = indexed_p_values[i].0;
let prev_index = indexed_p_values[i - 1].0;
let n_t = T::from(n).unwrap_or(one);
let denominator_t = T::from(n - i).unwrap_or(one);
let hochberg_value =
num_traits::Float::min(indexed_p_values[i].1 * n_t / denominator_t, one);
adjusted_p_values[current_index] =
num_traits::Float::min(hochberg_value, adjusted_p_values[prev_index]);
}
Ok(adjusted_p_values)
}
pub fn storey_qvalues<T>(p_values: &[T], lambda: T) -> Result<Vec<T>>
where
T: FloatOps,
{
let n = p_values.len();
if n == 0 {
return Err(anyhow!("Empty p-value array"));
}
let zero = T::zero();
let one = T::one();
if lambda <= zero || lambda >= one {
return Err(anyhow!("Lambda must be between 0 and 1, got {:?}", lambda));
}
for (i, &p) in p_values.iter().enumerate() {
if p < zero || p > one {
return Err(anyhow!("Invalid p-value at index {}: {:?}", i, p));
}
}
let w = p_values.iter().filter(|&&p| p > lambda).count();
let w_t = T::from(w).unwrap_or(zero);
let n_t = T::from(n).unwrap_or(one);
let pi0 = w_t / (n_t * (one - lambda));
let pi0 = num_traits::Float::min(pi0, one);
let bh_adjusted = benjamini_hochberg_correction(p_values)?;
let q_values = bh_adjusted
.iter()
.map(|&p| num_traits::Float::min(p * pi0, one))
.collect();
Ok(q_values)
}
pub fn adaptive_storey_qvalues<T>(p_values: &[T]) -> Result<Vec<T>>
where
T: FloatOps,
{
let n = p_values.len();
if n == 0 {
return Err(anyhow!("Empty p-value array"));
}
let zero = T::zero();
let one = T::one();
for (i, &p) in p_values.iter().enumerate() {
if p < zero || p > one {
return Err(anyhow!("Invalid p-value at index {}: {:?}", i, p));
}
}
let lambda_values = [0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9];
let lambda_grid: Vec<T> = lambda_values
.iter()
.filter_map(|&val| T::from(val))
.collect();
let mut pi0_estimates = Vec::with_capacity(lambda_grid.len());
for &lambda in &lambda_grid {
let w = p_values.iter().filter(|&&p| p > lambda).count();
let w_t = T::from(w as f64).unwrap_or(zero);
let n_t = T::from(n as f64).unwrap_or(one);
let pi0 = w_t / (n_t * (one - lambda));
pi0_estimates.push(num_traits::Float::min(pi0, one));
}
let pi0_sum: T = pi0_estimates.iter().copied().sum();
let estimates_len = T::from(pi0_estimates.len() as f64).unwrap_or(one);
let pi0_mean = pi0_sum / estimates_len;
let pi0 = if pi0_estimates.is_empty() {
one
} else {
num_traits::Float::min(pi0_mean, one)
};
let bh_adjusted = benjamini_hochberg_correction(p_values)?;
let q_values = bh_adjusted
.iter()
.map(|&p| num_traits::Float::min(p * pi0, one))
.collect();
Ok(q_values)
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_vec_relative_eq(a: &[f64], b: &[f64], epsilon: f64) {
assert_eq!(a.len(), b.len(), "Vectors have different lengths");
for (i, (x, y)) in a.iter().zip(b.iter()).enumerate() {
if (x - y).abs() > epsilon {
panic!("Vectors differ at index {}: {} != {}", i, x, y);
}
}
}
#[test]
fn test_bonferroni() {
let p_values = vec![0.01, 0.02, 0.03, 0.1, 0.2];
let expected = vec![0.05, 0.1, 0.15, 0.5, 1.0];
let adjusted = bonferroni_correction(&p_values).unwrap();
assert_vec_relative_eq(&adjusted, &expected, 1e-10);
}
use approx::assert_relative_eq;
#[test]
fn test_benjamini_hochberg_empty_input() {
let result = benjamini_hochberg_correction::<f64>(&[]);
assert!(result.is_err());
assert_eq!(result.unwrap_err().to_string(), "Empty p-value array");
}
#[test]
fn test_benjamini_hochberg_invalid_pvalues() {
let result = benjamini_hochberg_correction(&[0.01, -0.5, 0.03]);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Invalid p-value at index 1")
);
let result = benjamini_hochberg_correction(&[0.01, 1.5, 0.03]);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Invalid p-value at index 1")
);
}
#[test]
fn test_benjamini_hochberg_identical_pvalues() {
let p_values = vec![0.05, 0.05, 0.05];
let expected = vec![0.05, 0.05, 0.05];
let adjusted = benjamini_hochberg_correction(&p_values).unwrap();
for (a, e) in adjusted.iter().zip(expected.iter()) {
assert_relative_eq!(*a, *e, epsilon = 1e-10);
}
}
#[test]
fn test_benjamini_hochberg_ordered_pvalues() {
let p_values = vec![0.01, 0.02, 0.03, 0.04, 0.05];
let expected = vec![0.05, 0.05, 0.05, 0.05, 0.05];
let adjusted = benjamini_hochberg_correction(&p_values).unwrap();
for (i, (&a, &e)) in adjusted.iter().zip(expected.iter()).enumerate() {
assert_relative_eq!(a, e, epsilon = 1e-10, max_relative = 1e-10);
if num_traits::Float::abs(a - e) > 1e-10 {
panic!("mismatch at index {}: expected {}, got {}", i, e, a);
}
}
}
#[test]
fn test_benjamini_hochberg_unordered_pvalues() {
let p_values = vec![0.05, 0.01, 0.1, 0.04, 0.02];
let expected = vec![0.0625, 0.05, 0.1, 0.0625, 0.05];
let adjusted = benjamini_hochberg_correction(&p_values).unwrap();
for (i, (&a, &e)) in adjusted.iter().zip(expected.iter()).enumerate() {
if num_traits::Float::abs(a - e) > 1e-3 {
panic!(
"mismatch at index {}: expected {}, got {}, whole: {:?}",
i, e, a, adjusted
);
}
}
}
#[test]
fn test_benjamini_hochberg_edge_cases() {
let p_values = vec![1e-10, 1e-9, 1e-8];
let adjusted = benjamini_hochberg_correction(&p_values).unwrap();
assert!(adjusted.iter().all(|&p| p > 0.0 && p < 0.001));
let p_values = vec![0.1, 0.2, 1.0];
let adjusted = benjamini_hochberg_correction(&p_values).unwrap();
assert_relative_eq!(adjusted[2], 1.0, epsilon = 1e-10);
}
#[test]
fn test_benjamini_hochberg_real_example() {
let pvalues = vec![0.1, 0.2, 0.3, 0.4, 0.1];
let expected = [0.25, 0.3333333333333333, 0.375, 0.4, 0.25];
let adjusted = benjamini_hochberg_correction(&pvalues).unwrap();
for (i, (&a, &e)) in adjusted.iter().zip(expected.iter()).enumerate() {
assert_relative_eq!(a, e, epsilon = 1e-3, max_relative = 1e-3);
if num_traits::Float::abs(a - e) > 1e-3 {
panic!("mismatch at index {}: expected {}, got {}", i, e, a);
}
}
}
#[test]
fn test_benjamini_hochberg_single_pvalue() {
let p_values = vec![0.025];
let adjusted = benjamini_hochberg_correction(&p_values).unwrap();
assert_relative_eq!(adjusted[0], 0.025, epsilon = 1e-10);
}
#[test]
fn test_holm_bonferroni() {
let p_values = vec![0.01, 0.02, 0.03];
let expected = vec![0.03, 0.04, 0.03];
let adjusted = holm_bonferroni_correction(&p_values).unwrap();
assert_vec_relative_eq(&adjusted, &expected, 1e-10);
}
#[test]
fn test_storey_qvalues() {
let p_values = vec![0.01, 0.02, 0.03, 0.6, 0.7];
let qvalues = storey_qvalues(&p_values, 0.5).unwrap();
for &q in &qvalues {
assert!((0.0..=1.0).contains(&q));
}
assert!(qvalues[0] <= qvalues[2]);
assert!(qvalues[3] <= qvalues[4]);
}
#[test]
fn test_invalid_inputs() {
assert!(bonferroni_correction::<f32>(&[]).is_err());
assert!(benjamini_hochberg_correction::<f32>(&[]).is_err());
assert!(holm_bonferroni_correction::<f32>(&[]).is_err());
assert!(storey_qvalues(&[0.5], -0.1).is_err());
assert!(storey_qvalues(&[0.5], 1.0).is_err());
let invalid_p = vec![-0.1, 0.5, 1.1];
assert!(bonferroni_correction(&invalid_p).is_err());
assert!(benjamini_hochberg_correction(&invalid_p).is_err());
}
}