Survey Planning and Sample Size Calculation in Python

Sample size requirements for estimation and comparison objectives

Tutorials
Survey Planning
Sample Size
Python
Learn to plan complex surveys with svy. Calculate required sample sizes for estimation and comparison objectives, account for design effects, clustering, and nonresponse adjustments.
Author

Mamadou S. Diallo, Ph.D.

Published

January 18, 2026

Modified

April 11, 2026

Keywords

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 (default 1.0)
  • resp_rate: anticipated response rate (default 1.0, i.e. no adjustment)
  • pop_size: target population size for finite population correction (default None, no FPC)
  • alpha: significance level (default 0.05)

How Sample Sizes Are Computed

For each objective, the procedure for computing the sample size is:

  1. Specify target precision (e.g., half-width of CI) or power
  2. Compute the effective sample size under simple random sampling (\(n_0\))
  3. Apply the finite population correction if pop_size is provided (\(n_1\))
  4. Inflate for the design effect (\(n_2\))
  5. 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)
import svy

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  
                                          
╰──────────────────────────────────────────╯
Tip

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  
                                  
╰──────────────────────────────────╯
Note

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 group
  • power: 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)
Note

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

World Bank. 2023a. “Synthetic Data for an Imaginary Country, Full Population, 2023.” World Bank, Development Data Group. https://doi.org/10.48529/78M1-AE09.
———. 2023b. “Synthetic Data for an Imaginary Country, Sample, 2023.” World Bank, Development Data Group. https://doi.org/10.48529/MC1F-QH23.

Footnotes

  1. 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.↩︎