Add the inequality package, so the repository's name is earned - #24
Merged
Merged
Conversation
Counted before starting: 27 Python files outside tests, 872 lines, of which survey_estimation, impact_evaluation and one cleaning pipeline were 732. Exactly one thing in the repository was about distribution, and the file whose name promised it, public_health_access_index.py, was a five-line row mean. inequality/ is 24 functions across five modules. Indices: Gini, the full generalised entropy family with Theil T and the mean log deviation named, Atkinson at any aversion, Palma, the 80/20 ratio, quantile shares, the Lorenz curve, weighted quantiles. Concentration: the concentration index, which is the measure that answers the question an average cannot, whether an outcome falls on the poor and how steeply. With it the Erreygers and Wagstaff corrections, because a raw index is not comparable between two places whose prevalence differs and comparing them anyway is a standard error in published work. Plus the concentration curve, a by-group table, and Wagstaff's achievement index. Decomposition: the Theil within/between split, exact for GE(0) and GE(1) and refusing any other alpha rather than returning parts that do not sum. Blinder-Oaxaca in twofold and threefold form with four reference choices. Incidence: benefit incidence analysis, single-service and split by level of service, which is where the finding usually is. Primary care progressive, tertiary regressive, the two cancelling in the aggregate. Three things are centralised rather than reimplemented per function. Weighted fractional ranks live in one file and are shared by the Gini, the concentration index and both curves, so those agree by construction. Ties take the block midpoint, which is what makes a wealth quintile a legitimate ranking variable. And every function refuses rather than returning a plausible wrong number when an assumption breaks: a Gini on negative values, GE(0) on zeros, Wagstaff where everyone has the outcome, a level with no budget, an Oaxaca on three groups. Tests: 25 to 101. Every new one is an identity or a hand-computed figure, never previous output. The Gini against brute-force mean absolute difference on fifteen random datasets. Curve areas against their indices by trapezoid. within + between == total for both alphas, with weights. Oaxaca's components summing to the gap under all four references. GE(2) equalling half the squared coefficient of variation. A weight of two equalling the row appearing twice. The mean fractional rank being exactly 0.5, which three derivations here depend on. Two of my own test expectations were wrong on first run and the code was right, which is the point of pinning to closed forms: an Oaxaca test drew both groups from the same distribution and then asserted the realised endowment gap was zero, and a composite-index test hardcoded arithmetic I had done in my head. Both now compute the expectation from the definition. Also: social_sector/composite_index.py replaces the row mean with an index that makes its three decisions arguments rather than assumptions (direction, normalisation, weights), offers goalpost normalisation so a series is comparable across years, geometric aggregation where indicators should not substitute for each other, a minimum non-missing count so a district reporting two of six indicators is not scored as though it reported all six, and rank_sensitivity to show how far a ranking moves under an equally defensible choice. compute_access_index keeps working. Deleted workflows/ci.yml: dead configuration, pinned to Python 3.10 and running R tests in a repository with no R, duplicating the real workflow at .github/workflows/python-tests.yml. The landing page described it as "start-to-finish routes through an analysis", which it never was. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DMYhECx2Dzbqad6KJxWZ7t
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 was here
Counted before starting: 27 Python files outside
tests/, 872 lines.survey_estimation(357),impact_evaluation(220) and one cleaning pipeline (155) were 732 of them. Exactly one thing in the repository was about distribution, and the file whose name promised it,public_health_access_index.py, was a five-line row mean.What this adds
inequality/, 24 functions across five modules, ~950 lines.indices.pyconcentration.pydecomposition.pyincidence.pyranks.pyThe concentration index is the one that makes the name true. An average tells you the level; it cannot tell you whether stunting falls on the poor, which is the question a programme budget answers to.
Three decisions that are centralised rather than repeated
Ranks live in one file. The Gini, the concentration index and both curves share
fractional_rank, so they agree by construction. Ties take the block midpoint, which is what makes a wealth quintile a legitimate ranking variable and what the World Bank's DHS equity work does.A raw concentration index is not comparable across prevalences. Its possible range shrinks as a binary outcome's mean moves away from 0.5, so the same indicator at 8 per cent and at 60 per cent cannot go in one table uncorrected. Both fixes are here and they can disagree about the direction of change over time; the README says to pick one per table and state it.
Every function refuses rather than returning a plausible wrong number. A Gini on negative values, GE(0) on zeros, Wagstaff where everyone has the outcome, a service level with no budget, an Oaxaca on three groups.
Tests: 25 → 101
Every new test is an identity or a hand-computed figure, never previous output. A distributional index cannot be checked by eye: 0.31 and 0.34 are both plausible consumption Ginis, and a sign error gives a number in exactly the right range that says the opposite of the truth.
within + between == totalfor GE(0) and GE(1), with weightsTwo of my own test expectations were wrong on first run and the code was right, which is the point of pinning to closed forms. An Oaxaca test drew both groups from the same distribution and then asserted the realised endowment gap was zero; a composite-index test hardcoded arithmetic I had done in my head. Both now compute the expectation from the definition.
Also
social_sector/composite_index.pyreplaces the row mean with an index that makes its three decisions arguments rather than assumptions: direction, normalisation and weights. Adds goalpost normalisation so a series is comparable across years, geometric aggregation where indicators should not substitute for each other, a minimum non-missing count so a district reporting two of six indicators is not scored as though it reported all six, andrank_sensitivityto show how far a ranking moves under an equally defensible choice.compute_access_indexkeeps working.Deleted
workflows/ci.yml: dead configuration, pinned to Python 3.10 and running R tests in a repository with no R, duplicating the real workflow. The landing page described it as "start-to-finish routes through an analysis".🤖 Generated with Claude Code
https://claude.ai/code/session_01DMYhECx2Dzbqad6KJxWZ7t
Generated by Claude Code