adjustp/
benjamini_yekutieli.rs

1use crate::utils::{rank_rev, reindex, sort_vector_rev};
2use num_traits::{Float, FromPrimitive};
3use std::iter::Sum;
4
5pub struct BenjaminiYekutieli<T: Float + FromPrimitive + Sum> {
6    num_elements: usize,
7    current_max: T,
8    cumulative: T,
9}
10
11impl<T: Float + FromPrimitive + Sum> Default for BenjaminiYekutieli<T> {
12    fn default() -> Self {
13        Self::new(0)
14    }
15}
16
17impl<T: Float + FromPrimitive + Sum> BenjaminiYekutieli<T> {
18    /// Creates a new instance of BenjaminiYekutieli
19    ///
20    /// # Arguments
21    ///
22    /// * `num_elements` - The number of elements in the dataset
23    #[must_use]
24    pub fn new(num_elements: usize) -> Self {
25        let cumulative = Self::calculate_cumulative(num_elements);
26        Self {
27            num_elements,
28            current_max: T::one(),
29            cumulative,
30        }
31    }
32
33    /// Calculates the cumulative sum used in the BY procedure
34    fn calculate_cumulative(num_elements: usize) -> T {
35        (1..=num_elements)
36            .map(|x| T::from_usize(x).unwrap().recip())
37            .sum()
38    }
39
40    /// Calculates the adjusted p-value given the p-value and the rank.
41    ///
42    /// This function is not deterministic and may give different q-values for the same
43    /// p-value depending on the internal state (i.e., if the current max has changed).
44    ///
45    /// # Arguments
46    ///
47    /// * `pvalue` - The p-value to adjust
48    /// * `rank` - The rank of the p-value
49    pub fn adjust(&mut self, pvalue: T, rank: usize) -> T {
50        let n = T::from_usize(self.num_elements).unwrap();
51        let r = T::from_usize(rank).unwrap();
52        let qvalue = (pvalue * self.cumulative * (n / r))
53            .min(self.current_max)
54            .min(T::one());
55        self.current_max = qvalue;
56        qvalue
57    }
58
59    /// Performs the procedure on a slice of floats.
60    ///
61    /// This first sorts the p-values in descending order,
62    /// then performs the correction using the ascending order ranks,
63    /// and finally reindexes the array to return it in the same order as provided.
64    ///
65    /// # Arguments
66    ///
67    /// * `slice` - A slice of p-values to adjust
68    #[must_use]
69    pub fn adjust_slice(slice: &[T]) -> Vec<T> {
70        if slice.is_empty() {
71            return Vec::new();
72        }
73
74        let mut method = Self::new(slice.len());
75        let original_index = rank_rev(slice);
76        let max = original_index.len();
77
78        let sorted_qvalues = sort_vector_rev(slice)
79            .iter()
80            .enumerate()
81            .map(|(idx, &p)| method.adjust(p, max - idx))
82            .collect::<Vec<T>>();
83
84        reindex(&sorted_qvalues, &original_index)
85    }
86}
87
88#[cfg(test)]
89mod testing {
90    use super::BenjaminiYekutieli;
91
92    #[test]
93    fn example() {
94        let pvalues = vec![0.1, 0.2, 0.3, 0.4, 0.1];
95        let adj_pvalues = BenjaminiYekutieli::adjust_slice(&pvalues);
96        assert_eq!(
97            adj_pvalues,
98            vec![
99                0.5708333333333333,
100                0.7611111111111112,
101                0.8562500,
102                0.9133333333333333,
103                0.5708333333333333
104            ]
105        );
106    }
107
108    #[test]
109    fn example_null() {
110        let pvalues: Vec<f64> = vec![];
111        let adj_pvalues = BenjaminiYekutieli::adjust_slice(&pvalues);
112        assert_eq!(adj_pvalues, vec![]);
113    }
114
115    #[test]
116    fn example_adjust() {
117        let mut b = BenjaminiYekutieli::new(100);
118        assert_eq!(b.adjust(0.001, 1), 0.5187377517639621);
119        assert_eq!(b.adjust(0.001, 2), 0.25936887588198104);
120        assert_eq!(b.adjust(0.001, 3), 0.1729125839213207);
121    }
122}