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
3 changes: 3 additions & 0 deletions .beads/issues.jsonl
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{"id":"policyengine-uk-5qy","title":"Update student loan validation notebook with deeper analysis","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-29T21:01:35.49966-05:00","updated_at":"2025-11-29T21:02:51.469593-05:00","closed_at":"2025-11-29T21:02:51.469593-05:00"}
{"id":"policyengine-uk-75j","title":"Add student loan calibration targets to policyengine-uk-data","description":"Add student loan repayment and balance calibration targets to policyengine-uk-data loss function.\n\n## Proposed Calibration Targets (from SLC 2024-25 statistics)\n\n### Total Repayments by Country\n| Country | Repayments | Source |\n|---------|------------|--------|\n| England (HE) | £5.0bn | SLC 2024-25 |\n| Scotland | £203m | SLC 2024-25 |\n| Wales | £229m | SLC 2024-25 |\n| Northern Ireland | £182m | SLC 2024-25 |\n| **UK Total** | **~£5.6bn** | |\n\n### Repayments by Plan Type (England)\n| Plan | Amount | Share |\n|------|--------|-------|\n| Plan 1 | £1.9bn | 37% |\n| Plan 2 | £2.8bn | 55% |\n| Postgraduate | £0.3bn | 7% |\n| Plan 5 | £41m | 0.8% |\n\n### Number of Borrowers Repaying (England)\n- 3.0m via HMRC\n- 187k scheduled direct\n- 147k voluntary direct\n\n### Outstanding Balances\n- UK Total: £294bn (March 2025)\n\n## Implementation Notes\n1. Add targets to `loss.py` in policyengine-uk-data\n2. May need to adjust for timing (FRS year vs SLC reporting year)\n3. Consider whether to calibrate on modelled (`student_loan_repayment`) or reported (`student_loan_repayments`)\n\n## Sources\n- https://www.gov.uk/government/statistics/student-loans-in-england-2024-to-2025\n- https://www.gov.uk/government/statistics/student-loans-in-scotland-2024-to-2025\n- https://www.gov.uk/government/statistics/student-loans-in-northern-ireland-2024-to-2025\n- https://www.gov.uk/government/statistics/student-loans-in-wales-2024-to-2025","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-11-29T21:01:37.038332-05:00","updated_at":"2025-11-30T12:42:25.851958-05:00","closed_at":"2025-11-30T12:42:25.851958-05:00"}
{"id":"policyengine-uk-e55","title":"Impute student loan balance from WAS to FRS","description":"Impute student loan balance from WAS to FRS.\n\n**GitHub Issue:** https://github.com/PolicyEngine/policyengine-uk-data/issues/238\n\n## WAS Data (Round 7, 2018-2020)\n- Derived from: Tot_LosR7_aggr - Tot_los_exc_SLCR7_aggr\n- 1.66m weighted households with SLC debt\n- Mean balance: £20k, Total: £33.4bn\n- Undercounts vs admin (~24% of SLC total) but captures distribution shape\n\n## Implementation in policyengine-uk-data\n1. Add variables to wealth.py\n2. Impute to FRS\n3. Consider scaling to admin totals\n\n## Then in policyengine-uk\n1. Create student_loan_balance variable\n2. Use for capping repayments (policyengine-uk-exv)\n3. Use for interest accrual (policyengine-uk-lo8)","status":"open","priority":2,"issue_type":"feature","created_at":"2025-11-30T13:00:55.693284-05:00","updated_at":"2025-11-30T21:22:35.720239-05:00"}
{"id":"policyengine-uk-exv","title":"Cap student loan repayments at outstanding balance","description":"## Summary\nCurrently `student_loan_repayment` is calculated as:\n```python\nrepayment = rate * max_(0, income - threshold)\n```\n\nThis has no cap, so high earners can have modelled repayments exceeding their actual loan balance.\n\nExample from validation:\n- Person with £420k income\n- Modelled repayment: £35,470\n- Reported repayment: £1,903\n- Likely explanation: they paid off their loan during the year\n\n## Implementation\n1. Depends on: policyengine-uk-e55 (impute student loan balance)\n2. Add cap: `repayment = min_(repayment, student_loan_balance)`\n3. Consider interest accrual dynamics\n\n## References\n- Real repayments stop when balance reaches zero\n- SLC sends notification when approaching final payment","status":"open","priority":2,"issue_type":"feature","created_at":"2025-11-30T13:01:04.290596-05:00","updated_at":"2025-11-30T13:01:04.290596-05:00","dependencies":[{"issue_id":"policyengine-uk-exv","depends_on_id":"policyengine-uk-e55","type":"blocks","created_at":"2025-11-30T13:01:32.464187-05:00","created_by":"daemon"}]}
{"id":"policyengine-uk-lo8","title":"Add student loan interest accrual calculation","description":"## Summary\nWe have `student_loan_interest_rate` but no calculation of actual interest accrued. This would require:\n1. Outstanding balance (see policyengine-uk-e55)\n2. Interest rate (already implemented)\n3. Decision on timing: interest on opening or closing balance?\n\n## UK Rules\n- Interest is calculated daily on the outstanding balance\n- For Plan 2, rate varies by income (RPI to RPI+3%)\n- Interest is added monthly\n\n## Implementation\n```python\ninterest_accrued = student_loan_balance * student_loan_interest_rate\n```\n\n## Use cases\n- Calculating lifetime loan costs\n- Analysing distributional impact of interest rate changes\n- Understanding real cost of higher education","status":"open","priority":2,"issue_type":"feature","created_at":"2025-11-30T13:01:12.269846-05:00","updated_at":"2025-11-30T13:01:12.269846-05:00","dependencies":[{"issue_id":"policyengine-uk-lo8","depends_on_id":"policyengine-uk-e55","type":"blocks","created_at":"2025-11-30T13:01:32.503041-05:00","created_by":"daemon"}]}
{"id":"policyengine-uk-occ","title":"Research official student loan repayment aggregates for calibration","description":"## Research Findings\n\n### UK Student Loan Repayments 2024-25 (SLC Official Statistics)\n\n**England (HE):** £5.0bn total repayments\n- Plan 1: £1.9bn (37%)\n- Plan 2: £2.8bn (55%)\n- Plan 3/Postgraduate: £0.3bn (7%)\n- Plan 5: £41m (0.8%, voluntary only)\n\n**Scotland:** £203m total repayments (primarily Plan 4)\n\n**Wales:** ~£229m total repayments (6.9% increase from prior year)\n\n**Northern Ireland:** £182m total repayments\n\n**UK Total (estimated):** ~£5.6bn HE repayments\n\n### Borrowers Making Repayments (England)\n- 3.0m via HMRC (39.5% of those liable)\n- 187k scheduled direct to SLC\n- 147k voluntary direct to SLC\n\n### Outstanding Balances\n- England: £236.4bn (end March 2025)\n- Scotland: £9.4bn\n- Northern Ireland: £5.6bn\n- Wales: ~£9-10bn (estimated)\n- **UK Total: ~£260-295bn**\n\n### Sources\n- https://www.gov.uk/government/statistics/student-loans-in-england-2024-to-2025\n- https://www.gov.uk/government/statistics/student-loans-in-scotland-2024-to-2025\n- https://www.gov.uk/government/statistics/student-loans-in-northern-ireland-2024-to-2025\n- https://www.gov.uk/government/statistics/student-loans-in-wales-2024-to-2025","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-29T21:01:36.199753-05:00","updated_at":"2025-11-30T12:41:52.88068-05:00","closed_at":"2025-11-30T12:41:52.88068-05:00","dependencies":[{"issue_id":"policyengine-uk-occ","depends_on_id":"policyengine-uk-75j","type":"blocks","created_at":"2025-11-29T21:01:47.791464-05:00","created_by":"daemon"}]}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
description: Additional interest rate above RPI for Postgraduate loans
metadata:
label: Postgraduate additional rate
unit: /1
period: year
reference:
- title: Education (Student Loans) (Repayment) Regulations 2009, Regulation 21B(1)
href: https://www.legislation.gov.uk/uksi/2009/470/regulation/21B

values:
2016-09-01: 0.03
Original file line number Diff line number Diff line change
@@ -1,121 +1,153 @@
# Tests for student loan interest rate variables
# Plan 1/4: min(RPI, BoE base rate + 1%)
# Plan 2: RPI below lower threshold, tapered to RPI+3% at upper threshold
# Plan 5: RPI only
#
# Uses OBR RPI forecasts from gov.economic_assumptions.yoy_growth.obr.rpi:
# 2025: RPI 4.33%, BoE 4% -> Plan 1/4 = min(4.33%, 5%) = 4.33%
# 2026: RPI 3.71%, BoE 3.6% -> Plan 1/4 = min(3.71%, 4.6%) = 3.71%

# Plan 2 tests
- name: Plan 2 - 2025 - Income below lower threshold (RPI only)
# Test student loan interest rate calculations
# Interest rates vary by plan type and income (for Plan 2)
# Note: RPI for 2025 is ~4.33% per OBR forecasts

# Plan 2 income-contingent interest rate tests
# Below lower threshold: RPI only
# Above upper threshold: RPI + 3%
# Between: linear taper

- name: Plan 2 - Low income gets base rate (RPI only)
period: 2025
absolute_error_margin: 0.001
absolute_error_margin: 0.005
input:
people:
person:
student_loan_plan: PLAN_2
adjusted_net_income: 25_000
adjusted_net_income: 20_000
output:
# Below £28,470: RPI only (4.33%)
student_loan_interest_rate: 0.0433
# Below lower threshold, should be RPI (~4.33%)
plan_2_interest_rate: 0.0433

- name: Plan 2 - 2025 - Income above upper threshold (RPI + 3%)
- name: Plan 2 - High income gets max rate (RPI + 3%)
period: 2025
absolute_error_margin: 0.001
absolute_error_margin: 0.005
input:
people:
person:
student_loan_plan: PLAN_2
adjusted_net_income: 60_000
output:
# Above £51,245: RPI (4.33%) + 3% = 7.33%
student_loan_interest_rate: 0.0733
# Above upper threshold, should be RPI + 3% (~7.33%)
plan_2_interest_rate: 0.0733

- name: Plan 2 - 2026 - Income below lower threshold (RPI only)
period: 2026
absolute_error_margin: 0.001
- name: Plan 2 - Mid income gets tapered rate
period: 2025
absolute_error_margin: 0.01
input:
people:
person:
student_loan_plan: PLAN_2
adjusted_net_income: 40_000
output:
# Between thresholds, tapered rate ~5.8%
plan_2_interest_rate: 0.058

# Plan 5 interest rate (RPI only, regardless of income)

- name: Plan 5 - High income still gets RPI only
period: 2025
absolute_error_margin: 0.005
input:
people:
person:
student_loan_plan: PLAN_5
adjusted_net_income: 100_000
output:
# RPI only regardless of income (~4.33%)
plan_5_interest_rate: 0.0433

# Postgraduate interest rate (RPI + 3%)

- name: Postgraduate - Gets RPI + 3% regardless of income
period: 2025
absolute_error_margin: 0.005
input:
people:
person:
student_loan_plan: POSTGRADUATE
adjusted_net_income: 25_000
output:
# Below £29,385: RPI only (3.71%)
student_loan_interest_rate: 0.0371
# RPI + 3% regardless of income (~7.33%)
postgraduate_interest_rate: 0.0733

# Unified student_loan_interest_rate dispatch tests

- name: No loan returns zero interest rate
period: 2025
input:
people:
person:
student_loan_plan: NONE
adjusted_net_income: 50_000
output:
student_loan_interest_rate: 0

- name: Plan 2 - 2026 - Income above upper threshold (RPI + 3%)
period: 2026
absolute_error_margin: 0.001
- name: Dispatch to Plan 2 rate
period: 2025
absolute_error_margin: 0.005
input:
people:
person:
student_loan_plan: PLAN_2
adjusted_net_income: 60_000
output:
# Above £52,885: RPI (3.71%) + 3% = 6.71%
student_loan_interest_rate: 0.0671
# Should match plan_2_interest_rate at high income (~7.33%)
student_loan_interest_rate: 0.0733

- name: Plan 2 - 2026 - Income midpoint (tapered rate)
period: 2026
absolute_error_margin: 0.001
- name: Dispatch to Postgraduate rate
period: 2025
absolute_error_margin: 0.005
input:
people:
person:
student_loan_plan: PLAN_2
# 2026 calendar year uses 2025-09-01 thresholds: 28,470 / 51,245
# Midpoint: (28,470 + 51,245) / 2 = 39,857.5
adjusted_net_income: 39_858
student_loan_plan: POSTGRADUATE
adjusted_net_income: 30_000
output:
# RPI (3.71%) + half of 3% (1.5%) = 5.21%
student_loan_interest_rate: 0.0521
# Should match postgraduate_interest_rate (~7.33%)
student_loan_interest_rate: 0.0733

# Plan 1 tests - min(RPI, BoE + 1%)
- name: Plan 1 - 2026 - min(RPI, BoE+1%)
period: 2026
absolute_error_margin: 0.001
# Repayment rate tests

- name: No loan returns zero repayment rate
period: 2025
input:
people:
person:
student_loan_plan: PLAN_1
adjusted_net_income: 60_000
student_loan_plan: NONE
output:
# min(3.71%, 3.6% + 1%) = min(3.71%, 4.6%) = 3.71%
plan_1_interest_rate: 0.0371
student_loan_repayment_rate: 0

# Plan 4 tests - same as Plan 1
- name: Plan 4 - 2026 - min(RPI, BoE+1%)
period: 2026
absolute_error_margin: 0.001
- name: Plan 2 returns 9% repayment rate
period: 2025
input:
people:
person:
student_loan_plan: PLAN_4
adjusted_net_income: 60_000
student_loan_plan: PLAN_2
output:
# min(3.71%, 3.6% + 1%) = min(3.71%, 4.6%) = 3.71%
plan_4_interest_rate: 0.0371
student_loan_repayment_rate: 0.09

# Plan 5 tests - RPI only
- name: Plan 5 - 2026 - RPI only
period: 2026
absolute_error_margin: 0.001
- name: Postgraduate returns 6% repayment rate
period: 2025
input:
people:
person:
student_loan_plan: PLAN_5
adjusted_net_income: 60_000
student_loan_plan: POSTGRADUATE
output:
# RPI only (3.71%)
plan_5_interest_rate: 0.0371
student_loan_repayment_rate: 0.06

# Postgraduate repayment integration test
# Note: Threshold uprated by RPI so ~21,909 for 2025

# No student loan
- name: No student loan - Zero rate
period: 2026
- name: Postgraduate - Repayment above threshold at 6% rate
period: 2025
absolute_error_margin: 1
input:
people:
person:
student_loan_plan: NONE
adjusted_net_income: 50_000
employment_income: 30_000
student_loan_plan: POSTGRADUATE
output:
student_loan_interest_rate: 0
# Postgraduate threshold ~£21,909 (uprated from £21,000)
# 6% of (30,000 - 21,909) = 6% of 8,091 = ~485
student_loan_repayment: 485
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from policyengine_uk.model_api import *


class postgraduate_interest_rate(Variable):
value_type = float
entity = Person
label = "Postgraduate loan interest rate"
documentation = (
"Interest rate for Postgraduate loans (Master's and Doctoral). "
"Per Regulation 21B: 'RPI plus 3%'. "
"Unlike Plan 2, this rate applies regardless of income."
)
definition_period = YEAR
unit = "/1"
reference = "https://www.legislation.gov.uk/uksi/2009/470/regulation/21B"

def formula(person, period, parameters):
p = parameters(period).gov
# Per Regulation 21B: "RPI plus 3%"
return (
p.economic_assumptions.yoy_growth.obr.rpi
+ p.hmrc.student_loans.interest_rates.postgraduate_additional_rate
)
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class StudentLoanPlan(Enum):
PLAN_2 = "PLAN_2"
PLAN_4 = "PLAN_4"
PLAN_5 = "PLAN_5"
POSTGRADUATE = "POSTGRADUATE"


class student_loan_plan(Variable):
Expand All @@ -20,6 +21,7 @@ class student_loan_plan(Variable):
"Plan 1: Started before Sept 2012 (England/Wales) or any time (NI). "
"Plan 2: Started Sept 2012 - Aug 2023 (England/Wales). "
"Plan 4: Scotland. "
"Plan 5: Started Aug 2023 onwards (England)."
"Plan 5: Started Aug 2023 onwards (England). "
"Postgraduate: Master's or Doctoral loans."
)
definition_period = YEAR
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ class student_loan_repayment(Variable):
label = "Student loan repayment (modelled)"
documentation = (
"Annual student loan repayment calculated from income and plan type. "
"Repayments are 9% of income above the plan-specific threshold."
"Repayments are 9% of income above threshold for Plans 1/2/4/5, "
"and 6% for Postgraduate loans."
)
definition_period = YEAR
unit = GBP

def formula(person, period, parameters):
plan = person("student_loan_plan", period)
income = person("adjusted_net_income", period)
rate = parameters(period).gov.hmrc.student_loans.repayment_rate
thresholds = parameters(period).gov.hmrc.student_loans.thresholds
p = parameters(period).gov.hmrc.student_loans

# Get threshold based on plan type
threshold = select(
Expand All @@ -28,18 +28,20 @@ def formula(person, period, parameters):
plan == StudentLoanPlan.PLAN_2,
plan == StudentLoanPlan.PLAN_4,
plan == StudentLoanPlan.PLAN_5,
plan == StudentLoanPlan.POSTGRADUATE,
],
[
thresholds.plan_1,
thresholds.plan_2,
thresholds.plan_4,
thresholds.plan_5,
p.thresholds.plan_1,
p.thresholds.plan_2,
p.thresholds.plan_4,
p.thresholds.plan_5,
p.thresholds.postgraduate,
],
default=np.inf,
)

repayment = rate * max_(0, income - threshold)
return repayment
rate = person("student_loan_repayment_rate", period)
return rate * max_(0, income - threshold)


class has_student_loan(Variable):
Expand Down Expand Up @@ -76,12 +78,14 @@ def formula(person, period, parameters):
plan == StudentLoanPlan.PLAN_2,
plan == StudentLoanPlan.PLAN_4,
plan == StudentLoanPlan.PLAN_5,
plan == StudentLoanPlan.POSTGRADUATE,
],
[
person("plan_1_interest_rate", period),
person("plan_2_interest_rate", period),
person("plan_4_interest_rate", period),
person("plan_5_interest_rate", period),
person("postgraduate_interest_rate", period),
],
default=0,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from policyengine_uk.model_api import *
from policyengine_uk.variables.gov.hmrc.student_loans.student_loan_plan import (
StudentLoanPlan,
)


class student_loan_repayment_rate(Variable):
value_type = float
entity = Person
label = "Student loan repayment rate"
documentation = (
"Percentage of income above threshold paid toward student loan. "
"9% for Plans 1/2/4/5, 6% for Postgraduate loans."
)
definition_period = YEAR
unit = "/1"
reference = "https://www.gov.uk/repaying-your-student-loan/what-you-pay"

def formula(person, period, parameters):
plan = person("student_loan_plan", period)
p = parameters(period).gov.hmrc.student_loans

return where(
plan == StudentLoanPlan.POSTGRADUATE,
p.postgraduate_repayment_rate,
where(
plan == StudentLoanPlan.NONE,
0,
p.repayment_rate,
),
)