
Exact Two-Group Jonckheere-Terpstra Test via Full Randomization Enumeration (C++ Backend)
Source:R/helper_glm_fit.R
exact_jonckheere_terpstra_pval_cpp.RdComputes the exact randomization-distribution p-value and a probabilistic-index
effect size for the two-group Jonckheere-Terpstra statistic — which, with exactly
two groups (w in {0, 1}), coincides with the Wilcoxon-Mann-Whitney
\(U\) statistic generalized to handle ties (repeated ordinal levels in y):
$$U = \sum_{k} t_k \big(2 L_k + (n_k - t_k)\big) / 2,$$
summed over the \(K\) distinct observed levels of y (in increasing order),
where \(n_k\) is the total count at level \(k\), \(t_k\) is the observed count
of w == 1 subjects at level \(k\), and \(L_k\) is the number of subjects
at strictly lower levels (this is the standard "number of favorable comparisons"
Mann-Whitney statistic, adapted for tied/grouped ordinal data — a tie at the same
level contributes \(1/2\) rather than 0 or 1). The exact (not asymptotic,
not Monte Carlo) null/reference distribution of this statistic under the sharp null
of no treatment effect is obtained by enumerating, via a dynamic-programming
recursion over levels (recurse_jt_distribution()), every way to distribute
n_treat "treated" labels among the \(n\) subjects consistent with the fixed
per-level totals \(n_k\) — i.e. the exact multivariate hypergeometric
randomization distribution of the statistic conditional on the observed level
counts, with each configuration's probability computed in log-space from
log-binomial-coefficient weights to avoid overflow for larger \(n\).
Arguments
- y
Integer (or integer-coercible) vector of length \(n\) giving each subject's ordinal response value; ties (repeated values) are handled via the grouped Mann-Whitney formula above. Must not contain
NA.- w
Integer (or integer-coercible) vector of length \(n\) with values in
{0, 1}giving each subject's group membership; both groups must be non-empty and noNAis permitted.
Value
A list with components stat2 (twice the observed \(U\) statistic,
an integer, used internally to keep the enumeration in integer arithmetic),
n_treat, n_control (the two group sizes), superiority (the
probabilistic-index effect size described above), p_lower, p_upper
(the exact one-sided randomization-distribution tail probabilities), and
p_exact (the exact two-sided p-value).
Details
Randomization test, not a model-based test. This is a randomization
(permutation) test in the Fisherian sense: it conditions on the observed marginal
level counts \(n_k\) and the group sizes n_treat/n_control, and asks
how extreme the observed statistic is relative to every other way those same labels
could have been randomly assigned, so its validity does not depend on any
distributional assumption about y beyond exchangeability under the null. The
two-sided p-value is \(p_{\mathrm{exact}} = \min(1, 2 \min(p_{\mathrm{lower}},
p_{\mathrm{upper}}))\), where \(p_{\mathrm{lower}}\)/\(p_{\mathrm{upper}}\) are
the exact one-sided tail probabilities of the randomization distribution at or below
/ at or above the observed statistic.
Effect size (superiority). superiority is the probabilistic
index \(\Pr(Y_T > Y_C) + \tfrac{1}{2}\Pr(Y_T = Y_C)\) (equivalently \(U\)
rescaled to \([0, 1]\) by dividing by \(n_{\mathrm{treat}} \cdot
n_{\mathrm{control}}\)), the probability a randomly chosen treated subject's ordinal
outcome exceeds a randomly chosen control subject's, counting ties as half a win;
0.5 indicates no stochastic ordering between groups, and 1/0
indicate the treated group's outcomes are uniformly higher/lower.
Input conventions. y is coerced to integer and treated as an ordinal
(or any orderable-by-integer-value) response with an arbitrary number of tied
levels; w must be an integer/coercible-to-integer vector of {0, 1}
values with both groups non-empty. NA in either y or w is not
permitted and raises an error, as does a non-{0,1} value in w or an
empty input.
Complexity. The recursion's state space scales with the number of distinct possible statistic values (\(O(n_{\mathrm{treat}} \cdot n_{\mathrm{control}})\) many), and thread-local buffers are reused (not reallocated) across repeated calls within the same thread for the same or smaller problem sizes; this exact enumeration is exponential in the number of distinct levels/group sizes in the worst case (unlike an asymptotic normal-approximation JT test), so it is intended for small-to- moderate \(n\) where exactness matters more than raw speed.
See also
Mann-Whitney
U test and
Jonckheere's trend
test for background; analogous Python API:
SciPy
mannwhitneyu (method="exact" for the same exact-enumeration
approach, though SciPy's exact path does not handle ties the same way).