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/add-ons-land-value-targets.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add ONS land value calibration targets (household, corporate, total land, property wealth) from the National Balance Sheet 2025.
10 changes: 10 additions & 0 deletions policyengine_uk_data/targets/build_loss_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
compute_household_type,
compute_housing,
compute_income_band,
compute_land_value,
compute_obr_council_tax,
compute_pip_claimants,
compute_regional_age,
Expand Down Expand Up @@ -289,6 +290,15 @@ def _compute_column(target: Target, ctx: _SimContext, year: int) -> np.ndarray |
if name in ("housing/total_mortgage", "housing/rent_private"):
return compute_housing(target, ctx)

# Land and property wealth (ONS National Balance Sheet)
if name in (
"ons/household_land_value",
"ons/corporate_land_value",
"ons/land_value",
"ons/property_wealth",
):
return compute_land_value(target, ctx)

# Savings
if name == "ons/savings_interest_income":
return compute_savings_interest(target, ctx)
Expand Down
2 changes: 2 additions & 0 deletions policyengine_uk_data/targets/compute/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
)
from policyengine_uk_data.targets.compute.other import (
compute_housing,
compute_land_value,
compute_savings_interest,
compute_scottish_child_payment,
compute_student_loan_plan,
Expand All @@ -48,6 +49,7 @@
"compute_gender_age",
"compute_household_type",
"compute_housing",
"compute_land_value",
"compute_income_band",
"compute_obr_council_tax",
"compute_pip_claimants",
Expand Down
5 changes: 5 additions & 0 deletions policyengine_uk_data/targets/compute/other.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ def compute_scottish_child_payment(target, ctx) -> np.ndarray:
return ctx.household_from_person(scp)


def compute_land_value(target, ctx) -> np.ndarray:
"""Compute land/property wealth targets from household-level variables."""
return ctx.pe(target.variable)


def compute_student_loan_plan(target, ctx) -> np.ndarray:
"""Count England borrowers on a given plan with repayments > 0.
Expand Down
83 changes: 83 additions & 0 deletions policyengine_uk_data/targets/sources/ons_land_values.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""ONS National Balance Sheet land value targets.

Aggregate land values from the ONS National Balance Sheet 2025.
The ONS directly measured total UK land at £7.1 trillion for 2024,
broken down into household land (£4.31tn in 2020) and corporate
land (£1.76tn in 2020).

Source: https://www.ons.gov.uk/economy/nationalaccounts/uksectoraccounts/bulletins/nationalbalancesheet/2025
"""

from policyengine_uk_data.targets.schema import Target, Unit

# ONS National Balance Sheet 2025
# 2020 breakdown: household £4.31tn, corporate £1.76tn, total £6.07tn
# 2024 measured total: £7.1tn
# We scale the 2020 household/corporate split proportionally to match
# the 2024 measured total, then hold constant for 2025-2026 (no newer
# ONS measurement available).

_ONS_2020_HOUSEHOLD = 4.31e12
_ONS_2020_CORPORATE = 1.76e12
_ONS_2020_TOTAL = _ONS_2020_HOUSEHOLD + _ONS_2020_CORPORATE
_ONS_2024_TOTAL = 7.1e12

# Scale 2020 split to 2024 measured total
_SCALE = _ONS_2024_TOTAL / _ONS_2020_TOTAL
_ONS_2024_HOUSEHOLD = _ONS_2020_HOUSEHOLD * _SCALE
_ONS_2024_CORPORATE = _ONS_2020_CORPORATE * _SCALE

_REF_URL = "https://www.ons.gov.uk/economy/nationalaccounts/uksectoraccounts/bulletins/nationalbalancesheet/2025"


def get_targets() -> list[Target]:
return [
Target(
name="ons/household_land_value",
variable="household_land_value",
source="ons",
unit=Unit.GBP,
values={
2024: _ONS_2024_HOUSEHOLD,
2025: _ONS_2024_HOUSEHOLD,
2026: _ONS_2024_HOUSEHOLD,
},
reference_url=_REF_URL,
),
Target(
name="ons/corporate_land_value",
variable="corporate_land_value",
source="ons",
unit=Unit.GBP,
values={
2024: _ONS_2024_CORPORATE,
2025: _ONS_2024_CORPORATE,
2026: _ONS_2024_CORPORATE,
},
reference_url=_REF_URL,
),
Target(
name="ons/land_value",
variable="land_value",
source="ons",
unit=Unit.GBP,
values={
2024: _ONS_2024_TOTAL,
2025: _ONS_2024_TOTAL,
2026: _ONS_2024_TOTAL,
},
reference_url=_REF_URL,
),
Target(
name="ons/property_wealth",
variable="property_wealth",
source="ons",
unit=Unit.GBP,
values={
2024: _ONS_2024_TOTAL,
2025: _ONS_2024_TOTAL,
2026: _ONS_2024_TOTAL,
},
reference_url=_REF_URL,
),
]
88 changes: 88 additions & 0 deletions policyengine_uk_data/tests/test_land_value_targets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""Tests for ONS land value calibration targets.

These validate that the generated Enhanced FRS dataset reproduces
aggregate land and property wealth values from the ONS National
Balance Sheet 2025.

Source: https://www.ons.gov.uk/economy/nationalaccounts/uksectoraccounts/bulletins/nationalbalancesheet/2025
"""

import pytest

# ONS National Balance Sheet 2025
# 2024 measured total: £7.1tn
# 2020 split scaled proportionally: household £5.04tn, corporate £2.06tn
_ONS_2020_HOUSEHOLD = 4.31e12
_ONS_2020_CORPORATE = 1.76e12
_ONS_2020_TOTAL = _ONS_2020_HOUSEHOLD + _ONS_2020_CORPORATE
_ONS_2024_TOTAL = 7.1e12
_SCALE = _ONS_2024_TOTAL / _ONS_2020_TOTAL

LAND_TARGETS = {
"land_value": _ONS_2024_TOTAL,
"household_land_value": _ONS_2020_HOUSEHOLD * _SCALE,
"corporate_land_value": _ONS_2020_CORPORATE * _SCALE,
"property_wealth": _ONS_2024_TOTAL,
}

YEAR = 2025
TOLERANCE = 0.30 # 30% relative error allowed


@pytest.mark.xfail(reason="Will pass after recalibration with ONS land value targets")
@pytest.mark.parametrize(
"variable,target",
list(LAND_TARGETS.items()),
ids=list(LAND_TARGETS.keys()),
)
def test_land_value_aggregate(baseline, variable, target):
"""Check weighted aggregate land/property values against ONS targets."""
weights = baseline.calculate("household_weight", period=YEAR).values
values = baseline.calculate(variable, map_to="household", period=YEAR).values
estimate = (values * weights).sum()

rel_error = abs(estimate / target - 1)
assert rel_error < TOLERANCE, (
f"{variable}: expected £{target / 1e12:.2f}tn, "
f"got £{estimate / 1e12:.2f}tn "
f"(relative error = {rel_error:.1%})"
)


@pytest.mark.xfail(reason="Will pass after recalibration with ONS land value targets")
def test_land_value_composition(baseline):
"""Household + corporate land should equal total land value."""
weights = baseline.calculate("household_weight", period=YEAR).values
total = baseline.calculate("land_value", map_to="household", period=YEAR).values
hh = baseline.calculate(
"household_land_value", map_to="household", period=YEAR
).values
corp = baseline.calculate(
"corporate_land_value", map_to="household", period=YEAR
).values

total_agg = (total * weights).sum()
sum_agg = ((hh + corp) * weights).sum()

assert abs(total_agg / sum_agg - 1) < 0.01, (
f"Total land (£{total_agg / 1e12:.2f}tn) should equal "
f"household + corporate (£{sum_agg / 1e12:.2f}tn)"
)


@pytest.mark.xfail(reason="Will pass after recalibration with ONS land value targets")
def test_household_land_less_than_property_wealth(baseline):
"""Household land value should not exceed total property wealth."""
weights = baseline.calculate("household_weight", period=YEAR).values
hh_land = baseline.calculate(
"household_land_value", map_to="household", period=YEAR
).values
prop = baseline.calculate("property_wealth", map_to="household", period=YEAR).values

hh_land_agg = (hh_land * weights).sum()
prop_agg = (prop * weights).sum()

assert hh_land_agg <= prop_agg * 1.05, (
f"Household land (£{hh_land_agg / 1e12:.2f}tn) should not "
f"exceed property wealth (£{prop_agg / 1e12:.2f}tn)"
)
Loading