What the file contains
A sampling_design describes how to sample without
embedding the confidential frame. Saving it creates a versioned JSON
recipe that can be inspected, reviewed, and executed later.
Three functions cover the workflow.
| Function | Role |
|---|---|
write_design() |
Write a design or execution receipt to JSON |
read_design() |
Restore a design from a file or JSON string |
design_json() |
Return the same JSON in memory |
The format is native to samplyr and remains
experimental. It is not a finalized cross-tool interchange standard.
Save and restore a design
data(bfa_eas)
design <- sampling_design(title = "Burkina Faso EA survey") |>
stratify_by(region, alloc = "proportional") |>
draw(
n = 300,
method = "systematic",
control = c(serp(province, commune))
)
path <- tempfile(fileext = ".json")
write_design(design, path)
restored <- read_design(path)
restored
#> ── Sampling Design: Burkina Faso EA survey ─────────────────────────────────────
#>
#> ℹ 1 stage
#>
#> ── Stage 1 ─────────────────────────────────────────────────────────────────────
#> • Strata: region (proportional)
#> • Draw: n = 300 (total), method = systematic, control = serp(province, commune)Under the same compatible implementation, frame, and seed, the restored design executes like the original.
original_sample <- execute(design, bfa_eas, seed = 123)
restored_sample <- execute(restored, bfa_eas, seed = 123)
identical(original_sample$ea_id, restored_sample$ea_id)
#> [1] TRUE
identical(original_sample$.weight, restored_sample$.weight)
#> [1] TRUEExecution equality is the contract. Incidental R storage details can differ. For example, JSON does not distinguish integers from whole-valued doubles and restored data-frame specifications are tibbles.
design_json() is useful when the document belongs in a
database or API.
json <- design_json(design)
names(jsonlite::fromJSON(json))
#> [1] "format" "format_version" "schema" "design"
#> [5] "frame" "tools"Requirements and frame fingerprints
Every file records the variables needed by each stage and their roles. These requirements allow a candidate frame to be checked before selection.
frame_info <- attr(restored, "frame_info")
bind_rows(frame_info$required_variables)
#> # A tibble: 3 × 3
#> name role stage
#> <chr> <chr> <int>
#> 1 region strata 1
#> 2 province control 1
#> 3 commune control 1Passing a frame to write_design() adds a fingerprint
without writing the frame data.
write_design(design, path, frame = bfa_eas)
with_frame <- read_design(path)
fingerprint <- attr(with_frame, "frame_info")[["fingerprint"]]
fingerprint$name
#> [1] "bfa_eas"
fingerprint$nrow
#> [1] 44570
fingerprint$hash
#> [1] "dd7602cb0907d556f5d27e31d15c8137"The hash covers column names, values, and row order. It ignores whether the object is a tibble and ignores column order. It is an integrity check, not a proof that the register has good coverage, stable identifiers, or the correct target-population definition.
validate_frame() reports structural or
probability-relevant drift while still checking whether the revised
frame can execute the design.
validate_frame(with_frame, bfa_eas[-(1:10), ])
#> Frame differs from what was recorded when the design was saved:
#> • 44560 rows instead of the 44570 recorded
#> ℹ This is informational. The design remains executable on any frame that passes
#> the variable checks.A compatible but changed frame can run a new selection. It cannot
reproduce the old one. replay_design() therefore refuses a
fingerprint mismatch by default.
For a design with one register per stage, the file records one fingerprint per register in supplied order. Replay requires the same number and ordering of registers.
Save an execution receipt
Writing a tbl_sample adds the arguments that determined
its execution. These include the seed, executed stages, panel or
replicate request, selected count, timestamp, package versions, and RNG
configuration.
sample <- execute(design, bfa_eas, seed = 2026, panels = 4)
write_design(sample, path, frame = bfa_eas)
saved <- read_design(path)
receipt <- attr(saved, "execution")
receipt$seed
#> [1] 2026
receipt$panels
#> [1] 4
receipt$n_selected
#> [1] 300Replay is exact when the recorded frame content, method implementation, package behavior, and registered custom methods remain compatible.
replayed <- replay_design(saved, bfa_eas)
identical(replayed$ea_id, sample$ea_id)
#> [1] TRUE
identical(replayed$.weight, sample$.weight)
#> [1] TRUE
identical(replayed$.panel, sample$.panel)
#> [1] TRUEThe execution timestamp is new. The selected rows, their order, design columns, panels, and replicate assignments are reproduced.
The selected records themselves are not stored in the JSON recipe. Save them with an ordinary data format and keep the design document beside them as an audit artifact.
Replay boundaries
The receipt describes one execute() call. A stage
continuation or multi-phase pipeline uses several calls, so its final
receipt is marked chained and cannot replay the whole
pipeline. Save and replay each stage batch or phase separately.
A modified sample is also outside the contract. If rows or design columns changed after execution, the receipt still describes the original selection.
A materialized panel wave is derived from its master and is not
independently replayable. Save the master. A
rotation_program() is a registry rather than a sampling
design, so archive each cohort execution and the schedule used to
rebuild the registry.
A sample carrying shared weights from share_weights()
has a format of its own, samplyr/shared-sample. It records
the source selection and the transformation’s arguments, and nothing
else. The links and the target register are supplied again at replay,
the way a frame is, so the file contains no target unit and no link:
households <- data.frame(hh = paste0("h", 1:20))
people <- data.frame(
person = paste0("p", 1:40),
hh = rep(paste0("h", 1:20), each = 2)
)
shared <- share_weights(
sampling_design() |> draw(n = 8) |> execute(households, seed = 3),
targets = people,
links = people,
by = c(hh = "hh"),
to = c(person = "person"),
within = hh,
multiplicity = complete_links()
)
shared_path <- tempfile(fileext = ".json")
write_design(shared, shared_path, frame = households)
read_design(shared_path)
#> ── Shared Weight Design ────────────────────────────────────────────────────────
#>
#> ℹ Source keyed by hh, targets keyed by person
#> • Links grouped within hh
#> • Denominator counted from the supplied links
#> ℹ Replay with the source register, the links and the targets to rebuild the sample.Replaying it takes the register, the links and the targets. The source selection is re-executed, then the transformation is re-applied:
rebuilt <- replay_design(
read_design(shared_path),
frame = households,
links = people,
targets = people
)
identical(rebuilt$.weight, shared$.weight)
#> [1] TRUEBecause the links and the targets are not in the file, they cannot be checked before use. The file instead records what the source selection and the result hashed to, and replay is checked against both, so a mismatch says which input was wrong. A source that does not reproduce means the register is wrong; a result that does not means the links or the targets are.
saveRDS() preserves the object whole, though it records
an R object rather than a design another tool can read, and it does
embed the link structure that the JSON format deliberately leaves
out.
A stack_frames() collection has a format of its own,
samplyr/frame-stack, because it is one selection per
component rather than one in total. Give frame as a list
named by component, and each component is fingerprinted against its own
register:
population <- data.frame(
person_id = 1:400,
has_landline = rep(c(TRUE, FALSE), times = c(250, 150)),
has_cell = rep(c(FALSE, TRUE), times = c(100, 300))
)
landline_frame <- population[population$has_landline, ]
cell_frame <- population[population$has_cell, ]
frames <- stack_frames(
landline = sampling_design() |>
draw(n = 50) |>
execute(landline_frame, seed = 1),
cell = sampling_design() |>
draw(n = 60) |>
execute(cell_frame, seed = 2),
membership = c(landline = "has_landline", cell = "has_cell"),
key = person_id
)
path <- tempfile(fileext = ".json")
write_design(frames, path, frame = list(landline = landline_frame,
cell = cell_frame))
restored <- read_design(path)
restored
#> ── Frame Stack Design ──────────────────────────────────────────────────────────
#>
#> ℹ 2 frames over key person_id
#> • landline: has_landline, seed 1
#> • cell: has_cell, seed 2
#> ℹ Replay each component against its register to rebuild the collection.read_design() returns the components’ designs and
receipts rather than the collection, which needs the registers.
replay_design() supplies them and stacks the results:
replayed <- replay_design(
restored,
frame = list(landline = landline_frame, cell = cell_frame)
)
identical(as.data.frame(replayed), as.data.frame(frames))
#> [1] TRUETwo things a collection cannot carry are refused rather than dropped,
because either loss would return a collection that looks complete and
estimates differently: overlaps from exante_overlaps(),
which resolve to one chance per selected unit when the collection is
formed, and a component carrying shared weights, which a collection has
nowhere to put a link table for. Overlaps declared with
overlap_probabilities() or overlap_weights()
name columns of the components and travel with the file.
For PRN-coordinated samples, archive each executed sample and the versioned register containing stable identifiers and PRNs. Those artifacts audit sample selection. They do not preserve fieldwork outcomes, response histories, or later register maintenance.
Format and safety
The JSON stores declarative method descriptors alongside
samplyr metadata. Built-in methods use a versioned
vocabulary. A custom method is recorded as tool-specific and must be
registered with the same implementation before replay.
Control sorting is serialized as a restricted data structure for
ascending, descending, and serpentine terms. The reader rebuilds those
terms without parsing arbitrary R code. write_design()
refuses expressions outside that grammar.
unsupported_control <- sampling_design() |>
draw(n = 50, control = c(population / households))
design_json(unsupported_control)
#> Error in `design_json()`:
#> ! Cannot serialize the control expression `population/households`.
#> ℹ Only bare column names, `desc()`, and `serp()` calls on bare column names can
#> be written to a design file.
#> ℹ Namespace prefixes are not supported: write `desc(pop)`, not
#> `dplyr::desc(pop)`.Files carry a format_version. A reader refuses newer
versions it does not understand and validates the fields required by
older versions. Unknown tool namespaces are retained when a file is read
and written again, but preservation does not imply cross-tool
statistical equivalence.
Archive checklist
Keep these artifacts together:
- the executed sample data
- the design JSON and execution receipt
- the exact frame vintage or an access-controlled reference to it
- stable identifiers and PRNs when coordination is used
- custom method definitions and package versions
- schedules linking separately executed longitudinal cohorts
Use vignette("design-semantics") for the detailed design
contract and vignette("rotating-panels") for
cohort-specific persistence rules.