Fix Universal Credit claimant and qualifying-child roles - #1850
Conversation
vahid-ahmadi
left a comment
There was a problem hiding this comment.
Review pass at e02e87a3 (Claude Code, high effort; fresh clone, ruff clean, the policy suite 1,157 passed, the non-microsimulation suite 211 passed / 15 skipped, towncrier draft renders under Fixed; the five new YAML files run against main's formulas: 22 of 25 cases fail there, so the regressions genuinely demonstrate the fix; an H5 with and without an is_uc_claimant column loads and the fallback produces the same result for the lone-parent case).
The core fix is right
Against reg 36 the standard allowance is single iff the claimant count is at most one and the older rate iff the eldest claimant is 25 or over, so joint with either partner 25+ takes the higher rate; against reg 22 the work allowance counts any non-claimant child or qualifying young person; against reg 24/24A/24B claimants are excluded from uc_child_index and uc_is_child_limit_affected, ordering stays eldest-first, and the Scottish two-child payment reads the flag so it follows. The is_child_or_qualifying_young_person_for_universal_credit predicate is deliberately unchanged and the tests pin that a young claimant still satisfies it. Both worked examples reproduce and hand-check (400.14×12 + 339×12 − 0.55×(12,000 − 684×12) = £6,784.08; 316.98×12 + 292.81×12 = £7,317.48). The diff touches exactly eleven files, no benunit or family variables and no parameters.
Findings
1. Should-fix — the fallback demotes a young cohabiting partner to a "child" of the unit. is_uc_claimant.py:24-32 makes a person a claimant if head, parent, or adult-and-not-QYP. A 24-year-old with a 19-year-old partner in non-advanced education and no is_parent or is_uc_claimant inputs (I ran it): the PR gives is_uc_claimant [True, False], SINGLE_YOUNG, the partner assigned uc_child_index 1 with the higher first-child element, the work allowance switched on, UC £7,871.76; main gives COUPLE_YOUNG and £10,038.60. So a reg 3 couple becomes a single claimant plus a qualifying child, which is a new wrong live result on the fallback path, and the household calculator and API never supply is_uc_claimant. The body names the ambiguity but the fallback resolves it the wrong way. A rule that only demotes an adult QYP when another member of the unit has is_parent true keeps every adult a claimant otherwise; every new YAML that relies on demotion already sets is_parent, so they would still pass.
2. Should-fix — the claimant exclusion is not applied to the disability additions to the child element (reg 24(2)). uc_individual_disabled_child_element.py:14-16 and the severely-disabled counterpart still read the raw predicate. An 18-year-old disabled claimant in education in their own unit gets uc_individual_disabled_child_element £1,905.12 for themselves while their main child element is correctly zero. Pre-existing on main, which also paid the main element, but the PR's stated claim is that claimants are excluded from their own child element.
Questions
- For microcosm's consumers: the readers that apply the exclusion are
uc_child_index,uc_is_child_limit_affectedandis_uc_work_allowance_eligible; the two disability elements do not (finding 2), anduc_is_child_born_before_child_limitdoes not but is gated by the index. Any measure counting "own qualifying children" must go through the index or the flag, never the raw predicate, or young claimants leak into the two-child-limit denominator. That is the contract #883 needs. - Fallback sensitivity to education defaults.
current_educationdefaults an 18–19-year-old to TERTIARY, so the headline lone-parent example only works whenis_in_non_advanced_education(and for 19-year-olds the two further inputs) are supplied; without them the 18-year-old is a second claimant and the unit isCOUPLE_OLDat £7,537.20, the same as main. Not a regression; worth stating in the docstring. - Work allowance scope change beyond QYPs: 16–17-year-olds not in education no longer count and under-16 looked-after children are excluded. That is the law, but it is a behaviour change the body does not name.
- No exceptions machinery exists for the two-child limit (multiple births, adoption) anywhere under
universal_credit, so there was nothing for the reordering to break; the body should not be read as implying it was preserved.
Nits
The fragment is .fixed.md; a new input variable is arguably added → minor. uc_child_index ranks by integer age rather than date of birth, so twins tie arbitrarily but count consistently (pre-existing).
Nothing else blocks; the substance is correct and well tested. 1 is a genuine new regression on the fallback path and needs fixing before merge; 2 completes the claim the PR makes.
Refs PolicyEngine/microcosm#882
An 18- or 19-year-old dependent in qualifying education can be counted as an adult when selecting a Universal Credit standard allowance, while being omitted from the work allowance's child test. A young claimant in education can also satisfy the qualifying-child predicate and receive their own child element or consume a place under the two-child limit.
This adds a person-level
is_uc_claimantinput and fallback to identify the claimant or partner separately from dependent members. The standard allowance uses claimant count and age. Work-allowance eligibility counts nonclaimant children and qualifying young people. Child-element ordering, both disability additions and the affected-child flag exclude claimants. Benefit-unit membership and generic family variables retain their existing definitions.The fallback includes the head, identified parents and adults, excluding an adult qualifying young person only when the benefit unit contains an identified parent. This keeps a young cohabiting partner in qualifying education as a claimant when no parent or claimant-role inputs are supplied. Dataset producers should provide
is_uc_claimantfrom recorded relationships. Ambiguous relationships in units with identified parents, and nonqualifying adult dependants, still need explicit role inputs.Qualification also depends on education inputs. The generic education default treats 18–19-year-olds as being in tertiary education. The lone-parent examples therefore supply non-advanced education; the 19-year-old case also supplies the qualifying entry-age and terminal-date conditions. The claimant flag identifies roles, rather than establishing UC eligibility.
The work-allowance change also excludes 16–17-year-olds outside qualifying education or training and children looked after by a local authority from its child-based route. Disability additions remain payable for qualifying nonclaimant children, including those beyond the ordinary two-child element limit.
This work does not implement the under-18 UC eligibility exceptions or the two-child-limit exceptions machinery. Child ordering still uses the model's integer ages; it does not reconstruct exact dates of birth or resolve twin ordering by birth date.
Synthetic regressions cover lone parents with 18/19-year-old qualifying children, young cohabiting couples without role inputs, genuine couples with an older child, explicit claimant inputs, young claimants with their own children, and both disability tiers for claimants and dependants. They assert final UC awards as well as component amounts and child ordering. With the explicit parent/education inputs and zero capital and deductions used in the 2025 regressions, a lone parent earning £12,000 with an 18-year-old qualifying child receives £6,784.08 annual UC; an 18-year-old parent with one child receives one child element and £7,317.48 UC.
Companion pipeline PR: Microcosm #883, stacked on Microcosm #879. Its own-qualifying-child measurements exclude recorded claimant roles before aggregating within each person's benefit unit. Its statistical two-child-limit cohort deliberately differs from the model's element-denial flag; that statistical definition does not imply that the engine implements exception rules. The broader tracking issue stays open for the remaining pipeline and target-measurement work.
The changelog separates the new public claimant input (
added) from the UC calculation corrections (fixed); no manual version update is included.Local validation used the checked-out PolicyEngine-UK 2.96.1 source on the current
mainbase (5e57717f), core 3.32.3 and Python 3.13.14:uv run --no-sync policyengine-core test policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit -c policyengine_uk— 138 passed.uv run --no-sync policyengine-core test policyengine_uk/tests/policy/baseline/finance/benefit/child_qualifying_young_person.yaml -c policyengine_uk— 8 passed after adding the missingis_parent: trueinput to its intended parent; expected child indices stayed unchanged.uv run --no-sync policyengine-core test policyengine_uk/tests/policy -c policyengine_uk— 1,164 passed on the final full-suite run.uv run --no-sync pytest policyengine_uk/tests/ -m 'not microsimulation' --maxfail=0 -q— 211 passed, 15 skipped, 29 deselected.is_uc_claimant.py; the affected eight-file policy YAML check then passed 42 cases.uv run --no-sync towncrier build --draft— passed, rendering both Added and Fixed sections without writing files.7b365884) — all six checks passed, covering tests, docs, lint and Python 3.11–3.14 imports.The independent Microcosm consumer check passed six synthetic cases at
9d13d1dd, including a young claimant who also satisfies the raw qualifying-child predicate. No Microcosm source changed in this update. Dataset-dependent microsimulation tests and a population rebuild were not run; this update contains model code and synthetic tests only.