Repository navigation
Conversation
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 year, warn when that statistic is held flat (policyengine-uk before #2322), and refuse full builds against such a version. Fixes #564 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
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. |
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). The target keeps its name and reference.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 OBR year from 2024 to 2030, 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_receipts_sheet.py's synthetic workbook now carries £100bn a year so its SDLT row stays above the statistic.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(policyengine-uk 2.122.2). Calibratingstamp_duty_land_taxto the whole line therefore counted the corporate share twice.Measured
Microsimulation runs on the released 1.58.0
enhanced_frs_2024_25.h5(sha2565ba399ea…d960b6), which was calibrated to the old target. £bn, PolicyEngine years (2025 = 2025-26).cafd3f1dstamp_duty_land_taxon 1.58.0corporate_sdlton 1.58.0, with #2322With #2322, 1.58.0's household SDLT is 1.54 times the new 2025-26 target, and the new full-build test fails on it as it should. With 2.122.2 the ratio is 1.33. Weighted
corporate_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 645 national targets only, with #2322's head. The loss was the pipeline's symmetric relative error, run for 512 Adam steps at lr 0.1, once with the old SDLT target (OBR total, £15.41bn) and once with the new one (£10.60bn), all else equal. Both runs hit their SDLT target within 0.02% (£15.41bn and £10.60bn), and the final loss was the same to six decimal places (0.038323). No other target's relative error differs between the runs by more than 0.3 percentage points, and 93.5% of the other targets are within 10% in both. The pipeline also calibrates 650 constituencies jointly, so this shows the target is cheap to meet nationally, not what the full build will reach.Invariants
For every year the OBR forecasts (2024-25 to 2030-31):
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: dropping the subtraction fails the identity test. Dropping the non-residential part of the statistic fails the differential test.Guard and dependency
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…"statistics.*.householdfigures; that is separate.Tests
test_obr_sdlt_target.py,test_obr_receipts_sheet.py,test_build_environment.py,test_obr_efo_fallback.py,test_obr_nics.py,test_obr_nic_signal.py,test_target_registry.py: 80 passed, 5 skipped, under both policyengine-uk 2.122.2 and #2322's head.test_property_purchased.pyon 1.58.0: passes with 2.122.2 (full andTESTING=1bounds). With #2322 it passes underTESTING=1and fails the full bound (16.32 vs 10.60), as intended.axiom: n/a: calibration target in the data build, no policy rule changed.
Evidence:
~/reviews/uk-data-sdlt-corporate-564/(measure.py, measure_2122.json, measure_2322.json, sim1.py, stat.py, reweight_experiment.py/.json/.log).🤖 Generated with Claude Code