1use anyhow::{Result, anyhow};
2use single_utilities::traits::FloatOps;
3use std::cmp::Ordering;
4
5pub fn bonferroni_correction<T>(p_values: &[T]) -> Result<Vec<T>>
25where
26 T: FloatOps,
27{
28 let n = p_values.len();
29
30 if n == 0 {
31 return Err(anyhow!("Empty p-value array"));
32 }
33
34 for (i, &p) in p_values.iter().enumerate() {
36 if !(T::zero()..=T::one()).contains(&p) {
37 return Err(anyhow!("Invalid p-value at index {:?}: {:?}", i, p));
38 }
39 }
40
41 let n_t = T::from(n).unwrap();
43 let adjusted = p_values
44 .iter()
45 .map(|&p| num_traits::Float::min((p * n_t), T::one()))
46 .collect();
47
48 Ok(adjusted)
49}
50
51pub fn benjamini_hochberg_correction<T>(p_values: &[T]) -> Result<Vec<T>>
68where
69 T: FloatOps,
70{
71 let n = p_values.len();
72 if n == 0 {
73 return Err(anyhow::anyhow!("Empty p-value array"));
74 }
75
76 for (i, &p) in p_values.iter().enumerate() {
78 if !(T::zero()..=T::one()).contains(&p) {
79 return Err(anyhow::anyhow!("Invalid p-value at index {:?}: {:?}", i, p));
80 }
81 }
82
83 let n_f = T::from(n).unwrap();
84
85 let mut indices: Vec<usize> = (0..n).collect();
86 indices.sort_unstable_by(|&a, &b| {
87 p_values[a]
88 .partial_cmp(&p_values[b])
89 .unwrap_or(Ordering::Equal)
90 });
91
92 let mut adjusted = vec![T::zero(); n];
93 let mut running_min = T::one();
94
95 for i in (0..n).rev() {
96 let orig_idx = indices[i];
97 let rank = T::from(i + 1).unwrap();
98 let adjustment = num_traits::Float::min(p_values[orig_idx] * n_f / rank, T::one());
99 running_min = num_traits::Float::min(adjustment, running_min);
100 adjusted[orig_idx] = running_min;
101 }
102
103 Ok(adjusted)
104}
105
106pub fn benjamini_yekutieli_correction<T>(p_values: &[T]) -> Result<Vec<T>>
123where
124 T: FloatOps,
125{
126 let n = p_values.len();
127 if n == 0 {
128 return Err(anyhow!("Empty p-value array"));
129 }
130
131 let c_n: T = (1..=n).map(|i| T::one() / T::from(i).unwrap()).sum();
133
134 let mut indexed_p_values: Vec<(usize, T)> =
136 p_values.iter().enumerate().map(|(i, &p)| (i, p)).collect();
137
138 indexed_p_values.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
140
141 let mut adjusted_p_values = vec![T::zero(); n];
143 let mut current_min = T::one();
144 let n_f64 = T::from(n).unwrap();
145 for i in (0..n).rev() {
147 let (orig_idx, p_val) = indexed_p_values[i];
148 let rank = i + 1;
149
150 let adjustment =
152 num_traits::Float::min((p_val * c_n * n_f64 / T::from(rank).unwrap()), T::one());
153 current_min = num_traits::Float::min(adjustment, current_min);
154 adjusted_p_values[orig_idx] = current_min;
155 }
156
157 Ok(adjusted_p_values)
158}
159
160pub fn holm_bonferroni_correction<T>(p_values: &[T]) -> Result<Vec<T>>
177where
178 T: FloatOps,
179{
180 let n = p_values.len();
181
182 if n == 0 {
183 return Err(anyhow!("Empty p-value array"));
184 }
185
186 let zero = T::zero();
187 let one = T::one();
188
189 for (i, &p) in p_values.iter().enumerate() {
191 if p < zero || p > one {
192 return Err(anyhow!("Invalid p-value at index {}: {:?}", i, p));
193 }
194 }
195
196 let mut indexed_p_values: Vec<(usize, T)> =
198 p_values.iter().enumerate().map(|(i, &p)| (i, p)).collect();
199
200 indexed_p_values.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
201
202 let mut adjusted_p_values = vec![zero; n];
204
205 for (i, &(idx, p_val)) in indexed_p_values.iter().enumerate() {
207 let multiplier = T::from((n - i) as f64).unwrap_or(one);
209 let adjusted_p = p_val * multiplier;
210 adjusted_p_values[idx] = num_traits::Float::min(adjusted_p, one);
211 }
212
213 Ok(adjusted_p_values)
214}
215
216pub fn hochberg_correction<T>(p_values: &[T]) -> Result<Vec<T>>
233where
234 T: FloatOps,
235{
236 let n = p_values.len();
237
238 if n == 0 {
239 return Err(anyhow!("Empty p-value array"));
240 }
241
242 let zero = T::zero();
243 let one = T::one();
244
245 let mut indexed_p_values: Vec<(usize, T)> =
247 p_values.iter().enumerate().map(|(i, &p)| (i, p)).collect();
248
249 indexed_p_values.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(Ordering::Equal));
250
251 let mut adjusted_p_values = vec![zero; n];
253
254 adjusted_p_values[indexed_p_values[0].0] = indexed_p_values[0].1;
256
257 for i in 1..n {
259 let current_index = indexed_p_values[i].0;
260 let prev_index = indexed_p_values[i - 1].0;
261
262 let n_t = T::from(n).unwrap_or(one);
264 let denominator_t = T::from(n - i).unwrap_or(one);
265
266 let hochberg_value =
267 num_traits::Float::min((indexed_p_values[i].1 * n_t / denominator_t), one);
268 adjusted_p_values[current_index] =
269 num_traits::Float::min(hochberg_value, adjusted_p_values[prev_index]);
270 }
271
272 Ok(adjusted_p_values)
273}
274
275pub fn storey_qvalues<T>(p_values: &[T], lambda: T) -> Result<Vec<T>>
293where
294 T: FloatOps,
295{
296 let n = p_values.len();
297
298 if n == 0 {
299 return Err(anyhow!("Empty p-value array"));
300 }
301
302 let zero = T::zero();
303 let one = T::one();
304
305 if lambda <= zero || lambda >= one {
306 return Err(anyhow!("Lambda must be between 0 and 1, got {:?}", lambda));
307 }
308
309 for (i, &p) in p_values.iter().enumerate() {
311 if p < zero || p > one {
312 return Err(anyhow!("Invalid p-value at index {}: {:?}", i, p));
313 }
314 }
315
316 let w = p_values.iter().filter(|&&p| p > lambda).count();
318 let w_t = T::from(w).unwrap_or(zero);
319 let n_t = T::from(n).unwrap_or(one);
320
321 let pi0 = w_t / (n_t * (one - lambda));
322 let pi0 = num_traits::Float::min(pi0, one); let bh_adjusted = benjamini_hochberg_correction(p_values)?;
326
327 let q_values = bh_adjusted
329 .iter()
330 .map(|&p| num_traits::Float::min((p * pi0), one))
331 .collect();
332
333 Ok(q_values)
334}
335
336pub fn adaptive_storey_qvalues<T>(p_values: &[T]) -> Result<Vec<T>>
353where
354 T: FloatOps,
355{
356 let n = p_values.len();
357
358 if n == 0 {
359 return Err(anyhow!("Empty p-value array"));
360 }
361
362 let zero = T::zero();
363 let one = T::one();
364
365 for (i, &p) in p_values.iter().enumerate() {
367 if p < zero || p > one {
368 return Err(anyhow!("Invalid p-value at index {}: {:?}", i, p));
369 }
370 }
371
372 let lambda_values = [0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9];
374 let lambda_grid: Vec<T> = lambda_values
375 .iter()
376 .filter_map(|&val| T::from(val))
377 .collect();
378
379 let mut pi0_estimates = Vec::with_capacity(lambda_grid.len());
381 for &lambda in &lambda_grid {
382 let w = p_values.iter().filter(|&&p| p > lambda).count();
383 let w_t = T::from(w as f64).unwrap_or(zero);
384 let n_t = T::from(n as f64).unwrap_or(one);
385
386 let pi0 = w_t / (n_t * (one - lambda));
387 pi0_estimates.push(num_traits::Float::min(pi0, one));
388 }
389
390 let pi0_sum: T = pi0_estimates.iter().copied().sum();
393 let estimates_len = T::from(pi0_estimates.len() as f64).unwrap_or(one);
394 let pi0_mean = pi0_sum / estimates_len;
395
396 let pi0 = if pi0_estimates.is_empty() {
397 one
398 } else {
399 num_traits::Float::min(pi0_mean, one)
400 };
401
402 let bh_adjusted = benjamini_hochberg_correction(p_values)?;
404
405 let q_values = bh_adjusted
407 .iter()
408 .map(|&p| num_traits::Float::min((p * pi0), one))
409 .collect();
410
411 Ok(q_values)
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417
418 fn assert_vec_relative_eq(a: &[f64], b: &[f64], epsilon: f64) {
419 assert_eq!(a.len(), b.len(), "Vectors have different lengths");
420 for (i, (x, y)) in a.iter().zip(b.iter()).enumerate() {
421 if (x - y).abs() > epsilon {
422 panic!("Vectors differ at index {}: {} != {}", i, x, y);
423 }
424 }
425 }
426
427 #[test]
428 fn test_bonferroni() {
429 let p_values = vec![0.01, 0.02, 0.03, 0.1, 0.2];
430 let expected = vec![0.05, 0.1, 0.15, 0.5, 1.0];
431 let adjusted = bonferroni_correction(&p_values).unwrap();
432 assert_vec_relative_eq(&adjusted, &expected, 1e-10);
433 }
434
435 use approx::assert_relative_eq;
436
437 #[test]
438 fn test_benjamini_hochberg_empty_input() {
439 let result = benjamini_hochberg_correction::<f64>(&[]);
441 assert!(result.is_err());
442 assert_eq!(result.unwrap_err().to_string(), "Empty p-value array");
443 }
444
445 #[test]
446 fn test_benjamini_hochberg_invalid_pvalues() {
447 let result = benjamini_hochberg_correction(&[0.01, -0.5, 0.03]);
449 assert!(result.is_err());
450 assert!(
451 result
452 .unwrap_err()
453 .to_string()
454 .contains("Invalid p-value at index 1")
455 );
456
457 let result = benjamini_hochberg_correction(&[0.01, 1.5, 0.03]);
459 assert!(result.is_err());
460 assert!(
461 result
462 .unwrap_err()
463 .to_string()
464 .contains("Invalid p-value at index 1")
465 );
466 }
467
468 #[test]
469 fn test_benjamini_hochberg_identical_pvalues() {
470 let p_values = vec![0.05, 0.05, 0.05];
472 let expected = vec![0.05, 0.05, 0.05];
473 let adjusted = benjamini_hochberg_correction(&p_values).unwrap();
474
475 for (a, e) in adjusted.iter().zip(expected.iter()) {
476 assert_relative_eq!(*a, *e, epsilon = 1e-10);
477 }
478 }
479
480 #[test]
481 fn test_benjamini_hochberg_ordered_pvalues() {
482 let p_values = vec![0.01, 0.02, 0.03, 0.04, 0.05];
484 let expected = vec![0.05, 0.05, 0.05, 0.05, 0.05];
485 let adjusted = benjamini_hochberg_correction(&p_values).unwrap();
486
487 for (i, (&a, &e)) in adjusted.iter().zip(expected.iter()).enumerate() {
488 assert_relative_eq!(a, e, epsilon = 1e-10, max_relative = 1e-10);
489 if num_traits::Float::abs(a - e) > 1e-10 {
491 panic!("mismatch at index {}: expected {}, got {}", i, e, a);
492 }
493 }
494 }
495
496 #[test]
497 fn test_benjamini_hochberg_unordered_pvalues() {
498 let p_values = vec![0.05, 0.01, 0.1, 0.04, 0.02];
500 let expected = vec![0.0625, 0.05, 0.1, 0.0625, 0.05];
501 let adjusted = benjamini_hochberg_correction(&p_values).unwrap();
502
503 for (i, (&a, &e)) in adjusted.iter().zip(expected.iter()).enumerate() {
504 if num_traits::Float::abs(a - e) > 1e-3 {
507 panic!(
508 "mismatch at index {}: expected {}, got {}, whole: {:?}",
509 i, e, a, adjusted
510 );
511 }
512 }
513 }
514
515 #[test]
516 fn test_benjamini_hochberg_edge_cases() {
517 let p_values = vec![1e-10, 1e-9, 1e-8];
519 let adjusted = benjamini_hochberg_correction(&p_values).unwrap();
520 assert!(adjusted.iter().all(|&p| p > 0.0 && p < 0.001));
522
523 let p_values = vec![0.1, 0.2, 1.0];
525 let adjusted = benjamini_hochberg_correction(&p_values).unwrap();
526 assert_relative_eq!(adjusted[2], 1.0, epsilon = 1e-10);
528 }
529
530 #[test]
531 fn test_benjamini_hochberg_real_example() {
532 let pvalues = vec![0.1, 0.2, 0.3, 0.4, 0.1];
534 let expected = [0.25, 0.3333333333333333, 0.375, 0.4, 0.25];
535 let adjusted = benjamini_hochberg_correction(&pvalues).unwrap();
536
537 for (i, (&a, &e)) in adjusted.iter().zip(expected.iter()).enumerate() {
538 assert_relative_eq!(a, e, epsilon = 1e-3, max_relative = 1e-3);
539 if num_traits::Float::abs(a - e) > 1e-3 {
541 panic!("mismatch at index {}: expected {}, got {}", i, e, a);
542 }
543 }
544 }
545
546 #[test]
547 fn test_benjamini_hochberg_single_pvalue() {
548 let p_values = vec![0.025];
550 let adjusted = benjamini_hochberg_correction(&p_values).unwrap();
551 assert_relative_eq!(adjusted[0], 0.025, epsilon = 1e-10);
552 }
553 #[test]
554 fn test_holm_bonferroni() {
555 let p_values = vec![0.01, 0.02, 0.03];
556 let expected = vec![0.03, 0.04, 0.03];
557 let adjusted = holm_bonferroni_correction(&p_values).unwrap();
558 assert_vec_relative_eq(&adjusted, &expected, 1e-10);
559 }
560
561 #[test]
562 fn test_storey_qvalues() {
563 let p_values = vec![0.01, 0.02, 0.03, 0.6, 0.7];
564 let qvalues = storey_qvalues(&p_values, 0.5).unwrap();
565 for &q in &qvalues {
567 assert!((0.0..=1.0).contains(&q));
568 }
569 assert!(qvalues[0] <= qvalues[2]);
571 assert!(qvalues[3] <= qvalues[4]);
572 }
573
574 #[test]
575 fn test_invalid_inputs() {
576 assert!(bonferroni_correction::<f32>(&[]).is_err());
578 assert!(benjamini_hochberg_correction::<f32>(&[]).is_err());
579 assert!(holm_bonferroni_correction::<f32>(&[]).is_err());
580
581 assert!(storey_qvalues(&[0.5], -0.1).is_err());
583 assert!(storey_qvalues(&[0.5], 1.0).is_err());
584
585 let invalid_p = vec![-0.1, 0.5, 1.1];
587 assert!(bonferroni_correction(&invalid_p).is_err());
588 assert!(benjamini_hochberg_correction(&invalid_p).is_err());
589 }
590}