Skip to main content

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}