Skip to content

Commit 688e45f

Browse files
committed
initial WIP
0 parents  commit 688e45f

File tree

8 files changed

+352
-0
lines changed

8 files changed

+352
-0
lines changed

.Rbuildignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
^vfisher\.Rproj$
2+
^\.Rproj\.user$
3+
^LICENSE\.md$

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.Rproj.user
2+
.Rhistory
3+
.RData
4+
.Ruserdata

DESCRIPTION

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Package: vfisher
2+
Title: Vectorized Fisher Test
3+
Version: 0.0.0.9000
4+
Authors@R:
5+
person("Carl", "Pearson", , "[email protected]", role = c("aut", "cre"),
6+
comment = c(ORCID = "0000-0003-0701-7860"))
7+
Description: Provides a vectorized version of stats::fisher.test to bulk-apply
8+
that function to many 2x2 contingency table combinations, such as might
9+
be needed when doing analyses of simulated trials for evaluating designs.
10+
License: MIT + file LICENSE
11+
Encoding: UTF-8
12+
Roxygen: list(markdown = TRUE)
13+
RoxygenNote: 7.3.2

LICENSE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
YEAR: 2024
2+
COPYRIGHT HOLDER: vfisher authors

LICENSE.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# MIT License
2+
3+
Copyright (c) 2024 vfisher authors
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

NAMESPACE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Generated by roxygen2: do not edit by hand
2+

R/vfisher.R

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
2+
support_spans <- function(m, n, k) {
3+
return(mapply(
4+
function(lo, hi) { lo:hi },
5+
lo = pmax(0L, k - n), hi = pmin(k, m),
6+
SIMPLIFY = FALSE
7+
))
8+
}
9+
10+
logdc <- function(m, n, k, support) {
11+
return(mapply(
12+
function(support, m, n, k) dhyper(support, m, n, k, log = TRUE),
13+
support = support, m = m, n = n, k = k, SIMPLIFY = FALSE
14+
))
15+
}
16+
17+
dnhyper <- function(ncp, logdc, support) {
18+
return(mapply(
19+
function(ncp, logdc, support) {
20+
d <- logdc + log(ncp) * support
21+
d <- exp(d - max(d))
22+
return(d/sum(d))
23+
},
24+
ncp = ncp, logdc = logdc, support = support, SIMPLIFY = FALSE
25+
))
26+
}
27+
28+
mnhyper <- function(ncp, lo, hi, logdc, support) {
29+
lims <- which(ncp == 0 | is.infinite(ncp))
30+
if (ncp == 0) {
31+
return(lo)
32+
} else if (ncp == Inf) {
33+
return(hi)
34+
} else {
35+
return(sum(support * dnhyper(ncp, logdc, support)))
36+
}
37+
}
38+
39+
pnhyper <- function(
40+
q, ncp = 1, upper.tail = FALSE, m, n, k
41+
) {
42+
if (ncp == 1) {
43+
phyper(x - upper.tail, m, n, k, lower.tail = !upper.tail)
44+
} else if (ncp == 0) {
45+
return(as.numeric(if (upper.tail) q <= lo else q >= lo))
46+
} else if (ncp == Inf) {
47+
return(as.numeric(if (upper.tail) q <= hi else q >= hi))
48+
} else {
49+
return(sum(dnhyper(ncp)[if (upper.tail) support >= q else support <= q]))
50+
}
51+
}
52+
53+
#' @title Vectorized fisher.test
54+
#'
55+
#' @description
56+
#' Provides a vectorized version of [stats::fisher.test()] for evaluating a
57+
#' series of 2x2 contingency tables.
58+
#'
59+
#' @inheritParams stats::fisher.test
60+
#'
61+
#' @param a `x[1, 1]` in the matrix version of [stats::fisher.test()]
62+
#' @param b `x[1, 2]` in the matrix version of [stats::fisher.test()]
63+
#' @param c `x[2, 1]` in the matrix version of [stats::fisher.test()]
64+
#' @param d `x[2, 2]` in the matrix version of [stats::fisher.test()]
65+
#'
66+
#' @details
67+
#' N.b. `vfisher.test` does less input validation than [stats::fisher.test()].
68+
#'
69+
#' Because `vfisher.test` does not support anything other than 2x2 tests, the
70+
#' arguments to `fisher.test` associated with other tests (e.g. `hybrid`) do not
71+
#' appear.
72+
#'
73+
#' @return a data.frame, columns `a`, `b`, `c`, `d`, `or`, `estimate`,
74+
#' `p.value`. If `conf.int == TRUE` (the default), will also include column(s)
75+
#' for the confidence interval (two if `alternative == "two.sided"` (default)
76+
#' or one otherwise.). The column names are `ci.lo` and/or `ci.hi`.
77+
#'
78+
#' @export
79+
vfisher.test <- function(
80+
a, b, c, d, conf.int = TRUE, conf.level = 0.95, or = rep(1, length(a)),
81+
alternative = c("two.sided", "less", "greater")
82+
) {
83+
84+
if (
85+
length(a) != length(b) || length(a) != length(c) || length(a) != length(d)
86+
) stop("all a, b, c, d must be same length.")
87+
88+
storage.mode(a) <- "integer"
89+
storage.mode(b) <- "integer"
90+
storage.mode(c) <- "integer"
91+
storage.mode(d) <- "integer"
92+
93+
if (any(c(a, b, c, d) < 0) || anyNA(c(a, b, c, d)))
94+
stop("all entries of 'a', 'b', 'c', 'd' must be nonnegative and finite")
95+
96+
con <- list(mult = 30)
97+
con[names(control)] <- control
98+
if ((mult <- as.integer(con$mult)) < 2)
99+
stop("'mult' must be integer >= 2, typically = 30")
100+
101+
alternative <- match.arg(alternative)
102+
103+
if (!((length(conf.level) == 1L) && is.finite(conf.level) &&
104+
(conf.level > 0) && (conf.level < 1)))
105+
stop("'conf.level' must be a single number between 0 and 1")
106+
107+
if (any(is.na(or) | or < 0))
108+
stop("'or' must be a single number between 0 and Inf")
109+
110+
# matrix =
111+
# a, b
112+
# c, d
113+
114+
m <- a + c
115+
n <- b + d
116+
k <- a + b
117+
x <- a
118+
119+
lo <- pmax(0L, k - n)
120+
hi <- pmin(k, m)
121+
122+
core <- function(m, n, k, x, lo, hi) {
123+
124+
}
125+
126+
mapply(function(
127+
lo, hi
128+
) {
129+
support <- lo:hi
130+
logdc <- dhyper(support, m, n, k, log = TRUE)
131+
dnhyper <- function(ncp) {
132+
d <- logdc + log(ncp) * support
133+
d <- exp(d - max(d))
134+
d/sum(d)
135+
}
136+
mnhyper <- function(ncp) {
137+
if (ncp == 0) return(lo)
138+
if (ncp == Inf) return(hi)
139+
sum(support * dnhyper(ncp))
140+
}
141+
pnhyper <- function(q, ncp = 1, upper.tail = FALSE) {
142+
if (ncp == 1) {
143+
phyper(x - upper.tail, m, n, k, lower.tail = !upper.tail)
144+
}
145+
if (ncp == 0) {
146+
return(as.numeric(if (upper.tail) q <= lo else q >= lo))
147+
}
148+
if (ncp == Inf) {
149+
return(as.numeric(if (upper.tail) q <= hi else q >= hi))
150+
}
151+
sum(dnhyper(ncp)[if (upper.tail) support >= q else support <= q])
152+
}
153+
p.value <- switch(alternative,
154+
less = pnhyper(x, or), greater = pnhyper(x, or, upper.tail = TRUE),
155+
two.sided = {
156+
if (or == 0) as.numeric(x == lo) else if (or == Inf) as.numeric(x == hi) else {
157+
relErr <- 1 + 10^(-7)
158+
d <- dnhyper(or)
159+
sum(d[d <= d[x - lo + 1] * relErr])
160+
}
161+
}
162+
)
163+
mle <- if (x == lo) { 0 } else if (x == hi) { Inf } else {
164+
mu <- mnhyper(1)
165+
if (mu > x)
166+
uniroot(function(t) mnhyper(t) - x, c(0, 1))$root
167+
else if (mu < x)
168+
1/uniroot(function(t) mnhyper(1/t) - x, c(.Machine$double.eps, 1))$root
169+
else 1
170+
}
171+
list(estimate = mle, p.value = p.value)
172+
}, lo = lo, hi = hi)
173+
174+
175+
mapply(
176+
\(lo, hi) {
177+
support <- lo:hi
178+
logdc <- dhyper(support, m, n, k, log = TRUE)
179+
dnhyper <- function(ncp) {
180+
d <- logdc + log(ncp) * support
181+
d <- exp(d - max(d))
182+
d/sum(d)
183+
}
184+
mnhyper <- function(ncp) {
185+
if (ncp == 0)
186+
return(lo)
187+
if (ncp == Inf)
188+
return(hi)
189+
sum(support * dnhyper(ncp))
190+
}
191+
pnhyper <- function(q, ncp = 1, upper.tail = FALSE) {
192+
if (ncp == 1) {
193+
return(if (upper.tail) phyper(x - 1, m, n, k,
194+
lower.tail = FALSE) else phyper(x, m, n, k))
195+
}
196+
if (ncp == 0) {
197+
return(as.numeric(if (upper.tail) q <= lo else q >=
198+
lo))
199+
}
200+
if (ncp == Inf) {
201+
return(as.numeric(if (upper.tail) q <= hi else q >=
202+
hi))
203+
}
204+
sum(dnhyper(ncp)[if (upper.tail) support >= q else support <=
205+
q])
206+
}
207+
208+
209+
list(estimate = mle, p.value = p.value)
210+
}, lo, hi)
211+
}
212+
213+
214+
function (x, y = NULL, workspace = 2e+05, hybrid = FALSE, hybridPars = c(expect = 5,
215+
percent = 80, Emin = 1), control = list(), or = 1, alternative = "two.sided",
216+
conf.int = TRUE, conf.level = 0.95, simulate.p.value = FALSE,
217+
B = 2000)
218+
{
219+
220+
PVAL <- NULL
221+
else {
222+
PVAL <- switch(alternative, less = pnhyper(x, or),
223+
greater = pnhyper(x, or, upper.tail = TRUE),
224+
two.sided = {
225+
if (or == 0) as.numeric(x == lo) else if (or ==
226+
Inf) as.numeric(x == hi) else {
227+
relErr <- 1 + 10^(-7)
228+
d <- dnhyper(or)
229+
sum(d[d <= d[x - lo + 1] * relErr])
230+
}
231+
})
232+
}
233+
mle <- function(x) {
234+
if (x == lo)
235+
return(0)
236+
if (x == hi)
237+
return(Inf)
238+
mu <- mnhyper(1)
239+
if (mu > x)
240+
uniroot(function(t) mnhyper(t) - x, c(0, 1))$root
241+
else if (mu < x)
242+
1/uniroot(function(t) mnhyper(1/t) - x, c(.Machine$double.eps,
243+
1))$root
244+
else 1
245+
}
246+
ESTIMATE <- c(`odds ratio` = mle(x))
247+
if (conf.int) {
248+
ncp.U <- function(x, alpha) {
249+
if (x == hi)
250+
return(Inf)
251+
p <- pnhyper(x, 1)
252+
if (p < alpha)
253+
uniroot(function(t) pnhyper(x, t) - alpha,
254+
c(0, 1))$root
255+
else if (p > alpha)
256+
1/uniroot(function(t) pnhyper(x, 1/t) - alpha,
257+
c(.Machine$double.eps, 1))$root
258+
else 1
259+
}
260+
ncp.L <- function(x, alpha) {
261+
if (x == lo)
262+
return(0)
263+
p <- pnhyper(x, 1, upper.tail = TRUE)
264+
if (p > alpha)
265+
uniroot(function(t) pnhyper(x, t, upper.tail = TRUE) -
266+
alpha, c(0, 1))$root
267+
else if (p < alpha)
268+
1/uniroot(function(t) pnhyper(x, 1/t, upper.tail = TRUE) -
269+
alpha, c(.Machine$double.eps, 1))$root
270+
else 1
271+
}
272+
CINT <- switch(alternative, less = c(0, ncp.U(x,
273+
1 - conf.level)), greater = c(ncp.L(x, 1 - conf.level),
274+
Inf), two.sided = {
275+
alpha <- (1 - conf.level)/2
276+
c(ncp.L(x, alpha), ncp.U(x, alpha))
277+
})
278+
attr(CINT, "conf.level") <- conf.level
279+
}
280+
RVAL <- c(RVAL, list(conf.int = if (conf.int) CINT, estimate = ESTIMATE,
281+
null.value = NVAL))
282+
}
283+
structure(c(RVAL, alternative = alternative, method = METHOD,
284+
data.name = DNAME), class = "htest")
285+
}

vfisher.Rproj

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Version: 1.0
2+
3+
RestoreWorkspace: No
4+
SaveWorkspace: No
5+
AlwaysSaveHistory: Default
6+
7+
EnableCodeIndexing: Yes
8+
UseSpacesForTab: Yes
9+
NumSpacesForTab: 2
10+
Encoding: UTF-8
11+
12+
RnwWeave: Sweave
13+
LaTeX: pdfLaTeX
14+
15+
AutoAppendNewline: Yes
16+
StripTrailingWhitespace: Yes
17+
LineEndingConversion: Posix
18+
19+
BuildType: Package
20+
PackageUseDevtools: Yes
21+
PackageInstallArgs: --no-multiarch --with-keep.source
22+
PackageRoxygenize: rd,collate,namespace

0 commit comments

Comments
 (0)