From f8af4bb96917367c42a31b1a572526fe65912d3a Mon Sep 17 00:00:00 2001 From: vahid-ahmadi Date: Thu, 13 Aug 2026 09:28:52 +0100 Subject: [PATCH 1/2] Refresh social rent target to EHS 2024-25 vintage The three housing targets are all set for 2025, but the social rent basis used the English Housing Survey 2023-24 mean (GBP 118/week) while the private rent basis used ONS PRHI from February 2026. Anchoring targets for the same year to vintages two years apart understated the social basis by roughly 10% against its own target year. EHS 2024-25 reports GBP 129/week (local authority GBP 120, housing association GBP 134), moving the target from ~GBP 30.7bn to ~GBP 33.5bn. Adds a test pinning the implied weekly mean to the published EHS figure, so the target cannot silently go stale against a newer release again, and documents on the module that these are totals only: at dataset 1.40.3 the private target is met at 1.04x with 6.99m renters at GBP 256/week against a basis of 5.4m at GBP 317/week, so a count error and an offsetting mean error cancel and the target validates nothing. This does not by itself correct modelled social rents, which sit 1.55x above the target (5.77m households at GBP 159/week). That is a calibration convergence question tracked in #454. Co-Authored-By: Claude Opus 5 (1M context) --- .../social-rent-target-vintage.fixed.md | 1 + .../targets/sources/housing.py | 24 +++++++++++++++---- .../tests/test_housing_targets.py | 13 +++++++++- uv.lock | 6 ++--- 4 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 changelog.d/social-rent-target-vintage.fixed.md diff --git a/changelog.d/social-rent-target-vintage.fixed.md b/changelog.d/social-rent-target-vintage.fixed.md new file mode 100644 index 000000000..4a859926b --- /dev/null +++ b/changelog.d/social-rent-target-vintage.fixed.md @@ -0,0 +1 @@ +Refresh the social rent calibration target to the English Housing Survey 2024-25 mean (GBP 129/week, from GBP 118/week in EHS 2023-24). The target is set for 2025 but was anchored to a 2023-24 vintage while the private rent target used ONS data from February 2026, understating the social basis by roughly 10% relative to its own target year. diff --git a/policyengine_uk_data/targets/sources/housing.py b/policyengine_uk_data/targets/sources/housing.py index 4a11198a4..3d89f9888 100644 --- a/policyengine_uk_data/targets/sources/housing.py +++ b/policyengine_uk_data/targets/sources/housing.py @@ -2,12 +2,24 @@ Total mortgage payments, private rent, and social rent. +All three targets are set for 2025 and should be anchored to comparable +vintages. The social rent basis previously used EHS 2023-24 while the +private basis used ONS PRHI from February 2026, understating the social +target by roughly 10% relative to its own year. + Sources: - ONS PRHI Feb 2026: UK avg private rent £1,374/month https://www.ons.gov.uk/economy/inflationandpriceindices/bulletins/privaterentandhousepricesuk/march2026 -- English Housing Survey 2023-24: avg social rent £118/week - https://www.gov.uk/government/statistics/english-housing-survey-2023-to-2024-rented-sectors +- English Housing Survey 2024-25: avg social rent £129/week (local authority + £120, housing association £134) + https://www.gov.uk/government/statistics/chapters-for-english-housing-survey-2024-to-2025-headline-findings-on-demographics-and-household-resilience/chapter-2-housing-costs-and-affordability - EHS + devolved stats: ~5.4m private renters, ~5.0m social renters UK-wide + +Known limitation: these are totals only, so an error in the household count +and an offsetting error in the mean cannot be detected. At the 1.40.3 +dataset the private target is met (1.04x) with 6.99m renters at £256/week +against a basis of 5.4m at £317/week — the total matches by cancellation. +See PolicyEngine/policyengine-uk-data#454. """ from policyengine_uk_data.targets.schema import Target, Unit @@ -18,12 +30,14 @@ # Mortgage: avg £1,100/month × 7.5m owner-occupiers with mortgage _MORTGAGE_TOTAL = 1_100 * 12 * 7.5e6 # ~£99bn -# Social rent: EHS 2023-24 mean £118/week × 52 × 5.0m UK social renters -_SOCIAL_RENT_TOTAL = 118 * 52 * 5.0e6 # ~£30.7bn +# Social rent: EHS 2024-25 mean £129/week × 52 × 5.0m UK social renters +_SOCIAL_RENT_TOTAL = 129 * 52 * 5.0e6 # ~£33.5bn _EHS_REF = ( "https://www.gov.uk/government/statistics/" - "english-housing-survey-2023-to-2024-rented-sectors" + "chapters-for-english-housing-survey-2024-to-2025-headline-findings-on-" + "demographics-and-household-resilience/chapter-2-housing-costs-and-" + "affordability" ) _ONS_RENT_REF = ( "https://www.ons.gov.uk/economy/inflationandpriceindices/" diff --git a/policyengine_uk_data/tests/test_housing_targets.py b/policyengine_uk_data/tests/test_housing_targets.py index 15bdb8dfc..7e8e53da5 100644 --- a/policyengine_uk_data/tests/test_housing_targets.py +++ b/policyengine_uk_data/tests/test_housing_targets.py @@ -20,10 +20,21 @@ def test_private_rent_total_in_range(): def test_social_rent_total_in_range(): - """Social rent total should be £20bn-£40bn (5.0m × ~£118/wk).""" + """Social rent total should be £20bn-£40bn (5.0m × ~£129/wk).""" assert 20e9 < _SOCIAL_RENT_TOTAL < 40e9 +def test_social_rent_basis_matches_published_ehs_mean(): + """The social basis should track the latest published EHS mean. + + EHS 2024-25 reports a £129/week mean social rent. Pinning this catches + the target silently going stale against a newer EHS release, which is + how it drifted ~10% low against its own 2025 target year. + """ + implied_weekly = _SOCIAL_RENT_TOTAL / 5.0e6 / 52 + assert 125 < implied_weekly < 135, implied_weekly + + def test_mortgage_total_in_range(): """Mortgage total should be £80bn-£120bn (7.5m × ~£1,100/mo).""" assert 80e9 < _MORTGAGE_TOTAL < 120e9 diff --git a/uv.lock b/uv.lock index e90f2d481..5a6dfbf9f 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" }, From c6ab89272527e9f0916379ecea097b8fb4ed275b Mon Sep 17 00:00:00 2001 From: vahid-ahmadi Date: Thu, 13 Aug 2026 13:05:41 +0100 Subject: [PATCH 2/2] Replace the staleness test with an honest basis pin The previous test asserted that _SOCIAL_RENT_TOTAL / 5.0e6 / 52 fell between 125 and 135 - recomputing the constant from the constant. It could not detect a newer EHS release, which is what its docstring and the PR body claimed. Replaced with an explicit pin on named basis components, documented as a review guard rather than a staleness check. Also drops unrelated uv.lock churn from the branch. Co-Authored-By: Claude Opus 5 (1M context) --- .../targets/sources/housing.py | 7 +++++-- .../tests/test_housing_targets.py | 18 +++++++++++------- uv.lock | 6 ++++-- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/policyengine_uk_data/targets/sources/housing.py b/policyengine_uk_data/targets/sources/housing.py index 3d89f9888..724db7d6a 100644 --- a/policyengine_uk_data/targets/sources/housing.py +++ b/policyengine_uk_data/targets/sources/housing.py @@ -30,8 +30,11 @@ # Mortgage: avg £1,100/month × 7.5m owner-occupiers with mortgage _MORTGAGE_TOTAL = 1_100 * 12 * 7.5e6 # ~£99bn -# Social rent: EHS 2024-25 mean £129/week × 52 × 5.0m UK social renters -_SOCIAL_RENT_TOTAL = 129 * 52 * 5.0e6 # ~£33.5bn +# Social rent basis, kept as named components so the arithmetic is auditable +# and the vintage is explicit rather than buried in a product. +_SOCIAL_RENT_WEEKLY_MEAN = 129 # EHS 2024-25 (was 118 in EHS 2023-24) +_SOCIAL_RENT_HOUSEHOLDS = 5.0e6 # UK, derived: share x ONS household total +_SOCIAL_RENT_TOTAL = _SOCIAL_RENT_WEEKLY_MEAN * 52 * _SOCIAL_RENT_HOUSEHOLDS _EHS_REF = ( "https://www.gov.uk/government/statistics/" diff --git a/policyengine_uk_data/tests/test_housing_targets.py b/policyengine_uk_data/tests/test_housing_targets.py index 7e8e53da5..496ea1a04 100644 --- a/policyengine_uk_data/tests/test_housing_targets.py +++ b/policyengine_uk_data/tests/test_housing_targets.py @@ -5,6 +5,8 @@ from policyengine_uk_data.targets.sources.housing import ( _MORTGAGE_TOTAL, + _SOCIAL_RENT_HOUSEHOLDS, + _SOCIAL_RENT_WEEKLY_MEAN, _PRIVATE_RENT_TOTAL, _SOCIAL_RENT_TOTAL, get_targets, @@ -24,15 +26,17 @@ def test_social_rent_total_in_range(): assert 20e9 < _SOCIAL_RENT_TOTAL < 40e9 -def test_social_rent_basis_matches_published_ehs_mean(): - """The social basis should track the latest published EHS mean. +def test_social_rent_basis_components(): + """Pin the documented EHS basis so a silent edit is visible in review. - EHS 2024-25 reports a £129/week mean social rent. Pinning this catches - the target silently going stale against a newer EHS release, which is - how it drifted ~10% low against its own 2025 target year. + This does NOT detect a newer EHS release — it only fails if someone + changes the constant without changing this pin. A real staleness check + would have to fetch EHS, as ons_tenure.py and ons_households.py do for + their sources. Tracked as future work in the module docstring. """ - implied_weekly = _SOCIAL_RENT_TOTAL / 5.0e6 / 52 - assert 125 < implied_weekly < 135, implied_weekly + assert _SOCIAL_RENT_WEEKLY_MEAN == 129 # EHS 2024-25 + assert _SOCIAL_RENT_HOUSEHOLDS == 5.0e6 + assert _SOCIAL_RENT_TOTAL == 129 * 52 * 5.0e6 def test_mortgage_total_in_range(): diff --git a/uv.lock b/uv.lock index 5a6dfbf9f..e90f2d481 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.13" [[package]] @@ -446,6 +446,7 @@ 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" }, @@ -456,6 +457,7 @@ 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" }, @@ -1364,7 +1366,7 @@ wheels = [ [[package]] name = "policyengine-uk-data" -version = "1.56.14" +version = "1.56.3" source = { editable = "." } dependencies = [ { name = "google-auth" },