Skip to content

Benefit cap applies to 2x too many households (5.3% of UC vs 1.6% administrative) #1818

Description

@vahid-ahmadi

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

model DWP admin (Nov 2025)
UC households capped 222k 111k
share of UC households 5.30% 1.60%
London capped rate 9.62% 3.30%
non-London capped rate 4.51% —
capped households with children 68.4% 80%

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

  • Cap levels are correct. GBP 22,020 / 25,323 / 14,753 / 16,967, frozen since April 2023 and unchanged for 2026/27, verified against DWP benefit and pension rates 2026/27.
  • Missing exemptions explain under 10% of the excess. Measured directly against the capped population.
  • The earnings test's net basis is right. gov.uk specifies earnings after tax and National Insurance, which is what the code does.
  • The UC childcare costs element is not an exemption (it is not on the gov.uk list), so its absence is correct.

Defects found along the way

These are real but individually small; none closes the gap.

  1. Armed Forces Independence Payment is a statutory exemption and is not applied. The variable armed_forces_independence_payment exists (variables/gov/dwp/afip.py) but is never referenced by is_benefit_cap_exempt_health_disability, which checks afcs only.
  2. Exempting benefits with no model variable at all: Guardian's Allowance, Adult Disability Payment, Child Disability Payment, Scottish Adult DLA, Pension Age Disability Payment, war pensions and War Widow's/Widower's Pension. The Scottish disability payments matter structurally, since Scotland has migrated PIP/DLA caseloads onto ADP/CDP — those households are currently capped when they should be exempt.
  3. The 9-month grace period is not modelled. 84% of capped units have zero earnings, so this bounds the grace-period effect at roughly 15% of the excess, but it is a real omission and an annual model handles it badly.
  4. The earnings threshold is hardcoded and stale: earnings_threshold = 10_152 (GBP 846/month) in is_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.
  5. Dead code. is_benefit_cap_exempt_earnings and is_benefit_cap_exempt_other each compute ~40 lines of qualifying-benefit variables that are then discarded — copy-paste from is_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

  • Capped benefit amounts overstated, most likely housing. The cap binds on 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.
  • UC caseload composition. The model has 4.22m UC benefit units against ~7.2m administrative (UC caseload runs ~40% below administrative counts, capping aggregate reform estimates policyengine-uk-data#452). If the ~3m missing households are disproportionately low-entitlement in-work claimants, the capped share inflates mechanically. This explains the rate gap but not the 2x absolute count.
  • Annualisation. The cap is a monthly test; applying it to annual totals mis-scores households with variable income within the year.

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:

PolicyEngine UK JRF (IPPR TBM v02_78)
cost GBP 899m GBP 340m
families gaining 257k ~90k
mean gain GBP 67/wk ~GBP 75/wk

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.

Activity

  1. vahid-ahmadi commented on Aug 12, 2026

    @vahid-ahmadi
    CollaboratorAuthor

    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_bedrooms is 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:

    1. num_bedrooms is a dead variable — declared in variables/household/demographic/num_bedrooms.py with no formula and no data behind it, and referenced nowhere else in the model. Anything built on it silently gets zero.
    2. 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.

  2. vahid-ahmadi commented on Aug 13, 2026

    @vahid-ahmadi
    CollaboratorAuthor

    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-data already targets this exact thing (targets/sources/dwp.py:106-127, wired into build_loss_matrix.py:352-355):

    target (2025) model target ratio
    dwp/benefit_capped_households 222,000 110,637 2.0x
    dwp/benefit_cap_total_reduction GBP 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) while housing/rent_private converges 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.py computes
    sum(employment_income + self_employment_income - income_tax - national_insurance),
    and income_tax is 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_152 is the 2025/26 value (GBP 846/month).

    Smaller corrections

    • AFIP: armed_forces_independence_payment is 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.py returns 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 includes carer_support_payment while 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.

  3. vahid-ahmadi commented on Aug 13, 2026

    @vahid-ahmadi
    CollaboratorAuthor

    The 3.1x on total reduction has a distinct, mechanical cause

    benefit_cap_reduction measures 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.py already 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_reduction against 2.0x on dwp/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 introduces uc_benefit_cap_reduction for 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-data assigns 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:

    1. Gross-vs-applied reduction (above) — mechanical, explains the amount running 1.55x hotter than the count.
    2. UC caseload/award inflation upstream — explains much of the count, pending confirmation.
    3. 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.

  4. vahid-ahmadi commented on Aug 13, 2026

    @vahid-ahmadi
    CollaboratorAuthor

    @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_reduction measures gross excess, DWP measures amount applied mechanical; 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 award PolicyEngine/policyengine-uk-data#452
    loss under-penalises overshoots past 2x gradient peaks at ratio 2.0 then declines; sre(1.55x) numerically indistinguishable from sre(0.645x), so the optimiser is indifferent between the miss and its own fix PolicyEngine/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.csv is 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 to name == "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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions