From a precision target to a draw
Planning answers how many units to issue and how to distribute them.
Selection then applies that plan to a frame. svyplan owns
the first calculation and samplyr owns the second.
We use the synthetic ken_enterprises frame. Suppose the
main estimate is the proportion of exporting enterprises, with a margin
of error of three percentage points at 95 percent confidence.
data(ken_enterprises)
p_export <- mean(ken_enterprises$exporter)
target <- n_prop(p = p_export, moe = 0.03)
target
#> Sample size for proportion (wald)
#> n = 504 (p = 0.14, moe = 0.030, deff = 1)
#> expected cases = 68.9The result is a svyplan_n object. draw()
reads its planned sample size directly.
sample <- sampling_design(title = "Enterprise survey") |>
draw(n = target) |>
execute(ken_enterprises, seed = 1)
nrow(sample)
#> [1] 504Use a plausible planning value from earlier rounds or a pilot when
the true population value is unknown. For a proportion,
p = 0.5 is the conservative choice under the ordinary Wald
variance because it maximizes p * (1 - p).
Means, response, and design effects
For a mean, supply the population variance or a prior estimate of it.
revenue_target <- n_mean(
var = var(ken_enterprises$revenue_millions),
moe = 30
)
revenue_target
#> Sample size for mean
#> n = 611 (var = 143091.44, moe = 30.000, deff = 1)Expected nonresponse increases the number issued. Here the target remains the same, but only 85 percent of issued units are expected to respond.
response_target <- n_prop(
p = p_export,
moe = 0.03,
resp_rate = 0.85
)
c(
full_response = as.integer(target),
issued_at_85_percent = as.integer(response_target)
)
#> full_response issued_at_85_percent
#> 504 593deff expresses the variance relative to simple random
sampling under the planning model. It can reflect clustering, unequal
weights, or another design feature, but the value should match the
estimand being planned.
n_prop(p = p_export, moe = 0.03, deff = 1.5, resp_rate = 0.85)
#> Sample size for proportion (wald)
#> n = 890 gross (net: 756) (p = 0.14, moe = 0.030, deff = 1.50, resp_rate = 0.85)
#> expected cases = 103.3A cross-sectional design effect does not automatically describe a change or a pooled longitudinal estimate. Correlation across occasions can help one estimand and hurt another.
Read the calculation in reverse
The prec_*() functions evaluate a sample size rather
than solving for it.
prec_prop(p = p_export, n = 600)
#> Sampling precision for proportion (wald)
#> n = 600
#> se = 0.0140, moe = 0.0275, cv = 0.1026, rmoe = 0.2011
#> expected cases = 82.0
prec_mean(
var = var(ken_enterprises$revenue_millions),
n = 300,
mu = mean(ken_enterprises$revenue_millions)
)
#> Sampling precision for mean
#> n = 300
#> se = 21.8397, moe = 42.8050, cv = 0.2485, rmoe = 0.4870This is useful when budget fixes the take. confint()
gives the corresponding planning interval, while predict()
can compare supported assumptions such as response rates and design
effects.
plan <- n_prop(p = p_export, moe = 0.03, deff = 1.5)
predict(
plan,
expand.grid(
deff = c(1, 1.5, 2),
resp_rate = c(0.8, 0.9, 1)
)
)
#> deff resp_rate n se moe cv rmoe
#> 1 1.0 0.8 629.7685 0.0153064 0.03 0.1119441 0.2194065
#> 2 1.5 0.8 944.6527 0.0153064 0.03 0.1119441 0.2194065
#> 3 2.0 0.8 1259.5370 0.0153064 0.03 0.1119441 0.2194065
#> 4 1.0 0.9 559.7942 0.0153064 0.03 0.1119441 0.2194065
#> 5 1.5 0.9 839.6913 0.0153064 0.03 0.1119441 0.2194065
#> 6 2.0 0.9 1119.5884 0.0153064 0.03 0.1119441 0.2194065
#> 7 1.0 1.0 503.8148 0.0153064 0.03 0.1119441 0.2194065
#> 8 1.5 1.0 755.7222 0.0153064 0.03 0.1119441 0.2194065
#> 9 2.0 1.0 1007.6296 0.0153064 0.03 0.1119441 0.2194065Several indicators or domains
n_multi() finds the binding requirement across several
means and proportions.
indicators <- data.frame(
name = c("exporter_rate", "mean_revenue"),
p = c(p_export, NA),
var = c(NA, var(ken_enterprises$revenue_millions)),
moe = c(0.03, 30)
)
multi_target <- n_multi(indicators)
multi_target
#> Multi-indicator sample size
#> n = 611 (binding: mean_revenue)
#>
#> name .n .cv_target .cv_achieved .binding
#> exporter_rate 504 0.1119441 0.1016724
#> mean_revenue 611 NA NA *A domain-specific plan needs enough observations within every
reporting domain. When n_multi() returns a domain table,
the sampling design needs a matching stratify_by() so those
counts have an operational meaning.
domain_indicators <- data.frame(
name = "exporter_rate",
size_class = c("Small", "Medium", "Large"),
p = c(0.10, 0.22, 0.45),
cv = 0.10
)
domain_target <- n_multi(domain_indicators, domains = "size_class")
domain_sample <- sampling_design() |>
stratify_by(size_class) |>
draw(n = domain_target) |>
execute(ken_enterprises, seed = 2)
count(domain_sample, size_class, name = "n_sampled")
#> # A tibble: 3 × 2
#> size_class n_sampled
#> <fct> <int>
#> 1 Small 900
#> 2 Medium 355
#> 3 Large 123Allocate a total across strata
When strata are already defined, n_alloc() distributes a
total using the chosen planning criterion. This example uses Neyman
allocation for mean revenue, with at least 20 enterprises in every
sector.
allocation_frame <- ken_enterprises |>
summarise(
N = n(),
sd = sd(revenue_millions),
.by = sector
) |>
transmute(stratum = sector, N, sd)
allocation <- n_alloc(
allocation_frame,
n = 600,
alloc = "neyman",
min_n_stratum = 20
)
as.data.frame(allocation)[, c("stratum", "N", "n_int", "weight")]
#> stratum N n_int weight
#> 1 Chemicals & Chemical Products 282 26 10.95380
#> 2 Construction 2343 77 30.54684
#> 3 Food 856 62 13.79354
#> 4 Hotels and Restaurants 3065 60 50.63273
#> 5 Other Manufacturing 2113 118 17.91983
#> 6 Other Services 6187 158 39.08919
#> 7 Retail 2158 99 21.84907The result maps its stratum names to the values used by
stratify_by().
allocated_sample <- sampling_design(title = "Allocated enterprise survey") |>
stratify_by(sector) |>
draw(n = allocation) |>
execute(ken_enterprises, seed = 3)
count(allocated_sample, sector, name = "n_sampled")
#> # A tibble: 7 × 2
#> sector n_sampled
#> <fct> <int>
#> 1 Food 62
#> 2 Chemicals & Chemical Products 26
#> 3 Other Manufacturing 118
#> 4 Construction 77
#> 5 Retail 99
#> 6 Hotels and Restaurants 60
#> 7 Other Services 158Do not also request an allocation in stratify_by().
Either svyplan supplies the stratum takes or
samplyr computes them, not both.
Neyman allocation minimizes the planned variance for the variable
represented by sd, subject to the stated constraints. A
cost-weighted allocation adds the declared costs. Neither choice is
automatically optimal for other variables or domains.
Plan a clustered design
Clustering usually increases variance because units in the same
cluster are similar. For a one-stage approximation with equal cluster
size, the familiar design effect is determined by the homogeneity
measure icc and the take per cluster.
design_effect(icc = 0.05, n_per_psu = 10)
#> Planning design effect: 1.4500n_cluster() can trade the number of clusters against the
number of units per cluster under declared costs and homogeneity. Its
result is optimal only under that planning model.
cluster_plan <- n_cluster(
stage_cost = c(500, 50),
icc = 0.05,
budget = 30000
)
cluster_plan
#> Optimal 2-stage allocation
#> field design: n_psu = 25 | n_per_psu = 14 -> total n = 350
#> cv = 0.0687, cost = 30000
#> continuous optimum: n_psu = 25.22699 | n_per_psu = 13.78405 (cv = 0.0687, cost = 30000)
#> design df = 24
as.integer(cluster_plan)
#> [1] 25 14In a two-stage samplyr design, the cluster plan supplies
the stage-1 cluster count and the within-cluster take. See
?n_cluster for the feasibility checks and
vignette("introduction") for multistage execution.
Plan a repeated survey
A repeated survey often needs precision for a change. That calculation depends on how much of the responding sample appears at both occasions and how strongly the outcome is associated within those shared units.
design_overlap() computes issued-sample overlap from a
rotation schedule. A cohort interviewed on four consecutive occasions
produces 75 percent overlap between adjacent steady-state samples.
membership <- design_overlap("4")
membership[1]
#> [1] 0.75At complete response, this issued overlap can be passed directly to
n_change(). Below complete response, convert it to the
expected overlap among respondents under a stated response-persistence
assumption. For illustration, assume response is independent between
occasions at rate 0.80. A shared issued unit then responds again with
probability 0.80, so respondent overlap is issued overlap multiplied by
that rate.
change_response_rate <- 0.80
respondent_overlap <- membership[1] * change_response_rate
change_target <- n_change(
p = c(p_export, p_export + 0.05),
moe = 0.02,
overlap = respondent_overlap,
overlap_cor = 0.6
)
change_target
#> Sample size for change (proportion scale)
#> n = 1667 per occasion (p = 0.137 to 0.187, moe = 0.02, deff = 1)
#> overlap = 0.6, overlap_cor = 0.6 (64.3% of the independent variance)overlap_cor is an outcome assumption, not another name
for membership overlap. Use prior rounds or a sensitivity range rather
than deriving it from the rotation pattern.
Recruit a rotating panel
n_panel() translates a cross-sectional responding target
into recruitment under initial response and conditional retention.
panel_plan <- n_panel(
n_prop(p = p_export, moe = 0.03),
retention = c(0.90, 0.95, 0.95),
resp_rate = 0.80,
design = "rotating",
start = "immediate"
)
panel_plan
#> Panel recruitment (rotating, 4-wave life)
#> entrants: 177 per occasion -> 505 responding, pooled over 4 cohorts
#> proportion (wald): se = 0.01531, moe = 0.03, cv = 0.112
#>
#> wave retention n_resp se moe
#> 1 142 0.02891 0.05666
#> 2 0.9 127 0.03047 0.05973
#> 3 0.95 121 0.03126 0.06128
#> 4 0.95 115 0.03208 0.06287
#>
#> # summary() for the launch, the loss and per-wave cvA rotating svyplan_panel contains several counts because
start-up and later intakes are different operations.
intake_n <- ceiling(panel_plan$n_entrants)
fieldwork_counts <- c(
startup = panel_plan$n_cohorts * intake_n,
intake_each_occasion = intake_n,
expected_respondents = panel_plan$n_resp
)
fieldwork_counts
#> startup intake_each_occasion expected_respondents
#> 708.0000 177.0000 503.8148Use startup for the immediate start-up master and
partition it into four cohort ages. Use
intake_each_occasion for every later refreshment draw.
draw(n = panel_plan) is intentionally unsupported because
it cannot know which operation the caller means.
n_panel() treats retention as an unbroken chain. It does
not yet turn a gapped life such as 4-8-4 into a recruitment and
reserve-cohort plan. It also does not estimate attrition, adjust
nonresponse weights, or combine cohort weights. Those are separate
design and analysis decisions.
The complete execution handoff is in
vignette("rotating-panels").
What planning does not decide
Planning results depend on their inputs. Before treating a number as a field requirement, record:
- the estimand and reporting domains
- the source of variances, proportions, and homogeneity assumptions
- whether the count means issued units or expected respondents
- the response and retention model
- the design effect and the estimand it describes
- the rounding rule and any finite-frame limit
svyplan does not choose a sampling algorithm or validate
a register. samplyr does not decide the precision target.
Keeping that boundary explicit makes it possible to revisit assumptions
without silently changing the selection design.
Next steps
- Use
vignette("introduction")to execute an ordinary or multistage design. - Use
vignette("rotating-panels")for the longitudinal fieldwork handoff. - Use
vignette("survey-analysis")after data collection. - Consult the svyplan reference pages for power, two-phase designs, stratification boundaries, pooled estimates, and detailed sensitivity analysis.