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/slc-zero-targets.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Preserve zero-valued SLC borrower targets so calibration can enforce explicit zero years.
3 changes: 2 additions & 1 deletion policyengine_uk_data/targets/sources/slc.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
},
"plan_5": {
"above_threshold": {
2025: 0,
2026: 35_000,
2027: 145_000,
2028: 390_000,
Expand Down Expand Up @@ -145,7 +146,7 @@ def parse_values(row, start_index, years):
if cell_idx >= len(row):
continue
value_text = row[cell_idx].get("text", "")
if value_text and value_text not in ("no data", "0"):
if value_text and value_text != "no data":
data[year] = int(value_text.replace(",", ""))
return data

Expand Down
49 changes: 48 additions & 1 deletion policyengine_uk_data/tests/test_student_loan_targets.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def test_slc_snapshot_values_match_higher_education_total_rows():
assert targets["slc/plan_2_borrowers_liable"].values[2025] == 8_940_000
assert targets["slc/plan_2_borrowers_liable"].values[2030] == 10_525_000

assert 2025 not in targets["slc/plan_5_borrowers_above_threshold"].values
assert targets["slc/plan_5_borrowers_above_threshold"].values[2025] == 0
assert targets["slc/plan_5_borrowers_above_threshold"].values[2026] == 35_000
assert targets["slc/plan_5_borrowers_above_threshold"].values[2030] == 1_235_000
assert targets["slc/plan_5_borrowers_liable"].values[2025] == 10_000
Expand Down Expand Up @@ -120,6 +120,53 @@ def raise_for_status():
slc._fetch_slc_data.cache_clear()


def test_slc_parser_preserves_zero_value_years(monkeypatch):
"""A literal zero should remain a real target year, not be dropped."""
from policyengine_uk_data.targets.sources import slc

table_json = {
"thead": [
[],
[{"text": "2024-25"}] * 6 + [{"text": "2024-25"}] * 6,
],
"tbody": [
[{"text": "Higher education total"}, {"text": "liable"}]
+ [{"text": "8,940,000"}] * 6
+ [{"text": "10,000"}] * 6,
[
{
"text": "Number of borrowers liable to repay and earning above repayment threshold"
}
]
+ [{"text": "3,985,000"}] * 6
+ [{"text": "0"}] * 6,
],
}
html = (
'<script id="__NEXT_DATA__" type="application/json">'
+ json.dumps(
{"props": {"pageProps": {"data": {"table": {"json": table_json}}}}}
)
+ "</script>"
)

class DummyResponse:
text = html

@staticmethod
def raise_for_status():
return None

slc._fetch_slc_data.cache_clear()
monkeypatch.delenv("TESTING", raising=False)
monkeypatch.setattr(slc.requests, "get", lambda *args, **kwargs: DummyResponse())

data = slc._fetch_slc_data()
assert data["plan_5"]["above_threshold"][2025] == 0

slc._fetch_slc_data.cache_clear()


def test_student_loan_target_compute_distinguishes_liable_from_repaying():
"""Above-threshold counts should require repayments, while liable counts should not."""
from policyengine_uk_data.targets.compute.other import (
Expand Down
Loading