Skip to content

Add net_wealth variable and mortgage_debt / consumer_debt inputs - #1761

Open
MaxGhenis wants to merge 1 commit into
mainfrom
net-wealth-variable
Open

Add net_wealth variable and mortgage_debt / consumer_debt inputs#1761
MaxGhenis wants to merge 1 commit into
mainfrom
net-wealth-variable

Conversation

@MaxGhenis

Copy link
Copy Markdown
Collaborator

What

Adds a net_wealth household variable and the mortgage_debt and consumer_debt inputs that feed it.

  • net_wealth = total_wealthmortgage_debtconsumer_debt − household student_loan_balance.
  • mortgage_debt (gross outstanding mortgage) and consumer_debt (non-mortgage, non-student-loan borrowing) are new household inputs.

Why

Gross total_wealth (property + corporate + savings) has no liabilities netted out, so it is never negative. That makes the bottom of the wealth distribution an artefact: ~13% of households own no property/savings and pile up at exactly £0, leaving the lowest wealth decile empty and hiding the genuinely underwater households (debts > assets). net_wealth puts those households where they belong — the bottom of the distribution — so distributional analysis by wealth reflects reality.

total_wealth is left unchanged (the web app's percentage wealth-change charts rely on it staying non-negative).

Companion

The mortgage_debt / consumer_debt inputs are imputed from the Wealth and Assets Survey by a companion policyengine-uk-data PR. Until a dataset release carries them, both default to 0 (so net_wealth == total_wealth).

Tests

policyengine_uk/tests/policy/baseline/household/wealth/net_wealth.yaml covers the subtraction, a negative-net-wealth case, and the no-debt case.

net_wealth is total_wealth less mortgage, consumer, and student-loan debt.
Unlike gross total_wealth (property + corporate + savings, never negative), net
wealth can be negative when a household's debts exceed its assets, so it ranks
the genuinely underwater households into the bottom of the wealth distribution.

mortgage_debt and consumer_debt are new household inputs imputed by
policyengine-uk-data from the Wealth and Assets Survey.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@vahid-ahmadi vahid-ahmadi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed. The design is sound and the reasoning in the description is right — gross total_wealth piling ~13% of households at exactly £0 is a real artefact, and netting liabilities is the correct fix. Three things before this lands, one of which is blocking in practice.

The companion PR is closed, so this ships inert

The description says mortgage_debt / consumer_debt are "imputed from the Wealth and Assets Survey by a companion policyengine-uk-data PR". That PR is PolicyEngine/policyengine-uk-data#426, and it was closed on 19 June — two weeks after this one was opened — with:

Migrated as part of the uk-data archive cleanup. The debt variables should be owned by Populace compact UK source stages, with provenance and gates tracked in PolicyEngine/microcosm#145.

So as things stand no dataset will ever populate these inputs, both default to 0, and net_wealth reduces to total_wealth − student_loan_balance. The stated purpose — moving underwater households to the bottom of the distribution and filling the empty lowest wealth decile — does not materialise until populace#145 delivers.

That is not an argument against merging: shipping the variable first is reasonable, and the description already says the defaults are zero until a dataset carries them. But the description points at a dead PR, so anyone reading it will look in the wrong place. Worth updating to reference populace#145, and worth confirming that work is actually live — if it is not, this sits inert indefinitely.

student_loan_balance carries "unknown" semantics, not "zero"

Its own documentation says:

Outstanding student loan balance. When not provided, repayments are left uncapped.

and student_loan_repayment.py:23 reads it as max_(0, ...). So in its existing use, default_value = 0 means no data, and the repayment logic deliberately treats that as "do not cap". Here the same 0 is read as "this household has no student debt".

For households where the balance is genuinely missing rather than genuinely zero, net_wealth will be overstated. Given the whole point is to identify underwater households, that bias runs against the goal. Worth at least a note in the net_wealth documentation, or an explicit decision to treat missing as zero.

Test coverage misses the only non-trivial line

The three cases cover the subtraction, a negative result, and the no-debt case — all on what look like single-person households. But the only line that does anything other than arithmetic is:

student_loan_debt = add(household, period, ["student_loan_balance"])

which aggregates a Person-level variable to the household. A two-adult case with a balance on each would pin that, and would catch a regression if the entity or aggregation changed. Everything else in the formula is household-level and cannot really break.

Checked and fine

  • Uprating is consistent, which was my main worry given assets and debts are netted. mortgage_debt and consumer_debt use gov.economic_assumptions.indices.obr.per_capita.gdp, and every asset component resolves to the same index (main_residence_value, other_residential_property_value, non_residential_property_value, corporate_wealth, savings all at 1.297 for 2026). residential_property_value shows uprating=None but is an adds aggregate of two uprated components, so it inherits correctly. Assets and debts therefore grow together and net_wealth will not drift artificially.
  • student_loan_balance itself is not uprated, so student debt shrinks in real terms relative to everything else. Small, but it is the one asymmetry in the formula.
  • Leaving total_wealth untouched for the web app's percentage charts is the right call, and quantity_type = STOCK is consistent throughout.

Housekeeping

Last updated 5 June and mergeable currently reports UNKNOWN — it will need a rebase and a fresh CI run before merge. Note CI on dataset-backed jobs was broken between 31 July and 11 August and is now fixed (#1817), so a rerun should be clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants