Skip to content

Commit 2dda097

Browse files
committed
Adding a paired version of the tests -- purely updating permutation_test_builder. Documentation updates to match, test suite updates. (also added broken args for weights).
1 parent 6472636 commit 2dda097

3 files changed

Lines changed: 70 additions & 2 deletions

File tree

‎R/documentation.R‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ NULL
1515
#' @param power power to raise test stat to
1616
#' @param keep.boots Should the bootstrap values be saved in the output?
1717
#' @param keep.samples Should the samples be saved in the output?
18+
#' @param weights.a Weights for observations in sample a. Not currently implemented -- reserved for future use.
19+
#' @param weights.b Weights for observations in sample b. Not currently implemented -- reserved for future use.
20+
#' @param paired Logical. If TRUE, performs a paired test where samples are assumed to be in corresponding order. Samples must have equal length.
1821
#' @return Output is a length 2 Vector with test stat and p-value in that order. That vector has 3 attributes -- the sample sizes of each sample, and the number of bootstraps performed for the pvalue.
1922
#' @details The KS test compares two ECDFs by looking at the maximum difference between them. Formally -- if E is the ECDF of sample 1 and F is the ECDF of sample 2, then \deqn{KS = max |E(x)-F(x)|^p} for values of x in the joint sample. The test p-value is calculated by randomly resampling two samples of the same size using the combined sample.
2023
#'
@@ -52,6 +55,9 @@ NULL
5255
#' @param power power to raise test stat to
5356
#' @param keep.boots Should the bootstrap values be saved in the output?
5457
#' @param keep.samples Should the samples be saved in the output?
58+
#' @param weights.a Weights for observations in sample a. Not currently implemented -- reserved for future use.
59+
#' @param weights.b Weights for observations in sample b. Not currently implemented -- reserved for future use.
60+
#' @param paired Logical. If TRUE, performs a paired test where samples are assumed to be in corresponding order. Samples must have equal length.
5561
#' @return Output is a length 2 Vector with test stat and p-value in that order. That vector has 3 attributes -- the sample sizes of each sample, and the number of bootstraps performed for the pvalue.
5662
#' @details The Kuiper test compares two ECDFs by looking at the maximum positive and negative difference between them. Formally -- if E is the ECDF of sample 1 and F is the ECDF of sample 2, then \deqn{KUIPER = |max_x E(x)-F(x)|^p + |max_x F(x)-E(x)|^p}. The test p-value is calculated by randomly resampling two samples of the same size using the combined sample.
5763
#'
@@ -90,6 +96,9 @@ NULL
9096
#' @param power power to raise test stat to
9197
#' @param keep.boots Should the bootstrap values be saved in the output?
9298
#' @param keep.samples Should the samples be saved in the output?
99+
#' @param weights.a Weights for observations in sample a. Not currently implemented -- reserved for future use.
100+
#' @param weights.b Weights for observations in sample b. Not currently implemented -- reserved for future use.
101+
#' @param paired Logical. If TRUE, performs a paired test where samples are assumed to be in corresponding order. Samples must have equal length.
93102
#' @return Output is a length 2 Vector with test stat and p-value in that order. That vector has 3 attributes -- the sample sizes of each sample, and the number of bootstraps performed for the pvalue.
94103
#' @details The CVM test compares two ECDFs by looking at the sum of the squared differences between them -- evaluated at each point in the joint sample. Formally -- if E is the ECDF of sample 1 and F is the ECDF of sample 2, then \deqn{CVM = \sum_{x\in k}|E(x)-F(x)|^p}{CVM = SUM_(x in k) |E(x)-F(x)|^p} where k is the joint sample. The test p-value is calculated by randomly resampling two samples of the same size using the combined sample. Intuitively the CVM test improves on KS by using the full joint sample, rather than just the maximum distance -- this gives it greater power against shifts in higher moments, like variance changes.
95104
#'
@@ -128,6 +137,9 @@ NULL
128137
#' @param power power to raise test stat to
129138
#' @param keep.boots Should the bootstrap values be saved in the output?
130139
#' @param keep.samples Should the samples be saved in the output?
140+
#' @param weights.a Weights for observations in sample a. Not currently implemented -- reserved for future use.
141+
#' @param weights.b Weights for observations in sample b. Not currently implemented -- reserved for future use.
142+
#' @param paired Logical. If TRUE, performs a paired test where samples are assumed to be in corresponding order. Samples must have equal length.
131143
#' @return Output is a length 2 Vector with test stat and p-value in that order. That vector has 3 attributes -- the sample sizes of each sample, and the number of bootstraps performed for the pvalue.
132144
#' @details The AD test compares two ECDFs by looking at the weighted sum of the squared differences between them -- evaluated at each point in the joint sample. The weights are determined by the variance of the joint ECDF at that point, which peaks in the middle of the joint distribution (see figure below). Formally -- if E is the ECDF of sample 1, F is the ECDF of sample 2, and G is the ECDF of the joint sample then \deqn{AD = \sum_{x \in k} \left({|E(x)-F(x)| \over \sqrt{2G(x)(1-G(x))/n} }\right)^p }{AD = SUM_(x in k) (|E(x)-F(x)|/sqrt(2G(x)*(1-G(x)))/n)^p} where k is the joint sample. The test p-value is calculated by randomly resampling two samples of the same size using the combined sample. Intuitively the AD test improves on the CVM test by giving lower weight to noisy observations.
133145
#'
@@ -169,6 +181,9 @@ NULL
169181
#' @param power power to raise test stat to
170182
#' @param keep.boots Should the bootstrap values be saved in the output?
171183
#' @param keep.samples Should the samples be saved in the output?
184+
#' @param weights.a Weights for observations in sample a. Not currently implemented -- reserved for future use.
185+
#' @param weights.b Weights for observations in sample b. Not currently implemented -- reserved for future use.
186+
#' @param paired Logical. If TRUE, performs a paired test where samples are assumed to be in corresponding order. Samples must have equal length.
172187
#' @return Output is a length 2 Vector with test stat and p-value in that order. That vector has 3 attributes -- the sample sizes of each sample, and the number of bootstraps performed for the pvalue.
173188
#' @details The Wasserstein test compares two ECDFs by looking at the Wasserstein distance between the two. This is of course the area between the two ECDFs. Formally -- if E is the ECDF of sample 1 and F is the ECDF of sample 2, then \deqn{WASS = \int_{x \in R} |E(x)-F(x)|^p}{WASS = Integral |E(x)-F(x)|^p} across all x. The test p-value is calculated by randomly resampling two samples of the same size using the combined sample. Intuitively the Wasserstein test improves on CVM by allowing more extreme observations to carry more weight. At a higher level -- CVM/AD/KS/etc only require ordinal data. Wasserstein gains its power because it takes advantages of the properties of interval data -- i.e. the distances have some meaning.
174189
#'
@@ -207,6 +222,9 @@ NULL
207222
#' @param power also the power to raise the test stat to
208223
#' @param keep.boots Should the bootstrap values be saved in the output?
209224
#' @param keep.samples Should the samples be saved in the output?
225+
#' @param weights.a Weights for observations in sample a. Not currently implemented -- reserved for future use.
226+
#' @param weights.b Weights for observations in sample b. Not currently implemented -- reserved for future use.
227+
#' @param paired Logical. If TRUE, performs a paired test where samples are assumed to be in corresponding order. Samples must have equal length.
210228
#' @return Output is a length 2 Vector with test stat and p-value in that order. That vector has 3 attributes -- the sample sizes of each sample, and the number of bootstraps performed for the pvalue.
211229
#' @details The DTS test compares two ECDFs by looking at the reweighted Wasserstein distance between the two. See the companion paper at [arXiv:2007.01360](https://arxiv.org/abs/2007.01360) or <https://codowd.com/public/DTS.pdf> for details of this test statistic, and non-standard uses of the package (parallel for big N, weighted observations, one sample tests, etc).
212230
#'
@@ -245,6 +263,9 @@ NULL
245263
#' @description (**Warning!** This function has changed substantially between v1.2.0 and v2.0.0) This function takes a two-sample test statistic and produces a function which performs randomization tests (sampling with replacement) using that test stat. This is an internal function of the `twosamples` package.
246264
#' @param test_stat_function a function of the joint vector and a label vector producing a positive number, intended as the test-statistic to be used.
247265
#' @param default.p This allows for some introduction of defaults and parameters. Typically used to control the power functions raise something to.
266+
#' @param weights.a Weights for observations in sample a. Not currently implemented -- reserved for future use.
267+
#' @param weights.b Weights for observations in sample b. Not currently implemented -- reserved for future use.
268+
#' @param paired Logical. If TRUE, performs a paired test where samples are assumed to be in corresponding order. Samples must have equal length.
248269
#' @return This function returns a function which will perform permutation tests on given test stat.
249270
#' @details test_stat_function must be structured to take two vectors -- the first a combined sample vector and the second a logical vector indicating which sample each value came from, as well as a third and fourth value. i.e. (fun = function(jointvec,labelvec,val1,val2) ...). See examples.
250271
#'

‎R/misc.R‎

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ permutation_test_builder = function(test_stat_function,default.p=2.0) {
1010

1111
# little function that finds the *_stat name and saves it for later use.
1212
fun.name = toupper(strsplit(as.character(match.call()[2]),"_")[[1]][1])
13-
fun = function(a,b,nboots=2000,p=default.p,keep.boots=T,keep.samples=F){
13+
14+
fun = function(a,b,nboots=2000,p=default.p,keep.boots=T,keep.samples=F,
15+
weights.a=NULL,weights.b=NULL,paired=FALSE){
16+
# Weights not yet implemented
17+
if (!is.null(weights.a) || !is.null(weights.b)) {
18+
stop("Weights are not currently implemented.")
19+
}
1420
na = length(a)
1521
nb = length(b)
1622
n = na+nb
@@ -20,13 +26,32 @@ permutation_test_builder = function(test_stat_function,default.p=2.0) {
2026
comb = comb[ord_inds]
2127
vec_labels = vec_labels[ord_inds]
2228

29+
# For paired resampling: track which pair each sorted element belongs to
30+
# pair_id maps each element in the sorted combined vector to its pair index (1..na)
31+
if (paired) {
32+
if (na != nb) {
33+
stop("For a paired test, samples must have equal length.")
34+
}
35+
pair_id = c(1:na, 1:na)[ord_inds]
36+
# For each pair, find the two positions in the sorted vector
37+
# pair_pos[j,] gives the two indices in `comb` that belong to pair j
38+
pair_pos = matrix(order(pair_id), ncol=2, byrow=TRUE)
39+
flips = cbind(1:na,1) #initialize flips mat
40+
}
41+
2342
test_stat = test_stat_function(comb,vec_labels,p,na) #Finds test stat
2443
nboots = as.integer(nboots) #Speeds up comparison below.
2544
reps = bigger = 0L #Initializes Counter
2645
if (keep.boots) boots = numeric(nboots) #initialize storage of boots
2746
while (reps < nboots) { #Loops over vector
2847
vec_labels = rep(F,n)
29-
vec_labels[sample.int(n,na,F)] = T #Samples indexes
48+
if (paired) {
49+
# For each pair, randomly pick which of its two elements is labeled "a"
50+
flips[,2] = sample.int(2L, na, replace=T) # 1 or 2 for each pair
51+
vec_labels[pair_pos[flips]] = T
52+
} else {
53+
vec_labels[sample.int(n,na,F)] = T #samples indexes
54+
}
3055
boot_t = test_stat_function(comb,vec_labels,p,na) #boot strap test stat
3156
if(boot_t >= test_stat) bigger = 1L+bigger #if new stat is bigger, increment
3257
reps = 1L+reps

‎tests/testthat/test-two_samples.R‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,28 @@ test_that("power against obvious null rejection",{
6565
expect_lt(dts_test(vec1,vec2)[2],0.05)
6666
})
6767

68+
# paired tests
69+
test_that("paired test runs and returns twosamples class",{
70+
out = dts_test(rnorm(20),rnorm(20),paired=TRUE)
71+
expect_s3_class(out,"twosamples")
72+
})
73+
74+
test_that("paired test errors on unequal lengths",{
75+
expect_error(dts_test(rnorm(10),rnorm(20),paired=TRUE))
76+
})
77+
78+
test_that("weights error when provided",{
79+
expect_error(dts_test(rnorm(10),rnorm(10),weights.a=rep(1,10)))
80+
expect_error(dts_test(rnorm(10),rnorm(10),weights.b=rep(1,10)))
81+
})
82+
83+
test_that("paired test has power on obvious shift",{
84+
base = rnorm(200)
85+
a = base + rnorm(200,0,0.1)
86+
b = base + rnorm(200,1,0.1)
87+
expect_lt(dts_test(a,b,paired=TRUE)[2],0.05)
88+
})
89+
6890
#Class functions are quasi functional
6991
test_that("Classes are working",{
7092
vec1 = rnorm(100)

0 commit comments

Comments
 (0)