107 lines
3.3 KiB
Python
107 lines
3.3 KiB
Python
import polars as pl
|
|
import pytest
|
|
|
|
from pipeline.download.education import (
|
|
BAND_MAP,
|
|
OUTPUT_BUCKETS,
|
|
_qualification_percentages,
|
|
)
|
|
|
|
|
|
def _long_rows(geo: str, counts: dict[str, int]) -> list[dict]:
|
|
"""Build NOMIS-shaped long rows for one LSOA from {band_label: count}.
|
|
|
|
NOMIS emits a 0-count row when an LSOA has none in a band, so bands not
|
|
given default to 0 to mirror that.
|
|
"""
|
|
return [
|
|
{
|
|
"GEOGRAPHY_CODE": geo,
|
|
"C2021_HIQUAL_8_NAME": label,
|
|
"OBS_VALUE": counts.get(label, 0),
|
|
}
|
|
for label in BAND_MAP
|
|
]
|
|
|
|
|
|
def test_qualification_percentages_keyed_by_lsoa_with_seven_buckets():
|
|
df = pl.DataFrame(
|
|
_long_rows(
|
|
"E01000001",
|
|
{
|
|
"No qualifications": 10,
|
|
"Level 2 qualifications": 20,
|
|
"Level 3 qualifications": 20,
|
|
"Level 4 qualifications or above": 50,
|
|
},
|
|
)
|
|
)
|
|
|
|
result = _qualification_percentages(df)
|
|
|
|
assert result.columns[0] == "lsoa21"
|
|
assert set(result.columns) == {"lsoa21", *(f"% {b}" for b in OUTPUT_BUCKETS)}
|
|
row = result.filter(pl.col("lsoa21") == "E01000001").to_dicts()[0]
|
|
assert row["% Degree or higher"] == 50.0
|
|
assert row["% No qualifications"] == 10.0
|
|
assert row["% Good GCSEs"] == 20.0
|
|
assert row["% A-levels"] == 20.0
|
|
# Percentages always sum to exactly 100 (largest-remainder rounding).
|
|
assert round(sum(row[f"% {b}"] for b in OUTPUT_BUCKETS), 1) == 100.0
|
|
|
|
|
|
def test_qualification_colloquial_band_mapping():
|
|
"""ONS jargon bands fold into the colloquial buckets, not 'Level N' names."""
|
|
df = pl.DataFrame(
|
|
_long_rows(
|
|
"E01000002",
|
|
{
|
|
"Level 1 and entry level qualifications": 40,
|
|
"Apprenticeship": 60,
|
|
},
|
|
)
|
|
)
|
|
|
|
row = _qualification_percentages(df).to_dicts()[0]
|
|
assert row["% Some GCSEs"] == 40.0
|
|
assert row["% Apprenticeship"] == 60.0
|
|
# No raw ONS "Level N" column names leak through.
|
|
assert not any("Level" in c for c in row)
|
|
|
|
|
|
def test_qualification_percentages_independent_per_lsoa():
|
|
df = pl.concat(
|
|
[
|
|
pl.DataFrame(_long_rows("E01000010", {"Level 4 qualifications or above": 100})),
|
|
pl.DataFrame(_long_rows("E01000011", {"No qualifications": 100})),
|
|
]
|
|
)
|
|
|
|
result = _qualification_percentages(df).sort("lsoa21")
|
|
assert result["% Degree or higher"].to_list() == [100.0, 0.0]
|
|
assert result["% No qualifications"].to_list() == [0.0, 100.0]
|
|
|
|
|
|
def test_qualification_percentages_rejects_unexpected_band():
|
|
rows = _long_rows("E01000003", {"Level 4 qualifications or above": 10})
|
|
rows.append(
|
|
{
|
|
"GEOGRAPHY_CODE": "E01000003",
|
|
"C2021_HIQUAL_8_NAME": "Level 5 brand new census band",
|
|
"OBS_VALUE": 5,
|
|
}
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="do not match the expected"):
|
|
_qualification_percentages(pl.DataFrame(rows))
|
|
|
|
|
|
def test_qualification_percentages_rejects_missing_band():
|
|
rows = [
|
|
r
|
|
for r in _long_rows("E01000004", {"Level 4 qualifications or above": 10})
|
|
if r["C2021_HIQUAL_8_NAME"] != "Apprenticeship"
|
|
]
|
|
|
|
with pytest.raises(ValueError, match="missing"):
|
|
_qualification_percentages(pl.DataFrame(rows))
|