import svySurvey Planning and Sample Size Calculation in Python
Sample size requirements for estimation and comparison objectives
sample size calculation Python, survey planning Python, power analysis survey Python, design effect sample size, margin of error calculation Python, survey sample size estimation, nonresponse adjustment sample size, stratified sampling sample size, cluster sampling sample size, two-stage sampling design Python, survey methodology Python
Planning1 involves several steps, including developing a protocol with clearly defined primary objectives, the target population, and sample size requirements. Here we plan a survey for a synthetic country, state its primary objectives, and compute the minimum required sample sizes.
The Target Population
We plan against The World Bank synthetic census (World Bank 2023a) — a hypothetical middle-income country of over 10 million individuals in 2.5 million households, carrying the usual census variables (demography, education, occupation, dwelling characteristics, fertility, mortality, migration) plus household expenditure, child anthropometrics, and asset ownership. We treat this census as the current state of the population.
A sample of 8,000 households (~32,000 individuals) was drawn from it (World Bank 2023b). Both the census and the sample come in household- and individual-level files. See Datasets for the full description and how to load them (including the small bundled versions used in the other tutorials).
Study Objectives
As researchers, our goal is to estimate expenditure and poverty rate (Poverty Headcount Ratio) levels for the imaginary country. Specifically, we focus on two main objectives:
1. Average household expenditure: Produce disaggregated estimates by admin 1 level (first-level administrative units)
2. Poverty rate comparisons: Compare urban vs. rural poverty rates within each admin 1 unit
In this study, we define the poverty rate as the proportion of individuals with per capita expenditure below the poverty line, where the poverty line is defined as 60% of the national median per capita expenditure.
Sample Size Calculation
After defining the study objectives, we will calculate the minimum required sample size. Because we have multiple primary objectives, each can lead to a different sample size. There are two common approaches:
- Choose one objective to drive the calculation, or
- Compute a sample size for each objective and take the largest (conservative)
We will use the second approach: compute the required sample size for each objective and then adopt the maximum as the study’s sample size.
The SampleSize Class
svy provides the svy.SampleSize class to compute required sample sizes. The class uses a goal-based API: you create an empty SampleSize() instance, then call a goal method that specifies the objective and all its parameters in one call.
Four goal methods are available:
| Method | Objective | Key Inputs |
|---|---|---|
estimate_mean() |
Estimate a population mean | sigma, moe |
estimate_prop() |
Estimate a population proportion | p, moe |
compare_props() |
Compare two proportions (power analysis) | p1, p2, power |
compare_means() |
Compare two means (power analysis) | mu1, mu2, sigma1, power |
Every goal method accepts these common parameters for design adjustments:
deff: design effect to account for clustering, stratification, and unequal weighting (default1.0)resp_rate: anticipated response rate (default1.0, i.e. no adjustment)pop_size: target population size for finite population correction (defaultNone, no FPC)alpha: significance level (default0.05)
How Sample Sizes Are Computed
For each objective, the procedure for computing the sample size is:
- Specify target precision (e.g., half-width of CI) or power
- Compute the effective sample size under simple random sampling (\(n_0\))
- Inflate for the design effect (\(n_1\))
- Apply the finite population correction if
pop_sizeis provided (\(n_2\)) - Inflate for anticipated non-response (\(n\))
svy performs all these steps and reports each intermediate result:
| Field | Description |
|---|---|
n0 |
Base sample size under SRS (no adjustments) |
n1_deff |
After design effect inflation |
n2_fpc |
After finite population correction |
n |
Final required sample size (after non-response adjustment) |
Objective 1: Average Household Expenditure
We want to estimate the average household expenditure per admin-1 area with a margin of error of 1,000. Using previous surveys and censuses, we expect a standard deviation (sigma) of 7,000. We assume a design effect of 1.2 and an anticipated response rate of 0.9.
obj1 = svy.SampleSize().estimate_mean(
sigma=7000,
moe=1000,
deff=1.2,
resp_rate=0.9,
)
print(obj1)╭────────── Sample Size ───────────╮ │ │ │ n0 n1_deff n2_fpc n │ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ │ 189.0 227 227 253 │ │ │ ╰──────────────────────────────────╯
The output shows each step of the calculation: the base SRS sample size (\(n_0\)), the design-adjusted size, and the final size after non-response adjustment. This is the minimum required sample size per admin-1 area.
Since the country has 10 admin-1 areas, a simple national target under equal allocation is to multiply by 10.
Stratified Calculations
If you need area-specific assumptions (different variability, response rates, or design effects across regions), pass dicts instead of scalars. Stratification is inferred automatically whenever any parameter is a dict:
obj1_strat = svy.SampleSize().estimate_mean(
sigma={"region1": 7000, "region2": 11000, "region3": 5000},
moe={"region1": 1000, "region2": 1300, "region3": 700},
deff={"region1": 1.2, "region2": 1.0, "region3": 1.05},
resp_rate={"region1": 0.90, "region2": 0.90, "region3": 0.85},
)
print(obj1_strat)╭────────────── Sample Size ───────────────╮ │ │ │ stratum n0 n1_deff n2_fpc n │ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ │ region1 189 227 227 253 │ │ region2 276 276 276 307 │ │ region3 196 206 206 243 │ │ │ ╰──────────────────────────────────────────╯
You can mix scalars and dicts freely. Scalars are broadcast to all strata. For example, if deff=1.2 but sigma is a dict, every stratum gets deff=1.2.
Finite Population Correction
When the sampling fraction is non-negligible (say, more than 5% of the population), the finite population correction reduces the required sample size. Pass pop_size to apply it:
obj1_fpc = svy.SampleSize().estimate_mean(
sigma=7000,
moe=1000,
deff=1.2,
resp_rate=0.9,
pop_size=5000,
)
print(obj1_fpc)╭────────── Sample Size ───────────╮ │ │ │ n0 n1_deff n2_fpc n │ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ │ 189.0 227 218 243 │ │ │ ╰──────────────────────────────────╯
Notice that n2_fpc is smaller than n1_deff — the FPC reduces the requirement because we’re sampling a substantial fraction of the population. When pop_size is omitted (or very large relative to \(n_0\)), the FPC has negligible effect.
Estimating a Proportion
For surveys focused on prevalence or rates (e.g., vaccination coverage, poverty headcount), use estimate_prop(). This requires an anticipated proportion p and a desired margin of error moe:
obj_prop = svy.SampleSize().estimate_prop(
p=0.30,
moe=0.05,
deff=1.5,
resp_rate=0.85,
)
print(obj_prop)╭────────── Sample Size ───────────╮ │ │ │ n0 n1_deff n2_fpc n │ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ │ 323.0 485 485 571 │ │ │ ╰──────────────────────────────────╯
When you are unsure about the expected proportion, using p=0.5 gives the most conservative (largest) sample size, since the variance \(p(1-p)\) is maximized at 0.5.
The method parameter controls the calculation approach:
"wald"(default): standard normal approximation"fleiss": applies a continuity correction, producing slightly larger sample sizes
obj_fleiss = svy.SampleSize().estimate_prop(
p=0.30,
moe=0.05,
deff=1.5,
resp_rate=0.85,
method="fleiss",
)
print(obj_fleiss)╭────────── Sample Size ───────────╮ │ │ │ n0 n1_deff n2_fpc n │ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ │ 401.0 602 602 709 │ │ │ ╰──────────────────────────────────╯
Objective 2: Poverty Rate Comparisons
The poverty line is defined as 60% of the national median per capita expenditure, here set at 6,000. We want to test for a statistically significant difference in poverty rates between urban and rural populations.
This is a comparison objective, so we use compare_props(). Instead of a margin of error, we specify:
p1,p2: anticipated proportions in each grouppower: desired statistical power (probability of detecting a real difference)alpha: significance level
obj2 = svy.SampleSize().compare_props(
p1=0.4,
p2=0.5,
two_sides=True,
deff=1.1,
resp_rate=0.9,
)
print(obj2)╭─────────────── Sample Size ───────────────╮ │ │ │ group n0 n1_deff n2_fpc n │ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ │ group1 385.0 424 424 472 │ │ group2 385.0 424 424 472 │ │ │ ╰───────────────────────────────────────────╯
The output shows the required sample size per group (group 1 and group 2). The total sample size for this comparison is the sum of both groups.
Unequal Allocation Between Groups
Say we want more samples in the urban areas to do additional secondary analyses. The alloc_ratio parameter controls the ratio \(n_2 / n_1\). A ratio of 1.5 (or 60/40) allocates 60% to group 2 and 40% to group 1:
obj2_unequal = svy.SampleSize().compare_props(
p1=0.4,
p2=0.5,
alloc_ratio=60 / 40,
two_sides=True,
deff=1.1,
resp_rate=0.9,
group_labels=["urban", "rural"],
)
print(obj2_unequal)╭────────────── Sample Size ───────────────╮ │ │ │ group n0 n1_deff n2_fpc n │ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ │ rural 480.0 528 528 587 │ │ urban 320.0 352 352 392 │ │ │ ╰──────────────────────────────────────────╯
Variance Modes for Proportion Comparisons
The var_mode parameter controls how the variance is estimated under the null hypothesis:
var_mode |
Description | When to use |
|---|---|---|
"alt-props" (default) |
Uses \(p_1\) and \(p_2\) separately | Standard approach |
"pooled-prop" |
Uses pooled proportion \(\bar{p}\) | When assuming equal proportions under \(H_0\) |
"max-var" |
Uses \(p = 0.5\) for maximum variance | Most conservative, useful when proportions are uncertain |
# Most conservative — assumes maximum variance
obj2_conservative = svy.SampleSize().compare_props(
p1=0.4,
p2=0.5,
var_mode="max-var",
deff=1.1,
resp_rate=0.9,
)Non-Inferiority and Equivalence Margins
The delta parameter allows testing for non-inferiority or equivalence rather than simple superiority:
# Non-inferiority: is group 2 no worse than group 1 by more than 5
# percentage points? The margin is signed (H0: effect <= delta) and the
# test is one-sided.
obj2_ni = svy.SampleSize().compare_props(
p1=0.4,
p2=0.5,
delta=-0.05,
two_sides=False,
deff=1.1,
resp_rate=0.9,
)Exporting Results
All SampleSize results can be exported to a Polars DataFrame for further analysis or reporting:
df = obj1_strat.to_polars()
print(df)shape: (3, 5)
┌─────────┬───────┬─────────┬────────┬───────┐
│ stratum ┆ n0 ┆ n1_deff ┆ n2_fpc ┆ n │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ f64 ┆ f64 ┆ f64 ┆ f64 │
╞═════════╪═══════╪═════════╪════════╪═══════╡
│ region1 ┆ 189.0 ┆ 227.0 ┆ 227.0 ┆ 253.0 │
│ region2 ┆ 276.0 ┆ 276.0 ┆ 276.0 ┆ 307.0 │
│ region3 ┆ 196.0 ┆ 206.0 ┆ 206.0 ┆ 243.0 │
└─────────┴───────┴─────────┴────────┴───────┘
Quick Reference
Goal Methods
| Method | Parameters | Use case |
|---|---|---|
estimate_mean() |
sigma, moe |
Estimate a population mean with desired precision |
estimate_prop() |
p, moe, method |
Estimate a proportion with desired precision |
compare_props() |
p1, p2, power, alloc_ratio, delta, var_mode |
Test for difference between two proportions |
compare_means() |
mu1, mu2, sigma1, sigma2, power, alloc_ratio |
Test for difference between two means |
Common Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
deff |
scalar or dict | 1.0 |
Design effect |
resp_rate |
scalar or dict | 1.0 |
Anticipated response rate |
pop_size |
scalar, dict, or None | None |
Population size for FPC |
alpha |
scalar or dict | 0.05 |
Significance level |
power |
scalar or dict | 0.80 |
Statistical power (comparison methods only) |
All parameters accept either a scalar (applied uniformly) or a dict (per-stratum values). When any parameter is a dict, stratification is inferred automatically.
Next Steps
Now that we’ve calculated the required sample sizes, learn how to actually draw the sample in the next tutorial.
Master the basics?
Continue to Sample Selection →
References
Footnotes
The datasets are entirely synthetic and are not intended for real-world applications. They are provided solely for educational purposes. The study objectives and design are intentionally simplified to illustrate survey analysis concepts in these tutorials. The concepts, definitions, targets, and other specifications used in these tutorials do not necessarily reflect those of the World Bank, The World Health Organization (WHO), The United Nations Children’s Fund (UNICEF), or any other referenced institutions.↩︎