Skip to main content

ci_core/ci_tests/
modified_likelihood.rs

1use crate::strategy::{CITest, CITestDataType, TestResult};
2use crate::utils::power_divergence::power_divergence;
3use ndarray::{Array1, Array2};
4
5const MODIFIED_LIKELIHOOD_LAMBDA: f64 = -1.0;
6
7/// Modified log-likelihood ratio conditional independence test (λ = −1).
8///
9/// Operates on discrete data only. Delegates to the power-divergence family
10/// with λ = −1, the modified log-likelihood ratio statistic.
11#[derive(Debug, Clone, PartialEq)]
12pub struct ModifiedLikelihood {
13    pub boolean: bool,
14    pub significance_level: f64,
15}
16
17impl ModifiedLikelihood {
18    #[must_use]
19    pub fn new(boolean: bool, significance_level: f64) -> Self {
20        Self {
21            boolean,
22            significance_level,
23        }
24    }
25}
26
27impl CITest for ModifiedLikelihood {
28    fn run_test(
29        &self,
30        x_values: Array1<f64>,
31        y_values: Array1<f64>,
32        z: Array2<f64>,
33    ) -> anyhow::Result<TestResult> {
34        power_divergence(
35            &x_values,
36            &y_values,
37            &z,
38            self.boolean,
39            self.significance_level,
40            MODIFIED_LIKELIHOOD_LAMBDA,
41        )
42    }
43
44    fn data_types(&self) -> &'static [CITestDataType] {
45        &[CITestDataType::Discrete]
46    }
47}
48
49#[cfg(test)]
50#[allow(clippy::many_single_char_names)]
51mod tests {
52    use super::*;
53    use crate::utils::EPS;
54    use ndarray::{array, Array2};
55
56    fn unwrap_correlated(r: &TestResult) -> (f64, f64, usize) {
57        match r {
58            TestResult::Statistic(a, b, c) => (*a, *b, *c),
59            _ => panic!("expected Correlated2"),
60        }
61    }
62
63    #[test]
64    fn uncond_independent_data_accepted() {
65        let t = ModifiedLikelihood {
66            boolean: false,
67            significance_level: 0.05,
68        };
69        let x = array![1., 1., 2., 2., 1., 1., 2., 2.];
70        let y = array![1., 2., 1., 2., 1., 2., 1., 2.];
71        let empty = Array2::<f64>::zeros((0, 0));
72
73        let (p, stat, dof) = unwrap_correlated(&t.run_test(x, y, empty).unwrap());
74        assert!(stat.abs() < EPS);
75        assert!(p > 0.99);
76        assert_eq!(dof, 1);
77    }
78
79    #[test]
80    fn cond_independent_data_accepted() {
81        let t = ModifiedLikelihood {
82            boolean: false,
83            significance_level: 0.05,
84        };
85        let x = array![1., 1., 2., 2., 1., 1., 2., 2.];
86        let y = array![1., 2., 1., 2., 1., 2., 1., 2.];
87        let z = array![[1.], [1.], [1.], [1.], [2.], [2.], [2.], [2.]];
88
89        let (p, stat, dof) = unwrap_correlated(&t.run_test(x, y, z).unwrap());
90
91        assert!(stat.abs() < EPS, " got stat {stat}");
92        assert!(p > 0.99, " got p {p}");
93        assert_eq!(dof, 2);
94    }
95
96    #[test]
97    fn uncond_dependent_data_rejected() {
98        let t = ModifiedLikelihood {
99            boolean: false,
100            significance_level: 0.05,
101        };
102        let x = array![1., 1., 1., 1., 1., 1., 2., 2., 2., 2., 2., 2.];
103        let y = array![1., 1., 1., 1., 1., 2., 1., 2., 2., 2., 2., 2.];
104        let empty = Array2::<f64>::zeros((0, 0));
105
106        let (p, stat, dof) = unwrap_correlated(&t.run_test(x, y, empty).unwrap());
107        assert!((stat - 7.053_439_978_825_427).abs() < EPS, "got {stat}");
108        assert!((p - 0.007_911_317_670_556_329).abs() < EPS, "got {p}");
109        assert_eq!(dof, 1);
110    }
111
112    #[test]
113    fn cond_dependent_data_rejected() {
114        let t = ModifiedLikelihood {
115            boolean: false,
116            significance_level: 0.05,
117        };
118        let x = array![1., 1., 1., 2., 2., 2., 1., 1., 1., 2., 2., 2.];
119        let y = array![1., 1., 2., 2., 2., 1., 1., 1., 2., 2., 2., 1.];
120        let z = array![
121            [1.],
122            [1.],
123            [1.],
124            [1.],
125            [1.],
126            [1.],
127            [2.],
128            [2.],
129            [2.],
130            [2.],
131            [2.],
132            [2.]
133        ];
134
135        let (p, stat, dof) = unwrap_correlated(&t.run_test(x, y, z).unwrap());
136
137        assert!(
138            (stat - 1.413_396_427_876_601_6).abs() < EPS,
139            "got stat {stat}"
140        );
141        assert!((p - 0.493_270_184_272_571_97).abs() < EPS, "got p {p}");
142        assert_eq!(dof, 2);
143    }
144
145    #[test]
146    fn uncond_bool_rejects_dependent() {
147        let t = ModifiedLikelihood {
148            boolean: true,
149            significance_level: 0.05,
150        };
151        let x = array![1., 1., 1., 1., 1., 1., 2., 2., 2., 2., 2., 2.];
152        let y = array![1., 1., 1., 1., 1., 2., 1., 2., 2., 2., 2., 2.];
153        let empty = Array2::<f64>::zeros((0, 0));
154        let r = t.run_test(x, y, empty).unwrap();
155        assert!(matches!(r, TestResult::Boolean(false)));
156    }
157
158    #[test]
159    fn cond_bool_accepts_independent() {
160        let t = ModifiedLikelihood {
161            boolean: true,
162            significance_level: 0.05,
163        };
164        let x = array![1., 1., 2., 2., 1., 1., 2., 2.];
165        let y = array![1., 1., 2., 2., 1., 1., 2., 2.];
166        let z = array![[1.], [1.], [1.], [1.], [2.], [2.], [2.], [2.]];
167        let r = t.run_test(x, y, z).unwrap();
168        assert!(matches!(r, TestResult::Boolean(false)));
169    }
170}