Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"schema_version": 1,
"country": "uk",
"policy": "spec-only country package; Python execution lives in shared runtime modules",
"resources": []
"resources": ["wealth_source_stages.json"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
{
"version": 1,
"country": "uk",
"policy": "UK household-wealth source stages are manifest-defined. Wealth holdings are imputed onto the UK population from the ONS Wealth and Assets Survey (WAS) by a weighted donor model over the listed predictors, clipped to the donor's realized range. Cash ISA and stocks & shares (investment) ISA holdings are surfaced as standalone outputs; the investment-ISA component is also folded into corporate_wealth for back-compatibility with the legacy archived UK-data wealth imputation, which summed investment ISAs into corporate_wealth and never represented cash ISAs. Executable Python belongs only in shared Populace runtimes.",
"stages": [
{
"stage": "household_wealth",
"survey": "ONS Wealth and Assets Survey, Round 8 (2018-2020)",
"source": "https://www.ons.gov.uk/peoplepopulationandcommunity/personalandhouseholdfinances/incomeandwealth/methodologies/wealthandassetssurveyqmi",
"grain": "household",
"artifacts": [
{
"kind": "public_microdata",
"format": "tab",
"vintage": "round-8-2018-2020",
"locator": "ONS Wealth and Assets Survey, UK Data Service end-user licence"
}
],
"operations": [
{ "kind": "read_table", "table": "was_household", "weight": "R8xshhwgt" },
{
"kind": "derive",
"outputs": [
"cash_isa",
"stocks_and_shares_isa",
"savings",
"corporate_wealth",
"gross_financial_wealth",
"net_financial_wealth",
"property_wealth",
"main_residence_value",
"other_residential_property_value",
"non_residential_property_value",
"owned_land",
"num_vehicles",
"student_loan_balance"
]
},
{
"kind": "fit_weighted_qrf",
"predictors": [
"is_adult",
"is_child",
"region",
"employment_income",
"self_employment_income",
"private_pension_income",
"savings_interest_income",
"dividend_income",
"hbai_household_net_income",
"tenure_type",
"num_bedrooms",
"council_tax"
]
},
{ "kind": "support_clip", "range": "donor_realized" },
{ "kind": "fold_into", "output": "corporate_wealth", "component": "stocks_and_shares_isa" }
],
"outputs": [
"cash_isa",
"stocks_and_shares_isa",
"savings",
"corporate_wealth",
"gross_financial_wealth",
"net_financial_wealth",
"property_wealth",
"main_residence_value",
"other_residential_property_value",
"non_residential_property_value",
"owned_land",
"num_vehicles",
"student_loan_balance"
],
"nonnegative_outputs": [
"cash_isa",
"stocks_and_shares_isa",
"savings",
"corporate_wealth",
"gross_financial_wealth",
"property_wealth",
"main_residence_value",
"other_residential_property_value",
"non_residential_property_value",
"owned_land",
"num_vehicles",
"student_loan_balance"
],
"notes": "Household wealth holdings imputed from the ONS Wealth and Assets Survey (Round 8). WAS field mapping: cash_isa <- DVCISAVR8 (value of cash ISAs, not mapped in the legacy imputation); stocks_and_shares_isa <- DVIISAVR8_aggr (value of investment ISAs, previously folded silently into corporate_wealth); savings <- DVSaValR8_aggr; property_wealth <- DVPropertyR8; main_residence_value <- DVhvalueR8; gross/net financial wealth <- HFINWR8_SUM/HFINWNTR8_Sum; owned_land <- DVLUKValR8_sum; num_vehicles <- vcarnr8; student_loan_balance <- Tot_LosR8_aggr - Tot_los_exc_SLCR8_aggr. corporate_wealth aggregates non-DB pensions, employee shares/options, UK shares, investment ISAs and unit trusts. Imputed onto the UK population by a weighted donor model over the listed predictors and clipped to the donor's realized range. cash_isa and stocks_and_shares_isa are surfaced as standalone outputs; stocks_and_shares_isa is also folded into corporate_wealth for back-compatibility."
}
]
}
65 changes: 65 additions & 0 deletions packages/populace-build/tests/test_uk_wealth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Contract tests for the UK household-wealth source manifest.

The UK build package is spec-only: the wealth holdings are declared in
``uk/wealth_source_stages.json`` and loaded by the shared source-manifest
runtime. These tests assert the manifest is valid and, in particular, that it
surfaces the cash / stocks-and-shares ISA split (with the investment-ISA
component folded into corporate_wealth for back-compatibility).
"""

from __future__ import annotations

import json
from pathlib import Path

from populace.build.source_manifest import load_source_manifest

UK_PACKAGE = Path(__file__).resolve().parents[1] / "src/populace/build/uk"
WEALTH_MANIFEST_PATH = UK_PACKAGE / "wealth_source_stages.json"
ISA_OUTPUTS = {"cash_isa", "stocks_and_shares_isa"}


def _manifest():
return load_source_manifest(WEALTH_MANIFEST_PATH)


class TestUkWealthManifest:
def test_manifest_is_uk_household_wealth(self) -> None:
manifest = _manifest()
assert manifest.country == "uk"
assert manifest.version >= 1
assert {stage.stage for stage in manifest.stages} == {"household_wealth"}

def test_isa_outputs_present_and_nonnegative(self) -> None:
stage = _manifest().stages[0]
assert ISA_OUTPUTS <= set(stage.outputs)
assert ISA_OUTPUTS <= set(stage.nonnegative_outputs)

def test_stage_imputes_then_clips(self) -> None:
kinds = [op.kind for op in _manifest().stages[0].operations]
assert "fit_weighted_qrf" in kinds
assert "support_clip" in kinds

def test_investment_isa_folded_into_corporate_wealth(self) -> None:
# Back-compat: investment ISAs remain part of corporate_wealth.
folds = [
op
for op in _manifest().stages[0].operations
if op.kind == "fold_into"
]
assert any(
op.parameters.get("output") == "corporate_wealth"
and op.parameters.get("component") == "stocks_and_shares_isa"
for op in folds
)

def test_donor_source_is_cited(self) -> None:
assert _manifest().stages[0].source.startswith("https://")


class TestUkCountryPackage:
def test_manifest_is_registered_as_a_resource(self) -> None:
country_package = json.loads(
(UK_PACKAGE / "country_package.json").read_text(encoding="utf-8")
)
assert "wealth_source_stages.json" in country_package["resources"]
Loading