
A tidy grammar for survey sampling design
useR! 2026 / Warsaw / 06-09 July

National survey in rural Gambia. D’Alessandro et al. (1994), Bull. WHO 72(3): 391–394.
We run the survey. Answer: ≈ 58 %
Malaria hits children first, and the insecticide-treated bed net is one of our cheapest tools.
Before a national programme, we need to know what share of the population sleeps under a net.
Two more questions steer the roll-out:

Sharon L. Lohr, Sampling: Design and Analysis, example 7.1.
D’Alessandro’s survey became example 7.1 in Lohr, a standard sampling textbook.
R tends to drift far from the plain description of the plan.SAS PROC SURVEYSELECT and SPSS CSPLAN, whose dedicated survey-design syntax reads almost like the plan.samplyr}: express a design in R as plainly as you describe it.base::sample()?To express a design as plainly as we describe it, we first need to know what a design has to capture. On the ground, four constraints rule out that one-liner. The plan stays random all the way down, but it stops being simple, and each constraint names an idea.
Remember the colours. They follow us all the way to the code.
In 3 stages: 3 regions × 5 districts × 4 villages × 6 compounds = 360 visits. Bigger -> more likely to be drawn: that is PPS, probability proportional to size.
The Gambian survey protocol, as published:
① Villages were stratified by region (East, Centre, West) and by presence of a health centre. In each region, five districts were drawn proportional to their population. ② In each district, four villages, again proportional to population. ③ Finally, six compounds were drawn in each village.

strata clusters stages PPS
sampling} and in SPSS* SPSS Complex Samples.
CSPLAN SAMPLE /PLAN FILE='gambia.csplan'
/DESIGN STAGELABEL='districts'
STRATA=region CLUSTER=district
/METHOD TYPE=PPS_WOR
/MOS VARIABLE=district_pop
/SIZE VALUE=5
/DESIGN STAGELABEL='villages'
STRATA=phc CLUSTER=village
/METHOD TYPE=PPS_WOR
/MOS VARIABLE=village_pop
/SIZE VALUE=2
/DESIGN STAGELABEL='compounds'
CLUSTER=compound_id
/METHOD TYPE=SIMPLE_WOR
/SIZE VALUE=6.
library(sampling)
# Stage 1: 5 districts per region, PPS (no Brewer here: systematic)
d <- unique(gmb_frame[c("region", "district", "district_pop")])
d <- d[order(d$region), ]
s1 <- strata(d, "region", size = rep(5, 3), method = "systematic",
pik = d$district_pop)
# Stage 2: in each sampled district, 2 PHC + 2 non-PHC villages, PPS
v <- unique(gmb_frame[c("district", "phc", "village", "village_pop")])
v <- v[v$district %in% getdata(d, s1)$district, ]
v <- v[order(v$district, v$phc), ]
s2 <- strata(v, c("district", "phc"), size = rep(2, 30),
method = "systematic", pik = v$village_pop)
# Stage 3: 6 compounds per sampled village (take all if fewer)
cpd <- gmb_frame[gmb_frame$village %in% getdata(v, s2)$village, ]
cpd <- cpd[order(cpd$village), ]
s3 <- strata(cpd, "village", size = pmin(6, table(droplevels(cpd$village))),
method = "srswor")
# then getdata() three times, and compose the weights by hand...SPSS keeps the plan closer to the description. In R using {sampling}, the stages become three hand-chained draws, and strata, clusters, sizes and probabilities are yours to keep aligned.
samplyr}: the grammar follows the colours| The idea | In the report | In {samplyr} |
|---|---|---|
| Stratum | “stratified by region” | stratify_by(region) |
| Cluster | “five districts drawn together” | cluster_by(district) |
| Stage | “then, in each district…” | add_stage() |
| Sampling | “proportional to population” | draw(n, method, mos) |
These four verbs describe the design. The weight is not written, it results from the design composed by execute() across the stages (.weight).
① In each region, draw five districts proportional to population (PPS).
② In each district, by health centre, two villages PPS.
③ In each village, draw six compounds.
design <- sampling_design("Gambia bed nets") |>
add_stage(label = "Districts") |>
stratify_by(region) |>
cluster_by(district) |>
draw(n = 5, method = "pps_brewer",
mos = district_pop) |>
add_stage(label = "Villages") |>
stratify_by(phc) |>
cluster_by(village) |>
draw(n = 2, method = "pps_brewer",
mos = village_pop) |>
add_stage(label = "Compounds") |>
draw(n = 6)
The code mirrors the report, colour for colour, verb for verb.
── Sampling Design: Gambia bed nets ────────────────
ℹ 3 stages
── Stage 1: Districts ──────────────────────────────
• Strata: region
• Cluster: district
• Draw: n = 5 (per stratum), method = pps_brewer, mos = district_pop
── Stage 2: Villages ───────────────────────────────
• Strata: phc
• Cluster: village
• Draw: n = 2 (per stratum), method = pps_brewer, mos = village_pop
── Stage 3: Compounds ──────────────────────────────
• Draw: n = 6, method = srswor
It shows the three stages, each with its strata, clusters and draw. Defined without any data so it can be reviewed and versioned before the frame exists.
Rows: 19,344
Columns: 8
$ region <fct> Eastern, Easter…
$ district <fct> Eas-D01, Eas-D0…
$ phc <fct> PHC, PHC, PHC, …
$ village <fct> V0001, V0001, V…
$ compound_id <int> 1, 2, 3, 4, 5, …
$ population <int> 18, 19, 16, 19,…
$ district_pop <int> 9062, 9062, 906…
$ village_pop <int> 506, 506, 506, …
The frame is synthetic, built to mirror the survey. It holds 19,344 compounds with their village and size, but no nets or beds yet.
# A tbl_sample: 360 × 6 | Gambia bed nets
# Weights: 53.65 [32.53, 77.69]
region village compound_id phc population .weight
* <fct> <fct> <int> <fct> <int> <dbl>
1 Eastern V0007 242 PHC 22 60.5
2 Eastern V0007 224 PHC 8 60.5
3 Eastern V0007 236 PHC 27 60.5
4 Eastern V0007 241 PHC 19 60.5
5 Eastern V0007 234 PHC 22 60.5
# ℹ 355 more rows
execute() lists the 360 compounds to visit and already composes their weights across the three stages. But beds and nets are not there yet, and need to be collected on field.
# A tbl_sample: 3 × 5 | Gambia bed nets
# Weights: 60.54 [60.54, 60.54]
village compound_id nets beds .weight
* <fct> <int> <int> <int> <dbl>
1 V0007 242 6 18 60.5
2 V0007 224 4 9 60.5
3 V0007 236 0 4 60.5
estimate 2.5 % 97.5 %
nets/beds 0.584394 0.5078807 0.6609073
There are the 58 % promised at the start, with a margin of error that accounts for the plan.
And the health-centre villages? Let’s use {srvyr} to find out
59 % versus 58 %, almost identical. No marked coverage gap in villages without a health centre. The question we asked earlier now has its answer, with a margin of error.
Sampling methods
sondage::register_method())Allocation and coordination
min_n / max_n bounds, PRN coordinationMigrating from SAS / SPSS
sampling_design() |>
stratify_by(region) |>
cluster_by(school) |>
draw(n = 50, method = "pps_brewer",
mos = enrollment) |>
execute(frame, seed = 1)
* SAS proc surveyselect;
proc surveyselect data=frame
method=PPS_SAMPFORD n=50 seed=1;
strata region; cluster school;
size enrollment;
run;
* SPSS Complex Sample
CSPLAN SAMPLE
/DESIGN STRATA=region CLUSTER=school
/METHOD TYPE=PPS_SAMPFORD
/MOS VARIABLE=enrollment
/SIZE VALUE=50.
Maps directly onto SAS PROC SURVEYSELECT (strata / cluster / size) and SPSS CSPLAN (STRATA / CLUSTER / MOS).
samplyr}, the design stays readable, from protocol to code.survey} / {srvyr} for analysis.Documentation: dickoa.gitlab.io/samplyr
Installation:
Contact: mail@ahmadoudicko.com
Questions?
useR! 2026 - dickoa.gitlab.io/samplyr-user2026