diff --git a/changelog.d/452.fixed.md b/changelog.d/452.fixed.md new file mode 100644 index 00000000..e12f875f --- /dev/null +++ b/changelog.d/452.fixed.md @@ -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. diff --git a/policyengine_uk_data/targets/build_loss_matrix.py b/policyengine_uk_data/targets/build_loss_matrix.py index 06d53752..9552aeea 100644 --- a/policyengine_uk_data/targets/build_loss_matrix.py +++ b/policyengine_uk_data/targets/build_loss_matrix.py @@ -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, @@ -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) diff --git a/policyengine_uk_data/targets/compute/__init__.py b/policyengine_uk_data/targets/compute/__init__.py index 426d59af..c7000eb4 100644 --- a/policyengine_uk_data/targets/compute/__init__.py +++ b/policyengine_uk_data/targets/compute/__init__.py @@ -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, ) @@ -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", diff --git a/policyengine_uk_data/targets/compute/benefits.py b/policyengine_uk_data/targets/compute/benefits.py index 0a6d291b..0daca850 100644 --- a/policyengine_uk_data/targets/compute/benefits.py +++ b/policyengine_uk_data/targets/compute/benefits.py @@ -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") diff --git a/policyengine_uk_data/targets/sources/dwp.py b/policyengine_uk_data/targets/sources/dwp.py index e8dc42df..b095f055 100644 --- a/policyengine_uk_data/targets/sources/dwp.py +++ b/policyengine_uk_data/targets/sources/dwp.py @@ -168,7 +168,7 @@ 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( @@ -176,12 +176,29 @@ def get_targets() -> list[Target]: 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( diff --git a/policyengine_uk_data/tests/test_target_registry.py b/policyengine_uk_data/tests/test_target_registry.py index 51e9cc69..22cf7b2d 100644 --- a/policyengine_uk_data/tests/test_target_registry.py +++ b/policyengine_uk_data/tests/test_target_registry.py @@ -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(): @@ -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) diff --git a/uv.lock b/uv.lock index e90f2d48..5a6dfbf9 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.13" [[package]] @@ -446,7 +446,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, - { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, @@ -457,7 +456,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" }, @@ -1366,7 +1364,7 @@ wheels = [ [[package]] name = "policyengine-uk-data" -version = "1.56.3" +version = "1.56.14" source = { editable = "." } dependencies = [ { name = "google-auth" },