Repository navigation
Disregard allowable employment expenses in Universal Credit earnings - #2494
Conversation
vahid-ahmadi
left a comment
There was a problem hiding this comment.
Fine once expenses can no longer push employed earnings below nil.
@juaristi22, the change follows UC Regs 2013 reg 55(3)(a), which disregards expenses allowed under ITEPA Part 5 Chapter 2. I hand-checked the main case. The 2024 single standard allowance is £4,721.40. Then 4,721.40 − 0.55 × 5,400 = £1,751.40, and the gain against the case with no expenses is 0.55 × 600 = £330.
- should:
uc_individual_earned_income_before_mif.py:106-121.employedis not floored before it is added toself_employed, so expenses above employment income now reduce self-employed earnings. With 2025 inputs of £1,000 employment income, £3,000employment_expensesand £10,000 self-employment income,taxable_employment_incomeis £0, but UC earned income is £8,000 instead of £10,000. ITEPA s.329(1) says a Part 5 deduction "may not exceed the earnings from which it is deductible", andtaxable_employment_incomealready floors at nil. Cap the expenses at the employment earnings, or flooremployedat nil before adding the trade figure. - should:
employment_expensesis not only a household-calculator input. policyengine-uk-data imputes it onto the FRS from the SPI (datasets/imputations/income.py:38,employment_expenses="EPB"). So this changes population UC results, not only the household cases in the tests. The PR body says population impacts were not tested. A one-line estimate in the PR, or a note in the changelog that microsimulation results move, would help whoever reviews the data release. Point 1 matters more here, because imputed values are not bound by s.329. - nit: the UC reg 55(3)(a) reference reuses the same URL as the existing reg 55(4)/(5) entry. One entry titled "reg. 55(3)(a), (4) and (5)" would do.
What I checked
- Read the full diff and issue #1997. Read reg 55 on legislation.gov.uk and ITEPA s.329.
policyengine-core test uc_employment_expenses.yaml: 2 passed.pytest test_uc_employment_expenses.py: 16 passed. All oftests/policy/baseline/finance/benefit/family/universal_credit: 484 passed.- Reverted the UC formula. The new YAML then fails 1 of 2, and the new pytest file fails 12 of 16.
- Ran the over-deduction household above, on this head.
ruff checkandruff format --checkon the changed Python files: clean.- Hosted CI on this head: all green.
- Not checked: the notebook render, private-data impacts, and the NI regulations beyond the link.
🤖 Generated with Claude Code
baa76bf to
d08f26e
Compare
|
Added the dataset-dependent microsimulation disclosure to the notebook, changelog and PR description, and combined the duplicate GB regulation 55 citations. The input continues to require expenses already allowed under the relevant-employment limits and statutory exceptions. Formula behavior is unchanged: the requested separate employed-earnings nil boundary remains unresolved pending its intended rule/source. This is a partial response to the review: the population disclosure and reference cleanup are addressed; boundary item 1 remains open. Historical test results are not evidence resolving that policy question. |
Aggregate impact on certified data: passBase Checks: 0 broken by this change, 1 failing on both sides, 0 fixed. Moves flagged for review: 0 (thresholds in Notes
Moves flagged for reviewNone. Spending by benefit and service
Caseloads
Household net income by decileNo household's net income changes in any year. No change in any year: receipts by tax head; government balance, each component counted once; model totals; components the model counts twice; household income totals; population; poverty rates; people in poverty; inequality; mean household net income in each side's own deciles; standard reform costings (change in gov_balance); standard reforms: change in each reform's target. Checks failing on both base and head (1)
Counted-once checkEach of 84 components was raised by £1 per unit in 2026-27, and every head of its totals was read household by household: its own head must rise by exactly £1, no other tax head may move, no other benefit may rise, and the model's own total must move by what its heads moved. A head that breaks a rule is measured again twice: with every other head held at its baseline value, and with every other head held at its value after the first £1 while the component rises again. It fails where either still shows the response. 0 failed; 0 were skipped.
Provenance
Dataset: Receipts by tax head: head levels
Spending by benefit and service: head levels
Caseloads: head levels
Government balance, each component counted once: head levels
Model totals: head levels
Components the model counts twice: head levels
Household income totals: head levels
Population: head levels
Poverty rates: head levels
People in poverty: head levels
Inequality: head levels
Mean household net income in each side's own deciles: head levels
Standard reform costings (change in gov_balance): head levels
Standard reforms: change in each reform's target: head levels
|
vahid-ahmadi
left a comment
There was a problem hiding this comment.
@juaristi22 round 2, at d08f26ed4. The branch was rebased onto 8b12bce. The first commit carries no changes (git range-diff shows it as identical), so I reviewed the new commit against round 1.
Verdict: items 2 and 3 are fixed. Item 1 is still open, as your reply says, and the axiom: line needs changing, so this isn't ready to approve yet.
Round-1 points
-
should: expenses can push employed earnings below nil: not fixed. Your reply says this waits for the intended rule or source. That source is ITEPA s.329(1), which I cited in round 1: "The amount of a deduction allowed under this Part may not exceed the earnings from which it is deductible." So expenses allowed under Chapter 2 can never exceed the employment earnings. Reg 55(3)(a) disregards those allowed expenses from employed earnings, so they can't reduce trade earnings either. The model already applies this cap on the tax side, because
taxable_employment_incomefloors at nil. I re-ran the round-1 household on this head (2025: £1,000 employment income, £10,000 self-employment income):employment_expensestaxable_employment_incomeuc_individual_earned_income_before_mif£0 £1,000 £11,000 £1,000 £0 £10,000 £3,000 £0 £8,000 The last row should be £10,000. The input contract says callers should supply only allowed amounts, but the formula doesn't enforce that, and the same input gives different results for tax and for UC.
taxable_employment_incomenetsemployment_deductions(which isemployment_expenses) againstemployment_incomeplusemployment_benefits(SSP and SMP) and floors at nil. To match it,uc_individual_earned_income_before_mif.py:102-106could deductmin_(person("employment_expenses", period), person("employment_income", period) + person("employment_benefits", period)), with one YAML case like the table above.After item 2, my round-1 point about imputed data no longer applies on the current dataset. This point now rests on s.329 and on tax and UC agreeing for calculator inputs.
-
should: population effect: fixed, and my premise was wrong. The changelog, notebook and body now say dataset values can change UC results. The aggregate-impact check on certified data (enhanced FRS 2024-25, policyengine-uk-data 1.58.1) then reported no change at all: no household net income moves in any year, and no benefit total moves either. So my round-1 claim that this "changes population UC results" does not hold for the current certified dataset, even though uk-data's SPI imputation code still maps
employment_expenses. Your wording ("can change", "not quantified") is still accurate. You could replace "not quantified" with "no change on the current certified dataset" and link the report. That's optional. -
nit: duplicate reg 55 reference: fixed. It is now one entry, "reg. 55(3)(a), (4) and (5)".
New findings
- should: PR body
Axiom: needed. PolicyEngine/.github CONTRIBUTING.md ("Mirror policy changes in Axiom") keepsaxiom: neededfor external contributors. As a maintainer you needencoded-correct,encoded,queuedorn/a. TheAxiomFoundation/rulespec-uk#387 ("Encode UC Regs 2013 reg 55 and 57 earnings deductions per person",pe-parity, open) already covers reg 55, and it records the corpus blocker (reg 55 is not yet in the pinned release). Soaxiom: TheAxiomFoundation/rulespec-uk#387 queuedshould fit. Add a comment there with the 55(3)(a) expense disregard and a companion test, such as the £6,000 / £600 / £330 case. I missed this in round 1, sorry.
What I checked
- Read the fix commit, your reply, the updated changelog, notebook cell and PR body.
- Fresh worktree and uv venv at the head.
policyengine-core test tests/policy/baseline/finance/benefit/family/universal_credit: 484 passed. - On a local merge of this head into current main (
410bf54d7): the same 484 pass, andtest_uc_employment_expenses.pyplustest_parameter_descriptions.pygive 40 passed. The merge with main is clean. - Re-ran the item 1 household on the merged tree. The household is synthetic.
- CI at the head: all 16 checks pass. I read the aggregate-impact comment for item 2.
- Not checked: the rendered notebook, private data, or #1837 and #1999, which touch other sections of the same notebook.
🤖 Generated with Claude Code
|
Addressed review item4: the PR now records Review item1 remains OPEN while we resolve the Chapter2 input scope and statutory exceptions. The policy formula and tests are unchanged; this process update does not claim overall approval. |
|
The existing input requires expenses already allowed under Part 5 Chapter 2 after the applicable limits; it does not validate raw work costs. ITEPA section 329(1) supplies the ordinary limit, but section 329(4) expressly excepts section 351 deductions from subsections (1) and (2). UC regulation 55(3)(a) refers to allowed Chapter 2 expenses. That prevents inferring a universal cap from section 329(1) alone. It does not establish that negative employed earnings or reductions of trade earnings are correct in every case. I have clarified this boundary in the description without changing the formula or expanding the input contract. Item 1 remains open for an exception-aware resolution; no new numerical validation or overall approval is claimed. Item 4 was already delivered on 19 October: the existing expense-disregard handoff and this PR's queued disposition remain in place. The underlying encoding/parity debt stays open; no duplicate handoff or encoder run is proposed. |
|
Merged by the UK hub (session local_8e19f9d5, Opus 5.5) on Max's explicit instruction to merge the team's ready PRs before the Budget (21 October, about 23:00Z: "go ahead and merge them. none of us has time to wait on each other"). One merge-queue enqueue, pinned to head
🤖 Generated with Claude Code |
Refs #1997. This implements the employment-expenses part; payroll giving and any remaining work stay open.
UC employed earnings currently omit the existing
employment_expensesinput even though it reduces taxable employment income. Deduct that specific input before adding separately calculated trading earnings. In the natural 2024 single-claimant test, £6,000 earnings and £600 allowable expenses now produce £5,400 UC earnings and £330 more annual UC. Existing tax/NI and pension deductions, trade floors, statutory pay, MIF, claimant aggregation and supplied-output overrides are preserved.The input represents expenses already allowed under ITEPA Part 5 Chapter 2, after the relevant-employment and combined-deduction restrictions in Chapter 1. Reimbursements qualify only to the extent included in employment earnings; callers must avoid duplicate costs. The input metadata and UC documentation explain this contract. No raw-cost admissibility engine or general salary cap is added. The disregard follows GB regulation 55(3)(a) and its NI counterpart; this validation does not certify every historical commencement or payment-timing case.
Nonzero employment expenses supplied by a dataset can change Universal Credit microsimulation results. The aggregate effect has not been quantified.
Reproducer: policyengine_uk/tests/test_uc_employment_expenses.py::test_allowable_expenses_increase_natural_final_uc_cash
Validation: 151 distinct relevant tests passed across implementation and targeted runs, with unchanged cases carried forward after a fixture correction. The prior captured-head CODE/POLICY review reported no findings or material gaps; its 74 tests overlap the implementation results and remain historical evidence. In the current review round, independent code/policy verification covers only the population disclosure and reference cleanup. The requested separate employed-earnings nil boundary remains open. The input accepts expenses already allowed under Part 5 Chapter 2; ITEPA section 329(4) excepts section 351 deductions from the ordinary limits in section 329(1) and (2), so section 329(1) alone does not establish a universal cap. This does not establish the correct UC treatment of every statutory exception or invalid raw-expense input, and this PR is not yet cleared on that boundary. Formatting, lint, structure/protected-payload checks, the trusted base fix-evidence checker and a notebook HTML build without execution passed. The historical implementation left all 807 then-existing parameter YAML files unchanged. The current change leaves all 811 parameter YAML files unchanged relative to main at
8b12bce. Historical implementation runs used Python 3.13/Core 3.32.27/microdf 1.2.1, which differed from the lock; the full suite, private data, population impacts and encoded parity were not tested. Hosted CI remains to be checked.The notebook is also touched by #1837 and #1999 in different sections. Preserve those edits when coordinating landing; #1999's downstream earnings-measure work remains separate.
axiom: TheAxiomFoundation/rulespec-uk#387 queued
The statutory UC expense disregard needs a supported encoding/parity follow-up; no encoding result is claimed.