Skip to contents

svyplan provides a toolkit for survey sample size determination. It covers sample sizes for proportions and means, precision analysis, multistage cluster allocation, multi-indicator optimization, strata boundary optimization, and power analysis. The core sizing and precision functions use paired S3 interfaces for round-trip conversions. Sensitivity analysis via predict() is available for supported result classes.

Function ecosystem

The package exports 32 functions organized into six families. The diagram below shows how they relate to each other.

Family Functions Purpose
Plan svyplan Reusable profile for design defaults
Sample size n_prop, n_mean, n_ratio, n_cluster, n_multi, n_alloc, n_twophase, n_change, n_panel, n_pooled “How many units do I need?”
Precision prec_prop, prec_mean, prec_ratio, prec_cluster, prec_multi, prec_alloc, prec_twophase, prec_change, prec_panel, prec_pooled “What precision does n achieve?”
Power power_prop, power_mean, power_did “Can I detect a difference?”
Design varcomp, design_effect, effective_n, design_df, design_rotation, design_overlap, design_schedule Variance components, design effects, degrees of freedom, and rotation patterns
Strata strata_bound Optimal stratification boundaries

Which function

The same functions read by problem rather than by family. Eleven problems, each with the function or pair that answers it. Every n_ has a prec_ reading the same design back the other way, so a row names one thing to learn rather than two.

I want to size, or evaluate Functions
a proportion, a mean or a ratio of two totals n_prop()/prec_prop(), n_mean()/prec_mean(), n_ratio()/prec_ratio()
several indicators at once, taking the most demanding n_multi()/prec_multi()
a two- or three-stage cluster design, for one indicator or a table of them n_cluster()/prec_cluster()
an allocation across strata or domains, under a budget or a CV target n_alloc()/prec_alloc(), strata_bound()
a two-phase design that screens or follows up n_twophase()/prec_twophase()
a change between two occasions of a repeated survey n_change()/prec_change()
the average of several occasions of a repeated survey n_pooled()/prec_pooled()
a panel that must still deliver a sample after attrition n_panel()/prec_panel()
the overlap and field schedule a rotation pattern produces design_rotation(), design_overlap(), design_schedule()
the power of a two-group comparison or a difference-in-differences power_prop(), power_mean(), power_did()
the design effect, effective size or degrees of freedom of a plan design_effect(), effective_n(), design_df(), varcomp()

Most planning functions return structured S3 objects. Available methods depend on the result class. See the relevant help page for details.

The n_*/prec_* duality

The most distinctive feature of svyplan is the bidirectional link between sample size and precision functions. Every n_* function has a prec_* counterpart, and each pair supports round-trip conversion. Under the same formula and continuous design assumptions, converting a result to its counterpart and back reproduces the original target. Changing methods or using an operational integer allocation can change the result.

Because n_* and prec_* are S3 generics, you can pass a result object directly to its inverse. All design parameters travel with the object:

# Compute sample size for a proportion
s <- n_prop(p = 0.3, moe = 0.05, deff = 1.5)
s
#> Sample size for proportion (wald)
#> n = 485 (p = 0.30, moe = 0.050, deff = 1.50)
#> expected cases = 145.2

# What precision does this n achieve?
p <- prec_prop(s)
p
#> Sampling precision for proportion (wald)
#> n = 485
#> se = 0.0255, moe = 0.0500, cv = 0.0850, rmoe = 0.1667
#> expected cases = 145.2

# Recover the original sample size
n_prop(p)
#> Sample size for proportion (wald)
#> n = 485 (p = 0.30, moe = 0.050, deff = 1.50)
#> expected cases = 145.2

You can also override a parameter on the way back:

n_prop(p, cv = 0.08)
#> Sample size for proportion (wald)
#> n = 547 (p = 0.30, cv = 0.080, deff = 1.50)
#> expected cases = 164.1

Sample sizes for proportions and means

Proportions

Estimate vaccination coverage (expected ~70%) with a 5 percentage point margin of error:

n_prop(p = 0.70, moe = 0.05)
#> Sample size for proportion (wald)
#> n = 323 (p = 0.70, moe = 0.050, deff = 1)
#> expected cases = 225.9

Four method options control how the margin of error is computed:

  • "wald" (default): the textbook normal approximation
  • "wilson": Wilson score interval, better coverage near 0 or 1
  • "logodds": log-odds transformation. Its back-transformed limits bound the proportion, or equivalently its odds. They do not bound an odds ratio or size a logistic regression.
  • "beta": Korn-Graubard interval, for a proportion near a boundary where the expected count in the nearer category is small. It stays inside [0, 1] by construction, and a df argument widens it for a variance estimated from few clusters.
n_prop(p = 0.05, moe = 0.02, method = "wald")
#> Sample size for proportion (wald)
#> n = 457 (p = 0.05, moe = 0.020, deff = 1)
#> expected cases = 22.8
n_prop(p = 0.05, moe = 0.02, method = "wilson")
#> Sample size for proportion (wilson)
#> n = 469 (p = 0.05, moe = 0.020, deff = 1)
#> expected cases = 23.4
n_prop(p = 0.05, moe = 0.02, method = "logodds")
#> Sample size for proportion (logodds)
#> n = 475 (p = 0.05, moe = 0.020, deff = 1)
#> expected cases = 23.7
n_prop(p = 0.05, moe = 0.02, method = "beta", df = 25)
#> Sample size for proportion (beta)
#> n = 558 (p = 0.05, moe = 0.020, deff = 1)
#> expected cases = 27.9

The four differ materially only when the expected count is small. For sizing that difference is rarely worth acting on, but for assessing an achieved design it often is. ?n_prop has the full comparison.

Target a coefficient of variation instead of a margin of error:

n_prop(p = 0.70, cv = 0.05)
#> Sample size for proportion (wald)
#> n = 172 (p = 0.70, cv = 0.050, deff = 1)
#> expected cases = 120.0

Or state the margin of error as a fraction of the proportion rather than in percentage points. rmoe = 0.07 at p = 0.70 asks for the same interval as moe = 0.049, and says it in the units UNICEF’s Multiple Indicator Cluster Surveys (MICS) state a requirement in:

n_prop(p = 0.70, rmoe = 0.07)
#> Sample size for proportion (wald)
#> n = 336 (p = 0.70, rmoe = 0.070, deff = 1)
#> expected cases = 235.2

The three are targets on one design, not three designs. moe and rmoe fix the interval the chosen method builds, cv fixes the sampling variance underneath it, and every result reports all of them back whichever one you supplied.

Means

Estimate average household expenditure (variance = 40,000, mean = 800) with a margin of error of 30:

n_mean(var = 40000, moe = 30)
#> Sample size for mean
#> n = 171 (var = 40000.00, moe = 30.000, deff = 1)

Common parameters

Most single-stage n_*() and prec_*() functions share these parameters:

Parameter Meaning
deff Design effect multiplier (default 1)
N Finite population size (enables FPC correction)
alpha Significance level (default 0.05)
resp_rate Expected response rate (inflates n by 1/resp_rate)

Dividing by resp_rate sets an expected respondent count and evaluates variance at that net size. It assumes response is ignorable under the planned adjustment. Inflation does not remove nonresponse bias or include variation from response weights. Use sensitivity analysis over plausible rates, and see the allocation article for explicit nonresponse follow-up.

n_prop(p = 0.70, moe = 0.05, deff = 1.5, N = 50000, resp_rate = 0.85)
#> Sample size for proportion (wald)
#> n = 564 gross (net: 480) (p = 0.70, moe = 0.050, deff = 1.50, resp_rate = 0.85)
#> expected cases = 335.6

The design effect can be less than 1 for efficient designs (e.g. well-stratified or PPS samples). svyplan accepts any deff > 0.

n_cluster() / prec_cluster() use cluster-specific inputs instead (stage_cost, icc, unit_relvar, and optionally resp_rate_psu), and n_alloc() / prec_alloc() have their own allocation-oriented interfaces.

Survey plan profiles

When the same deff, N, resp_rate, or alpha apply across many single-stage calls, bundle them into a svyplan() profile to avoid repetition:

plan <- svyplan(deff = 1.5, resp_rate = 0.85, N = 50000)

# Pass as a named argument
n_prop(p = 0.3, moe = 0.05, plan = plan)
#> Sample size for proportion (wald)
#> n = 564 gross (net: 480) (p = 0.30, moe = 0.050, deff = 1.50, resp_rate = 0.85)
#> expected cases = 143.8
n_mean(var = 100, moe = 2, plan = plan)
#> Sample size for mean
#> n = 169 gross (net: 144) (var = 100.00, moe = 2.000, deff = 1.50, resp_rate = 0.85)
power_prop(p1 = 0.30, p2 = 0.35, plan = plan)
#> Power analysis for proportions (solved for sample size)
#> n = 2329 (net: 1979, per group), power = 0.800, effect = 0.0500
#> (p1 = 0.300, p2 = 0.350, alpha = 0.05, deff = 1.50, resp_rate = 0.85)

The profile can also hold cluster context (stage_cost, icc) for n_cluster():

cl_plan <- svyplan(stage_cost = c(500, 50), icc = 0.05, resp_rate = 0.85)
n_cluster(cv = 0.05, plan = cl_plan)
#> Optimal 2-stage allocation
#> field design: n_psu = 52 | n_per_psu = 14 -> total n = 728 (net: 619)
#> (resp_rate = 0.85)
#> cv = 0.0500, cost = 62400
#> continuous optimum: n_psu = 49.9018 | n_per_psu = 14.9509 (cv = 0.0500, cost = 62255)
#> design df = 51

Piping works with both positional and named arguments:

plan |> n_prop(0.3, moe = 0.05)
#> Sample size for proportion (wald)
#> n = 564 gross (net: 480) (p = 0.30, moe = 0.050, deff = 1.50, resp_rate = 0.85)
#> expected cases = 143.8
plan |> n_prop(p = 0.3, moe = 0.05)
#> Sample size for proportion (wald)
#> n = 564 gross (net: 480) (p = 0.30, moe = 0.050, deff = 1.50, resp_rate = 0.85)
#> expected cases = 143.8
plan |> power_mean(200, effect = 5)
#> Power analysis for means (solved for sample size)
#> n = 221 (net: 188, per group), power = 0.800, effect = 5.0000
#> (alpha = 0.05, deff = 1.50, resp_rate = 0.85)

Explicit arguments always override plan defaults:

# plan has deff = 1.5, but deff = 2.0 wins here
n_prop(p = 0.3, moe = 0.05, plan = plan, deff = 2.0)
#> Sample size for proportion (wald)
#> n = 750 gross (net: 638) (p = 0.30, moe = 0.050, deff = 2.00, resp_rate = 0.85)
#> expected cases = 191.1

Ratios of two totals

Some headline indicators are neither a mean nor a proportion but a ratio of two totals estimated from the same sample: consumption per person, yield per hectare, spending per pupil. n_ratio() sizes for the ratio itself.

Give it the anticipated ratio, the coefficient of variation of each component, and the correlation between them across units:

# Per-capita household consumption: total consumption over total members
n_ratio(r = 420, cv_num = 1.20, cv_den = 0.45, component_cor = 0.65, cv = 0.05)
#> Sample size for ratio (linearization)
#> n = 377 (r = 420, unit_relvar = 0.941, cv = 0.050, deff = 1)

The printed unit_relvar is the coefficient the size is driven by. It is the relative variance of the linearized variable e = y - Rx, and the correlation is what makes it small: a numerator that moves with its denominator produces a stable ratio even when both components are individually variable. Moving component_cor from 0.30 to 0.90, with everything else held, nearly halves the required sample:

predict(
  n_ratio(r = 420, cv_num = 1.20, cv_den = 0.45, component_cor = 0.65, cv = 0.05),
  data.frame(component_cor = c(0.30, 0.65, 0.90))
)
#>   component_cor     n se      moe   cv      rmoe
#> 1          0.30 527.4 21 41.15924 0.05 0.0979982
#> 2          0.65 376.2 21 41.15924 0.05 0.0979982
#> 3          0.90 268.2 21 41.15924 0.05 0.0979982

Targets work on the ratio’s own scale. moe = 20 asks for an interval of plus or minus 20 currency units per person, and rmoe states the same thing relative to r:

n_ratio(r = 420, cv_num = 1.20, cv_den = 0.45, component_cor = 0.65,
        moe = 20, deff = 1.3, resp_rate = 0.85)
#> Sample size for ratio (linearization)
#> n = 2437 gross (net: 2072) (r = 420, unit_relvar = 0.941, moe = 20.000, deff = 1.30, resp_rate = 0.85)

prec_ratio() is the inverse, and the pair round-trips like every other:

size <- n_ratio(r = 420, cv_num = 1.20, cv_den = 0.45,
                component_cor = 0.65, cv = 0.05)
prec_ratio(size)
#> Sampling precision for ratio (linearization)
#> n = 377
#> se = 21.0000, moe = 41.1592, cv = 0.0500, rmoe = 0.0980

What this is, and is not: the estimand is R = T_y / T_x, with numerator and denominator observed on the same units. It is not the mean of the unit ratios y_i / x_i, which is an ordinary mean and belongs in n_mean(). A rate whose denominator is a subpopulation count, such as coverage among an eligible group, is usually easier to plan as a domain proportion with n_prop(). n_ratio() earns its place when the denominator is a continuous per-unit quantity, where there is no domain share to inflate by.

One warning is worth knowing about. When the two coefficients of variation are close and the correlation is near its bound, the ratio is nearly constant and the size collapses toward zero. The result then rests entirely on a correlation almost nobody knows to three decimals, so the function says so:

tryCatch(
  n_ratio(r = 420, cv_num = 1.20, cv_den = 1.19, component_cor = 0.9999,
          cv = 0.05),
  warning = function(w) conditionMessage(w)
)
#> [1] "the ratio's unit relative variance (0.000386) is far below its components': the plan rests on 'component_cor' = 0.9999 being known to that precision. Vary it with predict() before committing to this size"

Clustered ratios

For a multistage design, the quantity that matters is the homogeneity of the linearized variable, not of either component. They can differ by an order of magnitude in the same population, so substituting the numerator’s icc is not a conservative approximation but a different design.

Compute e = y - Rx from pilot data and pass its components to n_cluster():

set.seed(1)
psu <- rep(1:30, each = 20)
x <- rlnorm(600, 1, 0.4)
y <- 3 * x + rnorm(600, 0, 0.8)

R <- mean(y) / mean(x)
e <- y - R * x

vc <- suppressWarnings(varcomp(e, stage_id = list(psu)))
n_cluster(stage_cost = c(500, 50), icc = vc$icc,
          unit_relvar = var(e) / mean(y)^2,
          var_ratio = vc$var_ratio, cv = 0.05)
#> Warning: the design selects a single PSU, so no between-PSU variance can be
#> estimated. Raise the budget, the PSU count, or the number of stages
#> Optimal 2-stage allocation
#> field design: n_psu = 1 | n_per_psu = 5 -> total n = 5
#> cv = 0.0471, cost = 750
#> continuous optimum: n_psu = 0.3799752 | n_per_psu = 15.88247 (cv = 0.0500, cost = 492)
#> design df = 0

e has mean zero by construction, so varcomp() warns that its relative variances are infinite. Its icc and var_ratio do not depend on the mean and stay valid, which is why they are taken from the object while unit_relvar is supplied separately. Passing the whole vc object would carry the infinite value in with it.

Evaluating precision

The prec_*() functions answer the reverse question: given a fixed sample size, what precision can you achieve?

prec_prop(p = 0.3, n = 400)
#> Sampling precision for proportion (wald)
#> n = 400
#> se = 0.0229, moe = 0.0449, cv = 0.0764, rmoe = 0.1497
#> expected cases = 120.0

prec_mean(var = 40000, n = 300, mu = 800)
#> Sampling precision for mean
#> n = 300
#> se = 11.5470, moe = 22.6317, cv = 0.0144, rmoe = 0.0283

Design parameters work identically:

prec_prop(p = 0.3, n = 400, deff = 1.5, resp_rate = 0.85)
#> Sampling precision for proportion (wald)
#> n = 400 (net: 340)
#> se = 0.0304, moe = 0.0597, cv = 0.1015, rmoe = 0.1989
#> expected cases = 102.0

Solving for the level

n_prop() and prec_prop() are two readings of one equation relating the proportion, the sample size, and the precision. Fixing any two returns the third. Supplying cv in place of p takes the remaining direction and returns the smallest proportion the design measures that precisely:

prec_prop(n = 1500, cv = 0.10, N = 2e6)
#> Sampling precision for proportion (wald, solved for p)
#> n = 1500
#> p = 0.06246
#> se = 0.0062, moe = 0.0122, cv = 0.1000, rmoe = 0.1960
#> expected cases = 93.7

Because se(p) / p falls as p rises, the answer is a floor: every larger proportion meets the target, and everything below it is too rare for this design to report at that precision. It is the planning form of a publication threshold, and expected_cases is printed beside it so the precision floor and the min_cases floor n_prop() applies can be read together.

predict() traces the floor across designs, varying cv rather than the proportion it now solves for:

predict(prec_prop(n = 1500, cv = 0.10, N = 2e6),
        expand.grid(n = c(500, 1500, 5000)))
#>      n          p          se         moe  cv      rmoe
#> 1  500 0.16663201 0.016663201 0.032659274 0.1 0.1959964
#> 2 1500 0.06245608 0.006245608 0.012241167 0.1 0.1959964
#> 3 5000 0.01955979 0.001955979 0.003833649 0.1 0.1959964

prec_mean() takes cv the same way. The standard error of a mean does not involve the mean, so that inversion is a division rather than a search, and it returns the magnitude:

prec_mean(var = 25, n = 1500, cv = 0.10)$params$mu
#> [1] 1.290994

Supplying rmoe asks the same question of the interval instead of the variance: the smallest proportion the design can report at that relative half-width, under the interval method it will actually use. Unlike the cv solve, this one moves with method:

prec_prop(n = 1500, rmoe = 0.20, method = "wald", N = 2e6)$params$p
#> [1] 0.06012947
prec_prop(n = 1500, rmoe = 0.20, method = "beta", N = 2e6)$params$p
#> [1] 0.06327848

Two properties bound that search, and both are reported rather than approximated past. The Wilson and Korn-Graubard half-widths do not shrink to zero as p approaches 1, so their relative margin of error has a positive floor and a target below it is refused with the floor named. The back-transformed log-odds half-width turns upward once the logit spread outgrows logit(p), near p = 0.999 at n = 1500 but as low as p = 0.97 at n = 30, so under "logodds" the search brackets at that turn and returns its lower root.

Confidence intervals

confint() extracts the expected confidence interval from sizing and precision results for proportions, means, ratios, changes, and pooled estimates. Cluster, allocation, power, and multi-indicator results have no single interval to report and are not supported. A mean needs mu, since an interval has to be centered somewhere:

s <- n_prop(p = 0.3, moe = 0.05)
confint(s)
#>  2.5 % 97.5 %
#>   0.25   0.35

pr <- prec_prop(p = 0.3, n = 400)
confint(pr)
#>      2.5 %    97.5 %
#>  0.2550916 0.3449084

confint(pr, level = 0.99)
#>      0.5 %    99.5 %
#>  0.2409803 0.3590197

# a mean carries no center of its own, so supply mu
confint(n_mean(var = 100, moe = 2, mu = 50))
#>  2.5 % 97.5 %
#>     48     52

# a ratio always carries its center r
confint(n_ratio(r = 420, cv_num = 1.20, cv_den = 0.45,
                component_cor = 0.65, cv = 0.05))
#>     2.5 %   97.5 %
#>  378.8408 461.1592

# a repeated-survey result, the pooled level across two occasions
confint(prec_pooled(var = 100, n = 300, occasions = 2, mu = 50))
#>     2.5 %   97.5 %
#>  49.19985 50.80015

For proportions, Wald endpoints are truncated to [0, 1] when needed. The other methods stay within the parameter space by construction.

Explore planning assumptions

A fitted result keeps its planning assumptions. predict() evaluates compatible parameter grids while holding the remaining assumptions, including the interval method, fixed:

baseline <- n_prop(p = 0.3, moe = 0.05, deff = 1.5)
predict(baseline, data.frame(resp_rate = c(0.7, 0.8, 0.9, 1.0)))
#>   resp_rate        n         se  moe         cv      rmoe
#> 1       0.7 691.4626 0.02551067 0.05 0.08503558 0.1666667
#> 2       0.8 605.0298 0.02551067 0.05 0.08503558 0.1666667
#> 3       0.9 537.8042 0.02551067 0.05 0.08503558 0.1666667
#> 4       1.0 484.0238 0.02551067 0.05 0.08503558 0.1666667

Continue with the focused articles

The following detailed articles are available on the package website:

Scope

svyplan answers one question in several forms: how many units does a planned design need, and what precision does a given design achieve. Some neighbouring topics are deliberately outside that boundary. They are listed here so their absence reads as a decision rather than an oversight.

Totals. There is no n_total(). A total is a mean rescaled by N, so the CV target is identical and a margin of error on the total is moe / N on the mean. ?n_mean has a worked section on sizing for a total.

Ratio estimation as a variance-reduction technique. Using an auxiliary variable to sharpen an estimate of a mean or total is analysis-stage work, and is not the same thing as planning for a ratio estimand, which n_ratio() covers. Its planning effect is a smaller residual variance, so supply it directly: fit the ratio model, take the residual variance, and pass it as n_mean(var = ) with the mean of the analysis variable. A target stated as a CV then gives the same size as sizing for the ratio itself, because the two have the same relative variance.

Regression coefficients. Sizing for a regression slope is a general statistical problem rather than a survey one, covered by packages such as pwr. The survey-specific part, the variance inflation from a complex design, is already the deff argument. There is no n_reg().

Two-phase designs beyond stratification and follow-up. n_twophase() allocates a two-phase sample, phase 1 measuring a variable the frame does not carry and phase 2 measuring the variable of interest on a subsample of the phase-1 units. Double sampling for stratification and nonresponse follow-up are the same problem in that function, differing only in which strata are marked take_all. What is not covered is the third member of the family, double sampling for a regression or ratio estimator, where the phase-1 variable is continuous rather than a stratifier. That is a two-phase allocation question and is separate from n_ratio(), which sizes a single-phase design for a ratio estimand. Its variance effect is expressible today as a design effect below 1, since a regression estimator multiplies the variance by 1 - \rho^2, but the cost trade-off between the phases is not built in.

More than three stages. Two- and three-stage designs are supported. Surveys rarely go deeper, and the cost and variance models for a fourth stage would carry more assumptions than they would earn.

Attrition, and choosing a rotation. n_panel() is arithmetic on declared rates. It says what recruitment yields under a response rate and a retention curve, not where those rates come from. Attrition modelling, attrition weighting, and any adjustment for informative dropout are analysis-stage work and outside the boundary. Two consequences are worth stating. A unit lost at one wave is treated as lost for good, so a panel whose nonrespondents return later does better than planned here, and planning this way over-issues. Also, design_rotation() records a rotation that has already been chosen. It does not solve the inverse problem of finding one that delivers a target overlap while limiting how many times a unit may be interviewed.

Targeting nonresponse follow-up. Deciding how much follow-up to do is answered by n_twophase(). Deciding which cases to pursue is not, and is out of scope: prioritizing by response propensity, or adapting effort as fieldwork proceeds, needs paradata from the field that no planning stage has.

PPS selection inside joint allocation. varcomp() estimates variance components under PPS first-stage selection, but the joint allocator (n_alloc() with measures and targets) uses an anticipated variance model. Feeding it components estimated under PPS is therefore an approximation, and the more unequal the sizes, the rougher it is. A two-stage joint allocation can identify certainty PSUs from a register. It does not support take_all in a fixed-take multistage frame because a census of PSUs still leaves within-PSU sampling.

Domains not identifiable on the frame. Targets can be set per domain when domain membership is known before fielding, as it is for region or urban/rural. For domains realized only after contact, such as age or sex subgroups, the realized subsample is random and has to be inflated by hand from the expected domain share. The same line runs through design_df(): a domain that is a union of whole strata gets an exact degrees-of-freedom count in $domains, while one cutting across strata does not, because counting the PSUs that contain its members needs frame information a plan does not carry.

Converting between listing units and analysis units. Sizing works in the units the precision target refers to. Converting a size in eligible persons to a number of households to list, given a mean household size and an eligibility rate, is arithmetic the user does explicitly. The MICS and DHS section above and the README both show the calculation.