Coordinate Samples with Permanent Random Numbers
Source:vignettes/sampling-coordination.Rmd
sampling-coordination.RmdWhat coordination does
When several samples use the same register, we can deliberately control how much they overlap. Positive coordination keeps more of the same units. This can help with estimates of change, but it concentrates repeated response burden on those units. Negative coordination does the opposite. It spreads response burden and increases the number of distinct units covered.
Permanent random numbers, or PRNs, provide a practical way to coordinate samples. Each frame unit receives one random number between zero and one. The number stays with the unit and is reused or updated for later selections. This approach has a long history in business surveys (Brewer, Early, and Joyce 1972; Esbjörn Ohlsson 1995).
This article starts with Bernoulli sampling because its coordination
rule is exact and easy to see. It then shows the fixed-size PPS workflow
commonly used for business surveys. samplyr accepts PRNs
with bernoulli, pps_poisson,
pps_sps, and pps_pareto.
Coordination is not the same as a longitudinal design. A PRN controls
overlap between draws. A longitudinal plan also decides how long units
remain in the sample, how many units enter at each wave, and how change
will be estimated. See vignette("rotating-panels") for that
workflow.
Prepare the register
We use the synthetic ken_enterprises register and target
about 200 enterprises per wave.
data(ken_enterprises)
set.seed(1)
frame <- ken_enterprises |>
mutate(prn = runif(n()))
n_wave <- 200In production, store prn in the register rather than
recreating it in every sampling script. A stable identifier and a stable
PRN are both needed for coordination to survive register updates.
The simplest positive coordination
Under Bernoulli sampling, a unit is selected when its PRN is below the sampling fraction. Reusing both the PRN and the fraction therefore selects the same units. The realized sample size can vary around the target.
fraction <- n_wave / nrow(frame)
design_bernoulli <- sampling_design() |>
draw(frac = fraction, method = "bernoulli", prn = prn)
wave_1 <- execute(design_bernoulli, frame, seed = 10)
wave_2 <- execute(design_bernoulli, frame, seed = 20)
tibble(
size_wave_1 = nrow(wave_1),
size_wave_2 = nrow(wave_2),
overlap = length(intersect(wave_1$enterprise_id, wave_2$enterprise_id))
)
#> # A tibble: 1 × 3
#> size_wave_1 size_wave_2 overlap
#> <int> <int> <int>
#> 1 208 208 208The seeds differ, but the samples do not. Once a supported method receives a PRN, selection is determined by the PRN values and the design.
The simplest negative coordination
For a second Bernoulli wave, subtract the inclusion probability from each PRN and wrap the result back into the unit interval (Esbjörn Ohlsson 1995).
frame_wave_2 <- frame |>
mutate(prn = (prn - fraction) %% 1)
wave_2_negative <- execute(design_bernoulli, frame_wave_2, seed = 20)
tibble(
size_wave_1 = nrow(wave_1),
size_wave_2 = nrow(wave_2_negative),
overlap = length(intersect(
wave_1$enterprise_id,
wave_2_negative$enterprise_id
))
)
#> # A tibble: 1 × 3
#> size_wave_1 size_wave_2 overlap
#> <int> <int> <int>
#> 1 208 212 0Here the two samples are disjoint. That conclusion is exact for this equal- probability Bernoulli example because twice the sampling fraction is below one. More generally, high-probability units may have to appear in both samples.
The important operational rule is simple: update PRNs on the full frame, not only on the selected rows. Otherwise the next draw no longer follows the intended sampling design.
A fixed-size PPS sample
Business surveys often need a fixed sample size and want large
enterprises to be selected more often. Sequential Poisson sampling
(pps_sps) provides that combination and supports PRNs (Esbjorn Ohlsson
1998).
First compute each unit’s target inclusion probability on the full frame.
frame_pps <- frame |>
mutate(pik = inclusion_prob(revenue_millions, n_wave))
summary(frame_pps$pik)
#> Min. 1st Qu. Median Mean 3rd Qu. Max.
#> 0.0001479 0.0012640 0.0024473 0.0117619 0.0062391 1.0000000Then declare the design once and execute it.
design_pps <- sampling_design() |>
draw(
n = n_wave,
method = "pps_sps",
mos = revenue_millions,
prn = prn
)
pps_wave_1 <- execute(design_pps, frame_pps, seed = 1)
frame_pps_2 <- frame_pps |>
mutate(prn = (prn - pik) %% 1)
pps_wave_2 <- execute(design_pps, frame_pps_2, seed = 1)
tibble(
size_wave_1 = nrow(pps_wave_1),
size_wave_2 = nrow(pps_wave_2),
overlap = length(intersect(
pps_wave_1$enterprise_id,
pps_wave_2$enterprise_id
))
)
#> # A tibble: 1 × 3
#> size_wave_1 size_wave_2 overlap
#> <int> <int> <int>
#> 1 200 200 15The update encourages negative coordination, but the exact Bernoulli
argument does not carry over to pps_sps. Sequential Poisson
sampling orders all units and fixes the sample size. Its supplied
pik values are targets rather than exact first-order
inclusion probabilities. The approximation improves for large
populations and samples (Rosén 1997; Esbjorn Ohlsson 1998). The same
distinction applies to pps_pareto.
If exact first-order probabilities matter more than a fixed realized
size, use pps_poisson. Its sample size is random. Very
large measures of size can also produce certainty units or reduce the
expected sample size after probabilities are capped at one.
execute() reports that case. See ?draw for the
certainty options.
Repeat the update across waves
For a stable frame, a short loop is enough. Each sample uses the current PRN, then every frame unit receives its next PRN.
set.seed(2026)
frame_many <- ken_enterprises |>
mutate(
prn = runif(n()),
pik = inclusion_prob(revenue_millions, n_wave)
)
n_waves <- 4
waves <- vector("list", n_waves)
for (wave in seq_len(n_waves)) {
waves[[wave]] <- execute(design_pps, frame_many, seed = 1)
frame_many <- frame_many |>
mutate(prn = (prn - pik) %% 1)
}
ids <- lapply(waves, `[[`, "enterprise_id")
overlap <- outer(
ids,
ids,
Vectorize(\(x, y) length(intersect(x, y)))
)
dimnames(overlap) <- rep(list(paste("Wave", seq_len(n_waves))), 2)
overlap
#> Wave 1 Wave 2 Wave 3 Wave 4
#> Wave 1 200 22 35 39
#> Wave 2 22 200 11 41
#> Wave 3 35 11 200 15
#> Wave 4 39 41 15 200The matrix is descriptive. There is no general promise that adjacent fixed- size PPS samples will be disjoint or that a later pair will have a particular overlap.
Stratified samples
Compute pik within the same strata used by the design.
The PRN update itself still applies to every row.
set.seed(1980)
frame_stratified <- ken_enterprises |>
mutate(prn = runif(n())) |>
mutate(
pik = inclusion_prob(revenue_millions, 50),
.by = size_class
)
design_stratified <- sampling_design() |>
stratify_by(size_class) |>
draw(
n = 50,
method = "pps_sps",
mos = revenue_millions,
prn = prn
)
stratified_wave_1 <- execute(design_stratified, frame_stratified, seed = 1)
frame_stratified_2 <- frame_stratified |>
mutate(prn = (prn - pik) %% 1)
stratified_wave_2 <- execute(
design_stratified,
frame_stratified_2,
seed = 1
)
length(intersect(
stratified_wave_1$enterprise_id,
stratified_wave_2$enterprise_id
))
#> [1] 9If allocations, strata, or measures of size change, recompute the probabilities before updating the PRNs. Reusing a PRN still coordinates the draws, but it does not by itself maximize retention under a redesigned sample (Kish and Scott 1971).
Births, deaths, and register maintenance
For a cross-sectional survey, surviving units keep their PRNs, births receive fresh independent PRNs, and units no longer in the target population leave the frame. Recompute inclusion probabilities after the register changes.
These steps do not solve longitudinal attrition. If the target follows a fixed cohort, a closed enterprise or a nonrespondent needs an analysis rule as well as a frame update. PRNs also cannot detect identifier reuse, duplicate units, or incomplete coverage. Those are register-quality checks.
Coordination across surveys
Different surveys using the same register can share a PRN column. Reusing the same PRN tends to increase overlap. A half-turn shift is a simple way to seek less overlap.
For equal-probability Bernoulli samples, separated intervals give an exact result when their sampling fractions fit without overlap. For fixed-size PPS samples with different sizes, strata, or size measures, the half-turn is only a heuristic. Large survey programmes generally need a dedicated coordination system rather than repeated ad hoc shifts (Esbjörn Ohlsson 1995; Rivière 2001).
A practical checklist
- Add one stable PRN column to the full register.
- Compute inclusion probabilities on the full selection pool, within strata when the design is stratified.
- Pass the PRN column to a supported method.
- Keep the PRN for positive coordination, or update it for negative coordination.
- Save the updated full frame before the next wave.
- Recompute probabilities whenever the frame or design changes.
Start with the Bernoulli example if you are learning the mechanism.
Move to pps_sps only when you need both unequal selection
probabilities and an exact realized sample size. Use
vignette("rotating-panels") when the requirement is a
controlled entry-and-exit schedule rather than simply more or less
overlap.