Declare UK capital gains target facts (populace-uk has none; taxpayer count is 3.9x HMRC)#460
Open
vahid-ahmadi wants to merge 2 commits into
Open
Declare UK capital gains target facts (populace-uk has none; taxpayer count is 3.9x HMRC)#460vahid-ahmadi wants to merge 2 commits into
vahid-ahmadi wants to merge 2 commits into
Conversation
The published populace-uk release carries 149 targets and none constrain capital gains, so the calibrated weights leave the gains distribution unanchored. Reading capital_gains weighted by household_weight straight out of populace_uk_2023.h5 gives 1.47m CGT taxpayers holding GBP 96.0bn at a GBP 65,374 mean, against HMRC's 378k, GBP 65.9bn and ~GBP 174,000 - 3.9x the taxpayer count with a mean gain 62% below the administrative figure. That is distributional error rather than a level shift, so it survives any revenue-side correction: on an uncalibrated baseline the share of people affected by a CGT rate reform comes out roughly three times too high. Declares hmrc/capital_gains_total and hmrc/cgt_taxpayers as household-grain facts with HMRC citations, following us_runtime.fiscal_targets, plus a coverage requirement so dropping either fails the gate instead of passing quietly. Both measures are prepared columns (the registry refuses callables); the taxpayer count must track the annual exempt amount in force for the period, which moved GBP 12,300 -> 6,000 -> 3,000 across 2022-23 to 2024-25. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vahid-ahmadi
force-pushed
the
feat/uk-cgt-targets
branch
from
July 19, 2026 13:29
6337f51 to
5242b99
Compare
The declared CGT facts were person-measures wearing a household entity, and nothing built the columns they compile against. Correct both specs to entity="person" — matching the uk_runtime.hmrc_calibration convention where constraint rows are person-grain and the calibrated mass stays on household Weights — and rename their measures to the prepared columns uk_cgt_measure_gains_amount and uk_cgt_measure_taxpayer_count. Add uk_runtime.cgt_calibration.materialize_uk_cgt_calibration_frame, which prepares those columns from the persisted person-level capital_gains input (no simulation needed), assembles the person+household Frame with household Weights and the dataset mass log, and returns the declared facts as a TargetRegistry. The CGT taxpayer indicator is capital_gains > annual exempt amount, and the AEA is policy-dependent (12,300 -> 6,000 -> 3,000 across 2022-23 to 2024-25). The default is derived from dataset.time_period through an explicit mapping and an unmapped period raises: silently defaulting would change what "CGT taxpayer" means without changing the declared target value. Missing capital_gains and zero positive-mass support both fail closed rather than calibrating the positive HMRC facts to zero rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Closes #459 (partly — see "What this does not do").
Problem
The published
populace-ukrelease (populace-uk-2023-dd68c73-4aa4b14) carries 149 calibration targets and none of them constrain capital gains. The only CGT-adjacent entry in the target surface is the columnhousehold.household_is_capital_gains_clone, which is a feature flag from the imputation rather than a target.With nothing pulling the gains distribution toward administrative data, it drifts badly. Reading
capital_gainsweighted byhousehold_weightstraight out ofpopulace_uk_2023.h5:The calibrated dataset is 3.9x HMRC's taxpayer count with a mean gain 62% below the administrative figure — and on the two statistics that matter most for CGT work it is further from HMRC than the stock Enhanced FRS it improves on elsewhere. The shape is what you would expect from an unconstrained imputation: gains spread across far too many households in amounts that are individually far too small.
This is distributional error, not a level shift, so it is not fixed by correcting revenue. (Separately, PolicyEngine/policyengine-uk-data#439 restores an OBR CGT revenue target that was being silently dropped; that constrains what CGT raises, not how gains are distributed. The two are complementary.)
It matters in practice: calibrating a CGT reform analysis with these two targets added moved the share of people affected from 5.0% to 1.65% — a factor of three.
What this adds
uk_runtime/fiscal_targets.py, followingus_runtime/fiscal_targets.py:hmrc/capital_gains_total— £65.9bn, HMRC CGT statistics 2023-24hmrc/cgt_taxpayers— 378,000, same sourceTargetCoverageRequirementso a build that drops either fact fails the gate rather than passing quietly, which is the failure mode that let this go unnoticedUK_CGT_REQUIRED_COLUMNS, naming the prepared household columns the measures readValues are HMRC's published outturn at
period=2023rather than uprated approximations, on the principle that the registry declares facts and the build ages them.Both facts are household-grain to match the UK weights, so both need prepared columns — the registry refuses callables so it can serialize, and count-like facts are documented to use prepared indicator/count columns. Two caveats are recorded in
notes:What this does not do
These specs are not yet consumed by anything. I could not find the UK national build in this repo — no
enhanced_frsreference in any Python file, no UK release workflow (onlytest.yml), and the UK-side code here is local/geography plus apopulace-dataregistry entry pointing at the HF artifact. So I can declare the facts and prove they compile, but I cannot show the UK build picking them up. Pointers very welcome; wiring is the obvious follow-up and I am happy to do it.Two open questions for reviewers:
capital_gainsas a household sum andcgt_taxpayer_countas an above-AEA count.us_runtime/fiscal_targets.pyhas moved to value-free references with values arriving from an external Ledger artifact at build time. These specs carry inline values with citations, matching theTargetSpeccontract as documented ("a value, optionally a standard error, and always a citation"). If the UK should follow the US onto Ledger, this is the shape to migrate and I will redo it that way.Raised with @MaxGhenis, who confirmed UK targets should move to populace-native
TargetSpecs.Tests
Seven tests pin the declared facts, their provenance, the household grain, the prepared-column contract, the content-addressed registry, and the coverage requirement.
ruff checkandruff formatclean.