solow_stats/equivalence.rs
1//! Two one-sided tests (TOST) of equivalence for two independent samples.
2//!
3//! [`ttost_ind`] tests the null of *non*-equivalence — that the mean difference
4//! `m1 − m2` lies outside the interval `(low, upp)` — against the alternative
5//! that it lies inside. It runs an upper one-sided t-test at `low` and a lower
6//! one-sided t-test at `upp` and reports the larger of the two p-values.
7//!
8//! Mirrors the reference `…stats.weightstats.ttost_ind`.
9
10use crate::weightstats::{ttest_ind, Alternative, UseVar};
11use ndarray::Array1;
12use solow_core::error::Result;
13
14/// Outcome of a TOST equivalence test.
15#[derive(Debug, Clone, Copy)]
16pub struct TostResult {
17 /// p-value of the equivalence test (the larger of the two one-sided
18 /// p-values); reject non-equivalence when this is small.
19 pub pvalue: f64,
20 /// Statistic of the lower-bound (`larger`) one-sided test.
21 pub t1: f64,
22 /// p-value of the lower-bound one-sided test.
23 pub pv1: f64,
24 /// Statistic of the upper-bound (`smaller`) one-sided test.
25 pub t2: f64,
26 /// p-value of the upper-bound one-sided test.
27 pub pv2: f64,
28}
29
30/// Two one-sided equivalence test for two independent samples.
31///
32/// `low` and `upp` bound the equivalence interval `low < m1 − m2 < upp`.
33/// `usevar` selects the pooled (Student) or unequal-variance (Welch) two-sample
34/// t-test. The first one-sided test (`alternative = larger`, null difference
35/// `low`) and the second (`alternative = smaller`, null difference `upp`) are
36/// taken; the returned `pvalue` is `max(pv1, pv2)`. Mirrors the reference
37/// `ttost_ind(x1, x2, low, upp, usevar)`.
38pub fn ttost_ind(
39 x1: &Array1<f64>,
40 x2: &Array1<f64>,
41 low: f64,
42 upp: f64,
43 usevar: UseVar,
44) -> Result<TostResult> {
45 let tt1 = ttest_ind(x1, x2, Alternative::Larger, usevar, low);
46 let tt2 = ttest_ind(x1, x2, Alternative::Smaller, usevar, upp);
47 let pvalue = tt1.pvalue.max(tt2.pvalue);
48 Ok(TostResult {
49 pvalue,
50 t1: tt1.statistic,
51 pv1: tt1.pvalue,
52 t2: tt2.statistic,
53 pv2: tt2.pvalue,
54 })
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60 use ndarray::array;
61
62 #[test]
63 fn tost_runs_pooled_and_unequal() {
64 let x1 = array![1.0, 1.2, 0.9, 1.1, 1.05, 0.95, 1.15, 0.85];
65 let x2 = array![1.05, 1.1, 1.0, 0.9, 1.2, 1.0, 0.95, 1.1];
66 for uv in [UseVar::Pooled, UseVar::Unequal] {
67 let r = ttost_ind(&x1, &x2, -0.5, 0.5, uv).unwrap();
68 assert!((0.0..=1.0).contains(&r.pvalue));
69 assert!((r.pvalue - r.pv1.max(r.pv2)).abs() < 1e-15);
70 }
71 }
72}