sondage provides fast implementations of survey sampling
algorithms for finite populations. Every sampling function returns a
design object that carries the selected sample alongside the
design metadata (inclusion probabilities or expected hits, sample size,
method). A set of generics then operates on these objects to compute
quantities needed for variance estimation.
For the full object contract and field definitions, see
?sondage_sample.
For without-replacement designs, the stored pik vector
is the design-defining target inclusion probability vector. For methods
with exact first-order guarantees, this equals the true first-order
inclusion probabilities. For order-sampling methods such as
sps and pareto, the stored vector remains the
target pik, while the true finite-population first-order
inclusion probabilities are only approximately equal to that target.
The five entry-point functions are organized by two dimensions, plus a dedicated dispatcher for balanced sampling:
| Without replacement | With replacement | |
|---|---|---|
| Equal prob. | equal_prob_wor() |
equal_prob_wr() |
| Unequal prob. | unequal_prob_wor() |
unequal_prob_wr() |
| Balanced | balanced_wor() |
The simplest case: draw n units from a population of
size N with equal probabilities.
library(sondage)
s <- equal_prob_wor(N = 50, n = 5)
s
#> Equal prob WOR [srs] (n=5, N=50): 4 39 1 34 23The returned object stores the sampled unit indices and the inclusion probabilities:
s$sample
#> [1] 4 39 1 34 23
s$pik[1:10] # all equal to n/N = 0.1
#> [1] 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1Alternative methods are available. Systematic sampling provides implicit stratification based on the ordering of units:
s_sys <- equal_prob_wor(N = 50, n = 5, method = "systematic")
s_sys
#> Equal prob WOR [systematic] (n=5, N=50): 7 17 27 37 47Bernoulli sampling selects each unit independently with probability
p = n/N, producing a random sample size. The parameter
n is the expected sample size not the realized sample
size.
With-replacement designs track hits (how many times each unit was selected) instead of inclusion probabilities:
When units have different sizes (employees, revenue, area, …), probability-proportional-to-size (PPS) designs give larger units higher selection probabilities, which reduces the variance of Horvitz-Thompson estimators (Horvitz and Thompson 1952).
Start from a vector of positive size measures and compute inclusion probabilities (WOR) or expected hits (WR):
x <- c(10, 20, 5, 40, 25)
n <- 3
# Without replacement: inclusion probabilities (capped at 1)
pik <- inclusion_prob(x, n)
pik
#> [1] 0.3333333 0.6666667 0.1666667 1.0000000 0.8333333
sum(pik) # equals n
#> [1] 3
# With replacement: expected hits (can exceed 1)
eh <- expected_hits(x, n)
eh
#> [1] 0.30 0.60 0.15 1.20 0.75
sum(eh) # equals n
#> [1] 3Pass the inclusion probability vector to
unequal_prob_wor(). The default method is Conditional
Poisson Sampling (CPS, Chen et al. 1994),
which has maximum entropy among fixed-size designs:
s <- unequal_prob_wor(pik, method = "cps")
s
#> Unequal prob WOR [cps] (n=3, N=5): 1 4 5
s$sample
#> [1] 1 4 5
inclusion_prob(s) # extract the stored design-defining target pik
#> [1] 0.3333333 0.6666667 0.1666667 1.0000000 0.8333333Other fixed-size methods include "brewer" (draw-by-draw,
order-invariant), "sampford" (Sampford 1967), "systematic"
(O(N), implicit stratification), "sps" (Sequential Poisson sampling, Ohlsson 1998, supports
PRN), and "pareto" (Pareto
sampling, Rosén 1997, supports PRN). The "poisson"
method produces a random sample size.
For sps and pareto,
inclusion_prob(s) still returns the stored target
pik vector used to define the design. Those methods do not,
in general, provide exact finite-population first-order inclusion
probabilities equal to that target.
Pass the expected hits vector to unequal_prob_wr().
Chromy (1979)’s method is the default:
s_wr <- unequal_prob_wr(eh, method = "chromy")
s_wr
#> Unequal prob WR [chromy] (n=3, N=5): 2 4 5
s_wr$hits # realized selection counts
#> [1] 0 1 0 1 1The "multinomial" method draws each selection
independently.
The "systematic_mr" method is systematic PPS with
minimum replacement. It walks the cumulated expected hits from one
random start, in the order of the frame. A unit larger than the sampling
interval is hit once per interval it spans, where
unequal_prob_wor(method = "systematic") takes it once, with
certainty. Here the last unit expects 1.875 hits, so it receives one or
two:
When auxiliary information is available at the design stage, balanced sampling ensures that Horvitz-Thompson estimates of auxiliary totals are (approximately) exact: \(\sum_{k \in S} x_k / \pi_k \approx \sum_{k \in U} x_k\). This reduces variance for any study variable correlated with the balancing variables.
The balanced_wor() function implements the cube method
(Deville and Tillé 2004):
pik <- inclusion_prob(c(10, 20, 5, 40, 25), n = 3)
x <- matrix(c(10, 20, 5, 40, 25)) # auxiliary variable to balance on
s_bal <- balanced_wor(pik, aux = x)
s_bal
#> Balanced WOR [cube] (n=3, N=5): 3 4 5The cube method proceeds in two phases: a flight phase that
moves probabilities toward 0 or 1 while maintaining all balancing
constraints, and a landing phase that resolves the remaining
undecided units by progressively relaxing constraints (starting from the
last column of aux). The sample size constraint is always
placed first and never relaxed.
Users should order auxiliary variables by importance (most important
first) in the aux matrix.
For ill-conditioned or collinear auxiliary variables,
condition_aux = TRUE applies weighted centering and scaling
followed by rank pruning before running the cube algorithm.
When strata are known, the stratified cube method (Chauvet 2009) preserves within-stratum sample
sizes while balancing on auxiliary variables. Exact preservation
requires sum(pik) within each stratum to be close to an
integer. When this condition is not met, within-stratum and total sample
sizes will vary around their targets and the design is treated as
random-size (fixed_size = FALSE):
N <- 20
pik_strat <- rep(0.4, N)
x_strat <- matrix(as.double(1:N), ncol = 1)
strata <- rep(1:4, each = 5)
s_strat <- balanced_wor(pik_strat, aux = x_strat, strata = strata)
s_strat
#> Balanced WOR [cube] (n=8, N=20): 1 5 7 10 12 13 17 20
# Check per-stratum sample sizes
table(strata[s_strat$sample])
#>
#> 1 2 3 4
#> 2 2 2 2Joint inclusion probabilities for the cube use the high-entropy
approximation. It depends only on pik, so it ignores strata
and balancing constraints, and it can give a positive probability to a
pair that a stratified design excludes.
The cube method also accepts linear inequality constraints through
bounds = list(B, lower, upper) (Tripet and Tillé 2026). These constrain raw
realized-sample sums, \(\mathtt{lower}_j \leq
\sum_{k \in S} B_{kj} \leq \mathtt{upper}_j\), while preserving
the supplied first-order inclusion probabilities. A common use is to
keep category counts between the integers immediately below and above
their expectations:
pik_control <- rep(0.5, 12)
group <- rep(c("a", "b", "c"), each = 4)
B <- sapply(unique(group), function(g) as.double(group == g))
expected_counts <- colSums(B * pik_control)
s_control <- balanced_wor(
pik_control,
bounds = list(
B = B,
lower = floor(expected_counts),
upper = ceiling(expected_counts)
)
)
colSums(B[s_control$sample, , drop = FALSE])
#> a b c
#> 2 2 2It’s possible to select well-spread, spatially balanced samples using
lpm2 (local pivotal method 2,
Grafström et al. 2012) and scps (spatially correlated Poisson sampling, Grafström
2012).
coordinates <- as.matrix(expand.grid(x = 1:4, y = 1:4))
pik_spatial <- rep(0.25, nrow(coordinates))
s_lpm2 <- balanced_wor(
pik_spatial,
spread = coordinates,
method = "lpm2"
)
s_scps <- balanced_wor(
pik_spatial,
spread = coordinates,
method = "scps"
)
s_lpm2$sample
#> [1] 1 6 12 15
s_scps$sample
#> [1] 2 4 11 14These built-in methods are spread-only: they do not accept
aux, strata, or bounds. They
preserve the supplied first-order inclusion probabilities, but
intentionally produce low-entropy designs, so
joint_inclusion_prob() and sampling_cov() are
not available for them.
With permanent random numbers, scps visits the units in
row order and selects unit \(k\) when
\(u_k\) falls below its current
conditional probability (Bondesson and Thorburn
2008; Grafström and Matei 2018). Two surveys drawn with
prn = u and prn = 1 - u are then negatively
coordinated. Matei et al. (2023) spread on
a measure of response burden instead of coordinates, so that highly
burdened units rarely enter the same sample, and rarely enter both.
Once you have a design object, generics compute the quantities needed for variance estimation.
pik <- inclusion_prob(c(10, 20, 5, 40, 25), n = 3)
s <- unequal_prob_wor(pik, method = "cps")
pikl <- joint_inclusion_prob(s)
pikl
#> [,1] [,2] [,3] [,4] [,5]
#> [1,] 0.33333333 0.10026673 0.01935742 0.3333333 0.2137092
#> [2,] 0.10026673 0.66666667 0.04704252 0.6666667 0.5193574
#> [3,] 0.01935742 0.04704252 0.16666667 0.1666667 0.1002667
#> [4,] 0.33333333 0.66666667 0.16666667 1.0000000 0.8333333
#> [5,] 0.21370918 0.51935742 0.10026673 0.8333333 0.8333333The diagonal holds the first-order probabilities \(\pi_i\) and the off-diagonal entries are
\(\pi_{ij} = P(i \in S \text{ and } j \in
S)\). For sps and pareto, the
approximation is built from the stored target pik vector,
not from unknown exact finite-population first-order inclusion
probabilities.
eh <- expected_hits(c(10, 20, 5, 40, 25), n = 3)
s_wr <- unequal_prob_wr(eh, method = "chromy")
joint_expected_hits(s_wr)
#> [,1] [,2] [,3] [,4] [,5]
#> [1,] 0.2968 0.1126 0.0332 0.3504 0.0974
#> [2,] 0.1126 0.6011 0.0103 0.6382 0.4411
#> [3,] 0.0332 0.0103 0.1549 0.1630 0.1033
#> [4,] 0.3504 0.6382 0.1630 1.6063 0.8484
#> [5,] 0.0974 0.4411 0.1033 0.8484 0.7451By default, joint_inclusion_prob() and
joint_expected_hits() return the full N x N matrix, which
is prohibitive when N > 10,000 (~0.8 GB). In practice, variance
estimators only need the submatrix for sampled units. The
sampled_only = TRUE argument returns this n x n submatrix
directly:
pik_large <- inclusion_prob(runif(500), n = 10)
s_large <- unequal_prob_wor(pik_large, method = "brewer")
# Full 500 x 500 matrix
pikl_full <- joint_inclusion_prob(s_large)
dim(pikl_full)
#> [1] 500 500
# Only the 10 x 10 submatrix for sampled units
pikl_sub <- joint_inclusion_prob(s_large, sampled_only = TRUE)
dim(pikl_sub)
#> [1] 10 10All methods compute the submatrix directly without allocating the N x
N matrix, so sampled-only queries reach well beyond the population size
that a full matrix allows. Memory and computing time still depend on the
method, N and n, and the sampled output is limited to 10,000 units. The
sampling_cov() generic also accepts
sampled_only:
The sampling_cov() generic computes \(\Delta_{ij} = \pi_{ij} - \pi_i \pi_j\) for
WOR designs (or the analogous expression for WR designs):
sampling_cov(s)
#> [,1] [,2] [,3] [,4] [,5]
#> [1,] 0.22222222 -0.12195549 -0.03619814 0 -0.06406859
#> [2,] -0.12195549 0.22222222 -0.06406859 0 -0.03619814
#> [3,] -0.03619814 -0.06406859 0.13888889 0 -0.03862216
#> [4,] 0.00000000 0.00000000 0.00000000 0 0.00000000
#> [5,] -0.06406859 -0.03619814 -0.03862216 0 0.13888889With weighted = TRUE, it returns the Sen-Yates-Grundy
check quantities \(1 - \pi_i \pi_j /
\pi_{ij}\) (Sen 1953; Yates and Grundy
1953). For well-behaved WOR designs, these should be non-positive
off-diagonal:
sampling_cov(s, weighted = TRUE)
#> [,1] [,2] [,3] [,4] [,5]
#> [1,] 0.6666667 -1.21631058 -1.8699881 0 -0.29979336
#> [2,] -1.2163106 0.33333333 -1.3619296 0 -0.06969793
#> [3,] -1.8699881 -1.36192957 0.8333333 0 -0.38519411
#> [4,] 0.0000000 0.00000000 0.0000000 0 0.00000000
#> [5,] -0.2997934 -0.06969793 -0.3851941 0 0.16666667All five dispatchers accept an nrep parameter for
drawing multiple independent samples in one call, useful for simulation
studies. The result is always a design object (the same class as
nrep = 1) but with $sample holding all
replicates:
sim <- equal_prob_wor(N = 50, n = 5, nrep = 4)
sim # design object
#> Equal prob WOR [srs] (n=5, N=50): 4 replicates
#> rep 1: 3 49 40 18 13
dim(sim$sample) # 5 x 4 matrix: each column is one sample
#> [1] 5 4
# Generics still work on batch objects
inclusion_prob(sim)
#> [1] 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1
#> [20] 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1
#> [39] 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1For fixed-size designs $sample is a matrix
(n x nrep). For random-size designs (Bernoulli, Poisson) it
is a list of integer vectors, and realized sample sizes are available
via lengths(sim$sample).
The table below summarizes what sondage computes for
each method. Understanding these properties is essential for choosing
the right variance estimator.
| Method | Type | Fixed n | Exact design marginals? | Joint probs |
|---|---|---|---|---|
| SRS WOR | EP WOR | Yes | Yes | Exact |
| Systematic (EP) | EP WOR | Yes | Yes | Exact (zeros) |
| Bernoulli | EP WOR | No | Yes | Exact |
| SRS WR | EP WR | Yes | Yes | Exact |
| CPS | UP WOR | Yes | Yes | Exact |
| Brewer | UP WOR | Yes | Yes | HE approx |
| Sampford | UP WOR | Yes | Yes | Exact |
| Systematic PPS | UP WOR | Yes | Yes | Exact (zeros) |
| Poisson | UP WOR | No | Yes | Exact |
| SPS | UP WOR | Yes | Target only* | HE approx |
| Pareto | UP WOR | Yes | Target only* | HE approx |
| Chromy | UP WR | Yes | Yes | Simulated |
| Multinomial | UP WR | Yes | Yes | Exact |
| Systematic PPS (MR) | UP WR | Yes | Yes | Exact (zeros) |
| Cube | Balanced | Yes | Yes | HE approx |
| LPM2 | Balanced | Yes | Yes | Not available |
| SCPS | Balanced | Yes | Yes | Not available |
In sondage, joint_inclusion_prob() returns
exact joint inclusion probabilities computed via C code, using the same
algorithm as systematic PPS with uniform \(\pi_k = n/N\). Some \(\pi_{ij}\) are structurally zero (pairs of
units that can never co-occur in the same systematic sample), so the SYG
estimator is generally inapplicable. Consider replication or
successive-differences variance estimators for systematic designs.
Both are implemented via Rosén (1997)’s
order sampling framework. SPS uses ranking key \(\xi_k = u_k / \pi_k\) (equivalent to Ohlsson 1998’s sequential threshold
adjustment), and Pareto uses \(\xi_k =
[u_k/(1-u_k)] / [\pi_k/(1-\pi_k)]\). The \(n\) units with the smallest \(\xi_k\) are selected. Design objects store
the supplied pik as the design-defining target vector, and
inclusion_prob(s) returns that stored target. The true
finite-population first-order inclusion probabilities are approximately
(not exactly) equal to the target \(\pi_k\) for finite populations, and the
discrepancy vanishes as \(N\) grows.
Both support permanent random numbers (PRN) for sample coordination.
Joint inclusion probabilities are exact (computed via C code), but
some \(\pi_{ij}\) are structurally
zero, for pairs of units that can never co-occur.
sampling_cov(s, weighted = TRUE) returns NA
for those entries and issues a warning, because the SYG estimator is
undefined for such pairs. Consider successive-differences, Hartley-Rao,
or replication methods instead.
Pairwise expectations \(E(n_i n_j)\)
are estimated by Monte Carlo simulation in
joint_expected_hits() (controlled by nsim,
default 10 000).
Each unit receives the whole part of its expected hits or one more,
and the hits sum to \(n\). Pairwise
expectations from joint_expected_hits() are exact. A unit
with at least one expected hit is selected in every sample, so its pairs
with every unit of positive expected hits are positive. Two units below
one expected hit can still be a structural zero, as in systematic PPS,
and sampling_cov(s, weighted = TRUE) returns
NA for those pairs. In a two-stage design, a primary unit
hit twice needs two independent second-stage samples, and the
with-replacement variance approximation then treats each hit as a
separate primary unit.
For the cube method (Deville and Tillé
2004), joint_inclusion_prob() uses the high-entropy
approximation. It depends only on pik, so it does not
reflect strata or balancing constraints. A stratified design can have
pairs that never occur together, and the approximation reports a
positive probability for them. For a stratified or strongly constrained
design, prefer Monte Carlo estimates of the joint probabilities or a
variance method suited to balanced sampling. The stratified variant
(Chauvet 2009) preserves exact
within-stratum sample sizes. Order auxiliary variables by importance
(most important first) since the landing phase relaxes constraints from
last to first. Inequality bounds provide controlled selection, but tight
bounds can make the high-entropy approximation less accurate.
Both methods preserve exact first-order inclusion probabilities and
spread the sample using the variables supplied in spread.
Their low-entropy spatial dependence is deliberate, so no joint
inclusion probability or sampling covariance approximation is exposed.
Use a local-neighborhood variance estimator instead.
cps, and can work with approximate second-order quantities
in sondage.n = 1.sps and pareto, this approximation is built
from the stored target pik vector rather than the unknown
exact finite-population first-order probabilities. For the cube, it also
ignores strata and balancing constraints, as noted above. Use
method = "cps" when exact second-order structure is
required.sampling_cov().register_method() lets you plug custom
unequal-probability sampling algorithms into the existing dispatchers
and generics. Registered methods work with
unequal_prob_wor(), joint_inclusion_prob(),
sampling_cov(), batch mode, and all downstream tooling. See
vignette("custom-methods") for worked examples.