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}