Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/452.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Correct Universal Credit caseload calibration by preserving the published family-type values, adding explicit 2025 and 2026 household totals from DWP deductions statistics, and removing dead OBR jobseeker-split dispatch logic.
10 changes: 0 additions & 10 deletions policyengine_uk_data/targets/build_loss_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
compute_two_child_limit,
compute_uc_by_children,
compute_uc_by_family_type,
compute_uc_jobseeker,
compute_uc_outside_cap,
compute_uc_payment_dist,
compute_uk_population,
Expand Down Expand Up @@ -393,15 +392,6 @@ def _compute_column(target: Target, ctx: _SimContext, year: int) -> np.ndarray |
):
return compute_ss_ni_relief(target, ctx)

# UC jobseeker splits
if name in (
"obr/universal_credit_jobseekers",
"obr/universal_credit_non_jobseekers",
"obr/universal_credit_jobseekers_count",
"obr/universal_credit_non_jobseekers_count",
):
return compute_uc_jobseeker(target, ctx)

# UC outside benefit cap
if name == "obr/universal_credit_outside_cap":
return compute_uc_outside_cap(target, ctx)
Expand Down
2 changes: 0 additions & 2 deletions policyengine_uk_data/targets/compute/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
compute_two_child_limit,
compute_uc_by_children,
compute_uc_by_family_type,
compute_uc_jobseeker,
compute_uc_outside_cap,
compute_uc_payment_dist,
)
Expand Down Expand Up @@ -77,7 +76,6 @@
"compute_two_child_limit",
"compute_uc_by_children",
"compute_uc_by_family_type",
"compute_uc_jobseeker",
"compute_uc_outside_cap",
"compute_uc_payment_dist",
"compute_uk_population",
Expand Down
18 changes: 0 additions & 18 deletions policyengine_uk_data/targets/compute/benefits.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,24 +98,6 @@ def compute_uc_payment_dist(target, ctx) -> np.ndarray:
return ctx.household_from_family(in_band)


def compute_uc_jobseeker(target, ctx) -> np.ndarray:
"""Compute UC jobseeker / non-jobseeker splits."""
family = ctx.sim.populations["benunit"]
uc = ctx.sim.calculate("universal_credit")
on_uc = uc > 0
unemployed = family.any(ctx.sim.calculate("employment_status") == "UNEMPLOYED")

if "non_jobseekers" in target.name:
mask = on_uc * ~unemployed
else:
mask = on_uc * unemployed

if "_count" in target.name:
return ctx.household_from_family(mask)
else:
return ctx.household_from_family(uc * mask)


def compute_uc_outside_cap(target, ctx) -> np.ndarray:
"""Compute OBR UC outside benefit cap."""
uc = ctx.sim.calculate("universal_credit")
Expand Down
21 changes: 19 additions & 2 deletions policyengine_uk_data/targets/sources/dwp.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,20 +168,37 @@ def get_targets() -> list[Target]:
"couple_no_children": 231.368,
"couple_with_children": 839.379,
}
undercount_relative = 1.27921 / sum(_UC_BY_FAMILY_TYPE.values())
# This vintage sums to 6.096 million; 2025-26 totals are targeted separately.
for family_type, count_k in _UC_BY_FAMILY_TYPE.items():
targets.append(
Target(
name=f"dwp/uc/claimants_{family_type}",
variable="universal_credit",
source="dwp",
unit=Unit.COUNT,
values={2025: count_k * (1 + undercount_relative) * 1e3},
values={2025: count_k * 1e3},
is_count=True,
reference_url="https://stat-xplore.dwp.gov.uk/",
)
)

# DWP UC deductions statistics, Table 1: total households are households
# with a deduction divided by their share. This gives 6.38m in April 2025,
# 6.60m in May-August, 6.96m in September-November, and 7.17m in
# December 2025-February 2026. The 2025 value is the calendar-year average;
# the 2026 value represents the December-February plateau.
targets.append(
Target(
name="dwp/uc/households",
variable="universal_credit",
source="dwp",
unit=Unit.COUNT,
values={2025: 6_700_000, 2026: 7_200_000},
is_count=True,
reference_url="https://www.gov.uk/government/statistics/universal-credit-quarterly-statistics-29-april-2013-to-12-february-2026/universal-credit-deductions-statistics-march-2025-to-february-2026",
)
)

# Two-child limit statistics (April 2025 publication, modeled at 2026)
targets.append(
Target(
Expand Down
19 changes: 19 additions & 0 deletions policyengine_uk_data/tests/test_target_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"""

from policyengine_uk_data.targets import get_all_targets
from policyengine_uk_data.targets.build_loss_matrix import _compute_column


def test_registry_loads():
Expand Down Expand Up @@ -62,6 +63,24 @@ def test_dwp_pip_targets():
assert "dwp/pip_dl_enhanced_claimants" in names


def test_dwp_uc_households_target_uses_count_fallback():
"""The UC household total should be registered and produce a matrix column."""
target = next(
target
for target in get_all_targets(year=2025)
if target.name == "dwp/uc/households"
)

class DummyCtx:
@staticmethod
def pe_count(variable):
assert variable == "universal_credit"
return [1, 0, 1]

assert target.values[2025] == 6.7e6
assert _compute_column(target, DummyCtx(), 2025) == [1, 0, 1]


def test_voa_council_tax_targets():
"""VOA council tax band targets should exist."""
targets = get_all_targets(year=2025)
Expand Down
6 changes: 2 additions & 4 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.