Repository navigation
Conversation
|
Batch dependency with pe-uk #2241 (fixes policyengine-uk#2238) and uk-data #556. #2241 stops charging a household's other-property stock as an additional-dwelling purchase. If this PR's target (OBR SDLT less corporate SDLT) is built on a policyengine-uk carrying #2241, but without an imputation of additional-dwelling purchases, calibration will push main-home SDLT to absorb the missing higher-rates share. #556 adds that imputation, calibrated to HMRC's HRAD statistics; a draft PR is being built now. So #556 should land in the same d833 batch as this PR, or the target should be held until it does. Max's decision on adopting #556 is d1354. |
|
Heads-up for the batch: #592 (hash-verified OBR workbooks, header-read years) removes |
The OBR's SDLT line covers every buyer, but policyengine-uk adds SDLT from non-natural persons separately as corporate_sdlt, so targeting stamp_duty_land_tax at the whole line counted the corporate share twice. Subtract the installed policyengine-uk's corporate statistic for each of the active OBR manifest's years, warn when that statistic is held flat (policyengine-uk before #2322, first released in 2.152.0), and refuse full builds against such a version. A statistic above the OBR line raises SourceError, which the receipts parser does not swallow. Fixes #564 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
0544d69 to
092982e
Compare
The £23.29bn 2026-27 example came from policyengine-uk#2322's head before #2241, which no release matches. Use 1.58.0 under the locked 2.122.2 (households £16.32bn against the OBR's £15.41bn in 2025-26, corporate £3.13bn on top) and the 2025-26 target with 2.152.0 or later. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Calibration fit against the base build: pass
A drop of more than 2.0% fails. Notes
National targets in a worse error band on the head (8)
Flags are for review: builds of the same code move 2 to 12 national targets to a worse band (#598). Bands are of |estimate / target - 1|; no target's estimate or error is published. Income distribution and single ages against the base build: passHBAI FYE 2025 (DWP), period 2024: equivalised income in £ a week, rates and Gini in per cent. A value outside HBAI's 95% interval is marked (outside).
FlagsNone. These measures report and flag but do not fail yet: their same-code spread is unmeasured (#598). Aggregate impact of the data change: passBase dataset Checks: 0 broken by this change, 4 failing on both sides, 0 fixed. Moves flagged for review: 182 (thresholds in Notes
Moves flagged for review
142 more flags are in the report artifact. Receipts by tax head
Spending by benefit and service
Caseloads
Government balance, each component counted once
Model totals
Components the model counts twice
Household income totals
Population
Poverty rates
People in poverty
Inequality
Mean household net income in each side's own deciles
Standard reform costings (change in gov_balance)
Standard reforms: change in each reform's target
Household net income by decileThe datasets hold different households, so households are not matched; compare the mean income in each side's own deciles. Checks failing on both base and head (4)
Counted-once checkCounted-once check not run: not requested. Provenance
Base dataset: Head dataset: Receipts by tax head: head levels
Spending by benefit and service: head levels
Caseloads: head levels
Government balance, each component counted once: head levels
Model totals: head levels
Components the model counts twice: head levels
Household income totals: head levels
Population: head levels
Poverty rates: head levels
People in poverty: head levels
Inequality: head levels
Mean household net income in each side's own deciles: head levels
Standard reform costings (change in gov_balance): head levels
Standard reforms: change in each reform's target: head levels
|
Fixes #564.
What
obr/sdltnow targetsstamp_duty_land_taxat the OBR's SDLT line less policyengine-uk's corporate SDLT statistic for the same year:gov.hmrc.stamp_duty.statistics.{residential,non_residential}.corporate.revenue, read from the installed policyengine-uk (targets/sources/obr.py:corporate_sdlt_receipts,household_sdlt), for every year of the active OBR manifest (active_manifest().years, 2024-25 to 2030-31). The target keeps its name, reference and vintage.TESTING=1) stops at its preflight (utils/build_environment.py) if the resolved policyengine-uk holds the corporate statistic at the same value in every manifest year, as versions before #2322 do (£3.13bn, the 2020-21 figure). Target building, and so PR CI'sTESTING=1build, logs a warning in that case instead, so this PR's CI runs on the currently locked policyengine-uk 2.122.2.tests/test_property_purchased.pycompares households'stamp_duty_land_taxwith the household-only target, notexpected_sdltagainst total receipts, and checks that weightedcorporate_sdltequals the statistic subtracted. The newtests/test_obr_sdlt_target.pychecks the target identity each year and the invariants below.tests/test_obr_sources.py's fixed-column differential test subtracts the statistic forobr/sdlt.tests/test_obr_receipts_sheet.py's synthetic workbook now carries £100bn a year so its SDLT row stays above the statistic.Rebased on main
e1acdf96(2026-10-24): main's hash-bound manifest (#592) replaced_FY_COL_TO_YEAR, so years now come from the manifest and values fromread_row, and the tests load the verified workbook withload_workbook("receipts").Why
The OBR's SDLT line (EFO March 2026, Table 3.8; it equals Table 3.7's "Total SDLT" every year) covers all buyers in England and Northern Ireland. policyengine-uk adds SDLT paid by companies and other non-natural persons separately as
corporate_sdlt: the statistic times each household'sshareholding, which divides its corporate-sector wealth by the weighted total, so weightedcorporate_sdltis the statistic whatever the weights.corporate_sdltis ingov_taxandhousehold_tax. Calibratingstamp_duty_land_taxto the whole line therefore counted the corporate share twice.Measured
£bn, PolicyEngine years (2025 = 2025-26). Targets come from the committed EFO workbook and the installed policyengine-uk (
check569.py). The released policyengine-uk 2.157.0 carries #2322 and #2342; its statistic is identical, to the pound, to that of #2322's headcafd3f1d, measured before the rebase.Microsimulation runs on the released 1.58.0
enhanced_frs_2024_25.h5(sha2565ba399ea…d960b6), which was calibrated to the old target, 2025-26 (measure_2157.json,components_*.json):stamp_duty_land_taxsdlt_liablefilter)additional_residential_property_purchased(weighted value)non_residential_property_purchased(weighted value)corporate_sdltHousehold SDLT more than halves between the two versions because of pe-uk#2241 (in 2.134.1 and later), not this PR. It made additional-dwelling and non-residential purchases inputs that are nil unless a dataset imputes them, where before they were the household's property stocks priced as purchases whenever
property_purchased. The batch relock this PR needs brings #2241 as well. Without #573's imputation (d1354, parked), only main-home purchases carry household SDLT, so the calibration can reach the new target only by upweighting main-home buyers: £7.38bn to £10.60bn, 1.44 times, against 2.09 times for the old whole-line target. Weightedcorporate_sdltmatches the statistic to within £600 under both versions (float32). The household SDLT a rebuilt dataset reaches is pending: this PR's CI runs a reducedTESTING=1build on 2.122.2, and the full build comes with the batch.Is the new target attainable? This is a feasibility check, not the build pipeline (
reweight_experiment.py). Starting from 1.58.0's weights, I reweighted against the national targets only, using the pipeline's symmetric relative-error loss for 512 Adam steps at lr 0.1. Each run went once with the old SDLT target (the OBR total, £15.41bn) and once with the new one, all else equal. The pipeline also calibrates the constituencies and local authorities jointly, so this shows what meeting the target costs nationally, not what the full build will reach.Under 2.157.0 there are 648 national targets. One has no signal on 1.58.0, so the matrix is built with
require_coverage=False; a zero column adds the same constant to both losses (reweight_experiment_v2.py,reweight_experiment_2157.json). Households' SDLT starts at £7.36bn on the matrix. Both runs reach their SDLT target: the old one at £15.39bn (0.999 times) and the new one at £10.60bn (1.000 times). Their final losses agree to five decimal places (0.036688 and 0.036686). Both get there by upweighting main-home buyers (property_purchased), from 1.03m weighted households to 1.46m for the old target and 1.30m for the new one. No other target's absolute relative error differs between the runs by more than 0.4 percentage points (the largest isobr/state_pension, −7.8% against −7.4%), and 94.0% of the other targets are within 10% in both. Before the rebase, the same check on #2322's head (which predates #2241, so it still priced additional-dwelling purchases) hit both targets within 0.02%, with final losses equal to six decimal places (reweight_experiment.json). #573's imputation would let the calibration meet the target with fewer extra buyers; this check does not measure that.Invariants
For every year of the OBR manifest (2024-25 to 2030-31):
SourceErrornaming the year instead of producing a negative target (Hypothesis).SourceErroris not aValueError, so the receipts parser's per-row handler does not swallow it and the build stops (test_statistic_above_obr_sdlt_stops_the_receipts_parse); the manifest also requiresobr/sdlt, so the target cannot be dropped silently.corporate_sdltformula adds: a one-household simulation withshareholding= 1 in every year (rel. 1e-6), and weightedcorporate_sdltover the built dataset in 2025.With households on target, model SDLT (
stamp_duty_land_tax+corporate_sdlt) is therefore the OBR's figure. Mutation checks under 2.157.0 (test_obr_sdlt_target.py,test_obr_sources.py,test_build_environment.py), each caught: dropping the subtraction (5 failures); dropping the non-residential statistic (1); raisingValueErrorinstead ofSourceError(2); removing the negative check (2); treating every statistic as projected (2).Guard and dependency
policyengine-uk>=2.122.0), and no open PR relocks it. The batch that carries this PR must relock to 2.152.0 or later before its full build. Until then,make datawithoutTESTING=1stops with: "The resolved policyengine-uk holds corporate SDLT at £3.13bn in every year from 2024 to 2030. Full builds need a version with Update corporate SDLT and business rates statistics and project them with OBR receipts policyengine-uk#2322…" Verified: the preflight lists that one issue under 2.122.2 and none under 2.157.0.Tests
test_build_environment.py, everytest_obr_*.py,test_target_registry.pyandtest_property_purchased.py: 172 passed, 10 skipped (the dataset tests, with no built dataset in the checkout), under both the locked policyengine-uk 2.122.2 and 2.157.0.ruff format --checkandruff checkpass.test_property_purchased.pyon 1.58.0: the household ratio is 1.33 with 2.122.2 and 0.70 with 2.157.0, both inside the full bound (0.5, 1.5) and theTESTINGone (0.25, 4).axiom: n/a: calibration target in the data build, no policy rule changed.
Evidence:
~/reviews/uk-data-sdlt-corporate-564/(check569.py, check569_2122.json, check569_latest.json, measure_manifest.py, measure_2157.json, components.py, components_2122.json, components_2157.json, reweight_experiment_v2.py, reweight_experiment_2157.json, reweight_experiment_2157_full.json, mut569.sh, tests569_*.log, and the pre-rebase measure.py, measure_2122.json, measure_2322.json, reweight_experiment.py, reweight_experiment.json).🤖 Generated with Claude Code