Skip to main content

stats_claw/tests_stat/
mod.rs

1//! Statistical hypothesis tests and their effect-size reporting.
2//!
3//! This module defines the shared result vocabulary ([`TestResult`],
4//! [`Alternative`]) and houses the categorical, parametric, nonparametric,
5//! goodness-of-fit, and correlation test families in subgroup folders. Each test
6//! computes its statistic and p-value against the framework distributions
7//! ([`crate::distributions`]) within the tolerances this crate commits to:
8//! statistic relative error ≤ 1e-8, p-value absolute error ≤ 1e-8
9//! (asymptotic ≤ 1e-6). Equivalence
10//! is proven by the `tests/equiv.rs` suite (per-family modules under
11//! `tests/stat/`) against committed scipy/statsmodels golden fixtures.
12
13pub mod categorical;
14pub mod correlation;
15pub mod exact;
16pub mod goodness_of_fit;
17pub mod nonparametric;
18pub mod parametric;
19mod ranks;
20
21use crate::distributions::ChiSquaredDistribution;
22use crate::distributions::{Cdf, LogCdf};
23
24/// Upper-tail probability `P(χ²_k ≥ x)` of the chi-squared null distribution.
25///
26/// The asymptotic p-value of every chi-squared-distributed statistic
27/// (independence, Kruskal–Wallis, Friedman, Bartlett, Cochran) routes through
28/// this single helper so they all share the framework
29/// [`ChiSquaredDistribution`] CDF.
30///
31/// # Arguments
32///
33/// * `x` — the observed statistic; values `≤ 0` yield `1.0`.
34/// * `df` — the degrees of freedom (`> 0`).
35///
36/// # Returns
37///
38/// The upper-tail probability in `[0, 1]`.
39#[must_use]
40pub(crate) fn chi_squared_upper_tail(x: f64, df: i64) -> f64 {
41    let dist = ChiSquaredDistribution {
42        degrees_of_freedom: df,
43        ..Default::default()
44    };
45    (1.0 - dist.cdf(x)).clamp(0.0, 1.0)
46}
47
48/// Natural log of the upper-tail probability `ln P(χ²_k ≥ x)` — the log-space
49/// counterpart of [`chi_squared_upper_tail`].
50///
51/// The chi-squared-routed tests (independence, Kruskal–Wallis, Friedman,
52/// Bartlett, Cochran) report a log p-value through this single helper so the
53/// extreme tail stays finite where [`chi_squared_upper_tail`] underflows to `0.0`.
54///
55/// # Arguments
56///
57/// * `x` — the observed statistic; values `≤ 0` yield `0.0` (log of p = 1).
58/// * `df` — the degrees of freedom (`> 0`).
59///
60/// # Returns
61///
62/// `ln P(χ²_k ≥ x) ∈ (−∞, 0]`.
63#[must_use]
64pub(crate) fn chi_squared_upper_log_tail(x: f64, df: i64) -> f64 {
65    if x <= 0.0 {
66        return 0.0;
67    }
68    let dist = ChiSquaredDistribution {
69        degrees_of_freedom: df,
70        ..Default::default()
71    };
72    dist.logsf(x)
73}
74
75/// The sidedness of a hypothesis test, mirroring scipy's `alternative` argument.
76///
77/// Selects which tail (or both) contributes to the reported p-value. The exact
78/// mapping from a statistic to a p-value is per-test, but the convention is
79/// uniform: [`Self::TwoSided`] doubles the smaller tail (or integrates both),
80/// [`Self::Less`] takes the lower tail, [`Self::Greater`] the upper tail.
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum Alternative {
83    /// The effect may lie in either direction; both tails contribute.
84    TwoSided,
85    /// The alternative hypothesis is that the effect is below the null value.
86    Less,
87    /// The alternative hypothesis is that the effect is above the null value.
88    Greater,
89}
90
91/// The computation mode for a test that offers both an exact (combinatorial)
92/// null distribution and an asymptotic (large-sample) approximation.
93///
94/// Mirrors scipy's `method` argument for the rank and goodness-of-fit tests.
95/// [`Self::Exact`] enumerates the exact null distribution (correct for small
96/// samples but combinatorially expensive); [`Self::Asymptotic`] uses the normal
97/// or Kolmogorov approximation (cheap, accurate for large samples);
98/// [`Self::Auto`] resolves to exact below a documented per-test sample-size
99/// threshold and to asymptotic at or above it.
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum Mode {
102    /// Enumerate the exact combinatorial null distribution.
103    Exact,
104    /// Use the large-sample (normal / Kolmogorov) approximation.
105    Asymptotic,
106    /// Pick exact below the threshold, asymptotic at or above it.
107    Auto,
108}
109
110impl Mode {
111    /// Resolves [`Self::Auto`] for sample size `n` against the per-test
112    /// `threshold`, returning the effective mode actually used.
113    ///
114    /// [`Self::Exact`] and [`Self::Asymptotic`] pass through unchanged;
115    /// [`Self::Auto`] becomes [`Self::Exact`] when `n < threshold` and
116    /// [`Self::Asymptotic`] otherwise.
117    ///
118    /// # Arguments
119    ///
120    /// * `n` — the governing sample size for the test (per-test definition).
121    /// * `threshold` — the size at or above which `Auto` chooses asymptotic.
122    ///
123    /// # Returns
124    ///
125    /// [`Self::Exact`] or [`Self::Asymptotic`] (never [`Self::Auto`]).
126    #[must_use]
127    pub const fn resolve(self, n: usize, threshold: usize) -> Self {
128        match self {
129            Self::Auto if n < threshold => Self::Exact,
130            Self::Auto => Self::Asymptotic,
131            other => other,
132        }
133    }
134}
135
136/// The outcome of a hypothesis test: its statistic, p-value, and — where the test
137/// defines them — degrees of freedom and an effect size.
138///
139/// `df` and `effect_size` are `None` for tests that define no such quantity (e.g.
140/// Shapiro–Wilk, Fisher exact), matching the reference's contract rather than
141/// emitting a misleading numeric placeholder.
142///
143/// `log_p_value` is the natural log of the p-value (`ln(p_value)`, the
144/// `scipy.stats` `logsf`/`logcdf` convention), populated for the tests whose null
145/// is a continuous distribution with a numerically-stable log tail (the t-tests,
146/// ANOVA/F, and the chi-squared-routed tests). It stays finite in the extreme tail
147/// where the linear [`Self::p_value`] underflows to `0.0`. It is
148/// `None` for tests that report no continuous-tail p-value in log space (e.g. the
149/// exact rank tests and Shapiro–Wilk); [`Self::p_value`] is unchanged regardless.
150#[derive(Debug, Clone, PartialEq)]
151pub struct TestResult {
152    /// The test statistic (χ², t, F, U, …) — its meaning is per-test.
153    pub statistic: f64,
154    /// The p-value in `[0, 1]` for the test's selected alternative.
155    pub p_value: f64,
156    /// `ln(p_value)`, finite in the extreme tail where `p_value` underflows, or
157    /// `None` for tests with no log-space p-value path.
158    pub log_p_value: Option<f64>,
159    /// Degrees of freedom, or `None` when the test defines none.
160    pub df: Option<f64>,
161    /// The effect size (Cramér's V, η², rank-biserial, …), or `None`.
162    pub effect_size: Option<f64>,
163}
164
165#[cfg(test)]
166mod tests {
167    use super::Mode;
168
169    /// `Auto` picks the exact path strictly below the threshold and the
170    /// asymptotic path at or above it; explicit modes pass through.
171    #[test]
172    fn auto_resolves_against_threshold() {
173        assert_eq!(Mode::Auto.resolve(7, 8), Mode::Exact, "below threshold");
174        assert_eq!(
175            Mode::Auto.resolve(8, 8),
176            Mode::Asymptotic,
177            "at threshold is asymptotic"
178        );
179        assert_eq!(
180            Mode::Auto.resolve(20, 8),
181            Mode::Asymptotic,
182            "above threshold"
183        );
184        assert_eq!(Mode::Exact.resolve(99, 8), Mode::Exact, "explicit exact");
185        assert_eq!(
186            Mode::Asymptotic.resolve(1, 8),
187            Mode::Asymptotic,
188            "explicit asymptotic"
189        );
190    }
191}