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/pla-adg-targets.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add SLC calibration targets for Parents' Learning Allowance and Adult Dependants' Grant.
13 changes: 10 additions & 3 deletions policyengine_uk_data/targets/build_loss_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
compute_housing,
compute_income_band,
compute_land_value,
compute_maintenance_loan,
compute_regional_land_value,
compute_person_support,
compute_obr_council_tax,
compute_pip_claimants,
compute_regional_age,
Expand Down Expand Up @@ -321,8 +321,15 @@ def _compute_column(target: Target, ctx: _SimContext, year: int) -> np.ndarray |
# Student loan plan borrower counts (SLC)
if name.startswith("slc/student_loan_repayment/"):
return compute_student_loan_repayment(target, ctx)
if name in ("slc/maintenance_loan_recipients", "slc/maintenance_loan_spend"):
return compute_maintenance_loan(target, ctx)
if name in (
"slc/maintenance_loan_recipients",
"slc/maintenance_loan_spend",
"slc/parents_learning_allowance_recipients",
"slc/parents_learning_allowance_spend",
"slc/adult_dependants_grant_recipients",
"slc/adult_dependants_grant_spend",
):
return compute_person_support(target, ctx)
if name.startswith("slc/plan_") and "above_threshold" in name:
return compute_student_loan_plan(target, ctx)
if name.startswith("slc/plan_") and "liable" in name:
Expand Down
4 changes: 2 additions & 2 deletions policyengine_uk_data/targets/compute/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@
compute_ss_ni_relief,
)
from policyengine_uk_data.targets.compute.other import (
compute_maintenance_loan,
compute_housing,
compute_land_value,
compute_person_support,
compute_regional_land_value,
compute_savings_interest,
compute_scottish_child_payment,
Expand All @@ -54,10 +54,10 @@
"compute_household_type",
"compute_housing",
"compute_land_value",
"compute_maintenance_loan",
"compute_regional_land_value",
"compute_income_band",
"compute_obr_council_tax",
"compute_person_support",
"compute_pip_claimants",
"compute_regional_age",
"compute_savings_interest",
Expand Down
15 changes: 8 additions & 7 deletions policyengine_uk_data/targets/compute/other.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Miscellaneous compute functions (vehicles, housing, savings, SCP, student loans)."""

import numpy as np
from policyengine_uk_data.targets.schema import Unit

_STUDENT_LOAN_COUNTRIES = {
"england": "ENGLAND",
Expand Down Expand Up @@ -128,11 +129,11 @@ def compute_student_loan_repayment(target, ctx) -> np.ndarray:
return ctx.household_from_person(repayments * mask)


def compute_maintenance_loan(target, ctx) -> np.ndarray:
"""Compute maintenance-loan recipient-count and spend targets."""
maintenance_loan = ctx.pe_person("maintenance_loan")
if target.name == "slc/maintenance_loan_recipients":
return ctx.household_from_person((maintenance_loan > 0).astype(float))
if target.name == "slc/maintenance_loan_spend":
return ctx.household_from_person(maintenance_loan)
def compute_person_support(target, ctx) -> np.ndarray:
"""Compute recipient-count and spend targets for person-level support variables."""
values = ctx.pe_person(target.variable)
if target.is_count or target.unit == Unit.COUNT:
return ctx.household_from_person((values > 0).astype(float))
if target.unit == Unit.GBP:
return ctx.household_from_person(values)
return None
180 changes: 175 additions & 5 deletions policyengine_uk_data/targets/sources/slc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

Borrower counts for England only: Plan 2 and Plan 5.

Two target types are exposed:
Three target families are exposed:
- `above_threshold`: borrowers liable to repay and earning above threshold
- `liable`: all borrowers liable to repay, including below-threshold holders
- `maintenance_loan`: full-time undergraduate England maintenance-loan
recipient counts and total amount paid
- `parents_learning_allowance` and `adult_dependants_grant`: full-time
undergraduate England grant recipient counts and amounts awarded

Source: Explore Education Statistics — Student loan forecasts for England,
Table 6a: Forecast number of student borrowers liable to repay and number
Expand All @@ -18,6 +20,11 @@
England 2025, Table 3A: Maintenance Loans paid to full-time undergraduate
students. Academic year 20XX/YY maps to calendar year 20XX+1.

Parents' Learning Allowance and Adult Dependants' Grant targets come from
Student support for higher education in England 2025, Table 4C (i):
Other targeted support awarded to full-time applicants. Academic year
20XX/YY maps to calendar year 20XX+1.

Data permalink:
https://explore-education-statistics.service.gov.uk/data-tables/permalink/6ff75517-7124-487c-cb4e-08de6eccf22d
"""
Expand All @@ -37,7 +44,7 @@
f"https://explore-education-statistics.service.gov.uk"
f"/data-tables/permalink/{_PERMALINK_ID}"
)
_MAINTENANCE_LOAN_URL = (
_STUDENT_SUPPORT_URL = (
"https://assets.publishing.service.gov.uk/media/"
"691d9e662c6b98ecdbc5003f/slcsp052025.xlsx"
)
Expand Down Expand Up @@ -109,6 +116,68 @@
2025: 8_591_659_718,
},
}
_TARGETED_SUPPORT_TESTING_DATA = {
"adult_dependants_grant": {
"recipients": {
2014: 13_836,
2015: 14_420,
2016: 13_877,
2017: 14_222,
2018: 15_410,
2019: 16_336,
2020: 19_603,
2021: 22_453,
2022: 23_699,
2023: 20_226,
2024: 17_960,
2025: 18_611,
},
"amount_paid": {
2014: 33_050_091,
2015: 34_538_762,
2016: 34_065_350,
2017: 34_884_056,
2018: 38_145_981,
2019: 41_154_737,
2020: 48_060_995,
2021: 56_104_466,
2022: 60_506_271,
2023: 54_638_997,
2024: 51_719_576,
2025: 55_364_917,
},
},
"parents_learning_allowance": {
"recipients": {
2014: 49_219,
2015: 49_409,
2016: 47_414,
2017: 47_772,
2018: 52_568,
2019: 57_408,
2020: 66_407,
2021: 76_740,
2022: 82_911,
2023: 89_283,
2024: 95_287,
2025: 99_645,
},
"amount_paid": {
2014: 70_524_886,
2015: 71_710_128,
2016: 70_799_276,
2017: 70_955_697,
2018: 80_020_029,
2019: 90_006_145,
2020: 106_581_771,
2021: 126_314_502,
2022: 139_380_387,
2023: 153_266_251,
2024: 168_349_637,
2025: 181_421_659,
},
},
}


def get_snapshot_data() -> dict:
Expand All @@ -128,6 +197,14 @@ def get_maintenance_loan_snapshot_data() -> dict:
}


def get_targeted_support_snapshot_data() -> dict:
"""Return the checked-in targeted-support snapshot."""
return {
product: {key: values.copy() for key, values in product_data.items()}
for product, product_data in _TARGETED_SUPPORT_TESTING_DATA.items()
}


@lru_cache(maxsize=1)
def _fetch_slc_data() -> dict:
"""Fetch and parse SLC Table 6a data from Explore Education Statistics.
Expand Down Expand Up @@ -232,7 +309,7 @@ def _fetch_maintenance_loan_data() -> dict:
if os.environ.get("TESTING", "0") == "1":
return get_maintenance_loan_snapshot_data()

df = pd.read_excel(_MAINTENANCE_LOAN_URL, sheet_name="Table 3A", header=None)
df = pd.read_excel(_STUDENT_SUPPORT_URL, sheet_name="Table 3A", header=None)

count_header_row = _find_row(df, "Number of students paid (000s) [27]")
count_year_row = count_header_row + 1
Expand Down Expand Up @@ -266,10 +343,65 @@ def _fetch_maintenance_loan_data() -> dict:
}


def _series_from_row(
df: pd.DataFrame, year_row: int, value_row: int, multiplier: int
) -> dict:
year_columns = {}
for column, value in df.iloc[year_row].items():
if isinstance(value, str) and re.fullmatch(r"\d{4}/\d{2}", value):
year_columns[column] = int(value[:4]) + 1

if not year_columns:
raise ValueError("Could not find year columns")

values = {}
for column, year in year_columns.items():
raw_value = df.iloc[value_row, column]
if pd.notna(raw_value) and raw_value not in (".", "-", ":"):
values[year] = int(round(float(raw_value) * multiplier))
return values


@lru_cache(maxsize=1)
def _fetch_targeted_support_data() -> dict:
"""Fetch Adult Dependants' Grant and Parents' Learning Allowance targets."""
if os.environ.get("TESTING", "0") == "1":
return get_targeted_support_snapshot_data()

df = pd.read_excel(_STUDENT_SUPPORT_URL, sheet_name="Table 4C (i)(ii)", header=None)

count_year_row = _find_row(df, "2013/14")
amount_year_row = _find_row(df, "2013/14", start=count_year_row + 1)
adg_count_row = _find_row(df, "Adult Dependents Grant", start=count_year_row)
pla_count_row = _find_row(df, "Parents Learning Allowance", start=count_year_row)
adg_amount_row = _find_row(df, "Adult Dependents Grant", start=amount_year_row)
pla_amount_row = _find_row(df, "Parents Learning Allowance", start=amount_year_row)

return {
"adult_dependants_grant": {
"recipients": _series_from_row(
df, count_year_row, adg_count_row, multiplier=1_000
),
"amount_paid": _series_from_row(
df, amount_year_row, adg_amount_row, multiplier=1_000_000
),
},
"parents_learning_allowance": {
"recipients": _series_from_row(
df, count_year_row, pla_count_row, multiplier=1_000
),
"amount_paid": _series_from_row(
df, amount_year_row, pla_amount_row, multiplier=1_000_000
),
},
}


def get_targets() -> list[Target]:
"""Generate SLC calibration targets by fetching live data."""
slc_data = _fetch_slc_data()
maintenance_loan_data = _fetch_maintenance_loan_data()
targeted_support_data = _fetch_targeted_support_data()

targets = []

Expand Down Expand Up @@ -299,15 +431,53 @@ def get_targets() -> list[Target]:
unit=Unit.COUNT,
is_count=True,
values=maintenance_loan_data["recipients"],
reference_url=_MAINTENANCE_LOAN_URL,
reference_url=_STUDENT_SUPPORT_URL,
),
Target(
name="slc/maintenance_loan_spend",
variable="maintenance_loan",
source="slc",
unit=Unit.GBP,
values=maintenance_loan_data["amount_paid"],
reference_url=_MAINTENANCE_LOAN_URL,
reference_url=_STUDENT_SUPPORT_URL,
),
Target(
name="slc/parents_learning_allowance_recipients",
variable="parents_learning_allowance",
source="slc",
unit=Unit.COUNT,
is_count=True,
values=targeted_support_data["parents_learning_allowance"][
"recipients"
],
reference_url=_STUDENT_SUPPORT_URL,
),
Target(
name="slc/parents_learning_allowance_spend",
variable="parents_learning_allowance",
source="slc",
unit=Unit.GBP,
values=targeted_support_data["parents_learning_allowance"][
"amount_paid"
],
reference_url=_STUDENT_SUPPORT_URL,
),
Target(
name="slc/adult_dependants_grant_recipients",
variable="adult_dependants_grant",
source="slc",
unit=Unit.COUNT,
is_count=True,
values=targeted_support_data["adult_dependants_grant"]["recipients"],
reference_url=_STUDENT_SUPPORT_URL,
),
Target(
name="slc/adult_dependants_grant_spend",
variable="adult_dependants_grant",
source="slc",
unit=Unit.GBP,
values=targeted_support_data["adult_dependants_grant"]["amount_paid"],
reference_url=_STUDENT_SUPPORT_URL,
),
]
)
Expand Down
Loading
Loading