Repository navigation
Benefit cap applies to 2x too many households (5.3% of UC vs 1.6% administrative) #1818
Description
Activity
Traced: overstated rents account for essentially all of the over-capping
Scaling the UC housing costs element down reproduces the administrative count almost exactly:
housing element capped units total cap reduction as modelled 222k GBP 995m −10% 186k GBP 768m −20% 123k GBP 575m −25% 114k GBP 502m −30% 97k GBP 440m DWP administrative: 111k. A 25% reduction lands on 114k.
That figure is not arbitrary — it is independently the amount by which modelled rents exceed published averages.
Modelled rents vs published averages
tenure model (2026) benchmark council GBP 145/wk (median 128) — housing association GBP 181/wk (median 161) — social, blended GBP 166/wk GBP 113.69/wk at March 2025 (RSH), ~GBP 123 uprated to 2026 at the observed 8%/yr private GBP 261/wk (median 225) ~GBP 200–230/wk Social rents run ~25–35% high, concentrated in housing associations, whose GBP 181/wk exceeds even the London social average of GBP 140.70. Private rents are ~15% high. Blending in affordable-rent tenancies (up to 80% of market) would raise the social benchmark somewhat, but not to GBP 166.
Housing is 46% of capped benefits and the median excess over the cap is only GBP 2,733/yr against a mean housing element of GBP 11,604/yr, so a rent error of this size moves a large share of households across the threshold. The two magnitudes agreeing is strong circumstantial evidence, though not proof — it should be confirmed in the data pipeline.
This points at rent imputation/uprating in policyengine-uk-data rather than at the benefit cap logic in this repo. The cap code appears to be behaving correctly on overstated inputs. Suggest a companion issue there; the fix is to the rents, not to the housing element, since scaling the element would corrupt UC entitlement generally.
Correction to the exemption analysis above
My earlier under-occupancy test was invalid.
num_bedroomsis identically zero for every household in the dataset, so the "0 spare bedrooms for 100% of capped social renters" result was an artefact of an empty input, not a finding. Two consequences:num_bedroomsis a dead variable — declared invariables/household/demographic/num_bedrooms.pywith no formula and no data behind it, and referenced nowhere else in the model. Anything built on it silently gets zero.- The under-occupancy deduction (removal of the spare room subsidy) is still entirely absent from the model — no match for under-occupancy, bedroom tax or spare room anywhere. Social renters receive their full rent as the housing costs element. Its effect on the capped population remains unmeasured, because measuring it requires bedroom data the dataset does not carry.
Whether under-occupancy matters here is now an open question rather than a closed one, but it is second-order next to the rent overstatement.
Corrections, and a finding that reframes this issue
An independent audit of this issue and its analysis found errors in what I posted. Correcting them, most important first.
The quantity in question is already a calibration target, and it is being missed
policyengine-uk-dataalready targets this exact thing (targets/sources/dwp.py:106-127, wired intobuild_loss_matrix.py:352-355):target (2025) model target ratio dwp/benefit_capped_households222,000 110,637 2.0x dwp/benefit_cap_total_reductionGBP 995m GBP 320,866,000 3.1x This reframes the issue. The question is not primarily "is the benefit cap logic or its inputs wrong" but "why is an existing, directly-binding calibration target on this quantity not converging". The same failure appears on
housing/rent_social(1.55x, PolicyEngine/policyengine-uk-data#454) whilehousing/rent_privateconverges at 1.04x — a shared cause is plausible, and a plausible mechanism is loss weighting that lets a GBP 321m target be swamped by GBP 89bn ones. Under investigation.The rent analysis in my 12 August comment is retracted
That comment claimed social rents run 25-35% high and that scaling the housing element by −25% reproduces the administrative count (114k vs 111k). Both are wrong. The benchmark used (RSH "general needs" social rent, GBP 113.69/wk) excludes affordable-rent tenancies, and the counterfactual cut private rents which are not overstated. Corrected in PolicyEngine/policyengine-uk-data#454: the gap is housing association +20%, council +7%, private −5%, and the tenure-correct counterfactual gives 145k, closing about 69% of the excess rather than all of it.
My grace-period bound was backwards
I wrote that "84% of capped units have zero earnings, so this bounds the grace-period effect at roughly 15%". That is the wrong way round: the 9-month grace period applies precisely to households whose earnings have stopped, so zero-earnings is where that population lives, not an upper bound on it. The bound is withdrawn and the grace period is unquantified.
The earnings test belongs in "defects", not "ruled out"
I wrote that the net-earnings basis "is what the code does". Not quite.
is_benefit_cap_exempt_earnings.pycomputes
sum(employment_income + self_employment_income - income_tax - national_insurance),
andincome_taxis total liability across all sources (property, dividends, pension). Tax on unearned income is therefore deducted from earnings, understating them, wrongly removing exemptions and increasing capping — the same direction as this bug. The statutory test is also net of relievable pension contributions, which the code omits.Also, the current threshold is GBP 881/month (2026/27); the hardcoded
10_152is the 2025/26 value (GBP 846/month).Smaller corrections
- AFIP:
armed_forces_independence_paymentis genuinely absent from the exemption logic, but the variable has no formula and no dataset input, so applying it will very likely change nothing numerically. My phrasing implied otherwise. - Dead code:
is_benefit_cap_exempt_other.pyreturns a three-term expression (has_pensioner | afcs | esa_support_component), not "a single unrelated expression" as I wrote. Roughly 28 dead lines there, ~45 in_earnings. Supporting evidence for the copy-paste diagnosis: the health/disability file includescarer_support_paymentwhile both copies do not. - Geography: DWP's benefit cap statistics are Great Britain, not England or UK. The model figures are UK-wide, so the comparison carries a small Northern Ireland mismatch (NI operates its own cap).
- Residuals do not yet add up: Tax Credits #454 leaves ~31% of the excess to other causes; this issue bounds missing exemptions at "under 10%" and the grace period is now unbounded. Annualisation of a monthly test remains entirely unmeasured and may be the larger term.
Fixes in flight
The exemption defects above are being fixed separately. The calibration convergence question is the one that matters for the 2x, and it sits in
policyengine-uk-data.- AFIP:
The 3.1x on total reduction has a distinct, mechanical cause
benefit_cap_reductionmeasures a different quantity from the DWP statistic it is calibrated against.variables/gov/dwp/benefit_cap_reduction.py:return max_(add(benunit, period, CAPPED_BENEFITS) - benunit("benefit_cap", period), 0)
That is the gross excess over the cap, unbounded. DWP publishes the amount actually removed, which is bounded by the UC/HB award in payment — Child Benefit, Child Tax Credit and contributory ESA/JSA all count towards the cap but cannot themselves be reduced. Where the excess exceeds the UC award, the model reports the full excess while DWP reports only what could be taken.
universal_credit.pyalready clips the award at zero but still reports the unclipped reduction.This inflates the amount without inflating the count, which is exactly the observed pattern: 3.1x on
dwp/benefit_cap_total_reductionagainst 2.0x ondwp/benefit_capped_households. The implied mean reduction is GBP 86/week modelled against GBP 55.8/week in the target (GBP 320.9m / 110,637 / 52) — 1.55x, and 2.0 x 1.55 = 3.1.Suggested fix: add a
benefit_cap_reduction_applied = min(benefit_cap_reduction, universal_credit_pre_benefit_cap + housing_benefit_pre_benefit_cap)and point the calibration target at that. It is the like-for-like match to the published statistic. Note #1819 introducesuc_benefit_cap_reductionfor the protected-floor case, which is related but not the same thing — that one is about the floor, this one about the award ceiling.And the 2.0x on the count is probably upstream
Separately,
policyengine-uk-dataassigns UC claim status with a take-up rate applied to all benefit units rather than entitled ones (utils/takeup.py:52, rate 0.55), capping the modelled caseload at 4.22m against the repo's own target of 6.096m — details in PolicyEngine/policyengine-uk-data#452.To deliver the OBR's GBP 79.29bn of UC spend from 4.22m units, the average award must run at roughly 1.44x the target-implied level. Benefit cap incidence is convex in the award, so a uniform inflation of that size pushes a disproportionate share over a fixed cap threshold. Modelled cap incidence is 222,000/4.22m = 5.3% of the caseload against DWP's 110,637/6.096m = 1.8%.
That is an inference rather than a measurement, and it is checkable in one step from the calibration log CI already publishes — see PolicyEngine/policyengine-uk-data#456.
Revised reading of this issue
Three distinct causes, in descending order of confidence:
- Gross-vs-applied reduction (above) — mechanical, explains the amount running 1.55x hotter than the count.
- UC caseload/award inflation upstream — explains much of the count, pending confirmation.
- Housing association rents ~20% high (Housing association rents ~20% above benchmark; housing/rent_social target missed by 1.55x policyengine-uk-data#454) — contributes, but smaller than I originally claimed.
The exemption defects listed in the issue body remain real but are second-order against these.
@MaxGhenis orienting summary, since this investigation has spread across two repos and four issues.
Where it started: trying to replicate JRF's protected minimum floor with the deductions module from #1815. The deductions side replicates well; the benefit cap side does not, and that is what this issue became.
What the evidence now says, in descending order of confidence:
cause evidence where benefit_cap_reductionmeasures gross excess, DWP measures amount appliedmechanical; explains the amount running 1.55x hotter than the count (GBP 86/wk modelled vs GBP 55.8/wk in the target) this issue UC take-up applied to all benefit units, not entitled ones utils/takeup.py:52; caps caseload at 4.22m against the repo's own 6.096m target; award must then run 1.44x, and cap incidence is convex in the awardPolicyEngine/policyengine-uk-data#452 loss under-penalises overshoots past 2x gradient peaks at ratio 2.0 then declines; sre(1.55x)numerically indistinguishable fromsre(0.645x), so the optimiser is indifferent between the miss and its own fixPolicyEngine/policyengine-uk-data#456 dwelling stock matched to household counts ONS SPREE and VOA independently; ~1.6-1.7m of the 2.76m household overshoot PolicyEngine/policyengine-uk-data#457 housing association rents ~20% high EHS 2024-25 comparison PolicyEngine/policyengine-uk-data#454 Open PRs: #1819 (deductions/floor reconciliation bugs from #1815), #1820 (the exemption defects in this issue), PolicyEngine/policyengine-uk-data#455 (target vintage). All green including the gated microsimulation job. None fixes the 2x — the exemption defects are second-order against the causes above.
Cheapest next step by a wide margin:
constituency_calibration_log.csvis already uploaded as a CI artifact on every push (.github/workflows/push.yaml:63-73) and contains per-target estimate/target/error for the final epoch. Filtering it toname == "UK"would settle most of the open questions across #452, #456, #457 and this issue with no rebuild — in particular whether the OBR UC spend targets are actually being hit, which is the load-bearing assumption in the take-up chain above.Corrections on the record: several claims in my earlier comments here were wrong and are retracted above — the 25-35% rent figure, the -25% counterfactual reproducing the admin count, the grace-period bound (backwards), and "the earnings test is ruled out" (it is a defect, fixed in #1820). Flagging so nobody builds on the retracted versions.
- added 4 commits that reference this issue
on Oct 8, 2026
Summary
The model caps 222k UC households (5.30% of its UC caseload). DWP's administrative count is 111k (1.60%) at November 2025. That is 2.0x the count and 3.3x the rate, and it is broad-based rather than concentrated.
Found while trying to reproduce JRF's protected minimum floor estimate with the deductions module from #1815. It is a pre-existing benefit cap problem, not a deductions one, but it dominates that reform: 99% of the floor's modelled cost comes from benefit-capped households.
Enhanced FRS 2023/24 v1.40.3, simulated 2026.
Evidence
Two things stand out. The over-capping is roughly 3x in London and outside it, so it is not a regional artefact. And our capped population is too childless — 31.6% without children against 20% in the admin data — which points at single adults, who face the lower cap (GBP 14,753 outside London) and are therefore easiest to tip over.
The excess over the cap is often marginal: p10 is GBP 82/yr and the median GBP 2,733/yr, so a systematic overstatement of capped benefit amounts would push many households over the line.
Ruled out
Defects found along the way
These are real but individually small; none closes the gap.
armed_forces_independence_paymentexists (variables/gov/dwp/afip.py) but is never referenced byis_benefit_cap_exempt_health_disability, which checksafcsonly.earnings_threshold = 10_152(GBP 846/month) inis_benefit_cap_exempt_earnings, against GBP 881/month on gov.uk today. It should be a parameter with dated values. Note the sign: correcting it upward increases capping, so it is not a candidate cause of this bug.is_benefit_cap_exempt_earningsandis_benefit_cap_exempt_othereach compute ~40 lines of qualifying-benefit variables that are then discarded — copy-paste fromis_benefit_cap_exempt_health_disability, whose return value is the only one that uses them. Both files return a single unrelated expression.Leading hypotheses for the remaining gap
universal_credit_pre_benefit_cap+housing_benefit_pre_benefit_cap; if modelled housing entitlement runs high, households cross the cap that would not in reality. The marginal excess distribution is consistent with this.Why it matters
For JRF-style protected floor analysis (#1814), the floor limits combined deductions and benefit cap reductions, so this error passes through almost undamped:
The mean per-family gain lands close, which is what you would expect if the per-household mechanics are right and the population is wrong. Until this is fixed, PolicyEngine UK cannot be used for benefit-cap-interacting reform estimates.