Conversation
is_household_head is an input, so it can flag several members of a household, or none. Council Tax Reduction settled on one head, but rent, non-dependant and maintenance-loan variables read the raw flag, so a malformed input left programmes disagreeing about whose household it is: two flagged families each got half the rent while one was neither a claimant nor a non-dependant, a couple both flagged had the rent counted twice, and a household with no one flagged had no rent at all. Add is_resolved_household_head (the eldest flagged member, else the eldest member; exactly one per household), define benunit_contains_household_head from it, and point every reader of the raw flag at one of the two. This is the rule council_tax_reduction_household_head had, so that variable goes. Survey data has exactly one flagged head per household, so results on it do not change. Fixes #2059 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 2, 2026
Draft
share_of_household_rent falls back to the head's family only when no one in the household is liable for the rent, which needs the head's family to have no claimant: a family of children under 16. The generator never made one, so a mutation that put that fallback back on the raw is_household_head flag went uncaught. It now makes a family of one child under 16 a quarter of the time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
share_of_household_rent gives the whole rent to the head's family when no one in the household is liable for it. That happens only when the head's family has no claimant, and random generation almost never builds a household where it also disagrees with the raw flags. Two explicit examples do: a household of two children with no one flagged beside a household that has the input, and two flagged children where the elder is alone in its family. With them, both properties catch a mutation that puts the fallback back on the raw flag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
Author
|
Composition note from #2078 (also stacked on #2009), which makes the CTR means test assess the applicant and partner rather than the benefit unit's claimant and partner. #2078 adds two variables that read
So does its property test,
|
This branch has not been deployed
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.
Fixes #2059
Stacked on #2009 (
ctr-claimant-household-head), which is stacked on #2006 and #1896. It merges after them.What this changes
is_household_headis an input, so it can flag several members of a household, or none. Council Tax Reduction settled on one head (#2009), but rent, non-dependant and maintenance-loan variables read the raw flag, so a malformed input left programmes disagreeing about whose household it is.This PR adds one person-level head and points every reader at it.
is_resolved_household_head(new, Person). Exactly one member of each household. It is Give Council Tax Reduction to the household head's family, not the oldest adult's #2009's rule, moved out of Council Tax Reduction:benunit_contains_household_headis now defined from it, and moves from the Council Tax Reduction folder tohousehold/demographic/.council_tax_reduction_household_head(added by Give Council Tax Reduction to the household head's family, not the oldest adult's #2009, not yet onmain) is removed. It was the same rule under a Council Tax Reduction name.is_household_headkeeps its formula and stays the input. Its documentation now points readers to the resolved head.No formula other than
is_resolved_household_headreadsis_household_headany more.is_liable_for_household_rent,share_of_household_rent,pays_rent_to_householderbenunit_contains_household_headpersonal_rentis_resolved_household_headfor who carries the family's rent,benunit_contains_household_headfor the familyis_non_dependant_of_household_head,uc_non_dep_deductions,LHA_allowed_bedrooms,housing_benefit_LHA_allowed_bedrooms,lha_renter_has_non_dependantbenunit_contains_household_headmaintenance_loan_living_arrangement,maintenance_loan_household_incomeis_resolved_household_headcouncil_tax_reduction_liable_personis_resolved_household_headdynamics/participation.pyWhat a malformed input did, and does now
Household calculations for a home with a £12,000 rent. "Before" is #2009's head,
4b84ea069.The first row is finding 8 of the review of #2009. In every row the result now equals the result with only the resolved head flagged.
Invariants
For any flag input (one member, several, both members of a couple, everyone, none, or no input):
benunit_contains_household_headholds for the head's family and no other.is_household_headinput, is identical to the simulation whose input flags exactly the resolved head.is_resolved_household_headreadsis_household_head.Survey data: where every household has exactly one flagged head, the resolved head is the flagged head, so nothing changes (measured below).
Tests
household/demographic/resolved_household_head.yaml, 8 cases: two flagged families, a couple both flagged, none flagged, none flagged with a tie, two flagged beside a sharer, a flagged lodger, a flagged student, and a household with no input beside one that has it. Each checks rent, Universal Credit and Housing Benefit non-dependants and Council Tax Reduction together. All 8 fail on Give Council Tax Reduction to the household head's family, not the oldest adult's #2009's head.test_household_head_agreement_properties.py. Invariants 1 to 3 against an independent implementation of the rule; invariant 4 by comparing every computed array between the two simulations; invariant 5 by scanning the package source.share_of_household_rentreads the head only when no one in the household is liable for the rent, and the generator never built such a household. The test now generates families of a child alone and has two explicit examples of it, and both properties catch that mutant. The full run of all 18 mutants against the final test is in progress.household,finance/benefit/family,gov/local_authorities,gov/dfeandgov/dwp: 815 passed.test_deterministic_variables.py, the maintenance-loan proxies and the participation tests: 63 passed.make testruns in CI by dispatch, because PR CI only runs for PRs intomain: run 37017050713 on057718b7f: green, 1,527 YAML and 2,094 pytest passed (1 skipped).Enhanced FRS: no change
Real
Microsimulationruns of base4b84ea069(#2009's head) against this branchdc149042f, on the same dataset file each time.e433e532)e433e532)ed1a2991)household_net_income, plusis_household_head,household_weightandpension_credit. The base'scouncil_tax_reduction_household_headis compared withis_resolved_household_head.Coordination with other open PRs
These open PRs add formulas that read
is_household_headdirectly. Whichever lands second has to readbenunit_contains_household_headoris_resolved_household_headinstead; invariant 5's test fails until it does.legacy_benefits_home_letting_income,rent_from_boarders_and_lodgers,pays_rent_to_householderLHA_additional_bedrooms,LHA_allowed_bedrooms,housing_benefit_LHA_additional_bedroomsuniversal_credit_renter_has_non_dependanthousing_benefit_LHA_allowed_bedrooms,non_dependant_normally_resides_withhousing_benefit_LHA_allowed_bedrooms,housing_benefit_shares_accommodation,joint_tenant_in_household_head_household01d140e77, after this PR was opened)uc_non_dependants_counted. #2006 lands before this PR, so the switch is made here when this PR mergesmain.#2015 and #2078 (both stacked on #2009) add
council_tax_reduction_head_applies_aloneandis_council_tax_reduction_applicant_or_partner, which readcouncil_tax_reduction_household_head, the variable this PR removes; #2078 also reads it intest_council_tax_reduction_applicant_properties.py. It has the same formula asis_resolved_household_head, so whichever lands second renames those reads and changes nothing else. A deleted variable fails loudly (VariableNotFound), so a missed read cannot pass CI.Not in this PR
axiom: n/a: model-structure change. It resolves a model input (which household member is the head) and changes no provision, rate or eligibility rule.
🤖 Generated with Claude Code