Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix fisher test #250

Merged
merged 5 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 28 additions & 20 deletions src/stats_tests/fisher.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
use super::Alternative;
use crate::distribution::{Discrete, DiscreteCDF, Hypergeometric};
use crate::StatsError;

#[derive(Debug, Copy, Clone)]
pub enum Alternative {
TwoSided,
Less,
Greater,
}

const EPSILON: f64 = 1.0 - 1e-4;

/// Binary search in two-sided test with starting bound as argument
Expand Down Expand Up @@ -105,23 +99,25 @@ fn binary_search(

/// Perform a Fisher exact test on a 2x2 contingency table.
/// Based on scipy's fisher test: https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.fisher_exact.html#scipy-stats-fisher-exact
/// Returns the odds ratio and p_value
/// Expects a table in row-major order
/// Returns the [odds ratio](https://en.wikipedia.org/wiki/Odds_ratio) and p_value
/// # Examples
///
/// ```
/// use statrs::statis_tests::fishers_exact;
/// use statrs::statis_tests::Alternative;
/// use statrs::stats_tests::fishers_exact_with_odds_ratio;
/// use statrs::stats_tests::Alternative;
/// let table = [3, 5, 4, 50];
/// let (odds_ratio, p_value) = fishers_exact_with_odds_ratio(&table, Alternative::Less).unwrap();
/// ```
pub fn fishers_exact_with_odds_ratio(
table: &[u64; 4],
alternative: Alternative,
) -> Result<(f64, f64), StatsError> {
// Calculate fisher's exact test with the odds ratio
if (table[0] == 0 && table[2] == 0) || (table[1] == 0 && table[3] == 0) {
// If both values in a row or column are zero, p-value is 1 and odds ratio is NaN.
return Ok((f64::NAN, 1.0));
// If both values in a row or column are zero, p-value is 1 and odds ratio is NaN.
match table {
[0, _, 0, _] | [_, 0, _, 0] => return Ok((f64::NAN, 1.0)), // both 0 in a row
[0, 0, _, _] | [_, _, 0, 0] => return Ok((f64::NAN, 1.0)), // both 0 in a column
_ => (), // continue
}

let odds_ratio = {
Expand All @@ -138,19 +134,22 @@ pub fn fishers_exact_with_odds_ratio(

/// Perform a Fisher exact test on a 2x2 contingency table.
/// Based on scipy's fisher test: https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.fisher_exact.html#scipy-stats-fisher-exact
/// Expects a table in row-major order
/// Returns only the p_value
/// # Examples
///
/// ```
/// use statrs::statis_tests::fishers_exact;
/// use statrs::statis_tests::Alternative;
/// use statrs::stats_tests::fishers_exact;
/// use statrs::stats_tests::Alternative;
/// let table = [3, 5, 4, 50];
/// let p_value = fishers_exact(&table, Alternative::Less).unwrap();
/// ```
pub fn fishers_exact(table: &[u64; 4], alternative: Alternative) -> Result<f64, StatsError> {
// If both values in a row or column are zero, the p-value is 1 and the odds ratio is NaN.
if (table[0] == 0 && table[2] == 0) || (table[1] == 0 && table[3] == 0) {
return Ok(1.0);
match table {
[0, _, 0, _] | [_, 0, _, 0] => return Ok(1.0), // both 0 in a row
[0, 0, _, _] | [_, _, 0, 0] => return Ok(1.0), // both 0 in a column
_ => (), // continue
}

let n1 = table[0] + table[1];
Expand Down Expand Up @@ -209,9 +208,8 @@ pub fn fishers_exact(table: &[u64; 4], alternative: Alternative) -> Result<f64,

#[cfg(test)]
mod tests {
use super::fishers_exact;
use super::*;
use crate::prec;
use crate::stats_tests::fisher::{fishers_exact_with_odds_ratio, Alternative};

/// Test fishers_exact by comparing against values from scipy.
#[test]
Expand Down Expand Up @@ -347,6 +345,16 @@ mod tests {
}
}
}

#[test]
fn test_fishers_exact_for_trivial() {
let cases = [[0, 0, 1, 2], [1, 2, 0, 0], [1, 0, 2, 0], [0, 1, 0, 2]];

for table in cases.iter() {
assert_eq!(fishers_exact(table, Alternative::Less).unwrap(), 1.0)
}
}

#[test]
fn test_fishers_exact_with_odds() {
let table = [3, 5, 4, 50];
Expand Down
16 changes: 16 additions & 0 deletions src/stats_tests/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,17 @@
pub mod fisher;

/// Specifies an [alternative hypothesis](https://en.wikipedia.org/wiki/Alternative_hypothesis)
#[derive(Debug, Copy, Clone)]
pub enum Alternative {
#[doc(alias = "two-tailed")]
#[doc(alias = "two tailed")]
TwoSided,
#[doc(alias = "one-tailed")]
#[doc(alias = "one tailed")]
Less,
#[doc(alias = "one-tailed")]
#[doc(alias = "one tailed")]
Greater,
}

pub use fisher::{fishers_exact, fishers_exact_with_odds_ratio};