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/317.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix private rent target to use UK-wide figures and add social rent calibration target (#317).
6 changes: 5 additions & 1 deletion policyengine_uk_data/targets/build_loss_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,11 @@ def _compute_column(target: Target, ctx: _SimContext, year: int) -> np.ndarray |
return compute_vehicles(target, ctx)

# Housing
if name in ("housing/total_mortgage", "housing/rent_private"):
if name in (
"housing/total_mortgage",
"housing/rent_private",
"housing/rent_social",
):
return compute_housing(target, ctx)

# Land and property wealth (ONS National Balance Sheet)
Expand Down
5 changes: 4 additions & 1 deletion policyengine_uk_data/targets/compute/other.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,16 @@ def compute_vehicles(target, ctx) -> np.ndarray:


def compute_housing(target, ctx) -> np.ndarray:
"""Compute housing targets (mortgage, private rent)."""
"""Compute housing targets (mortgage, private rent, social rent)."""
name = target.name
if name == "housing/total_mortgage":
return ctx.pe("mortgage_capital_repayment") + ctx.pe(
"mortgage_interest_repayment"
)
tenure = ctx.sim.calculate("tenure_type", map_to="household").values
if name == "housing/rent_social":
is_social = (tenure == "RENT_FROM_COUNCIL") | (tenure == "RENT_FROM_HA")
return ctx.pe("rent") * is_social
return ctx.pe("rent") * (tenure == "RENT_PRIVATELY")


Expand Down
45 changes: 34 additions & 11 deletions policyengine_uk_data/targets/sources/housing.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,34 @@
"""Housing affordability targets.

Total mortgage payments and private rent from ONS/English Housing Survey.
Total mortgage payments, private rent, and social rent.

Sources:
- ONS PRHI: https://www.ons.gov.uk/economy/inflationandpriceindices/bulletins/privaterentandhousepricesuk/january2025
- English Housing Survey mortgage data
- 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
- EHS + devolved stats: ~5.4m private renters, ~5.0m social renters UK-wide
"""

from policyengine_uk_data.targets.schema import Target, Unit

# Estimated total annual housing costs (£)
# Private rent: avg £1,400/month × 12 × 4.7m private renters
# Mortgage: avg £1,100/month × 12 × 7.5m owner-occupiers with mortgage
_PRIVATE_RENT_TOTAL = 1_400 * 12 * 4.7e6
_MORTGAGE_TOTAL = 1_100 * 12 * 7.5e6
# Private rent: ONS PRHI UK avg £1,374/month × 5.4m UK private renters
_PRIVATE_RENT_TOTAL = 1_374 * 12 * 5.4e6 # ~£89bn

# 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

_EHS_REF = (
"https://www.gov.uk/government/statistics/"
"english-housing-survey-2023-to-2024-rented-sectors"
)
_ONS_RENT_REF = (
"https://www.ons.gov.uk/economy/inflationandpriceindices/"
"bulletins/privaterentandhousepricesuk/march2026"
)


def get_targets() -> list[Target]:
Expand All @@ -24,14 +39,22 @@ def get_targets() -> list[Target]:
source="ons",
unit=Unit.GBP,
values={2025: _MORTGAGE_TOTAL},
reference_url="https://www.ons.gov.uk/economy/inflationandpriceindices/bulletins/privaterentandhousepricesuk/january2025",
reference_url=_ONS_RENT_REF,
),
Target(
name="housing/rent_private",
variable="rent",
source="ons",
source="ehs",
unit=Unit.GBP,
values={2025: _PRIVATE_RENT_TOTAL},
reference_url="https://www.ons.gov.uk/economy/inflationandpriceindices/bulletins/privaterentandhousepricesuk/january2025",
reference_url=_ONS_RENT_REF,
),
Target(
name="housing/rent_social",
variable="rent",
source="ehs",
unit=Unit.GBP,
values={2025: _SOCIAL_RENT_TOTAL},
reference_url=_EHS_REF,
),
]
77 changes: 77 additions & 0 deletions policyengine_uk_data/tests/test_housing_targets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Tests for housing affordability calibration targets.

See: https://github.com/PolicyEngine/policyengine-uk-data/issues/317
"""

from policyengine_uk_data.targets.sources.housing import (
_MORTGAGE_TOTAL,
_PRIVATE_RENT_TOTAL,
_SOCIAL_RENT_TOTAL,
get_targets,
)


# ── Target values ────────────────────────────────────────────────────


def test_private_rent_total_in_range():
"""Private rent total should be £70bn-£110bn (5.4m × ~£1,374/mo)."""
assert 70e9 < _PRIVATE_RENT_TOTAL < 110e9


def test_social_rent_total_in_range():
"""Social rent total should be £20bn-£40bn (5.0m × ~£118/wk)."""
assert 20e9 < _SOCIAL_RENT_TOTAL < 40e9


def test_mortgage_total_in_range():
"""Mortgage total should be £80bn-£120bn (7.5m × ~£1,100/mo)."""
assert 80e9 < _MORTGAGE_TOTAL < 120e9


def test_social_rent_less_than_private():
"""Total social rent should be less than total private rent."""
assert _SOCIAL_RENT_TOTAL < _PRIVATE_RENT_TOTAL


# ── Target structure ─────────────────────────────────────────────────


def test_get_targets_returns_three():
"""get_targets() should return mortgage, private rent, and social rent."""
targets = get_targets()
assert len(targets) == 3


def test_target_names():
"""Target names should match expected values."""
names = {t.name for t in get_targets()}
assert names == {
"housing/total_mortgage",
"housing/rent_private",
"housing/rent_social",
}


def test_all_targets_have_2025():
"""All housing targets should have a value for 2025."""
for t in get_targets():
assert 2025 in t.values, f"{t.name} missing value for 2025"


def test_social_rent_target_variable():
"""Social rent target should use the rent variable."""
targets = get_targets()
social = next(t for t in targets if t.name == "housing/rent_social")
assert social.variable == "rent"


def test_targets_in_registry():
"""Housing targets should appear in the global registry."""
from policyengine_uk_data.targets import get_all_targets

targets = get_all_targets(year=2025)
names = {t.name for t in targets}
assert "housing/rent_social" in names
assert "housing/rent_private" in names
assert "housing/total_mortgage" in names
Loading