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. In this tutorial, we use the World Bank synthetic population as our target, then state the primary objectives and compute the minimum required sample sizes.
World Bank Synthetic Data
In this first tutorial, we introduce the dataset used throughout the series and outline the study we will simulate end-to-end. Our goal is to build practical intuition for using svy, from defining a target population and choosing a design to estimating parameters and quantifying uncertainty, via a clear, step-by-step workflow.
In these tutorials, we will use The World Bank census (World Bank 2023a) and sample (World Bank 2023b) synthetic data.
The census dataset represents a hypothetical middle-income country with over 10 million individuals in 2.5 million households. It is organized into two files:
- Household-level file: variables measured at the household level
- Individual-level file: variables measured for each household member
The dataset includes variables typically found in population censuses: demography, education, occupation, dwelling characteristics, fertility, mortality, and migration. It also includes additional measures often collected in household surveys, such as household expenditure, child anthropometrics, and asset ownership. Only ordinary households are included (community households are excluded). We will use this dataset as the current state of the population of the imaginary country.
The sample dataset, drawn from the census, consists of 8,000 households and over 32,000 individuals. Like the census, it is provided in two files (household-level and individual-level) and contains the same range of variables.
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, 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\))
- Apply the finite population correction if
pop_sizeis provided (\(n_1\)) - Inflate for the design effect (\(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_fpc |
After finite population correction |
n2_deff |
After design effect inflation |
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_fpc n2_deff n │ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ │ 189.0 189 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_fpc n2_deff n │ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ │ region1 189 189 227 253 │ │ region2 276 276 276 307 │ │ region3 196 196 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_fpc n2_deff n │ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ │ 189.0 183 220 245 │ │ │ ╰──────────────────────────────────╯
Notice that n1_fpc is smaller than n0 — 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_fpc n2_deff n │ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ │ 323.0 323 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_fpc n2_deff n │ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ │ 401.0 401 602 709 │ │ │ ╰──────────────────────────────────╯
Objective 2: Poverty Rate Comparisons
The poverty line is defined as 60% of the national median per capita income, 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_fpc n2_deff n │ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ │ group1 385.0 385 424 472 │ │ group2 385.0 385 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_fpc n2_deff n │ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ │ rural 480.0 480 528 587 │ │ urban 320.0 320 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?
obj2_ni = svy.SampleSize().compare_props(
p1=0.4,
p2=0.5,
delta=0.05,
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_fpc ┆ n2_deff ┆ n │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ f64 ┆ f64 ┆ f64 ┆ f64 │
╞═════════╪═══════╪════════╪═════════╪═══════╡
│ region1 ┆ 189.0 ┆ 189.0 ┆ 227.0 ┆ 253.0 │
│ region2 ┆ 276.0 ┆ 276.0 ┆ 276.0 ┆ 307.0 │
│ region3 ┆ 196.0 ┆ 196.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, 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.↩︎