1use statrs::distribution::{Discrete, DiscreteCDF, Hypergeometric};
2use statrs::StatsError;
3
4#[derive(Debug, Clone, Copy)]
5pub enum Alternative {
6 TwoSided,
7 Less,
8 Greater,
9}
10
11const EPSILON: f64 = 1.0 - 1e-4;
12fn binary_search<F>(
14 min_val: u64,
15 max_val: u64,
16 p_exact: f64,
17 epsilon: f64,
18 upper: bool,
19 func: F,
20) -> u64
21where
22 F: Fn(u64) -> f64,
23{
24 let (mut min_val, mut max_val) = (min_val, max_val);
25
26 let mut guess = 0;
27 loop {
28 if max_val - min_val <= 1 {
29 break;
30 }
31 guess = {
32 if max_val == min_val + 1 && guess == min_val {
33 max_val
34 } else {
35 (max_val + min_val) / 2
36 }
37 };
38
39 let ng = {
40 if upper {
41 guess - 1
42 } else {
43 guess + 1
44 }
45 };
46
47 let pmf_comp = func(ng);
48 let p_guess = func(guess);
49 if p_guess <= p_exact && p_guess < pmf_comp {
50 break;
51 }
52 if p_guess < p_exact {
53 max_val = guess
54 } else {
55 min_val = guess
56 }
57 }
58
59 if guess == 0 {
60 guess = min_val
61 }
62 if upper {
63 while guess > 0 && func(guess) < p_exact * epsilon {
64 guess -= 1;
65 }
66 while func(guess) > p_exact / epsilon {
67 guess += 1;
68 }
69 } else {
70 while func(guess) < p_exact * epsilon {
71 guess += 1;
72 }
73 while guess > 0 && func(guess) > p_exact / epsilon {
74 guess -= 1;
75 }
76 }
77 guess
78}
79
80pub fn fishers_exact_with_odds_ratio(
91 table: &[u64; 4],
92 alternative: Alternative,
93) -> Result<(f64, f64), StatsError> {
94 if (table[0] == 0 && table[2] == 0) || (table[1] == 0 && table[3] == 0) {
95 return Ok((f64::NAN, 1.0));
97 }
98
99 let odds_ratio = {
100 if table[1] * table[2] == 0 {
101 f64::INFINITY
103 } else {
104 (table[0] * table[3]) as f64 / (table[1] * table[2]) as f64
105 }
106 };
107
108 let p_value = fishers_exact(table, alternative)?;
109 Ok((odds_ratio, p_value))
110}
111
112pub fn fishers_exact(table: &[u64; 4], alternative: Alternative) -> Result<f64, StatsError> {
123 if (table[0] == 0 && table[2] == 0) || (table[1] == 0 && table[3] == 0) {
126 return Ok(1.0);
127 }
128
129 let n1 = table[0] + table[1];
130 let n2 = table[2] + table[3];
131 let n = table[0] + table[2];
132
133 let population = n1 + n2;
134 let successes = n1;
135 let draws = n;
136 let dist = Hypergeometric::new(population, successes, draws)?;
137
138 match alternative {
139 Alternative::Less => Ok(dist.cdf(table[0])),
140 Alternative::Greater => {
141 let draws = table[1] + table[3];
142 let dist = Hypergeometric::new(population, successes, draws)?;
143 Ok(dist.cdf(table[1]))
144 }
145 Alternative::TwoSided => {
146 let p_exact = dist.pmf(table[0]);
147 let mode = ((n + 1) * (n1 + 1)) / (n1 + n2 + 2) as u64; let p_mode = dist.pmf(mode);
149
150 if (p_exact - p_mode).abs() / p_exact.max(p_mode) <= 1.0 - EPSILON {
151 return Ok(1.0);
152 }
153
154 let func = |x| dist.pmf(x);
155 if table[0] < mode {
156 let p_lower = dist.cdf(table[0]);
157 if dist.pmf(n) > p_exact / EPSILON {
158 Ok(p_lower)
159 } else {
160 let guess = binary_search(mode, n, p_exact, EPSILON, true, func);
161 Ok(p_lower + 1.0 - dist.cdf(guess - 1))
162 }
163 } else {
164 let p_upper = 1.0 - dist.cdf(table[0] - 1);
165 if dist.pmf(0) > p_exact / EPSILON {
166 Ok(p_upper)
167 } else {
168 let guess = binary_search(0, mode, p_exact, EPSILON, false, func);
169 Ok(p_upper + dist.cdf(guess))
170 }
171 }
172 }
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::{fishers_exact, fishers_exact_with_odds_ratio, Alternative};
179 use float_cmp::assert_approx_eq;
180
181 #[test]
183 fn test_fishers_exact() {
184 let cases = [
185 (
186 [3, 5, 4, 50],
187 0.9963034765672599,
188 0.03970749246529277,
189 0.03970749246529276,
190 ),
191 (
192 [61, 118, 2, 1],
193 0.27535061623455315,
194 0.9598172545684959,
195 0.27535061623455315,
196 ),
197 (
198 [172, 46, 90, 127],
199 1.0,
200 6.662405187351769e-16,
201 9.041009036528785e-16,
202 ),
203 (
204 [127, 38, 112, 43],
205 0.8637599357870167,
206 0.20040942958644145,
207 0.3687862842650179,
208 ),
209 (
210 [186, 177, 111, 154],
211 0.9918518696328176,
212 0.012550663906725129,
213 0.023439141644624434,
214 ),
215 (
216 [137, 49, 135, 183],
217 0.999999999998533,
218 5.6517533666400615e-12,
219 8.870999836202932e-12,
220 ),
221 (
222 [37, 115, 37, 152],
223 0.8834621182590621,
224 0.17638403366123565,
225 0.29400927608021704,
226 ),
227 (
228 [124, 117, 119, 175],
229 0.9956704915461392,
230 0.007134712391455461,
231 0.011588218284387445,
232 ),
233 (
234 [70, 114, 41, 118],
235 0.9945558498544903,
236 0.010384865876586255,
237 0.020438291037108678,
238 ),
239 (
240 [173, 21, 89, 7],
241 0.2303739114068352,
242 0.8808002774812677,
243 0.4027047267306024,
244 ),
245 (
246 [18, 147, 123, 58],
247 4.077820702304103e-29,
248 0.9999999999999817,
249 0.0,
250 ),
251 (
252 [116, 20, 92, 186],
253 0.9999999999998267,
254 6.598118571034892e-25,
255 8.164831402188242e-25,
256 ),
257 (
258 [9, 22, 44, 38],
259 0.01584272038710196,
260 0.9951463496539362,
261 0.021581786662999272,
262 ),
263 (
264 [9, 101, 135, 7],
265 3.3336213533847776e-50,
266 1.0,
267 3.3336213533847776e-50,
268 ),
269 (
270 [153, 27, 191, 144],
271 0.9999999999950817,
272 2.473736787266208e-11,
273 3.185816623300107e-11,
274 ),
275 (
276 [111, 195, 189, 69],
277 6.665245982898848e-19,
278 0.9999999999994574,
279 1.0735744915712542e-18,
280 ),
281 (
282 [125, 21, 31, 131],
283 0.99999999999974,
284 9.720661317939016e-34,
285 1.0352129312860277e-33,
286 ),
287 (
288 [201, 192, 69, 179],
289 0.9999999988714893,
290 3.1477232259550017e-09,
291 4.761075937088169e-09,
292 ),
293 (
294 [124, 138, 159, 160],
295 0.30153826772785475,
296 0.7538974235759873,
297 0.5601766196310243,
298 ),
299 ];
300
301 for (table, less_expected, greater_expected, two_sided_expected) in cases.iter() {
302 for (alternative, expected) in [
303 Alternative::Less,
304 Alternative::Greater,
305 Alternative::TwoSided,
306 ]
307 .into_iter()
308 .zip(vec![less_expected, greater_expected, two_sided_expected])
309 {
310 let p_value = fishers_exact(&table, alternative).unwrap();
311 assert_approx_eq!(f64, p_value, *expected, epsilon = 1e-12);
312 }
313 }
314 }
315
316 #[test]
317 fn test_fishers_exact_with_odds() {
318 let table = [3, 5, 4, 50];
319 let (odds_ratio, p_value) =
320 fishers_exact_with_odds_ratio(&table, Alternative::Less).unwrap();
321 assert_approx_eq!(f64, p_value, 0.9963034765672599, epsilon = 1e-12);
322 assert_approx_eq!(f64, odds_ratio, 7.5, epsilon = 1e-1);
323 }
324}