Add survey_estimation: design-based standard errors for clustered surveys - #18
Merged
Merged
Conversation
…veys EquityStack could weight a mean and could not put a standard error on it. For a stratified, clustered national survey that gap is not cosmetic: an independence SE is too small, often by a factor of two, and every interval and test built on it errs in the direction that flatters the result. survey_estimation/design_based_estimates.py implements Taylor linearisation (ultimate cluster), the estimator behind Stata's svy: prefix and R's survey package. Not DHS-specific; it needs a weight, a PSU and a stratum. Three things it gets right that a hand-rolled version usually does not: - Subgroups are domains, not subsets. Filtering before estimating discards PSUs holding none of the subgroup, which shrinks the SE. Those PSUs belong to the design and are kept. - Proportions get a logit interval. A 2.5% outcome concentrated in one cluster gives a linear lower bound of -0.024, which is not a proportion. - Intervals use t on clusters minus strata, and report which distribution produced them via ci_dist. Where scipy is missing the normal fallback warns instead of silently narrowing: on 40 clusters and 8 strata that is [17.1, 32.8] against [16.9, 33.2]. survey_estimation/dhs_stunting.py reproduces India's published NFHS-5 stunting table by wealth quintile from the raw children's recode. It consumes the CSV written by InsightStack's data_starters/dhs-south-asia loader, so the two repos couple through a file rather than an import. The comparison is the point. DHS published 35.5% nationally, 46.1% in the poorest quintile and 22.9% in the richest (API indicator CN_NUTS_C_HA2, survey IA2020DHS, retrieved 2026-09-08). A run that misses those has a fault upstream, and the script names the three that account for nearly all of them. 12 tests, each pinned to a closed-form answer rather than to the estimator's own output: one unit per cluster must collapse to s/sqrt(n) exactly; scaling every weight by a million must not move the SE; perfect intra-cluster correlation must give a design effect of exactly (N-1)/(n-1). Two defects surfaced during development. The scipy fallback hardcoded the 95% quantile, so conf=0.99 silently returned a 95% interval. And it applied the normal without saying so, which is the same class of quiet wrongness this module exists to catch. Both are now tested. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DMYhECx2Dzbqad6KJxWZ7t
12 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
EquityStack could weight a mean and could not put a standard error on it. For a stratified, clustered national survey that gap is not cosmetic: an independence standard error is too small, often by a factor of two, and every confidence interval and test built on it errs in the direction that flatters the result.
survey_estimation/design_based_estimates.pyimplements the Taylor linearisation (ultimate cluster) estimator, the same one behind Stata'ssvy:prefix and R'ssurveypackage. It is not tied to any survey; it needs a weight, a PSU and a stratum.Three things it gets right that a hand-rolled version usually does not:
ci_dist. Wherescipyis missing the normal fallback warns rather than silently narrowing: on 40 clusters and 8 strata that is [17.1, 32.8] against [16.9, 33.2].survey_estimation/dhs_stunting.pyreproduces India's published NFHS-5 stunting table by wealth quintile from the raw children's recode. It consumes the CSV written by InsightStack'sdata_starters/dhs-south-asialoader, so the two repositories couple through a file rather than an import and neither needs the other installed.The comparison step is the point. DHS published 35.5 percent nationally, 46.1 in the poorest wealth quintile and 22.9 in the richest (API indicator
CN_NUTS_C_HA2, surveyIA2020DHS, retrieved 2026-09-08). A run that misses those has a fault upstream, and the script names the three that account for nearly all of them.scipyis added torequirements.txt.Type of change
Checklist
tests/test_survey_estimation.py, all passing locallys / sqrt(n)exactly, scaling every weight by a million must not move the standard error, and perfect intra-cluster correlation must give a design effect of exactly(N - 1) / (n - 1)survey4.2.1 on R 4.3.3, the reference implementation. Estimates, standard errors and degrees of freedom agree to twelve significant figures. Those reference values are hardcoded into the suite against a dataset built with no random number generator, so the check is stable across library versionspytest tests/green, which is what CI runsTwo defects surfaced during development and are fixed here. The
scipyfallback hardcoded the 95 percent quantile, soconf=0.99silently returned a 95 percent interval, and it applied the normal without saying so, which is the same class of quiet wrongness this module exists to catch. Both are now tested.🤖 Generated with Claude Code
https://claude.ai/code/session_01DMYhECx2Dzbqad6KJxWZ7t
Generated by Claude Code