diff --git a/changelog.d/pc-reported-capital.added.md b/changelog.d/pc-reported-capital.added.md new file mode 100644 index 000000000..a4eb7e639 --- /dev/null +++ b/changelog.d/pc-reported-capital.added.md @@ -0,0 +1 @@ +Add `pension_credit_reported_capital`, a benefit-unit input that replaces the household proxy in Pension Credit assessable capital when a dataset records the claimant's and partner's own capital (default -1: no change). diff --git a/policyengine_uk/data/uprating_indices.yaml b/policyengine_uk/data/uprating_indices.yaml index c7175e2fc..fe3072c0c 100644 --- a/policyengine_uk/data/uprating_indices.yaml +++ b/policyengine_uk/data/uprating_indices.yaml @@ -98,6 +98,7 @@ gov.economic_assumptions.yoy_growth.obr.per_capita.gdp: - other_investment_income - other_residential_property_value - owned_land +- pension_credit_reported_capital - pension_income - private_pension_wealth - private_transfer_income diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_reported_capital.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_reported_capital.yaml new file mode 100644 index 000000000..7bd52c0ba --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_reported_capital.yaml @@ -0,0 +1,176 @@ +# Expected values are hand-computed from SPC Regs 2002 reg 15(6): deemed income +# is 1 pound a week for every 500 pounds, or part of 500 pounds, of capital +# above 10,000 pounds (2025: 52 weeks a year). + +- name: With no reported capital (the default, -1), household capital is the proxy. + period: 2025 + input: + people: + person: + age: 70 + benunits: + benunit: + members: [person] + households: + household: + members: [person] + savings: 50_000 + output: + pension_credit_reported_capital: -1 + pension_credit_assessable_capital: 50_000 + # (50,000 - 10,000) / 500 = 80 a week + pension_credit_deemed_income: 80 * 52 + +- name: Reported capital replaces the household proxy. + period: 2025 + input: + people: + person: + age: 70 + benunits: + benunit: + members: [person] + pension_credit_reported_capital: 3_000 + households: + household: + members: [person] + savings: 50_000 + owned_land: 20_000 + output: + pension_credit_assessable_capital: 3_000 + pension_credit_deemed_income: 0 + +- name: Reported capital of zero records no capital. + period: 2025 + input: + people: + person: + age: 70 + benunits: + benunit: + members: [person] + pension_credit_reported_capital: 0 + households: + household: + members: [person] + savings: 50_000 + output: + pension_credit_assessable_capital: 0 + pension_credit_deemed_income: 0 + +- name: Reported capital above the disregard gives deemed income, rounding part of 500 pounds up. + period: 2025 + input: + people: + person: + age: 70 + benunits: + benunit: + members: [person] + pension_credit_reported_capital: 15_001 + households: + household: + members: [person] + output: + pension_credit_assessable_capital: 15_001 + # 5,001 above the disregard is 11 steps of 500 or part of 500 + pension_credit_deemed_income: 11 * 52 + +- name: A couple's reported capital is the whole unit's. + period: 2025 + input: + people: + claimant: + age: 72 + partner: + age: 70 + benunits: + benunit: + members: [claimant, partner] + pension_credit_reported_capital: 12_000 + households: + household: + members: [claimant, partner] + savings: 90_000 + output: + pension_credit_assessable_capital: 12_000 + pension_credit_deemed_income: 4 * 52 + +- name: Reported capital replaces the person-level sources too. + period: 2025 + input: + people: + person: + age: 70 + lifetime_isa_countable_capital: 30_000 + benunits: + benunit: + members: [person] + pension_credit_reported_capital: 2_000 + households: + household: + members: [person] + output: + pension_credit_assessable_capital: 2_000 + +- name: A unit with no one over State Pension age has no Pension Credit capital, reported or not. + period: 2025 + input: + people: + person: + age: 40 + benunits: + benunit: + members: [person] + pension_credit_reported_capital: 50_000 + households: + household: + members: [person] + output: + pension_credit_assessable_capital: 0 + +- name: Another unit's reported capital leaves an unreported unit's household proxy unchanged. + period: 2025 + input: + people: + older: + age: 80 + parent: + age: 75 + other: + age: 72 + benunits: + reporting: + members: [older] + pension_credit_reported_capital: 1_000 + unreported: + members: [parent, other] + households: + household: + members: [older, parent, other] + savings: 30_000 + output: + # The unreported unit keeps its share of the household's pension-age + # adults: 30,000 x 2 / 3. + pension_credit_assessable_capital: [1_000, 20_000] + +- name: Reported capital lifts a reporter out of the deemed income that imputed household capital would give. + period: 2024 + input: + people: + person: + age: 75 + state_pension: 9_000 + benunits: + benunit: + members: [person] + pension_credit_reported_capital: 4_000 + households: + household: + members: [person] + savings: 120_000 + output: + pension_credit_deemed_income: 0 + pension_credit_income: 9_000 + # minimum guarantee 218.15 a week (2024-25, SI 2024/242) x 52 - 9,000 + guarantee_credit: 218.15 * 52 - 9_000 diff --git a/policyengine_uk/tests/test_pension_credit_reported_capital_properties.py b/policyengine_uk/tests/test_pension_credit_reported_capital_properties.py new file mode 100644 index 000000000..cf5fef6f3 --- /dev/null +++ b/policyengine_uk/tests/test_pension_credit_reported_capital_properties.py @@ -0,0 +1,190 @@ +"""Properties of the Pension Credit reported-capital override. + +`pension_credit_reported_capital` (benefit unit, default -1) replaces the +household proxy in `pension_credit_assessable_capital` when it is 0 or more. +Pension Credit counts the claimant's capital and, under the State Pension +Credit Act 2002 s. 5, the partner's, which a survey's benefit-unit capital +measure records directly. + +Invariants, over generated households of one or two benefit units: + +1. Override identity: a pension-age unit with reported capital R >= 0 has + assessable capital R, whatever the household holds. +2. Default differential: with nothing reported, assessable capital equals the + household proxy recomputed here from the inputs (household sources times + the unit's share of the household's pension-age adults, plus person-level + sources of the claimant or partner), the formula before this change. +3. Locality: reporting capital for one unit never changes another unit's + assessable capital. +4. Bounds: assessable capital is never negative, and is 0 for a unit with no + one over State Pension age. +5. Monotonicity: Pension Credit entitlement never rises with reported capital. +6. Deemed income: for reported capital R it is ceil(max(0, R - 10,000) / 500) + pounds a week (SPC Regs 2002 reg 15(6)). +""" + +import math + +import numpy as np +import yaml +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation +from policyengine_uk.model_api import WEEKS_IN_YEAR + +YEAR = 2025 +SETTINGS = settings( + max_examples=20, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +money = st.integers(min_value=0, max_value=200_000) +ages = st.sampled_from([30, 50, 67, 72, 80, 90]) +reported = st.one_of(st.just(-1), st.integers(min_value=0, max_value=60_000)) + + +@st.composite +def households(draw): + two_units = draw(st.booleans()) + units = [] + people = {} + for u in range(2 if two_units else 1): + size = draw(st.integers(min_value=1, max_value=2)) + names = [f"p{u}_{i}" for i in range(size)] + for name in names: + people[name] = { + "age": draw(ages), + "state_pension": draw(st.integers(min_value=0, max_value=12_000)), + "lifetime_isa_countable_capital": draw(st.sampled_from([0, 0, 5_000])), + } + units.append({"members": names, "reported": draw(reported)}) + household = { + "savings": draw(money), + "owned_land": draw(st.sampled_from([0, 0, 25_000])), + "corporate_wealth": draw(st.sampled_from([0, 40_000])), + } + return people, units, household + + +def simulate(people, units, household, overrides=None): + year = str(YEAR) + benunits = {} + for i, unit in enumerate(units): + value = unit["reported"] if overrides is None else overrides[i] + benunits[f"b{i}"] = { + "members": unit["members"], + "pension_credit_reported_capital": {year: value}, + } + situation = { + "people": { + name: {k: {year: v} for k, v in person.items()} + for name, person in people.items() + }, + "benunits": benunits, + "households": { + "h": { + "members": list(people), + **{k: {year: v} for k, v in household.items()}, + } + }, + } + return Simulation(situation=situation) + + +def proxy(people, units, household, index): + """Assessable capital before this change, recomputed from inputs.""" + sources = [ + "savings", + "owned_land", + "other_residential_property_value", + "non_residential_property_value", + "corporate_wealth", + ] + household_capital = sum(household.get(s, 0) for s in sources) + sp_age = {name: p["age"] >= 66 for name, p in people.items()} + unit = units[index]["members"] + unit_sp = sum(sp_age[n] for n in unit) + household_sp = sum(sp_age.values()) + if unit_sp == 0: + return 0.0 + person = sum( + people[n]["lifetime_isa_countable_capital"] + for n in unit + if people[n]["age"] >= 18 + ) + return household_capital * unit_sp / max(1, household_sp) + person + + +@SETTINGS +@given(households()) +def test_override_default_bounds_and_deemed_income(case): + people, units, household = case + sim = simulate(people, units, household) + capital = sim.calculate("pension_credit_assessable_capital", YEAR) + deemed = sim.calculate("pension_credit_deemed_income", YEAR) + for i, unit in enumerate(units): + any_sp = any(people[n]["age"] >= 66 for n in unit["members"]) + assert capital[i] >= 0 + if not any_sp: + assert capital[i] == 0 + elif unit["reported"] >= 0: + assert capital[i] == unit["reported"] + steps = math.ceil(max(0, unit["reported"] - 10_000) / 500) + assert deemed[i] == np.float32(steps * WEEKS_IN_YEAR) + else: + assert np.isclose(capital[i], proxy(people, units, household, i), rtol=1e-6) + + +@SETTINGS +@given(households(), st.integers(min_value=0, max_value=60_000)) +def test_locality(case, value): + people, units, household = case + if len(units) < 2: + return + before = simulate(people, units, household, overrides=[-1, units[1]["reported"]]) + after = simulate(people, units, household, overrides=[value, units[1]["reported"]]) + assert np.isclose( + before.calculate("pension_credit_assessable_capital", YEAR)[1], + after.calculate("pension_credit_assessable_capital", YEAR)[1], + ) + + +@SETTINGS +@given( + households(), + st.integers(min_value=0, max_value=60_000), + st.integers(min_value=0, max_value=60_000), +) +def test_entitlement_never_rises_with_reported_capital(case, a, b): + people, units, household = case + low, high = sorted([a, b]) + overrides_low = [low] + [u["reported"] for u in units[1:]] + overrides_high = [high] + [u["reported"] for u in units[1:]] + e_low = simulate(people, units, household, overrides_low).calculate( + "pension_credit_entitlement", YEAR + )[0] + e_high = simulate(people, units, household, overrides_high).calculate( + "pension_credit_entitlement", YEAR + )[0] + assert e_high <= e_low + 1e-6 + + +def test_reported_capital_uprates_with_savings(): + from pathlib import Path + + from policyengine_uk.system import system + + variables = system.variables + assert ( + variables["pension_credit_reported_capital"].uprating + == variables["savings"].uprating + ) + assert variables["pension_credit_reported_capital"].default_value == -1 + indices = yaml.safe_load( + (Path(__file__).parents[1] / "data" / "uprating_indices.yaml").read_text() + ) + group = [k for k, v in indices.items() if "savings" in v] + assert len(group) == 1 + assert "pension_credit_reported_capital" in indices[group[0]] diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_assessable_capital.py b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_assessable_capital.py index 6b3dfeed1..856bd49da 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_assessable_capital.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_assessable_capital.py @@ -12,7 +12,9 @@ class pension_credit_assessable_capital(Variable): "age adults. Person-level sources, such as a Lifetime ISA, count " "only for the holder's own benunit, and only when the holder is its " "claimant or partner (is_uc_claimant): a dependant's capital is not " - "the claimant's." + "the claimant's. Where `pension_credit_reported_capital` records the " + "benefit unit's own capital (0 or more), it replaces the household " + "proxy and the person-level sources." ) definition_period = YEAR unit = GBP @@ -37,8 +39,10 @@ def formula(benunit, period, parameters): household_capital_proxy = ( household_capital * benunit_pension_age_adults / adult_divisor ) - return where( - any_pension_age, - max_(0, household_capital_proxy + person_capital), - 0, + reported_capital = benunit("pension_credit_reported_capital", period) + assessed_capital = where( + reported_capital >= 0, + reported_capital, + household_capital_proxy + person_capital, ) + return where(any_pension_age, max_(0, assessed_capital), 0) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_reported_capital.py b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_reported_capital.py new file mode 100644 index 000000000..6738da89f --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_reported_capital.py @@ -0,0 +1,23 @@ +from policyengine_uk.model_api import * + + +class pension_credit_reported_capital(Variable): + value_type = float + entity = BenUnit + label = "reported Pension Credit capital" + documentation = ( + "Capital of the claimant and partner as recorded for this benefit unit, " + "such as a survey's benefit-unit capital measure. When it is 0 or more " + "it replaces the household-level proxy in Pension Credit assessable " + "capital: Pension Credit counts the claimant's capital and, under the " + "State Pension Credit Act 2002 s. 5, the partner's, and no one else's. " + "The default, -1, means none is " + "recorded and the household proxy applies. Set it to 0 to record no " + "capital." + ) + definition_period = YEAR + unit = GBP + quantity_type = STOCK + default_value = -1 + uprating = "gov.economic_assumptions.indices.obr.per_capita.gdp" + reference = "https://www.legislation.gov.uk/ukpga/2002/16/section/5"