Repository navigation
Read one resolved household head in every programme - #2062
Merged
Merged
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,
|
…ead's documentation - Add STUDENT_FLAGGED_BESIDE_PARENT as an explicit example in both household-head properties: a higher-education student flagged beside an earning parent in an owner-occupied home, the only case where the maintenance loan's living-arrangement and household-income proxies turn on the head. Both raw-reader mutants are caught with it. - Say in is_resolved_household_head's documentation that it is chosen from the input and age alone, not from who owns or rents the accommodation. - Make invariant 5 of the CTR claimant properties say what its mask checks: exactly one flagged member, or no input anywhere. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…head Conflicts: - LHA_allowed_bedrooms, lha_renter_has_non_dependant, uc_non_dep_deductions: take main's (#2006) `counted = benunit("uc_non_dependants_counted", period)`. - housing_benefit_LHA_allowed_bedrooms: take main's (#2006) rewrite, which reads is_liable_for_household_rent and no head. - test_council_tax_reduction_claimant_properties docstring: main's invariant 4, this branch's invariant 5. New raw reader from main: uc_non_dependants_counted now reads benunit_contains_household_head. The household-head property test computes it, and its Universal Credit assertion follows #2006's rule: deductions fall only on the counted claim, at most one per household, and on the head's family wherever that family is liable for the rent and claims. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's uc_non_dependants_counted (UC Regs 2013 Sch 4 para 9(2)(f)) gives the household's non-dependants to the head's family's Universal Credit claim if it claims, else to the first liable family that does. The resolved head here is 70, over State Pension age and not eligible for UC, so the sharer's claim carries the £1,000 deduction. The well-formed twin (only the elder flagged) gives the same values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review after the merge of main (finding 3): moving uc_non_dependants_counted onto the resolved head was guarded only by a random example. With the head and a sharer both flagged beside a non-dependant, main's raw-flag reading counts the non-dependant in both renters' Universal Credit claims, so the deduction is taken twice and both get a bedroom for them; UC Regs 2013 Sch 4 para 9(2)(f) allows one. Add that household as a YAML case and as an explicit example of both household-head properties. Also, from the same review: - the first YAML case's comment says what main did: the younger flagged adult was neither a CTR claimant nor a UC or HB non-dependant (CTR did treat them as a non-dependant); - the property test's UC invariant says which part restates the formula; - is_household_head's documentation says its default applies only where no one in the simulation has the input. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mple Review after-merge-delta, nit 1: no explicit example told #2006's head-first order in uc_non_dependants_counted from the order the families are given. The new HEAD_LISTED_AFTER_SHARER household flags no one, so the eldest member is the head, and lists the head's family after a sharer's, beside a non-dependant, all claiming Universal Credit. The head's family's claim counts the non-dependant; read from the raw flag, or without the head-first order, the sharer's would. Both properties carry it. Nit 2: the YAML comment on the head-and-sharer case now separates what UC Regs 2013 Sch 4 para 9(2)(f) requires (only one claim counts the non-dependant) from the model's choice of the head's family's claim. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
marked this pull request as ready for review
October 6, 2026 00:44
MaxGhenis
added a commit
that referenced
this pull request
Oct 6, 2026
Brings in #2062 (one resolved household head) since 60821c7. #2062 deletes council_tax_reduction_household_head, which this PR's applicant files read; the merge had no textual conflict, so without this change every Council Tax Reduction calculation on the merged tree fails with VariableNotFound. As agreed on #2062 ("whichever lands second"), the two reads in is_council_tax_reduction_applicant_or_partner and council_tax_reduction_head_applies_alone now use is_resolved_household_head, which has the same formula. Nothing else changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5 tasks done
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
#2009 and #2006, which this PR was stacked on, have merged; it now targets
mainand hasmain(60821c713) merged in.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, chosen from theis_household_headinput and age alone. 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/. It keeps its name, so code that reads it by name is unaffected by the move.council_tax_reduction_household_head(added by Give Council Tax Reduction to the household head's family, not the oldest adult's #2009) 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_dependants_countedbenunit_contains_household_headmaintenance_loan_living_arrangement,maintenance_loan_household_incomeis_resolved_household_headcouncil_tax_reduction_liable_personis_resolved_household_headdynamics/participation.pyuc_non_dep_deductions,LHA_allowed_bedrooms,housing_benefit_LHA_allowed_bedroomsandlha_renter_has_non_dependantalso read the raw flag when this PR was opened. #2006 has since rewritten them onmain:uc_non_dep_deductions,LHA_allowed_bedroomsandlha_renter_has_non_dependantnow readuc_non_dependants_counted, which this PR moves, andhousing_benefit_LHA_allowed_bedroomsreadsis_liable_for_household_rent. This PR leaves them asmainhas them.What a malformed input did, and does now
Household calculations for a home with a £12,000 rent. "Before" is
mainat60821c713(every value the same as at6b366b281).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.uc_non_dependants_counted, Give sharers, boarders and lodgers their own rent, tenure and LHA category #2006's rule under UC Regs 2013 Sch 4 para 9(2)(f)). At most one family in a household is counted, and where the head's family is liable for the rent and claims Universal Credit, it is the head's. Only families with a share of the rent have Housing Benefit non-dependant deductions.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, 9 cases: two flagged families, a couple both flagged, none flagged, none flagged with a tie, two flagged beside a sharer, the head and a sharer both flagged beside a non-dependant, a flagged lodger, a flagged student, and a household with no input beside one that has it. Together they cover rent, Universal Credit and Housing Benefit non-dependants and size criteria, Council Tax Reduction, boarders and lodgers, and the maintenance loan; most cases check several of these at once. All 9 fail onmain(60821c713, with theis_resolved_household_headlines dropped), each on an intended output.main, the "two flagged beside a sharer" case follows Give sharers, boarders and lodgers their own rent, tenure and LHA category #2006's rule: the resolved head is 70, over State Pension age and not eligible for Universal Credit, so the sharer's claim counts the non-dependant and carries the £1,000 deduction (UC Regs 2013 Sch 4 para 9(2)(f)). The same household with only the resolved head flagged gives the same values.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. Explicit@examplehouseholds cover branches random generation rarely reaches: no one liable for the rent, a student flagged beside an earning parent, the head and a sharer both flagged beside a non-dependant, and no one flagged with the head's family listed after a sharer's.main(057718b7f, and273762480for the two maintenance-loan mutants): 18 mutants, each putting one reader back on the raw flag or breaking the rule. All 18 were caught by at least one property: the comparison test (invariant 4) caught all 15 readers put back on the raw flag, and the direct test (invariants 1 to 3) caught all 3 rule mutants. The first runs found three survivors, each a branch random generation never reached:share_of_household_rent's fallback when no one in the household is liable, and the two maintenance-loan proxies, which turn on the head only for a student in higher education with an earning sponsor, in an owner-occupied home. The test now generates families of a child alone and carries three explicit@examplehouseholds for those branches.mainat6b366b281(5b27c0ae0): 14 mutants, since the four readers Give sharers, boarders and lodgers their own rent, tenure and LHA category #2006 rewrote no longer read the head (their five mutants are gone) anduc_non_dependants_countedgets one. All 14 were caught: the comparison test caught all 11 readers put back on the raw flag, and the direct test caught all 3 rule mutants. The comparison test caughtuc_non_dependants_countedonly through a random example, so two explicit examples now pin it:5cacd4959), both properties pass, and both fail withuc_non_dependants_countedback on the raw flag;48043f73c), it makes both properties fail withuc_non_dependants_countedon the raw flag, and the direct test fail without Give sharers, boarders and lodgers their own rent, tenure and LHA category #2006's head-first order (the comparison test cannot catch that one, since both of its simulations use the same order).48043f73c: the readers are unchanged since5b27c0ae0, Apply the UC minimum income floor only to claimants subject to all work-related requirements #2081 (in the merge ofmainat60821c713) changes nothing they depend on, and adding@examplehouseholds leaves Hypothesis's random draws as they were.48043f73cchanges only the property test and a YAML comment from5cacd4959, the merge ofmain:household,finance/benefit/family,gov/local_authorities,gov/dfeandgov/dwp: 1,670 passed on5cacd4959;resolved_household_head.yaml(9 cases) passed on48043f73c.test_deterministic_variables.pyandtest_dynamics_demographics.py) and thecode_healthfolder: 2,169 passed on5cacd4959.test_household_head_agreement_properties.pyin full: 3 passed on48043f73c.ruff format --check: clean.make test): passed on00126a3d9(run 37374413632) and on48043f73c(run 37391222133).Enhanced FRS: no change
Real
Microsimulationruns ofmainat60821c713against this branch at5cacd4959, on the same dataset file. The model code is the same at48043f73c, which changes only tests.e433e532)household_net_income, plusis_household_head,household_weightandpension_credit. The trace finds 138 on each side, the same apart from the renamed head variable.main'scouncil_tax_reduction_household_headis compared withis_resolved_household_head.mainat6b366b281against5b27c0ae0, also found all 141 bitwise identical.4b84ea069, branchdc149042f) found the head-dependent variables bitwise identical for 2025 and 2026 and on Give sharers, boarders and lodgers their own rent, tenure and LHA category #2006's sharer, boarder and lodger build, and all 982 model variables identical for 2026.Coordination with other open PRs
These open PRs add formulas that read
is_household_headdirectly. With this PR onmain, each 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_householdhousing_benefit_LHA_cannot_share_bedroomsand the helpers it refactorscouncil_tax_reduction_individual_non_dep_deduction_eligible#2006's
uc_non_dependants_counted(from its commit01d140e77, after this PR was opened) is switched here, in the merge ofmain.#2015 and #2078 (#2078 is stacked on #2015) 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 each renames those reads and changes nothing else. A deleted variable fails loudly (VariableNotFound), so a missed read cannot pass CI.Not in this PR
hrpid == 1).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