Skip to main content

ci_core/ci_tests/
freeman_tukey.rs

1use crate::strategy::{CITest, CITestDataType, TestResult};
2use crate::utils::power_divergence::power_divergence;
3use ndarray::{Array1, Array2};
4
5const FREEMAN_TUKEY_LAMBDA: f64 = -1.0 / 2.0;
6
7/// Freeman–Tukey conditional independence test (λ = −1/2).
8///
9/// Operates on discrete data only. Delegates to the power-divergence family
10/// with λ = −1/2, the Freeman–Tukey statistic.
11#[derive(Debug, Clone, PartialEq)]
12pub struct FreemanTukey {
13    pub boolean: bool,
14    pub significance_level: f64,
15}
16
17impl FreemanTukey {
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 FreemanTukey {
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            FREEMAN_TUKEY_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_not_rejected() {
65        let t = FreemanTukey {
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_not_rejected() {
81        let t = FreemanTukey {
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);
91        assert!(p > 0.99);
92        assert_eq!(dof, 2);
93    }
94
95    #[test]
96    fn uncond_dependent_rejected() {
97        let t = FreemanTukey {
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 - 6.319_453_539_579_289).abs() < EPS, "got {stat}");
107        assert!((p - 0.011_942_042_564_347_121).abs() < EPS, "got {p}");
108        assert_eq!(dof, 1);
109    }
110
111    #[test]
112    fn cond_dependent_rejected() {
113        let t = FreemanTukey {
114            boolean: false,
115            significance_level: 0.05,
116        };
117        let x = array![1., 1., 2., 2., 1., 2., 1., 1., 2., 2., 1., 2.];
118        let y = array![1., 2., 1., 2., 2., 1., 1., 2., 1., 2., 2., 1.];
119        let z = array![
120            [1.],
121            [1.],
122            [1.],
123            [1.],
124            [1.],
125            [1.],
126            [2.],
127            [2.],
128            [2.],
129            [2.],
130            [2.],
131            [2.]
132        ];
133
134        let (p, stat, dof) = unwrap_correlated(&t.run_test(x, y, z).unwrap());
135        assert!(
136            (stat - 1.382_538_273_265_069_5).abs() < EPS,
137            "got stat {stat}"
138        );
139        assert!((p - 0.500_939_904_278_208_8).abs() < EPS, "got p value {p}");
140        assert_eq!(dof, 2);
141    }
142
143    #[test]
144    fn uncond_boolean_accepts_independent() {
145        let t = FreemanTukey {
146            boolean: true,
147            significance_level: 0.05,
148        };
149        let x = array![1., 1., 2., 2., 1., 1., 2., 2.];
150        let y = array![1., 2., 1., 2., 1., 2., 1., 2.];
151        let empty = Array2::<f64>::zeros((0, 0));
152        let r = t.run_test(x, y, empty).unwrap();
153        assert!(matches!(r, TestResult::Boolean(true)));
154    }
155
156    #[test]
157    fn cond_boolean_rejects_dependent() {
158        let t = FreemanTukey {
159            boolean: true,
160            significance_level: 0.05,
161        };
162        let x = array![1., 1., 1., 2., 2., 2., 1., 1., 1., 2., 2., 2.];
163        let y = array![1., 1., 2., 2., 2., 2., 1., 1., 2., 2., 2., 2.];
164        let z = array![
165            [1.],
166            [1.],
167            [1.],
168            [1.],
169            [1.],
170            [1.],
171            [2.],
172            [2.],
173            [2.],
174            [2.],
175            [2.],
176            [2.]
177        ];
178
179        let r = t.run_test(x, y, z).unwrap();
180        assert!(matches!(r, TestResult::Boolean(false)));
181    }
182}