feat: add council-tax single-person discount#58
Merged
Conversation
This was referenced May 1, 2026
Households with exactly one adult (18+) now receive a 25% discount on the calculated council tax — Local Government Finance Act 1992 s.11(1)(a). Adds: - `single_person_discount_rate` field on `CouncilTaxParams` (default 0.25) - Updates `calculate_council_tax(hh, params, is_single_adult)` to apply the discount - Counts adults via `Person::is_adult()` (age >= 18) in `simulation.rs` - New `baseline_council_tax_calculated` / `reform_council_tax_calculated` per-household microdata columns - First-time exposure of `CouncilTaxParams` in the Python wrapper - 3 new Rust unit tests (band D + band A discount, zero-discount-rate edge) - 4 new YAML policy-test cases (`tests/policy/council_tax.yaml`) The baseline run still uses the FRS-recorded `hh.council_tax` for net income; the calculated value is for reform modelling, where now reforms to either band-D rate or the discount fraction take effect. Stacked on #57. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
85327fa to
8e4986e
Compare
2097c87 to
974dc21
Compare
vahid-ahmadi
added a commit
that referenced
this pull request
May 29, 2026
Mirrors the existing old-SP scaling pattern for the new-SP cohort: - If `person.state_pension > 0`: pass through, scaled by `(new_state_pension_weekly / baseline_new_sp_weekly)` for reform correctness - Else: fall back to `new_state_pension_weekly × 52` Previously the new-SP branch always returned the full parameter rate × 52, ignoring any recorded amount. This over-stated SP for partial- year claimants and broke parity for the pensioner_couple synthetic scenario in PR #53's parity harness (£946 diff). Implementation: - Plumb `baseline_new_sp_weekly` through `Simulation`, `calculate_benunit`, `calculate_state_pension`, and `person_state_pension`, parallel to the existing `baseline_old_sp_weekly` field - 3 new Rust unit tests (recorded-amount preserved, fallback to param when no record, recorded amount scales under reform) Parity-harness impact (synthetic pensioner_couple scenario): state_pension rust=23,000 py=23,000 diff=£0 (was £946) household_net_income diff=£-41 (was £905) Stacked on #58. Closes #59 (filed today as a follow-up to PR #53). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
Author
|
Rebased onto Now independent on |
# Conflicts: # interfaces/python/policyengine_uk_compiled/__init__.py # src/data/clean.rs
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.
Summary
Closes part of #42. Households with exactly one adult (18+) now receive a 25% single-person discount on the calculated council tax — Local Government Finance Act 1992 s.11(1)(a).
What's included
Engine (
src/parameters/mod.rs,src/variables/wealth_taxes.rs,src/engine/simulation.rs):single_person_discount_ratefield onCouncilTaxParams(default 0.25)calculate_council_tax(hh, params, is_single_adult)applies the discount when the flag is truesimulation.rscounts adults viaPerson::is_adult()(age >= 18) and passesadult_count == 1New microdata columns:
baseline_council_tax_calculatedandreform_council_tax_calculatedon each household row, so reform analyses can isolate the change without decomposing total_taxPython wrapper: first-time exposure of
CouncilTaxParams(the params struct existed in Rust but was never reformable from Python). Reform authors can now do:Rust unit tests (3): Band D discount, Band A discount, zero-discount-rate edge
YAML policy-test cases (4) in
tests/policy/council_tax.yaml: Band D two adults, Band D single adult, Band A single adult, Band D one adult + two children (children don't count for the discount)Changelog fragment under
changelog.d/added/Caveat — baseline runs are unchanged
Net income still uses the FRS-recorded
hh.council_taxamount, which already reflects whatever discount the household actually receives. The calculated value is for reform modelling. Wiring the calculated value into actual net-income computation is a follow-up — it'd require correctly handling Council Tax Reduction (CTR), exemptions, and devolved CTR variants (issues remaining under #42).Verified locally
cargo test: 162 passing (159 + 3 new)pytest interfaces/python/tests: 87 passingpython -m policyengine_uk_compiled.yaml_tests tests/policy: 29/29Stacking
vahid/council-tax-spd←vahid/dla-aa-from-flags(#57) ←vahid/pip-from-flags(#56) ←vahid/lbtt-ltt(#55) ←vahid/yaml-test-harness(#54) ←vahid/parity-harness(#53) ←vahid/from-situation(#52). Seven-deep stack.Out of scope (remaining for #42)
Test plan
single_person_discount_rateviaCouncilTaxParams🤖 Generated with Claude Code