The sequential Poisson method, and order sampling methods more generally, are simple and consequently not computationally expensive. This makes them suitable for a range of different applications, especially when drawing a sample from a large population. Despite this, there are two optimizations in this package to keep drawing samples fast.
The first optimization concerns the inclusion probabilities when there are
take-all units. As seen in vignette("take-all"), the algorithm finds
take-all units one at a time without recomputing the inclusion probabilities
many times. This is much faster than the naive approach when drawing a large
sample, and is on par with the usual algorithm that finds take-all units in
batches.
library(sps)
# Make a population with 200 take-all units.
x <- c(rep(1, 1e6 - 200), rep(1e6, 200))
n <- 1e3
# Naive implementation.
ip <- function(x, n, alpha = 0.001) {
p <- \(x, n) x * (n / sum(x))
ta_units <- integer(0)
pi <- p(x, n)
max_ts <- which.max(pi)
while (pi[max_ts] > 1 - alpha) {
ta_units <- c(ta_units, max_ts)
pi <- p(replace(pi, max_ts, 0), n - length(ta_units))
max_ts <- which.max(pi)
}
replace(pi, ta_units, 1)
}
cols <- c("expression", "median", "mem_alloc", "n_itr")
bench::mark(
inclusion_prob(x, n),
ip(x, n),
sampling::inclusionprobabilities(x, n)
)[cols]
#> # A tibble: 3 × 4
#> expression median mem_alloc n_itr
#> <bch:expr> <bch:tm> <bch:byt> <int>
#> 1 inclusion_prob(x, n) 24.18ms 61.1MB 21
#> 2 ip(x, n) 1.41s 3GB 1
#> 3 sampling::inclusionprobabilities(x, n) 70.1ms 134MB 8
The second optimization is recognizing that both the computation of inclusion
probabilities and the sequential Poisson method can benefit from partial sorting
algorithms. In both cases, only the \(n\) largest/smallest elements of a vector
are needed and, when installed, the topn() function from the kit package
is used to avoid a complete sort. This is a drop-in
replacement for order(x)[1:n] that can be faster
when n is much smaller than the length of x, and can have a modest impact on
performance when drawing a sample from a large population.
options(sps.usekit = TRUE)
bench::mark(sps(x, n))[cols]
#> # A tibble: 1 × 4
#> expression median mem_alloc n_itr
#> <bch:expr> <bch:tm> <bch:byt> <int>
#> 1 sps(x, n) 75.5ms 111MB 6
options(sps.usekit = FALSE)
bench::mark(sps(x, n))[cols]
#> # A tibble: 1 × 4
#> expression median mem_alloc n_itr
#> <bch:expr> <bch:tm> <bch:byt> <int>
#> 1 sps(x, n) 153ms 126MB 4
Partial sorting generally speeds up drawing smaller samples as well, but the effect is not as large because the sequential Poisson method is already quick.
Both optimizations above can be seen when iteratively drawing a sequential Poisson sample as creating an iterator involves finding the point at which each unit enters the take-all stratum. Although this is more expensive than drawing a single sample, it quickly pays off because of the small cost to iterating over units in the sample.
bench::mark(s <- sps_iterator(x, n), min_iterations = 10)[cols]
#> # A tibble: 1 × 4
#> expression median mem_alloc n_itr
#> <bch:expr> <bch:tm> <bch:byt> <int>
#> 1 s <- sps_iterator(x, n) 931ms 336MB 10
bench::mark(s())[cols]
#> # A tibble: 1 × 4
#> expression median mem_alloc n_itr
#> <bch:expr> <bch:tm> <bch:byt> <int>
#> 1 s() 2.82ms 15.2MB 80
bench::mark(sps(x, n))
#> # A tibble: 1 × 13
#> expression min median `itr/sec` mem_alloc `gc/sec` n_itr n_gc total_time
#> <bch:expr> <bch:tm> <bch:> <dbl> <bch:byt> <dbl> <int> <dbl> <bch:tm>
#> 1 sps(x, n) 158ms 159ms 6.13 126MB 7.66 4 5 653ms
#> # ℹ 4 more variables: result <list>, memory <list>, time <list>, gc <list>