Skip to main content

ci_core/ci_tests/
log_likelihood.rs

1use crate::strategy::{CITest, CITestDataType, TestResult};
2use crate::utils::power_divergence::power_divergence;
3use ndarray::{Array1, Array2};
4
5const LOG_LIKELIHOOD_LAMBDA: f64 = 0.0;
6
7/// Log-likelihood ratio (G-test) conditional independence test (λ = 0).
8///
9/// Operates on discrete data only. Delegates to the power-divergence family
10/// with λ = 0, which corresponds to the G-test / log-likelihood ratio statistic.
11#[derive(Debug, Clone, PartialEq)]
12pub struct LogLikelihood {
13    pub boolean: bool,
14    pub significance_level: f64,
15}
16
17impl LogLikelihood {
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 LogLikelihood {
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            LOG_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 = LogLikelihood {
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 = LogLikelihood {
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        assert!((stat).abs() < EPS, " got {stat}");
91        assert!(p > 0.99);
92        assert_eq!(dof, 2);
93    }
94
95    #[test]
96    fn uncond_dependent_data_rejected() {
97        let t = LogLikelihood {
98            boolean: false,
99            significance_level: 0.05,
100        };
101        let x = array![1., 1., 1., 1., 1., 1., 2., 2., 2., 2., 2., 2.];
102        let y = array![1., 1., 1., 1., 1., 2., 1., 2., 2., 2., 2., 2.];
103        let empty = Array2::<f64>::zeros((0, 0));
104
105        let (p, stat, dof) = unwrap_correlated(&t.run_test(x, y, empty).unwrap());
106        assert!((stat - 5.822_063_320_647_374).abs() < EPS, "got {stat}");
107        assert!((p - 0.015_826_368_796_540_195).abs() < EPS, "got {p}");
108        assert_eq!(dof, 1);
109    }
110
111    #[test]
112    fn cond_dependent_data_rejected() {
113        let t = LogLikelihood {
114            boolean: false,
115            significance_level: 0.05,
116        };
117        let x = array![1., 1., 2., 2., 1., 1., 2., 2.];
118        let y = array![1., 1., 2., 2., 1., 1., 2., 2.];
119        let z = array![[1.], [1.], [1.], [1.], [2.], [2.], [2.], [2.]];
120
121        let (p, stat, dof) = unwrap_correlated(&t.run_test(x, y, z).unwrap());
122        assert!(
123            (stat - 11.090_354_888_959_125).abs() < EPS,
124            "for stat got {stat}"
125        );
126        assert!((p - 0.003_906_249_999_999_994).abs() < EPS, "for p got {p}");
127        assert_eq!(dof, 2);
128    }
129
130    #[test]
131    fn uncond_bool_rejects_dependent() {
132        let t = LogLikelihood {
133            boolean: true,
134            significance_level: 0.05,
135        };
136        let x = array![1., 1., 1., 1., 1., 1., 2., 2., 2., 2., 2., 2.];
137        let y = array![1., 1., 1., 1., 1., 2., 1., 2., 2., 2., 2., 2.];
138        let empty = Array2::<f64>::zeros((0, 0));
139        let r = t.run_test(x, y, empty).unwrap();
140        assert!(matches!(r, TestResult::Boolean(false)));
141    }
142
143    #[test]
144    fn cond_bool_rejects_dependent() {
145        let t = LogLikelihood {
146            boolean: true,
147            significance_level: 0.05,
148        };
149        let x = array![1., 1., 1., 2., 2., 2., 1., 1., 1., 2., 2., 2.];
150        let y = array![1., 1., 2., 2., 2., 2., 1., 1., 2., 2., 2., 2.];
151        let z = array![
152            [1.],
153            [1.],
154            [1.],
155            [1.],
156            [1.],
157            [1.],
158            [2.],
159            [2.],
160            [2.],
161            [2.],
162            [2.],
163            [2.]
164        ];
165
166        let r = t.run_test(x, y, z).unwrap();
167        assert!(matches!(r, TestResult::Boolean(false)));
168    }
169}