Skip to content

Commit 395cf4d

Browse files
authored
Impute student loan balances from WAS (#338)
* Impute student loan balances from WAS * Format student loan balance imputation files
1 parent 7a76e0e commit 395cf4d

3 files changed

Lines changed: 130 additions & 46 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Impute `student_loan_balance` from WAS loan aggregates and retrain stale cached wealth models when that new output is missing.

policyengine_uk_data/datasets/imputations/wealth.py

Lines changed: 55 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from policyengine_uk_data.storage import STORAGE_FOLDER
1111
from policyengine_uk.data import UKSingleYearDataset
1212
from policyengine_uk import Microsimulation
13+
from policyengine_uk_data.utils.qrf import QRF
1314

1415
WAS_TAB_FOLDER = STORAGE_FOLDER / "was_2006_20"
1516

@@ -52,8 +53,51 @@
5253
"non_residential_property_value",
5354
"savings",
5455
"num_vehicles",
56+
"student_loan_balance",
5557
]
5658

59+
WAS_RENAMES = {
60+
"R7xshhwgt": "household_weight",
61+
# Components for estimating land holdings.
62+
"DVLUKValR7_sum": "owned_land", # In the UK.
63+
"DVPropertyR7": "property_wealth",
64+
"DVFESHARESR7_aggr": "emp_shares_options",
65+
"DVFShUKVR7_aggr": "uk_shares",
66+
"DVIISAVR7_aggr": "investment_isas",
67+
"DVFCollVR7_aggr": "unit_investment_trusts",
68+
"TotpenR7_aggr": "pensions",
69+
"DvvalDBTR7_aggr": "db_pensions",
70+
# Predictors for fusing to FRS.
71+
"dvtotgirR7": "gross_income",
72+
"NumAdultW7": "num_adults",
73+
"NumCh18W7": "num_children",
74+
# Household Gross Annual income from occupational or private pensions
75+
"DVGIPPENR7_AGGR": "private_pension_income",
76+
"DVGISER7_AGGR": "self_employment_income",
77+
# Household Gross annual income from investments
78+
"DVGIINVR7_aggr": "capital_income",
79+
# Household Total Annual Gross employee income
80+
"DVGIEMPR7_AGGR": "employment_income",
81+
"HBedrmW7": "num_bedrooms",
82+
"GORR7": "region",
83+
"DVPriRntW7": "is_renter", # {1, 2} TODO: Get codebook values.
84+
"CTAmtW7": "council_tax",
85+
# Other columns for reference.
86+
"DVLOSValR7_sum": "non_uk_land",
87+
"HFINWNTR7_Sum": "net_financial_wealth",
88+
"DVLUKDebtR7_sum": "uk_land_debt",
89+
"HFINWR7_Sum": "gross_financial_wealth",
90+
"TotWlthR7": "wealth",
91+
"DVhvalueR7": "main_residence_value",
92+
"DVHseValR7_sum": "other_residential_property_value",
93+
"DVBlDValR7_sum": "non_residential_property_value",
94+
"DVTotinc_bhcR7": "household_net_income",
95+
"DVSaValR7_aggr": "savings",
96+
"vcarnr7": "num_vehicles",
97+
"Tot_LosR7_aggr": "total_loans",
98+
"Tot_los_exc_SLCR7_aggr": "total_loans_exc_slc",
99+
}
100+
57101

58102
def generate_was_table(was: pd.DataFrame):
59103
"""
@@ -70,47 +114,7 @@ def generate_was_table(was: pd.DataFrame):
70114
to_remove = []
71115
to_add = {}
72116

73-
RENAMES = {
74-
"R7xshhwgt": "household_weight",
75-
# Components for estimating land holdings.
76-
"DVLUKValR7_sum": "owned_land", # In the UK.
77-
"DVPropertyR7": "property_wealth",
78-
"DVFESHARESR7_aggr": "emp_shares_options",
79-
"DVFShUKVR7_aggr": "uk_shares",
80-
"DVIISAVR7_aggr": "investment_isas",
81-
"DVFCollVR7_aggr": "unit_investment_trusts",
82-
"TotpenR7_aggr": "pensions",
83-
"DvvalDBTR7_aggr": "db_pensions",
84-
# Predictors for fusing to FRS.
85-
"dvtotgirR7": "gross_income",
86-
"NumAdultW7": "num_adults",
87-
"NumCh18W7": "num_children",
88-
# Household Gross Annual income from occupational or private pensions
89-
"DVGIPPENR7_AGGR": "private_pension_income",
90-
"DVGISER7_AGGR": "self_employment_income",
91-
# Household Gross annual income from investments
92-
"DVGIINVR7_aggr": "capital_income",
93-
# Household Total Annual Gross employee income
94-
"DVGIEMPR7_AGGR": "employment_income",
95-
"HBedrmW7": "num_bedrooms",
96-
"GORR7": "region",
97-
"DVPriRntW7": "is_renter", # {1, 2} TODO: Get codebook values.
98-
"CTAmtW7": "council_tax",
99-
# Other columns for reference.
100-
"DVLOSValR7_sum": "non_uk_land",
101-
"HFINWNTR7_Sum": "net_financial_wealth",
102-
"DVLUKDebtR7_sum": "uk_land_debt",
103-
"HFINWR7_Sum": "gross_financial_wealth",
104-
"TotWlthR7": "wealth",
105-
"DVhvalueR7": "main_residence_value",
106-
"DVHseValR7_sum": "other_residential_property_value",
107-
"DVBlDValR7_sum": "non_residential_property_value",
108-
"DVTotinc_bhcR7": "household_net_income",
109-
"DVSaValR7_aggr": "savings",
110-
"vcarnr7": "num_vehicles",
111-
}
112-
113-
RENAMES = {x.lower(): y for x, y in RENAMES.items()}
117+
RENAMES = {x.lower(): y for x, y in WAS_RENAMES.items()}
114118

115119
for key in RENAMES:
116120
key = key.lower()
@@ -145,19 +149,24 @@ def generate_was_table(was: pd.DataFrame):
145149
"unit_investment_trusts",
146150
]
147151
].sum(axis=1)
152+
was["student_loan_balance"] = was["total_loans"] - was["total_loans_exc_slc"]
148153
was["region"] = was["region"].map(REGIONS)
149154
return was
150155

151156

157+
def _wealth_model_outputs_are_current(model: QRF) -> bool:
158+
"""Check whether a cached wealth model includes all current output columns."""
159+
trained_outputs = getattr(model.model, "imputed_variables", None)
160+
return list(trained_outputs) == IMPUTE_VARIABLES
161+
162+
152163
def save_imputation_models():
153164
"""
154165
Train and save wealth imputation model.
155166
156167
Returns:
157168
Trained QRF model.
158169
"""
159-
from policyengine_uk_data.utils.qrf import QRF
160-
161170
was = pd.read_csv(
162171
WAS_TAB_FOLDER / "was_round_7_hhold_eul_march_2022.tab",
163172
sep="\t",
@@ -185,10 +194,10 @@ def create_wealth_model(overwrite_existing: bool = False):
185194
Returns:
186195
QRF model for wealth imputation.
187196
"""
188-
from policyengine_uk_data.utils.qrf import QRF
189-
190197
if (STORAGE_FOLDER / "wealth.pkl").exists() and not overwrite_existing:
191-
return QRF(file_path=STORAGE_FOLDER / "wealth.pkl")
198+
wealth = QRF(file_path=STORAGE_FOLDER / "wealth.pkl")
199+
if _wealth_model_outputs_are_current(wealth):
200+
return wealth
192201
return save_imputation_models()
193202

194203

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import importlib.util
2+
from pathlib import Path
3+
from types import SimpleNamespace
4+
5+
import pandas as pd
6+
7+
_WEALTH_PATH = (
8+
Path(__file__).resolve().parents[1] / "datasets" / "imputations" / "wealth.py"
9+
)
10+
_WEALTH_SPEC = importlib.util.spec_from_file_location(
11+
"student_loan_balance_wealth_module",
12+
_WEALTH_PATH,
13+
)
14+
wealth = importlib.util.module_from_spec(_WEALTH_SPEC)
15+
_WEALTH_SPEC.loader.exec_module(wealth)
16+
17+
18+
def test_generate_was_table_derives_student_loan_balance():
19+
row = {column: 0 for column in wealth.WAS_RENAMES}
20+
row["R7xshhwgt"] = 1
21+
row["GORR7"] = 11
22+
row["DVPriRntW7"] = 1
23+
row["TotpenR7_aggr"] = 100
24+
row["DvvalDBTR7_aggr"] = 25
25+
row["Tot_LosR7_aggr"] = 20_000
26+
row["Tot_los_exc_SLCR7_aggr"] = 5_000
27+
28+
was = wealth.generate_was_table(pd.DataFrame([row]))
29+
30+
assert "student_loan_balance" in was.columns
31+
assert was.student_loan_balance.iloc[0] == 15_000
32+
assert "student_loan_balance" in wealth.IMPUTE_VARIABLES
33+
34+
35+
def test_create_wealth_model_reuses_current_cached_model(tmp_path, monkeypatch):
36+
model_path = tmp_path / "wealth.pkl"
37+
model_path.write_bytes(b"placeholder")
38+
cached_model = SimpleNamespace(
39+
model=SimpleNamespace(imputed_variables=list(wealth.IMPUTE_VARIABLES))
40+
)
41+
42+
class DummyQRF:
43+
def __init__(self, file_path=None):
44+
assert file_path == model_path
45+
self.model = cached_model.model
46+
47+
monkeypatch.setattr(wealth, "STORAGE_FOLDER", tmp_path)
48+
monkeypatch.setattr(wealth, "QRF", DummyQRF)
49+
monkeypatch.setattr(
50+
wealth,
51+
"save_imputation_models",
52+
lambda: (_ for _ in ()).throw(AssertionError("should not retrain")),
53+
)
54+
55+
model = wealth.create_wealth_model()
56+
assert model.model.imputed_variables == list(wealth.IMPUTE_VARIABLES)
57+
58+
59+
def test_create_wealth_model_retrains_when_cached_outputs_stale(tmp_path, monkeypatch):
60+
model_path = tmp_path / "wealth.pkl"
61+
model_path.write_bytes(b"placeholder")
62+
63+
class DummyQRF:
64+
def __init__(self, file_path=None):
65+
assert file_path == model_path
66+
self.model = SimpleNamespace(imputed_variables=["owned_land"])
67+
68+
fresh_model = object()
69+
70+
monkeypatch.setattr(wealth, "STORAGE_FOLDER", tmp_path)
71+
monkeypatch.setattr(wealth, "QRF", DummyQRF)
72+
monkeypatch.setattr(wealth, "save_imputation_models", lambda: fresh_model)
73+
74+
assert wealth.create_wealth_model() is fresh_model

0 commit comments

Comments
 (0)