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