>>>>> Ravi Varadhan via R-devel writes:

Thanks.  I finally committed

r90155 | hornik | 2026-06-15 18:45:43 +0200 (Mon, 15 Jun 2026) | 2 lines
Add experimental code for faster computation of minlike two-sided
p-values for large n.  Based on R-devel post by Ravi Varadhan.

Needed to make a few other changes first.  I want to experiment some
more in the next few days and then fold in, likely already for n around
1000 (for which improvements become noticeable for me).

One should also modify the code in poisson.test() accordingly ...

-k

> This code block in binom.test() is the bottleneck for large n:
>                            else if (x < m) {
>                                i <- seq.int(from = ceiling(m), to = n)
>                                y <- sum(dbinom(i, n, p) <= d * relErr)
>                                pbinom(x, n, p) +
>                                    pbinom(n - y, n, p, lower.tail = FALSE)
>                            } else {
>                                i <- seq.int(from = 0, to = floor(m))
>                                y <- sum(dbinom(i, n, p) <= d * relErr)
>                                pbinom(y - 1, n, p) +
>                                    pbinom(x - 1, n, p, lower.tail = FALSE)
>                            }
> Instead of summing over the entire support, we can:

>   1.
> Find the left and right boundary integers, yL and yR, where the PMF drops to 
> or below the observed probability (using uniroot).
>   2.
> Compute the exact p-value as:  pbinom(yL, n, p) + (1 - pbinom(yR-1, n, p)).

> This is much faster: O(log n) for the search versus O(n).
> Here is a function (I used n=1e05 as the threshold):
> binom.test.fast <- function(x, n, p = 0.5, alternative = c("two.sided", 
> "less", "greater"), conf.level = 0.95) {
>   DNAME <- deparse1(substitute(x))
>   xr <- round(x)
>   if (any(is.na(x) | (x < 0)) || max(abs(x - xr)) > 1e-07)
>     stop("'x' must be nonnegative and integer")
>   x <- xr
>   if (length(x) == 2L) {
>     n <- sum(x)
>     x <- x[1L]
>   }
>   else if (length(x) == 1L) {
>     nr <- round(n)
>     if ((length(n) > 1L) || is.na(n) || (n < 1) || abs(n - nr) > 1e-07 || (x 
> > nr))
>       stop("'n' must be a positive integer >= 'x'")
>     DNAME <- paste(DNAME, "and", deparse1(substitute(n)))
>     n <- nr
>   }
>   else stop("incorrect length of 'x'")

>   if (!missing(p) && (length(p) > 1L || is.na(p) || p < 0 || p > 1))
>     stop("'p' must be a single number between 0 and 1")
>   alternative <- match.arg(alternative)
>   if (!((length(conf.level) == 1L) && is.finite(conf.level) && (conf.level > 
> 0) && (conf.level < 1)))
>     stop("'conf.level' must be a single number between 0 and 1")

>   PVAL <- switch(alternative,
>                  less = pbinom(x, n, p),
>                  greater = pbinom(x - 1, n, p, lower.tail = FALSE),
>                  two.sided = {
>                    if (p == 0) (x == 0) else if (p == 1) (x == n) else {
>                      if (n > 1e5) {
>                        # Fast O(log n) path for large n
>                        log_dx <- dbinom(x, n, p, log = TRUE)
>                        if (!is.finite(log_dx)) {
>                          0
>                        } else {
>                          relErr <- 1 + 1e-07
>                          log_thresh <- log_dx + log(relErr)
>                          mode <- floor((n + 1) * p)

>                          # Left boundary: largest k <= mode with dbinom(k) <= 
> dbinom(x)*relErr
>                          lo <- 0L; hi <- min(x, mode)
>                          while (lo < hi) {
>                            mid <- (lo + hi + 1L) %/% 2L
>                            if (dbinom(mid, n, p, log = TRUE) > log_thresh) hi 
> <- mid - 1L
>                            else lo <- mid
>                          }
>                          yL <- lo

>                          # Right boundary: smallest k >= mode with dbinom(k) 
> <= dbinom(x)*relErr
>                          lo <- max(x, mode); hi <- n
>                          while (lo < hi) {
>                            mid <- (lo + hi) %/% 2L
>                            if (dbinom(mid, n, p, log = TRUE) > log_thresh) lo 
> <- mid + 1L
>                            else hi <- mid
>                          }
>                          yR <- lo

>                          pbinom(yL, n, p) + pbinom(yR, n, p, lower.tail = 
> FALSE)
>                        }
>                      } else {
>             # Original vectorized path for small n
>                        relErr <- 1 + 1e-07
>                        d <- dbinom(x, n, p)
>                        m <- n * p
>                        if (x == m) 1 else if (x < m) {
>                          i <- seq.int(from = ceiling(m), to = n)
>                          y <- sum(dbinom(i, n, p) <= d * relErr)
>                          pbinom(x, n, p) + pbinom(n - y, n, p, lower.tail = 
> FALSE)
>                        } else {
>                          i <- seq.int(from = 0, to = floor(m))
>                          y <- sum(dbinom(i, n, p) <= d * relErr)
>                          pbinom(y - 1, n, p) + pbinom(x - 1, n, p, lower.tail 
> = FALSE)
>                        }
>                      }
>                    }
>                  }
>   )

>   p.L <- function(x, alpha) {
>     if (x == 0) 0 else qbeta(alpha, x, n - x + 1)
>   }
>   p.U <- function(x, alpha) {
>     if (x == n) 1 else qbeta(1 - alpha, x + 1, n - x)
>   }
>   CINT <- switch(alternative,
>                  less = c(0, p.U(x, 1 - conf.level)),
>                  greater = c(p.L(x, 1 - conf.level), 1),
>                  two.sided = {
>                    alpha <- (1 - conf.level)/2
>                    c(p.L(x, alpha), p.U(x, alpha))
>                  }
>   )
>   attr(CINT, "conf.level") <- conf.level
>   ESTIMATE <- x/n
>   names(x) <- "number of successes"
>   names(n) <- "number of trials"
>   names(ESTIMATE) <- names(p) <- "probability of success"
>   structure(list(statistic = x, parameter = n, p.value = PVAL,
>                  conf.int = CINT, estimate = ESTIMATE, null.value = p,
>                  alternative = alternative, method = "Exact binomial test",
>                  data.name = DNAME), class = "htest")
> }
> Ravi

> ________________________________
> From: R-devel <[email protected]> on behalf of Martin Maechler 
> <[email protected]>
> Sent: Monday, May 18, 2026 12:20
> To: Chris Chang <[email protected]>
> Cc: R Development List <[email protected]>
> Subject: Re: [Rd] Feature request: ..., binomial ... exact test efficiency 
> improvements


>       External Email - Use Caution



>>>>> Chris Chang
>>>>>     on Sat, 16 May 2026 14:24:19 -0700 writes:

> [............................]

>>> The one-sided binom.test() does not have this kind of
>>> performance problem, but the two-sided test (which can be
>>> written in a manner similar to qbinom(): instead of
>>> searching for minimal k satisfying pbinom(k, ...) >= p,
>>> search for the innermost opposite-tail k satisfying
>>> dbinom(k) <= dbinom(x)) does.
>>> 
>>> I can submit a patch with efficient two-sided binomial
>>> and Fisher's 2x2 exact test implementations if there is
>>> interest.
>>> 
>>> --Chris

> Dear Chris  (and other readers),

> Above, you also mention binom.test(),
> and indeed, when I choose numbers in the order of 1e8,
> I see binom.test() taking quite some time, e.g.,

>> system.time(btL <- binom.test(c(4e8, 1.01e8), p = 0.8, alternative = 
>> "two.sided"))
>    user  system elapsed
>   8.306   0.837   9.159

> and here, the time spent is really during p-value computation itself,
> everything else in binom.test() being very fast, AFAICS.

> Here it would be useful if you provide a patch to speed the
> p-value computation, still getting exact p-values, using
> pbinom() only instead of  dbinom(<large_support), ..).

> Traditionally, we as R core and any applied statistician would
> say that in such large N cases, simple normal approximations should be 
> accurate enough,
> which it would be practically in any case, OTOH, it would still
> be nice to get the exact probabilities / confidence intervals,
> with a faster calulation.

> Eventually using a new argument  `exact = N < 1e6`  (for some N)
> and using a normal (or better!) asymptotic approximation for
> exact = FALSE  may make sense here as well...

> Thank you for raising the issue!

> With best regards,
> Martin

> --
> Martin Maechler
> ETH Zurich and  R Core team

> ______________________________________________
> [email protected] mailing list
> https://stat.ethz.ch/mailman/listinfo/r-devel<https://stat.ethz.ch/mailman/listinfo/r-devel>

>       [[alternative HTML version deleted]]

> ______________________________________________
> [email protected] mailing list
> https://stat.ethz.ch/mailman/listinfo/r-devel

______________________________________________
[email protected] mailing list
https://stat.ethz.ch/mailman/listinfo/r-devel

Reply via email to