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}