Skip to main content

ci_core/ci_tests/
cressie_read.rs

1use crate::strategy::{CITest, CITestDataType, TestResult};
2use crate::utils::power_divergence::power_divergence;
3use ndarray::{Array1, Array2};
4
5const CRESSIE_READ_LAMBDA: f64 = 2.0 / 3.0;
6
7/// Cressie–Read conditional independence test (λ = 2/3).
8///
9/// Operates on discrete data only. Delegates to the power-divergence family
10/// with λ = 2/3, the Cressie–Read statistic.
11#[derive(Debug, Clone, PartialEq)]
12pub struct CressieRead {
13    pub boolean: bool,
14    pub significance_level: f64,
15}
16
17impl CressieRead {
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 CressieRead {
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            CRESSIE_READ_LAMBDA,
41        )
42    }
43    fn data_types(&self) -> &'static [CITestDataType] {
44        &[CITestDataType::Discrete]
45    }
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51    use crate::utils::EPS;
52    use ndarray::array;
53
54    fn unwrap_correlated(result: &TestResult) -> (f64, f64, usize) {
55        match result {
56            TestResult::Statistic(a, b, c) => (*a, *b, *c),
57            _ => panic!("expected Correlated2"),
58        }
59    }
60
61    fn unwrap_boolean(result: &TestResult) -> bool {
62        match result {
63            TestResult::Boolean(b) => *b,
64            _ => panic!("expected Boolean"),
65        }
66    }
67
68    #[test]
69    fn unconditional_independent_data_is_not_rejected() {
70        let test = CressieRead {
71            boolean: false,
72            significance_level: 0.05,
73        };
74        let x = array![1., 1., 2., 2., 1., 1., 2., 2.];
75        let y = array![1., 2., 1., 2., 1., 2., 1., 2.];
76        let empty_z = Array2::<f64>::zeros((0, 0));
77
78        let (p_value, statistic, dof) = unwrap_correlated(
79            &test
80                .run_test(x.clone(), y.clone(), empty_z.clone())
81                .unwrap(),
82        );
83        assert!(
84            statistic.abs() < EPS,
85            "expected statistic ~0, got {statistic}"
86        );
87        assert!(p_value > 0.99, "expected p ~1, got {p_value}");
88        assert_eq!(dof, 1);
89
90        let independent = unwrap_boolean(
91            &CressieRead {
92                boolean: true,
93                significance_level: 0.05,
94            }
95            .run_test(x, y, empty_z)
96            .unwrap(),
97        );
98        assert!(independent, "expected fail-to-reject (independent=true)");
99    }
100
101    #[test]
102    fn unconditional_dependent_data_is_rejected() {
103        let test = CressieRead {
104            boolean: false,
105            significance_level: 0.05,
106        };
107        let x = array![1., 1., 1., 1., 2., 2., 2., 2.];
108        let y = array![1., 1., 1., 1., 2., 2., 2., 2.];
109        let empty_z = Array2::<f64>::zeros((0, 0));
110
111        let (p_value, statistic, _dof) = unwrap_correlated(
112            &test
113                .run_test(x.clone(), y.clone(), empty_z.clone())
114                .unwrap(),
115        );
116        assert!(statistic > 5.0, "expected large statistic, got {statistic}");
117        assert!(
118            p_value < test.significance_level,
119            "expected p < {}, got {p_value}",
120            test.significance_level
121        );
122
123        let independent = unwrap_boolean(
124            &CressieRead {
125                boolean: true,
126                significance_level: 0.05,
127            }
128            .run_test(x, y, empty_z)
129            .unwrap(),
130        );
131        assert!(!independent, "expected reject (independent=false)");
132    }
133
134    #[test]
135    fn conditional_independent_per_group() {
136        let test = CressieRead {
137            boolean: false,
138            significance_level: 0.05,
139        };
140        let x = array![1., 1., 2., 2., 1., 1., 2., 2.];
141        let y = array![1., 2., 1., 2., 1., 2., 1., 2.];
142        let z = Array2::from_shape_vec((8, 1), vec![0., 0., 0., 0., 1., 1., 1., 1.]).unwrap();
143
144        let (p_value, statistic, dof) =
145            unwrap_correlated(&test.run_test(x.clone(), y.clone(), z.clone()).unwrap());
146        assert!(
147            statistic.abs() < EPS,
148            "expected statistic ~0, got {statistic}"
149        );
150        assert!(p_value > 0.99, "expected p ~1, got {p_value}");
151        assert_eq!(dof, 2);
152
153        let independent = unwrap_boolean(
154            &CressieRead {
155                boolean: true,
156                significance_level: 0.05,
157            }
158            .run_test(x, y, z)
159            .unwrap(),
160        );
161        assert!(independent);
162    }
163
164    #[test]
165    fn conditional_dependent_per_group() {
166        let test = CressieRead {
167            boolean: false,
168            significance_level: 0.05,
169        };
170        let x = array![1., 1., 2., 2., 1., 1., 2., 2.];
171        let y = array![1., 1., 2., 2., 1., 1., 2., 2.];
172        let z = Array2::from_shape_vec((8, 1), vec![0., 0., 0., 0., 1., 1., 1., 1.]).unwrap();
173
174        let (p_value, statistic, _dof) = unwrap_correlated(&test.run_test(x, y, z).unwrap());
175        assert!(statistic > 5.0, "expected large statistic, got {statistic}");
176        assert!(
177            p_value < test.significance_level,
178            "expected p < {}, got {p_value}",
179            test.significance_level
180        );
181    }
182}