Fit a Pareto upper tail to the capital gains imputation#443
Closed
vahid-ahmadi wants to merge 1 commit into
Closed
Fit a Pareto upper tail to the capital gains imputation#443vahid-ahmadi wants to merge 1 commit into
vahid-ahmadi wants to merge 1 commit into
Conversation
The Advani & Summers percentile table stops at p95, so fitting a linear spline through p05..p95 and sampling uniform quantiles extrapolated everything above the 95th percentile off the last segment. That left no Pareto tail: in the published enhanced_frs_2024_25.h5 only 1.6% of gains came from gains of £1m or more (HMRC: ~61%) and no gain exceeded ~£2m. Gains above p95 are now drawn from a Pareto tail anchored at each income band's p95. The shape parameter is fitted per band, not assumed: the same source table publishes mean_gains_given_gains, which the spline alone undershoots by 35-50% in every band, and that gap is the missing tail mass. We solve for the shape parameter that makes body + tail reproduce the published band mean. Fitted values cluster around 1.46. Fitting per income band preserves the existing income-band conditioning. The seeded generator and the spline body below p95 are unchanged. Adds HMRC Capital Gains Tax statistics Table 2.1a as a committed validation reference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collaborator
Author
|
Superseded by #444, which addresses the same missing upper tail with existing repo patterns instead of a fitted Pareto: zero-weight donor households at each HMRC Table 2.1a band mean, weighted by calibration against per-band count and gains targets. With the band structure targeted directly, the parametric above-p95 shape assumption is no longer needed, and merging both would double-correct the tail. The HMRC Table 2.1a reference CSV introduced here is carried over into #444. |
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.
Measured before/after
The bug is visible on the published
enhanced_frs_2024_25.h5(2026). Reading that file directly:To isolate this change I ran
impute_capital_gainsstandalone on that dataset with only the sampler swapped (same pipeline, same seed, both arms):What changed
UnivariateSplineover[0.05 … 0.95]withk=1, sampled atcg_rng.random(), linearly extrapolates every draw above p95 off the last segment. The heavy tail of capital gains is simply absent.Gains above p95 are now drawn from a Pareto tail anchored at each income band's p95. The shape parameter is fitted, not guessed. The same Advani & Summers table already publishes
mean_gains_given_gainsper income band — a column the imputation never used — and the spline alone undershoots it by 35–50% in every band. That gap is the missing tail mass, so for each band we solve (Brent, on a closed-form capped-Pareto mean) for the shape parameter making body + tail reproduce the published band mean. Fitted values cluster at 1.15–2.07, median 1.47, which is where the CGT literature puts this tail.Because the fit is per income band, the income-band conditioning is preserved for free rather than a single global tail being imposed. The spline body below p95 is bit-for-bit unchanged (test-pinned), as is the seeded generator and the number of draws consumed.
What this does not fix — please read
percent_with_gainsand the weight-blending step. HMRC's ~359k is still far off and is not addressable here.hmrc/capital_gains_totalandhmrc/cgt_taxpayersto do it with. The point of this PR is that reweighting could never have fixed concentration, only the level. A full pipeline rebuild is still needed to confirm the calibrated end state; I did not run one.Reference data
HMRC CGT statistics Table 2.1a (2023-24, individuals) is committed as
storage/capital_gains_size_distribution_hmrc.csv. The.odsasset URL embeds a content hash that changes each release, so it is not a stable fetch target; this follows theons_demographics.pyprecedent. Note HMRC's figures "only include taxpayers who have a CGT liability" — a narrower population than the microdata's, documented in the file header.Tests
policyengine_uk_data/tests/test_capital_gains_tail.py(9 tests) pins: body below p95 identical to the original spline; tail anchored at p95, monotone and capped; band means reproduced; determinism under the seed; fitted alphas in a plausible range; concentration delivered; and a guard test asserting the body alone undershoots the published means, so the justification for the tail can't silently rot.Incidental finding: the "spline" uses scipy's default smoothing
s, so it never interpolated the published percentiles exactly. Pre-existing; preserved, not changed.🤖 Generated with Claude Code