Skip to content

Store the tax credit severe disability flag, not a mix of premium tests - #494

Draft
MaxGhenis wants to merge 3 commits into
mainfrom
severe-disability-flag-tax-credit-definition
Draft

MaxGhenis wants to merge 3 commits into
mainfrom
severe-disability-flag-tax-credit-definition

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Draft until PolicyEngine/policyengine-uk#1946 is released. Then, in this PR:

  1. bump the lock (policyengine-uk>= that release in pyproject.toml; uv lock --upgrade-package policyengine-uk);
  2. confirm test_severe_flag_matches_the_policyengine_uk_formula passes;
  3. merge.

The build, calibration and release manifest all use the locked policyengine-uk, which is 2.93.0 today. On that version the stored flag still drives the legacy severe disability premium. Paired with an older model, this data lowers net income by £7.4m (2024), £8.5m (2025) and £8.1m (2026), mostly CTR. Those figures come from real policyengine-uk runs at main 44240bd8: the original Enhanced FRS against a copy whose stored flag is recomputed with this definition. The differential test below fails until the lock includes #1946. That is deliberate: CI stays red until the lock includes #1946, so the PR cannot pass a green-checks merge gate with an older model. The repository has no required status checks, so this is a signal rather than a GitHub-enforced block; the draft status and this note are the hard stop. After the bump it passes, as it does locally against #1946's code.

Summary

add_disability_benefit_flags_from_reported_amounts stored is_severely_disabled_for_benefits as:

Attendance Allowance > 0 | DLA care ≥ highest rate | PIP daily living ≥ enhanced rate | afcs_reported > 0

policyengine-uk uses that flag for three things:

Until PolicyEngine/policyengine-uk#1946 it also drove the legacy severe disability premium, which has a wider list. The stored flag served neither correctly:

  • Tax credits (CTC Regs 2002 reg 8; WTC Regs 2002 reg 17):
    • Only higher-rate Attendance Allowance counts (WTC reg 17(2)). Children cannot receive AA, so the AA term cannot matter for CTC or UC.
    • The armed forces benefit is Armed Forces Independence Payment. FRS code 8 is "Armed Forces Compensation Scheme (formerly War Disablement Pension), including Guaranteed Income Payments". It covers every AFCS and war disablement pension payment, including the guaranteed income payment every AFIP recipient also gets. The FRS 2024-25 code frame has no AFIP code, so AFIP cannot be separated out, and AFIP recipients are not flagged (a known under-count).
  • Severe disability premium (HB Regs 2006 Sch 3 para 14; IS Regs 1987 Sch 2 para 13; ESA Regs 2008 Sch 4 para 6; JSA Regs 1996 Sch 1 para 15): Attendance Allowance at either rate, the DLA care component at the middle or highest rate, the PIP daily living component at the standard or enhanced rate, and AFIP. The stored flag left out DLA middle and PIP standard.

The fix:

  • The stored flag is now the tax credit condition: DLA care highest rate, PIP daily living enhanced rate, or higher-rate Attendance Allowance.
  • It is derived with the category rule (_reaches_weekly_rate reuses _category_from_reported_amount), so it agrees with the stored aa_category, dla_sc_category and pip_dl_category by construction. The earlier amount comparison disagreed with the categories at exactly the £1/week tolerance edge, for example in survey year 2022 at rate minus £1.00.
  • Give the legacy severe disability premium its statutory conditions (stacked on #1896) policyengine-uk#1946 gives the severe disability premium its own variable, receives_severe_disability_premium_qualifying_benefit, which reads those categories, not this flag.
  • The same PR gives policyengine-uk's formula for is_severely_disabled_for_benefits this definition, so the stored value and the formula agree.

Law re-verified on legislation.gov.uk (current revised text) on 2026-09-30. The FRS code frame was checked against the UKDA data dictionary for study 9563 and the DWP FRS 2024-25 benefits documentation.

Tests

  • test_attendance_allowance_feeds_stronger_disability_flags: lower-rate AA is no longer severe; higher-rate AA is.
  • test_armed_forces_compensation_scheme_is_not_a_severe_disability: afcs_reported alone sets is_disabled_for_benefits but not the severe flag.
  • test_severe_flag_is_the_tax_credit_condition_on_the_categories: an exhaustive grid of AA, DLA care and PIP daily living amounts for survey years 2019 to 2026.
    • Offsets around every rate: 0, +£0.50, −£0.50, −£0.99, −£1.00, −£1.01 and −£1.50 a week.
    • On every row the stored flag must equal aa_category == HIGHER | dla_sc_category == HIGHER | pip_dl_category == ENHANCED on the categories the same amounts map to.
    • It is not vacuous: DLA middle and PIP standard cases are present, and the flag is neither all true nor all false.
    • It fails on the previous amount comparison.
  • test_severe_flag_matches_the_policyengine_uk_formula: a differential test between the stored flag and policyengine-uk's formula for every category combination on the grid.
    • It fails on policyengine-uk releases that predate #1946 by design. They still count AFCS and omit higher-rate AA, as 2.93.0 in the current lock does.
    • It passes against #1946's code (12 of 12 tests, run with that checkout on PYTHONPATH).
  • On the current lock, test_disability_benefits.py gives 11 passed and the intended differential failure. test_frs_only_imputation.py and test_legacy_benefit_proxies.py pass.

Effect

Measured with real policyengine-uk Microsimulation runs on the Enhanced FRS 2024-25, against a copy whose stored flag was recomputed from the stored categories with this definition. The script is make_flag_dataset.py; it touches no other column. The copy was compared with the unchanged dataset.

  • Who is affected. The flag is cleared for 0.61m people (weighted), those with lower-rate AA only or an AFCS payment only. No one gains it, and none of them is a child.
  • With #1946 there is no change to any benefit or to net income in 2024, 2025 or 2026. The CTC disability element and UC's disabled child addition are unaffected because no child changes. The WTC severe element counts adults, but the model pays no WTC from 2025, and the 2024 run (WTC £31m, CTC £81m) shows no change.
  • With policyengine-uk before #1946 there is a cost; see the note at the top.

Follow-up

The FRS codes for Adult Disability Payment (117, 118) and Child Disability Payment (121, 122) are not mapped, so Scottish recipients have no disability benefit in the model: #500.

🤖 Generated with Claude Code

`is_severely_disabled_for_benefits` is what policyengine-uk uses for the
Child Tax Credit and Working Tax Credit severe disability elements (CTC Regs
2002 reg 8; WTC Regs 2002 reg 17). Those count the DLA care component at the
highest rate, the PIP daily living component at the enhanced rate,
higher-rate Attendance Allowance (WTC) and Armed Forces Independence
Payment. The stored flag also counted lower-rate Attendance Allowance and FRS
code 8, which covers every Armed Forces Compensation Scheme payment
(including war disablement pensions and guaranteed income payments), not
Armed Forces Independence Payment, which the FRS does not code.

The legacy severe disability premium's wider list (any Attendance
Allowance, DLA care middle rate, PIP daily living standard rate) is now read
from the benefit categories in policyengine-uk rather than from this flag.
A grid test pins the stored flag to the tax credit condition on the
categories the same amounts map to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… model

The stored flag and the benefit categories used two different floating-point
comparisons, which disagreed at exactly the GBP 1/week tolerance edge (for
example survey year 2022 at rate minus GBP 1.00). The flag now uses the
category derivation itself, so the two agree by construction. The grid test
covers survey years 2019-2026 and offsets either side of the tolerance edge,
and fails on the old comparison. A differential test checks the stored flag
against policyengine-uk's formula for the same categories; it skips on
policyengine-uk releases that predate PolicyEngine/policyengine-uk#1946.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis marked this pull request as draft September 30, 2026 20:31
The differential test between the stored flag and policyengine-uk's formula
no longer skips on older policyengine-uk releases: it fails until the lock
includes PolicyEngine/policyengine-uk#1946, which keeps the new flag from
shipping with a model that still reads it for the severe disability premium.
The code comment and changelog now describe the flag as the tax credit
condition and note that UC's higher disabled child addition, which is also
keyed on it, differs (UC Regs reg 24(2)(b)).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant