The basic workflow
samplyr separates a sampling design from the frame on
which it runs. You describe the design first, validate or inspect it,
and then execute it against one or more frames.
The smallest complete example is a simple random sample without replacement.
library(samplyr)
library(dplyr)
data(bfa_eas)
design <- sampling_design(title = "Burkina Faso EA sample") |>
draw(n = 100)
sample <- execute(design, bfa_eas, seed = 24)
sample |>
as.data.frame() |>
dplyr::select(ea_id, region, households, .weight) |>
head(4)
#> ea_id region households .weight
#> 1 5866 Centre-Nord 146 445.7
#> 2 24905 Nord 32 445.7
#> 3 5145 Centre-Nord 156 445.7
#> 4 32861 Est 22 445.7The frame has one row per enumeration area. The result keeps its frame columns and adds the design information needed for analysis.
.weight is the overall design weight. In a one-stage
sample it equals .weight_1. A multi-stage sample also
carries one weight column per stage and multiplies them into
.weight.
The design grammar
Six functions cover the ordinary workflow.
| Function | Role |
|---|---|
sampling_design() |
Start a reusable design |
stratify_by() |
Divide a stage into sampling strata |
cluster_by() |
Name the units selected as clusters |
draw() |
Choose the size and selection method |
add_stage() |
Start another sampling stage |
execute() |
Run the design against its frame or frames |
Column names are stored in the design and resolved when a frame is supplied. This lets the same design be validated and reused against compatible frame vintages.
stratified_design <- sampling_design(title = "Stratified EA sample") |>
stratify_by(region, alloc = "proportional") |>
draw(n = 260)
validate_frame(stratified_design, bfa_eas)
#> Stage 1: `alloc = "proportional"` shares `n = 260` across 13 strata.
#> ℹ Stratum takes: "Boucle du Mouhoun" = 29, "Cascades" = 15, "Centre" = 23,
#> "Centre-Est" = 17, "Centre-Nord" = 20, "Centre-Ouest" = 22, "Centre-Sud" = 9,
#> "Est" = 32, and 5 more.
#> ℹ Without `alloc`, `n = 260` would be taken in each stratum.validate_frame() returns invisibly when the required
columns and values are usable. It reports problems before selection
begins, and under an allocation method it states how n was
shared across the strata.
Stratified sampling
Stratification samples independently within groups. Here
n = 260 is a total allocated across regions in proportion
to their frame sizes.
stratified_sample <- execute(stratified_design, bfa_eas, seed = 25)
stratified_sample |>
count(region, name = "n_sampled")
#> # A tibble: 13 × 2
#> region n_sampled
#> <fct> <int>
#> 1 Boucle du Mouhoun 29
#> 2 Cascades 15
#> 3 Centre 23
#> 4 Centre-Est 17
#> 5 Centre-Nord 20
#> 6 Centre-Ouest 22
#> 7 Centre-Sud 9
#> 8 Est 32
#> 9 Hauts-Bassins 28
#> 10 Nord 17
#> 11 Plateau-Central 10
#> 12 Sahel 24
#> 13 Sud-Ouest 14If stratify_by() has no allocation rule, a scalar
n is interpreted per stratum. You can also pass a named
vector or a svyplan::n_alloc() result to
draw() when each stratum needs its own take.
Equal allocation gives comparable precision within each stratum only
when the strata vary about equally, so it suits strata that are
reporting domains of similar variability. Proportional allocation
divides the total by the number of units each stratum holds at that
stage, which makes an element sample self-weighting. At a clustered
stage it counts clusters, not the households inside them. Given
importance, such as each stratum’s household total, it
allocates clusters in proportion to that size instead. Neyman and
cost-weighted allocations need prior variability or cost information and
optimize the criterion supplied for that planning variable. They are not
universal optima for every outcome and domain.
Sample-size bounds can protect small reporting domains.
bounded_design <- sampling_design() |>
stratify_by(region, alloc = "proportional") |>
draw(n = 260, min_n = 12, max_n = 30)
bounded_sample <- execute(bounded_design, bfa_eas, seed = 26)
bounded_sample |>
count(region, name = "n_sampled")
#> # A tibble: 13 × 2
#> region n_sampled
#> <fct> <int>
#> 1 Boucle du Mouhoun 29
#> 2 Cascades 14
#> 3 Centre 22
#> 4 Centre-Est 17
#> 5 Centre-Nord 20
#> 6 Centre-Ouest 21
#> 7 Centre-Sud 12
#> 8 Est 30
#> 9 Hauts-Bassins 28
#> 10 Nord 17
#> 11 Plateau-Central 12
#> 12 Sahel 24
#> 13 Sud-Ouest 14For without-replacement sampling, the population of a stratum remains
a hard upper bound. If a requested allocation cannot be reached,
execute() reports the affected pools and uses the
attainable take.
Cluster and PPS sampling
cluster_by() changes the sampling unit. The next example
selects enumeration areas with probability related to their household
counts.
pps_design <- sampling_design(title = "PPS EA sample") |>
cluster_by(ea_id) |>
draw(n = 80, method = "pps_brewer", mos = households)
pps_sample <- execute(pps_design, bfa_eas, seed = 27)
nrow(pps_sample)
#> [1] 80mos is the measure of size. A large measure gives a unit
a larger selection probability. If a resolved probability reaches one,
that unit is selected with certainty and contributes no variance at that
stage. Certainty at one stage does not make the final multi-stage weight
equal to one.
pps_brewer is one fixed-size PPS method. Method choice
also depends on the second-order information required for variance
estimation, whether a random sample size is acceptable, and whether
permanent random numbers are needed. The comparison is in
?selection_methods.
cluster_by() also names each unit’s identifier, and a
frame with one row per unit can use it too. With the same seed, the
design below selects the same EAs with the same weights as
draw(n = 200) alone, and it records ea_id as
the identity of each selected unit.
screening <- sampling_design(title = "Screening sample") |>
cluster_by(ea_id) |>
draw(n = 200)
screened <- execute(screening, bfa_eas, seed = 25)
nrow(screened)
#> [1] 200Declare it whenever a second phase may later subsample this sample.
The two phases are linked through an identifier both designs declare. An
ID column the design does not name does not link them, so the survey
export of the second phase would be refused. The examples in
?execute select a second phase, and
?as_svydesign gives its export.
A three-stage household sample
add_stage() starts another selection within each
selected parent. This section selects communes within regions,
enumeration areas (EAs) within those communes, and households within
those EAs. Selection pauses while the household listing is prepared,
then continues under the same design.
bfa_eas is a derived Burkina Faso frame. Its modeled
EAs, household counts and historical administrative labels are teaching
inputs, not a current official household register. The household listing
and interview outcomes below are synthetic.
Give each register its own sampling unit and size
ea_register <- bfa_eas |>
mutate(
region,
commune_id = paste(region, province, commune, sep = "/"),
ea_id,
ea_households = households,
.keep = "none"
)
commune_register <- ea_register |>
summarise(commune_households = sum(ea_households),
.by = c(region, commune_id))
head(commune_register, 3)
#> # A tibble: 3 × 3
#> region commune_id commune_households
#> <fct> <chr> <int>
#> 1 Boucle du Mouhoun Boucle du Mouhoun/Bale/Bagassi 5089
#> 2 Boucle du Mouhoun Boucle du Mouhoun/Bale/Bana 2586
#> 3 Boucle du Mouhoun Boucle du Mouhoun/Bale/Boromo 5857The measure of size belongs to the unit being selected, so each level
has its own register. Their order in execute() maps them to
stages, and names on a list are labels, not a join specification.
Declare all three stages
household_design <- sampling_design("Three-stage household survey") |>
add_stage("Communes") |>
stratify_by(region) |>
cluster_by(commune_id) |>
draw(n = 4, method = "pps_brewer", mos = commune_households) |>
add_stage("EAs") |>
cluster_by(ea_id) |>
draw(n = 3, method = "pps_brewer", mos = ea_households) |>
add_stage("Households") |>
draw(n = 12)
household_design
#> ── Sampling Design: Three-stage household survey ───────────────────────────────
#>
#> ℹ 3 stages
#>
#> ── Stage 1: Communes ───────────────────────────────────────────────────────────
#> • Strata: region
#> • Cluster: commune_id
#> • Draw: n = 4 (per stratum), method = pps_brewer, mos = commune_households
#>
#> ── Stage 2: EAs ────────────────────────────────────────────────────────────────
#> • Cluster: ea_id
#> • Draw: n = 3 (per commune_id), method = pps_brewer, mos = ea_households
#>
#> ── Stage 3: Households ─────────────────────────────────────────────────────────
#> • Draw: n = 12 (per ea_id), method = srsworThe takes are four communes per region, three EAs
per selected commune, and twelve households per
selected EA. The first stage has no allocation rule, so
n = 4 is per stratum. A take that differs by EA is a table
keyed by ea_id, described under n in
?draw.
Select areas, then continue from the listing
selected_eas <- execute(
household_design,
list(commune_register, ea_register),
stages = 1:2,
seed = 91
)
selected_eas |>
as.data.frame() |>
count(region, name = "selected_eas")
#> region selected_eas
#> 1 Boucle du Mouhoun 12
#> 2 Cascades 12
#> 3 Centre 12
#> 4 Centre-Est 12
#> 5 Centre-Nord 12
#> 6 Centre-Ouest 12
#> 7 Centre-Sud 12
#> 8 Est 12
#> 9 Hauts-Bassins 12
#> 10 Nord 12
#> 11 Plateau-Central 12
#> 12 Sahel 12
#> 13 Sud-Ouest 12The EA register covers all communes, and execution restricts it to the selected parents. In fieldwork, a listing team then enumerates households in the selected EAs. Here an artificial listing gives each selected EA as many households as its modeled count.
household_listing <- selected_eas |>
as.data.frame() |>
reframe(hh_id = seq_len(ea_households), .by = c(commune_id, ea_id))A real listing rarely finds exactly the modeled count. That does not
bias the weights, because the household stage uses the take over the
listed count. Passing the listing to execute() with the
partial sample continues the design.
issued <- execute(selected_eas, household_listing, seed = 92)
#> Warning: Stage 3: sample size exceeded the pool population in 3 of 156 pools.
#> ✖ Requested 1872 units, selected 1859.
#> ℹ Capped pools: "Boucle du Mouhoun/Kossi/Nouna > 33237", "Est/Tapoa/Kantchari >
#> 31552", and "Sahel/Oudalan/Tin-Akoff > 34718".
#> ℹ Inspect with `frame_summary(sample, detail = "pool")` and the capped column.
issued |>
as.data.frame() |>
select(commune_id, ea_id, hh_id, .weight_1, .weight_2, .weight_3, .weight) |>
head(4)
#> commune_id ea_id hh_id .weight_1 .weight_2 .weight_3
#> 1 Boucle du Mouhoun/Kossi/Dokui 41769 66 11.85575 24.46053 6.333333
#> 2 Boucle du Mouhoun/Kossi/Dokui 41769 55 11.85575 24.46053 6.333333
#> 3 Boucle du Mouhoun/Kossi/Dokui 41769 10 11.85575 24.46053 6.333333
#> 4 Boucle du Mouhoun/Kossi/Dokui 41769 13 11.85575 24.46053 6.333333
#> .weight
#> 1 1836.653
#> 2 1836.653
#> 3 1836.653
#> 4 1836.653The warning names the EAs listed with fewer households than the take.
Each is taken whole, which keeps its weight exact but makes the sample
non-self-weighting. The final weight is the reciprocal of the product of
the three conditional selection probabilities. Continuation keeps the
earlier factors, whereas starting a new design on
selected_eas would declare a new sampling phase.
Join observations and hand off to analysis
Keep every issued row and join the collected outcomes to it. The artificial consumption values stand for an outcome collected in the field.
set.seed(93)
interviews <- issued |>
as.data.frame() |>
mutate(ea_id, hh_id,
consumption = round(rlnorm(n(), log(100000), 0.5)),
.keep = "none")
observed <- issued |>
left_join(interviews, by = c("ea_id", "hh_id"), relationship = "one-to-one")
survey::svymean(~consumption, as_svydesign(observed))
#> mean SE
#> consumption 113266 1529.6This export uses Brewer’s variance approximation for the PPS stages.
?variance_estimation gives the alternatives, including RWYB
replicates, and vignette("survey-analysis") continues with
outcome joins, domains and nonresponse.
Fractions, replacement, and ordering
draw() also accepts frac. Fixed-size
methods convert the fraction to an integer take, while Bernoulli
sampling keeps a random realized size.
fraction_sample <- sampling_design() |>
draw(frac = 0.01) |>
execute(bfa_eas, seed = 29)
nrow(fraction_sample)
#> [1] 446With-replacement methods may select the same population unit several
times. Their samples carry .draw_1, .draw_2,
and later-stage equivalents so every draw occurrence remains
distinct.
Systematic and sequential methods depend on frame order. Use
control when an explicit ordering is part of the design.
Their variance properties depend on that ordering, so an SRS variance
approximation for systematic selection need not be conservative.
Coordinate draws with permanent random numbers
A permanent random number (PRN) is a uniform value stored with each
unit in the register. Methods that accept one select deterministically
from it, so a later draw that reuses the PRNs selects the same units,
and a draw on shifted PRNs avoids them. bernoulli,
pps_poisson, pps_sps, pps_pareto
and scps accept a prn column.
set.seed(30)
register <- bfa_eas |>
mutate(prn = runif(n()))
coordinated <- sampling_design() |>
draw(n = 100, method = "pps_sps", mos = households, prn = prn)
first <- execute(coordinated, register, seed = 1)
second <- execute(coordinated, register, seed = 2)
setequal(first$ea_id, second$ea_id)
#> [1] TRUEThe seeds differ, but the samples do not. ?draw gives
the methods that accept prn and what fixes an
scps selection. The book chapter Coordinate
samples with permanent random numbers covers negative coordination,
register updates and response burden.
Inspecting the result
A tbl_sample is a tibble with a design and an execution
receipt attached. Ordinary one-to-one joins and new analysis columns
preserve it. Removing or duplicating rows changes the realized sample
and prevents survey export.
summary(stratified_sample)
#> ── Sample Summary: Stratified EA sample ────────────────────────────────────────
#>
#> ℹ n = 260 of 44,570 | stages = 1/1 | seed = 25
#>
#> ── Stage 1 ─────────────────────────────────────────────────────────────────────
#> • srswor, by region (proportional)
#> • 13 strata: N_h 1,612-5,505, n_h 9-32, f_h 0.0056-0.0060
#>
#> ── Weights ─────────────────────────────────────────────────────────────────────
#> • Mean 171.42 [166.2, 179.11] | CV 0.01 | Kish DEFF 1 | n_eff 260frame_summary() reports what the execution resolved in
each selection pool.
frame_summary(stratified_sample, detail = "pool") |>
select(stage, region, N, n_target, n_realized) |>
slice_head(n = 6)
#> # A tibble: 6 × 5
#> stage region N n_target n_realized
#> <int> <fct> <dbl> <dbl> <dbl>
#> 1 1 Boucle du Mouhoun 5009 29 29
#> 2 1 Cascades 2508 15 15
#> 3 1 Centre 3888 23 23
#> 4 1 Centre-Est 2941 17 17
#> 5 1 Centre-Nord 3402 20 20
#> 6 1 Centre-Ouest 3723 22 22For domain estimates, convert the complete sample first and subset
the survey design afterwards. See
vignette("survey-analysis") for that workflow.
Choose your next step
This article covered the design grammar, a first sample, and a
three-stage household sample followed from area registers to households
and observations. The next step on the main path is to analyze the
sample: vignette("survey-analysis") covers outcome joins,
domain estimates and variance choices.
Use the other articles when their task arises:
| Task | Where |
|---|---|
| Choose sizes and allocations before selection |
vignette("svyplan", package = "svyplan"),
then Plan
a sample with svyplan
|
| Coordinate new draws with permanent random numbers |
?draw, argument prn, and Coordinate
samples with permanent random numbers
|
| Schedule panels and refreshment cohorts |
?panel_assignment,
?rotation_program, and Plan
and run a rotating panel
|
| Save or replay a design | vignette("serialization") |
For a method lookup, use ?selection_methods, and for how
each method’s variance is estimated, ?variance_estimation.
Consult vignette("design-semantics") for the underlying
contracts and Validation
on synthetic populations for simulation diagnostics. The book Survey
Sampling Design with R collects these chapters with the
svyplan planning guides.
A compact checklist
- Make sure one row represents the unit selected at the current stage.
- Declare strata, clusters, and selection methods in their sampling order.
- Validate the design against the intended frame vintage.
- Use a recorded seed when the selection must be reproducible.
- Keep the returned design columns unchanged.
- Archive the executed sample and its frame provenance.
Continue with vignette("survey-analysis") to analyze the
selected sample.