Skip to content
Open
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/wealth-share-components.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add `directly_held_shares` and `unit_and_investment_trusts` household inputs for the share-like components of `corporate_wealth`, count them and the stocks and shares ISA in place of `corporate_wealth` in every means-test capital sources list (with `unitemised_corporate_wealth` carrying any part a dataset does not itemise), and uprate them and the ISA balances with per-capita GDP.
2 changes: 1 addition & 1 deletion docs/book/assumptions/growthfactors.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ We derive these rates from OBR GDP growth and ONS population projections.
|-------------|------|------|------|------|------|------|------|------|
| Growth rate | 9.2% | 5.0% | 3.8% | 2.8% | 2.8% | 3.1% | 3.3% | 3.3% |

We use per capita GDP for these variables: `capital_gains`, `capital_gains_badr`, `capital_gains_before_response`, `capital_gains_carried_interest`, `capital_gains_residential_property`, `corporate_wealth`, `dividend_income`, `gross_financial_wealth`, `household_lifetime_isa_balance`, `lifetime_isa_balance`, `lump_sum_income`, `main_residence_value`, `maintenance_income`, `miscellaneous_income`, `mortgage_capital_repayment`, `net_financial_wealth`, `non_residential_property_value`, `other_investment_income`, `other_residential_property_value`, `owned_land`, `pension_income`, `private_pension_wealth`, `private_transfer_income`, `property_income`, `savings`, `savings_interest_income`, `sublet_income`
We use per capita GDP for these variables: `capital_gains`, `capital_gains_badr`, `capital_gains_before_response`, `capital_gains_carried_interest`, `capital_gains_residential_property`, `cash_isa`, `corporate_wealth`, `directly_held_shares`, `dividend_income`, `gross_financial_wealth`, `household_lifetime_isa_balance`, `lifetime_isa_balance`, `lump_sum_income`, `main_residence_value`, `maintenance_income`, `miscellaneous_income`, `mortgage_capital_repayment`, `net_financial_wealth`, `non_residential_property_value`, `other_investment_income`, `other_residential_property_value`, `owned_land`, `pension_credit_reported_capital`, `pension_income`, `private_pension_wealth`, `private_transfer_income`, `property_income`, `savings`, `savings_interest_income`, `stocks_and_shares_isa`, `sublet_income`, `unit_and_investment_trusts`

## Council tax

Expand Down
4 changes: 4 additions & 0 deletions policyengine_uk/data/uprating_indices.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ gov.economic_assumptions.yoy_growth.obr.per_capita.gdp:
- capital_gains_badr
- capital_gains_residential_property
- capital_gains_carried_interest
- cash_isa
- corporate_wealth
- directly_held_shares
- dividend_income
- gross_financial_wealth
- household_lifetime_isa_balance
Expand All @@ -111,7 +113,9 @@ gov.economic_assumptions.yoy_growth.obr.per_capita.gdp:
- property_income
- savings
- savings_interest_income
- stocks_and_shares_isa
- sublet_income
- unit_and_investment_trusts
gov.economic_assumptions.yoy_growth.obr.per_capita.mixed_income:
- self_employment_income
gov.economic_assumptions.yoy_growth.obr.private_pension_index:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ description: >
Support Allowance capital. The Employment and Support Allowance Regulations
2008 treat capital as assessable unless it is disregarded under Schedule 9.
The current mappings are `savings` for liquid capital, `owned_land` for land
held directly, `corporate_wealth` as the broad available proxy for share-like
financial assets, `other_residential_property_value` for residential
held directly, the share-like sources, `other_residential_property_value` for residential
property other than the home, and `non_residential_property_value` for other
land and premises. `main_residence_value` is excluded because Schedule 9
land and premises. `directly_held_shares`, `unit_and_investment_trusts` and `stocks_and_shares_isa` hold the share-like assets the means tests value separately, and `unitemised_corporate_wealth` holds any part of `corporate_wealth` a dataset does not itemise into them: all of it on datasets without the components, and nil on datasets that build `corporate_wealth` as their exact sum. `corporate_wealth` itself is not listed, so no holding counts twice. `main_residence_value` is excluded because Schedule 9
paragraph 1 disregards premises occupied as the claimant's home.
`private_pension_wealth` is excluded because Schedule 9 paragraphs 28 and 29
disregard the value of the right to receive an occupational or personal
Expand All @@ -33,6 +32,9 @@ values:
2008-10-27:
- savings
- owned_land
- corporate_wealth
- directly_held_shares
- unit_and_investment_trusts
- stocks_and_shares_isa
- unitemised_corporate_wealth
- other_residential_property_value
- non_residential_property_value
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ description: >
Allowance capital. The Jobseeker's Allowance Regulations 1996 treat capital
as assessable unless it is disregarded under Schedule 8. The current
mappings are `savings` for liquid capital, `owned_land` for land held
directly, `corporate_wealth` as the broad available proxy for share-like
financial assets, `other_residential_property_value` for residential
directly, the share-like sources, `other_residential_property_value` for residential
property other than the home, and `non_residential_property_value` for other
land and premises. `main_residence_value` is excluded because Schedule 8
land and premises. `directly_held_shares`, `unit_and_investment_trusts` and `stocks_and_shares_isa` hold the share-like assets the means tests value separately, and `unitemised_corporate_wealth` holds any part of `corporate_wealth` a dataset does not itemise into them: all of it on datasets without the components, and nil on datasets that build `corporate_wealth` as their exact sum. `corporate_wealth` itself is not listed, so no holding counts twice. `main_residence_value` is excluded because Schedule 8
paragraph 1 disregards premises occupied as the claimant's home.
`private_pension_wealth` is excluded because Schedule 8 paragraphs 28 and 29
disregard the value of the right to receive an occupational or personal
Expand Down Expand Up @@ -34,6 +33,9 @@ values:
1996-10-07:
- savings
- owned_land
- corporate_wealth
- directly_held_shares
- unit_and_investment_trusts
- stocks_and_shares_isa
- unitemised_corporate_wealth
- other_residential_property_value
- non_residential_property_value
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ description: >
The Housing Benefit Regulations 2006 treat the whole of a claimant's capital
as assessable unless it is disregarded under Schedule 6. The current mappings
are `savings` for liquid capital, `owned_land` for land held directly,
`corporate_wealth` as the broad available proxy for share-like financial
assets such as stocks, shares, unit trusts, and investment ISAs,
the share-like sources,
`other_residential_property_value` for residential property other than the
home, and `non_residential_property_value` for other land and premises.
home, and `non_residential_property_value` for other land and premises. `directly_held_shares`, `unit_and_investment_trusts` and `stocks_and_shares_isa` hold the share-like assets the means tests value separately, and `unitemised_corporate_wealth` holds any part of `corporate_wealth` a dataset does not itemise into them: all of it on datasets without the components, and nil on datasets that build `corporate_wealth` as their exact sum. `corporate_wealth` itself is not listed, so no holding counts twice.
`main_residence_value` is excluded because Schedule 6 paragraph 1 disregards
premises occupied as the claimant's home. `private_pension_wealth` is
excluded because Schedule 6 paragraphs 31 and 32 disregard the value of the
Expand All @@ -15,7 +14,7 @@ description: >
pension-age regulations and paragraphs 32 and 33 of Schedule 7 to the
Northern Ireland regulations make the same disregard). Datasets built before
private pension wealth was split out of `corporate_wealth` still carry it
inside that countable bucket. Other Schedule 6 disregards within these broad
inside `unitemised_corporate_wealth`. Other Schedule 6 disregards within these broad
asset classes are not yet split out separately. Because the
dataset only provides household-level capital stocks, the model allocates
them across benunits using an adult-share proxy.
Expand All @@ -42,6 +41,9 @@ values:
2006-04-10:
- savings
- owned_land
- corporate_wealth
- directly_held_shares
- unit_and_investment_trusts
- stocks_and_shares_isa
- unitemised_corporate_wealth
- other_residential_property_value
- non_residential_property_value
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ description: >
`private_pension_wealth` is excluded because Schedule 10 paragraphs 23 and
23A disregard the value of the right to receive an occupational or personal
pension and the value of funds held under a personal pension scheme.
`directly_held_shares`, `unit_and_investment_trusts` and `stocks_and_shares_isa` hold the share-like assets the means tests value separately, and `unitemised_corporate_wealth` holds any part of `corporate_wealth` a dataset does not itemise into them: all of it on datasets without the components, and nil on datasets that build `corporate_wealth` as their exact sum. `corporate_wealth` itself is not listed, so no holding counts twice.
metadata:
label: Income Support capital sources
period: year
Expand All @@ -22,6 +23,9 @@ values:
2010-01-01:
- savings
- owned_land
- corporate_wealth
- directly_held_shares
- unit_and_investment_trusts
- stocks_and_shares_isa
- unitemised_corporate_wealth
- other_residential_property_value
- non_residential_property_value
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: >
(Part I) disregard the value of the right to receive income from an
occupational or personal pension scheme or under a retirement annuity
contract; the notional income rule for undrawn money purchase pots
(regulation 18) is not modelled.
(regulation 18) is not modelled. `directly_held_shares`, `unit_and_investment_trusts` and `stocks_and_shares_isa` hold the share-like assets the means tests value separately, and `unitemised_corporate_wealth` holds any part of `corporate_wealth` a dataset does not itemise into them: all of it on datasets without the components, and nil on datasets that build `corporate_wealth` as their exact sum. `corporate_wealth` itself is not listed, so no holding counts twice.
metadata:
label: Pension Credit capital sources
unit: list
Expand All @@ -25,4 +25,7 @@ values:
- owned_land
- other_residential_property_value
- non_residential_property_value
- corporate_wealth
- directly_held_shares
- unit_and_investment_trusts
- stocks_and_shares_isa
- unitemised_corporate_wealth
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ description: >
treated as income or disregarded under regulation 48 and Schedule 10. The
current mappings are `savings` for liquid capital,
`other_residential_property_value` for residential property other than the
home, `non_residential_property_value` for other land and premises, and
`corporate_wealth` for shares and similar investment assets. `main_residence_value`
home, `non_residential_property_value` for other land and premises, and the
share-like sources. `directly_held_shares`, `unit_and_investment_trusts` and `stocks_and_shares_isa` hold the share-like assets the means tests value separately, and `unitemised_corporate_wealth` holds any part of `corporate_wealth` a dataset does not itemise into them: all of it on datasets without the components, and nil on datasets that build `corporate_wealth` as their exact sum. `corporate_wealth` itself is not listed, so no holding counts twice. `main_residence_value`
is excluded because Schedule 10 paragraph 1 disregards premises occupied as
the person's home. `private_pension_wealth` is excluded because Schedule 10
paragraph 10 disregards the value of any right to receive a pension under an
occupational or personal pension scheme (or any other scheme registered under
section 153 of the Finance Act 2004; paragraph 10 of Schedule 10 to the
Northern Ireland regulations makes the same disregard); pension income
actually drawn counts as unearned income instead. Other Schedule 10 disregards within these broad asset
actually drawn counts as unearned income instead; datasets built before it
was split out of `corporate_wealth` still carry it inside
`unitemised_corporate_wealth`. Other Schedule 10 disregards within these broad asset
classes are not yet split out separately. Where only household-level capital
is available for a multi-benunit household, the model uses a household-wide
adult-share proxy for residual capital because the dataset does not identify
Expand Down Expand Up @@ -45,4 +47,7 @@ values:
- savings
- other_residential_property_value
- non_residential_property_value
- corporate_wealth
- directly_held_shares
- unit_and_investment_trusts
- stocks_and_shares_isa
- unitemised_corporate_wealth
110 changes: 110 additions & 0 deletions policyengine_uk/tests/test_capital_share_components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""Itemising corporate_wealth never changes a household's capital total.

The means-test capital sources list corporate_wealth's components and
unitemised_corporate_wealth (the part a dataset does not itemise) instead of
corporate_wealth. With no valuation rule that tells the components apart, every
programme's assessable capital must equal what it is when the same holdings are
entered as one unitemised corporate_wealth.
"""

import pytest
from hypothesis import given, settings
from hypothesis import strategies as st

from policyengine_uk import CountryTaxBenefitSystem, Simulation

COMPONENTS = (
"directly_held_shares",
"unit_and_investment_trusts",
"stocks_and_shares_isa",
)
SHARE_LIKE_SOURCES = (*COMPONENTS, "unitemised_corporate_wealth")
SOURCE_LISTS = {
"uc_assessable_capital": "gov.dwp.universal_credit.means_test.capital.sources",
"housing_benefit_assessable_capital": "gov.dwp.housing_benefit.means_test.capital.sources",
"income_support_assessable_capital": "gov.dwp.income_support.means_test.capital.sources",
"jsa_income_assessable_capital": "gov.dwp.JSA.income.capital.sources",
"esa_income_assessable_capital": "gov.dwp.ESA.income.capital.sources",
"pension_credit_assessable_capital": "gov.dwp.pension_credit.income.capital.sources",
}
AGE = {"pension_credit_assessable_capital": 70}
YEAR = 2026

system = CountryTaxBenefitSystem()
amount = st.integers(min_value=0, max_value=60_000)


def _parameter(path):
node = system.parameters
for part in path.split("."):
node = getattr(node, part)
return node(f"{YEAR}-01-01")


def _capital(variable, household):
situation = {
"people": {"adult": {"age": {YEAR: AGE.get(variable, 30)}}},
"benunits": {"benunit": {"members": ["adult"], "would_claim_uc": {YEAR: True}}},
"households": {
"household": {
"members": ["adult"],
**{name: {YEAR: value} for name, value in household.items()},
}
},
}
return Simulation(situation=situation).calculate(variable, YEAR)[0]


@pytest.mark.parametrize("variable", SOURCE_LISTS)
def test_lists_name_the_components_not_corporate_wealth(variable):
sources = list(_parameter(SOURCE_LISTS[variable]))
assert "corporate_wealth" not in sources
assert set(SHARE_LIKE_SOURCES) <= set(sources)


@settings(max_examples=40, deadline=None)
@given(
savings=amount,
corporate_wealth=amount,
components=st.tuples(amount, amount, amount),
)
def test_unitemised_corporate_wealth_is_the_floored_remainder(
savings, corporate_wealth, components
):
household = {"savings": savings, "corporate_wealth": corporate_wealth}
household.update(dict(zip(COMPONENTS, components)))
situation = {
"people": {"adult": {"age": {YEAR: 30}}},
"households": {
"household": {
"members": ["adult"],
**{name: {YEAR: value} for name, value in household.items()},
}
},
}
residual = Simulation(situation=situation).calculate(
"unitemised_corporate_wealth", YEAR
)[0]
assert residual == max(0, corporate_wealth - sum(components))


@pytest.mark.parametrize("variable", SOURCE_LISTS)
@settings(max_examples=15, deadline=None)
@given(
savings=amount,
corporate_wealth=amount,
components=st.tuples(amount, amount, amount),
)
def test_itemising_never_changes_capital(
variable, savings, corporate_wealth, components
):
"""Split holdings count exactly as max(corporate_wealth, their sum) does."""
itemised = {"savings": savings, "corporate_wealth": corporate_wealth}
itemised.update(dict(zip(COMPONENTS, components)))
unitemised = {
"savings": savings,
"corporate_wealth": max(corporate_wealth, sum(components)),
}
assert _capital(variable, itemised) == pytest.approx(
_capital(variable, unitemised), abs=1e-2
)
46 changes: 46 additions & 0 deletions policyengine_uk/tests/test_wealth_components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""corporate_wealth's components share its entity, quantity type and uprating, so
a dataset's identity corporate_wealth == sum of components survives projection."""

from pathlib import Path

import yaml

from policyengine_uk import CountryTaxBenefitSystem

COMPONENTS = (
"directly_held_shares",
"unit_and_investment_trusts",
"stocks_and_shares_isa",
)

system = CountryTaxBenefitSystem()
UPRATING_INDICES = yaml.safe_load(
(Path(__file__).parents[1] / "data" / "uprating_indices.yaml").read_text()
)


def _uprating_index(variable):
return [index for index, names in UPRATING_INDICES.items() if variable in names]


def test_components_match_corporate_wealth():
corporate_wealth = system.variables["corporate_wealth"]
for name in COMPONENTS:
variable = system.variables[name]
assert variable.is_input_variable()
assert variable.entity.key == corporate_wealth.entity.key
assert variable.quantity_type == corporate_wealth.quantity_type
assert variable.uprating == corporate_wealth.uprating
assert _uprating_index(name) == _uprating_index("corporate_wealth")


def test_cash_isa_is_uprated_with_savings():
assert system.variables["cash_isa"].uprating == system.variables["savings"].uprating
assert _uprating_index("cash_isa") == _uprating_index("savings")


def test_components_are_not_part_of_total_wealth():
"""total_wealth adds corporate_wealth, which already holds the components."""
adds = system.variables["total_wealth"].adds
assert "corporate_wealth" in adds
assert not set(COMPONENTS) & set(adds)
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from policyengine_uk.model_api import *


class unitemised_corporate_wealth(Variable):
label = "corporate wealth not itemised into components"
documentation = (
"The part of corporate_wealth not carried in its itemised components "
"directly_held_shares, unit_and_investment_trusts and "
"stocks_and_shares_isa, floored at nil. On datasets without the "
"components it equals corporate_wealth (including the private pension "
"wealth that datasets built before that split fold into it); on datasets "
"that build corporate_wealth as the exact sum of the components it is "
"about nil. The means-test capital sources list the components and this "
"residual instead of corporate_wealth, so each itemised asset can carry "
"its own valuation rule while datasets without the split keep counting "
"corporate_wealth as before, and no holding is counted twice."
)
entity = Household
definition_period = YEAR
value_type = float
unit = GBP
quantity_type = STOCK

def formula(household, period, parameters):
itemised = add(
household,
period,
[
"directly_held_shares",
"unit_and_investment_trusts",
"stocks_and_shares_isa",
],
)
return max_(0, household("corporate_wealth", period) - itemised)
Loading
Loading