library(samplyr)
library(sondage)
library(dplyr)
data(ken_enterprises)
set.seed(1)
frame <- ken_enterprises |>
mutate(prn = runif(n()))
n_wave <- 20011 Coordinate samples with permanent random numbers
11.1 What 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 et al. 1972; Ohlsson 1995).
This chapter 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, pps_pareto, and scps.
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. Chapter 12 sets the retention and intake schedule. The estimator of change is left to the analysis.
11.2 Prepare the register
We use the synthetic ken_enterprises register and target about 200 enterprises per wave.
In 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.
11.3 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.
11.4 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 (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.
11.5 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 (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; 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.
11.6 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.
11.7 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).
11.8 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.
11.9 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.
frame_for_second_survey <- frame |>
mutate(prn_second = (prn + 0.5) %% 1)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 programs generally need a dedicated coordination system rather than repeated ad hoc shifts (Ohlsson 1995; Rivière 2001).
11.10 Spread the response burden
Spatially correlated Poisson sampling, scps, spreads a sample over its spread variables. Matei et al. (2023) spread it over a measure of response burden instead of geography, so burdened units repel each other within a sample and their number varies little from one sample to the next. A second survey drawn with 1 - prn then rarely takes the same units (Grafström and Matei 2018). They call this adapted SCP sampling. Here the burden marks the large enterprises an earlier survey selected.
set.seed(2027)
large <- ken_enterprises |>
filter(size_class == "Large") |>
mutate(prn_earlier = runif(n()))
earlier <- sampling_design() |>
stratify_by(region) |>
draw(n = 15, method = "pps_pareto", mos = employees, prn = prn_earlier) |>
execute(large, seed = 1)
large <- large |>
mutate(burden = as.numeric(enterprise_id %in% earlier$enterprise_id))
survey_1 <- sampling_design() |>
stratify_by(region) |>
draw(n = 10, method = "scps", mos = employees, spread = burden, prn = prn)
survey_2 <- sampling_design() |>
stratify_by(region) |>
draw(n = 8, method = "scps", mos = revenue_millions, spread = burden,
prn = prn_2)Each repetition gives the register fresh PRNs and draws both surveys. The comparison draws the same two surveys with pps_pareto and the same PRNs.
pareto_1 <- sampling_design() |>
stratify_by(region) |>
draw(n = 10, method = "pps_pareto", mos = employees, prn = prn)
pareto_2 <- sampling_design() |>
stratify_by(region) |>
draw(n = 8, method = "pps_pareto", mos = revenue_millions, prn = prn_2)
burden_counts <- function(design_1, design_2, frame) {
s1 <- execute(design_1, frame, seed = 1)
s2 <- execute(design_2, frame, seed = 1)
in_both <- intersect(s1$enterprise_id, s2$enterprise_id)
c(
burdened = sum(s1$burden) + sum(s2$burden),
burdened_in_both = sum(frame$burden[frame$enterprise_id %in% in_both])
)
}
runs <- lapply(1:20, function(i) {
frame <- large |> mutate(prn = runif(n()), prn_2 = 1 - prn)
rbind(
adapted_scp = burden_counts(survey_1, survey_2, frame),
pareto = burden_counts(pareto_1, pareto_2, frame)
)
})
summarise_runs <- function(column) {
values <- sapply(runs, function(run) run[, column])
data.frame(mean = rowMeans(values), sd = apply(values, 1, sd))
}
summarise_runs("burdened")
#> mean sd
#> adapted_scp 38.9 1.744163
#> pareto 38.9 3.522858
summarise_runs("burdened_in_both")
#> mean sd
#> adapted_scp 6.2 0.8944272
#> pareto 5.6 0.9947229Both designs take the same number of burdened units on average, since their inclusion probabilities are the same. Spreading on the burden makes that number much steadier from sample to sample, which is the control the method adds. The 1 - prn coordination keeps few burdened units in both samples under either design.
Three points differ from the earlier sections. With prn, scps visits each pool in the frame’s row order, or in the control order when the stage has one, so both draws need the same order, for example arrange(enterprise_id) or control = enterprise_id. The negative coordination taught for scps is 1 - prn, as in the method’s validation, while the (prn - pik) %% 1 update of the earlier sections is the rule for Bernoulli and order sampling and is not validated for scps. And scps has no linearization variance, so estimates use replicate weights (?variance_estimation).
11.11 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 an order-sampling method, pps_pareto or pps_sps, only when you need both unequal selection probabilities and an exact realized sample size. In small pools pps_pareto keeps much closer to its target probabilities, as ?selection_methods reports. Use the panel schedule of Chapter 12 when the requirement is a controlled entry-and-exit schedule rather than simply more or less overlap.