Repository navigation
Stop charging property stocks as purchases when a household buys its home - #2241
Conversation
…home additional_residential_property_purchased was other_residential_property_value times property_purchased, so any household that bought a home had its whole stock of other residential property charged as one additional-dwelling purchase at the higher rates; non_residential_property_purchased did the same with the non-residential stock. property_purchased now sets only the main-residence purchase. The additional-dwelling and non-residential purchase prices are inputs, nil unless set directly or imputed by a dataset, and are uprated like the stocks when a dataset supplies them. Fixes #2238 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 1 on #2241: the main-residence flag alone never creates an additional-dwelling purchase. A new main home bought while another dwelling is kept pays the higher rates, so it belongs in the additional-dwelling input, which holds purchases already classified as higher-rates transactions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
PE-UK main fix landed (UK hub, 2026-10-11 21:42Z): #2349 merged as |
vahid-ahmadi
left a comment
There was a problem hiding this comment.
Looks right, fine to approve. The two should items are about landing it with the data, not the model code.
@MaxGhenis, the bug is real and the fix is the right shape. A purchase of a main residence is one land transaction (FA 2003 s. 55). It doesn't buy the household's stock of other property, and the old formula charged that whole stock at the higher rates. Turning the two purchase prices into plain inputs is the honest fix until the data imputes them. Nothing else in the model reads additional_residential_property_purchased or non_residential_property_purchased except the SDLT, LBTT and LTT transaction formulas.
-
should: the uk-data SDLT calibration target will sit far above the household SDLT this PR leaves. policyengine-uk-data
targets/sources/obr.py:264maps the OBR "Stamp duty land tax" receipts line tostamp_duty_land_tax, andbuild_loss_matrix.pyuses it as a plain GBP target in the national calibration.- Your own run has household SDLT falling from £16.32bn to £7.62bn in 2025.
- PolicyEngine/policyengine-uk-data#564 already plans to take the corporate share out of that target. Even then, its 2026-27 household target is £12.17bn.
- So the next data build on this model will try to close a gap of roughly £4bn in 2026-27 (about £8.2bn modelled against £12.17bn) by raising the weights of the 3.85% of households that drew a purchase. That would distort everything else those households carry, unless policyengine-uk-data#556 lands in the same batch.
- Could the batch notes say that policyengine-uk-data#564's rescoped target goes in only together with policyengine-uk-data#556? Otherwise
obr/sdltshould be dropped until the HRAD imputation is in.
-
should: the 3.85% purchase draw now means a main-residence purchase, but its rate is all transactions. policyengine-uk-data
datasets/frs.py:1853-1879setsPROPERTY_PURCHASE_RATE = 0.0385from 1.1m UK transactions a year. That includes the 211,700 HRAD transactions and the 124,500 residential purchases by non-individuals. This PR relabelsproperty_purchasedas "Main residence bought this year" (property_purchased.py:5). When policyengine-uk-data#556 adds a separate additional-dwelling draw, those HRAD transactions are counted twice unless the main-residence rate comes down. policyengine-uk-data#556 doesn't mention this yet. It is worth adding there rather than changing anything here. -
nit: a non-replacement main-residence purchase is still charged at main rates. Under FA 2003 Sch 4ZA, a buyer who keeps another dwelling and isn't replacing their main residence pays the higher rates on the new main home. You disclose this in the body as earlier than this PR. A one-line note in the
main_residential_property_purchaseddocstring would keep it visible after merge.
Interaction with #2235. No file overlap, and the merge with main is clean. Together, the two PRs take 2025 SDLT from about £16.3bn of household SDLT plus a second copy of corporate SDLT down to about £7.6bn household plus £3.1bn corporate. #2322, stacked on #2235, would raise the corporate part to £4.8bn. Either way the model moves from above HMRC's total to below it until policyengine-uk-data#556 lands. That is the right direction for each fix on its own. It does mean the SDLT level will be low in any release cut between this merge and the data batch.
What I checked
- I read the PR body, the full diff, the comments, and policyengine-uk-data#556 and policyengine-uk-data#564. There are no earlier reviews.
- I read FA 2003 s. 55 and Sch 4ZA on legislation.gov.uk.
- I hand-computed the YAML expectations. In 2025, £600,000 at main rates is 2,500 + 17,500 = £20,000. In 2022, £400,000 at the higher rates is 3,750 + 6,250 + 12,000 = £22,000 on top.
- At head
5ac1df3f, in a freshuv syncenvironment in a worktree:- YAML under
gov/hmrc/stamp_duty,gov/wraandhousehold: 155 passed; test_property_transaction_tax_purchase_properties.py: 4 passed;ruff checkandruff format --checkon the 4 changed Python files are clean.
- YAML under
- I reverted the two purchase variables to main and ran
property_purchased_default.yamlagain: the stock case fails, as it should. - I grepped the model for every reader of the two purchase variables. I grepped policyengine-uk-data main for anything that stores them (nothing does) and for SDLT calibration targets (finding 1).
- Process:
changelog.d/2238.fixed.mdis the right type, and theReproducer:andaxiom:lines are present. Fix evidence CI passed. - I did not re-run the national tables or the replay. CI is green at this head.
🤖 Generated with Claude Code
vahid-ahmadi
left a comment
There was a problem hiding this comment.
Approving, per my review comment above.
|
UK hub, 2026-10-15: adopted. The creating session has been idle since 10/10, and the Budget replay needs this before 28 October.
|
|
Landed by the UK hub (merge queue, pinned to head
|
…into uc-hb-split-shared-cap-lha-rules No conflicts. Main's two edits to files this PR also changes (test_benefit_cap_specified_benefits_properties.py: REBALANCED_YEARS from REBALANCING_YEARS; test_legacy_award_readers_properties.py: comment) merged automatically. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes #2238.
What was wrong
additional_residential_property_purchasedwasother_residential_property_value × property_purchased.property_purchasedis the dataset's purchase flag: a seeded draw on 3.85% of households in the data build (3.3% weighted), set to match all UK housing transactions. So every household that bought a home had its whole stock of other residential property charged as a single additional-dwelling purchase at the higher rates. The variable's own documentation says "Only include the value of a single purchase."non_residential_property_purchased(defined_for = "property_purchased",adds = ["non_residential_property_value"]) did the same with the non-residential stock, so I fixed it here too.The issue's reproducer: a £600,000 main residence, £2,000,000 of other residential property, 2025. Buying the home cost £233,750 of SDLT, of which £213,750 was higher-rates SDLT on the £2m stock.
The law
SDLT is charged on the chargeable consideration for a land transaction (Finance Act 2003 s. 55). The higher rates for additional dwellings (Schedule 4ZA) apply to a purchase of a dwelling by someone who will own another dwelling and is not replacing their main residence. A stock of property already owned is not a transaction, and buying a main residence does not purchase that stock.
Changes
property_purchasednow means "bought its main residence this year". It sets onlymain_residential_property_purchased, as before.additional_residential_property_purchased(the price of one additional dwelling) andnon_residential_property_purchasedbecome inputs, nil unless set directly or by a dataset that imputes the purchases. They are listed with the property stocks indata/uprating_indices.yaml, so imputed prices are uprated the same way.This follows the replay's own construction rule (scorecard#157,
build_uk_event_registry.py): "The certified population must contain additional-home purchase values; absence yields an inert leg rather than an inferred national tax base."Tests
Examples (YAML). The issue's three cases:
Properties (
test_property_transaction_tax_purchase_properties.py, Hypothesis, households across England, Northern Ireland, Scotland and Wales, 2022-2027). Invariants:Stocks are never purchases: zeroing other residential and non-residential property leaves SDLT, LBTT, LTT and their expected values unchanged.
The main-residence flag alone never creates an additional-dwelling purchase. With that input unset, residential SDLT equals the main-rates tax on the main-residence price.
The additional-dwelling input is a purchase already classified as a higher-rates transaction. A new main home bought while another dwelling is kept, with no replacement of a main residence, pays the higher rates (Sch 4ZA), so it belongs in that input, not the main-residence price. That classification limitation predates this PR.
An additional-dwelling purchase adds exactly the higher-rates tax on its own price (nil below £40,000), and the higher rates are never below the main rates.
SDLT never falls as the additional-dwelling price rises.
Mutation check. On
main, invariants 1 and 2 fail. Invariant 2 leaves the input unset, so it exercises the old formula.Targeted suites. 159 YAML tests under stamp duty, LTT, council tax and household pass.
National impact
Data:
enhanced_frs_2024_25.h5@1.58.0, this branch againstmainat 6d89338 (this branch merged with it); the figures are unchanged from the run against e89b8ab, full microsimulation (£bn, branch minus main).stamp_duty_land_taxland_and_buildings_transaction_taxexpected_lbttland_transaction_taxgov_tax/gov_balanceWhat the figures cover:
Against HMRC. UK Stamp Tax Statistics 2024-25, England and Northern Ireland: Tables 6b and 8a.
On
main, FY2024-25 residential SDLT was £14.75bn: £6.99bn on main residences and £7.76bn at the higher rates. The higher-rates part sat on 172k households with a £104bn base.HMRC's 2024-25 figures:
So the old base charged more higher-rates SDLT to households than HMRC collects from all HRAD buyers.
After this PR households pay no higher-rates SDLT unless a dataset supplies the purchases. Individuals paid at least £4.2bn of HRAD receipts in 2024-25 (£5.44bn less at most £1.23bn from non-individual buyers). That gap stays until the data imputes additional-dwelling purchases. The issue's other suggestion was imputing one purchase price, or a purchase probability on the stock, calibrated to HMRC's HRAD statistics. That imputation and calibration belongs in policyengine-uk-data, which owns the purchase draw: PolicyEngine/policyengine-uk-data#556.
For the replay, Autumn Budget 2024's 2pp HRAD rise becomes an inert leg on data without imputed purchases, rather than 2pp of the £94.7bn inferred base.
Fiscal-event replay: Autumn Budget 2024 HRAD +2pp
Real microsimulation runs of the replay construction on the proposed certified data (
enhanced_frs_2024_25.h5@1.58.0), this branch againstmain6d89338. The construction is copied from policyengine-scorecardpipeline/build_uk_event_registry.pyon main, snapshotted 2026-10-11 (event_replay.py). Gain to the Exchequer, £bn.Forward delta: higher-rates scale 5/7/10/15/17% from 31 October 2024. The processed tree samples 30 April, so FY2024 has no effect in either run.
stamp_duty_land_taxInput support: not met on 1.58.0, so the national exposure is inert. The dataset has no
additional_residential_property_purchased, and this PR stops inferring it from the stock. The zero is that missing input, not an estimate of the measure. Read it as "not computable on this data", as the replay now does, never as an improvement on main's £2.2bn.The input belongs in the uk-data d833 batch: PolicyEngine/policyengine-uk-data#556 proposes a seeded draw calibrated to HMRC's HRAD transactions. Its design (purchase rate, price distribution) is a methodology choice for Max, queued with the batch decision.
Not in this PR (found while fixing it)
Filed as their own issues:
expected_sdltaddsproperty_sale_rate(+£0.045 per household) whereexpected_lbttandexpected_lttmultiply by it. Count corporate SDLT once and let SDLT abolition remove all of it #2235 fixes this, along with a corporate SDLT double count.expected_lbttandexpected_lttdiscount twice. Their transaction taxes already sit on the dataset's purchase draw, and the 4.5% sale rate is applied on top: in 2025, £2.21bn of LBTT becomes £0.10bn expected. expected_sdlt adds the property sale rate; expected_lbtt and expected_ltt discount purchases twice #2246.axiom: n/a: input mapping only. No SDLT, LBTT or LTT rule changes; the Schedule 4ZA conditions and rates are untouched. rulespec-uk has no FA 2003 SDLT module (rulespec-uk#63).
🤖 Generated with Claude Code
Reproducer:
policyengine_uk/tests/policy/baseline/gov/hmrc/stamp_duty/property_purchased_default.yaml::Issue 2238 - no purchase, no stamp duty,policyengine_uk/tests/policy/baseline/gov/hmrc/stamp_duty/property_purchased_default.yaml::Issue 2238 - buying the main residence does not charge the other-property stock,policyengine_uk/tests/policy/baseline/gov/hmrc/stamp_duty/property_purchased_default.yaml::Issue 2238 - an additional-dwelling purchase is charged on its own price,policyengine_uk/tests/test_property_transaction_tax_purchase_properties.py::test_property_stocks_are_never_purchases,policyengine_uk/tests/test_property_transaction_tax_purchase_properties.py::test_main_residence_flag_never_creates_an_additional_purchase,policyengine_uk/tests/test_property_transaction_tax_purchase_properties.py::test_additional_purchase_is_charged_on_its_own_price