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

Reply via email to