Skip to contents

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.9

The 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] 504

Use 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                  593

deff 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.3

A 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.4870

This 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.2194065

Several 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            123

Allocate 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.84907

The 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                      158

Do 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.4500

n_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 14

In 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.75

At 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 cv

A 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.8148

Use 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

References