diff --git a/changelog.d/2692-index-first-growth-year.fixed.md b/changelog.d/2692-index-first-growth-year.fixed.md new file mode 100644 index 0000000000..0433773041 --- /dev/null +++ b/changelog.d/2692-index-first-growth-year.fixed.md @@ -0,0 +1 @@ +Anchor ten growth indices (per-person GDP and mixed income, house prices, mortgage interest, petrol and diesel spending, rail journeys per person, population, the private pension index and lagged earnings) one year before their first listed growth year, so a direct input dated before that year is carried forward by that year's growth instead of a factor of 1. For example, a 2020 `rail_usage` input queried for 2021 grows by 2.5433. Dataset projection, which reads the growth rates, is unchanged, except that residential property values read the rounded house-price index, so their yearly ratios can move slightly (2021 to 2022: 1.0926 before, about 1.0925970 now). diff --git a/policyengine_uk/parameters/gov/contrib/create_private_pension_uprating.py b/policyengine_uk/parameters/gov/contrib/create_private_pension_uprating.py index c09fbe6b73..281f8c8a9b 100644 --- a/policyengine_uk/parameters/gov/contrib/create_private_pension_uprating.py +++ b/policyengine_uk/parameters/gov/contrib/create_private_pension_uprating.py @@ -17,6 +17,9 @@ def add_private_pension_uprating_factor( "gov.economic_assumptions.yoy_growth.obr.private_pension_index", data={ "values": values, + # Anchor the index at 1 on 1 January 2019 so it includes the + # 2020 growth, the first listed (#2692). + "metadata": {"index_base_year": YEARS[0] - 1}, }, ) diff --git a/policyengine_uk/parameters/gov/economic_assumptions/README.md b/policyengine_uk/parameters/gov/economic_assumptions/README.md index b0db05c16f..be915282e9 100644 --- a/policyengine_uk/parameters/gov/economic_assumptions/README.md +++ b/policyengine_uk/parameters/gov/economic_assumptions/README.md @@ -6,6 +6,21 @@ calibrate the input dataset. The source of the default economic series is generated from it by [`create_economic_assumption_indices.py`](./create_economic_assumption_indices.py). +An index is 1 in its series' first listed year by default, so that year's +growth is not in it. A series that sets `index_base_year` to the year before +is 1 there instead, and its first step is the first listed growth (#2692). +`ons.per_capita.household_interest_income`, `obr.per_capita.gdp`, +`obr.per_capita.mixed_income`, `obr.house_prices`, `obr.mortgage_interest`, +`obr.petrol_spending_litre_proxy`, `obr.diesel_spending_litre_proxy`, +`orr.per_capita.rail_passenger_journeys` and `ons.population` set it to 2020, +`obr.private_pension_index` to 2019 and `obr.lagged_average_earnings` to +2021. Only a direct input dated before the first growth year reads that step. +Dataset projection mostly uses the growth rates themselves, so it is +unchanged. The exception is residential property, which reads the +`obr.house_prices` index, rounded to five decimals, so its year-to-year +ratios can move slightly (2021 to 2022: 1.0926 before, about 1.0925970 +now). + The dedicated `capital_gains` growth/index paths copy GDP per capita by default. A simulation can instead supply a versioned aggregate gains projection, converted to per-record factors using population growth after diff --git a/policyengine_uk/parameters/gov/economic_assumptions/lag_average_earnings.py b/policyengine_uk/parameters/gov/economic_assumptions/lag_average_earnings.py index 9153fb0521..6a727173e2 100644 --- a/policyengine_uk/parameters/gov/economic_assumptions/lag_average_earnings.py +++ b/policyengine_uk/parameters/gov/economic_assumptions/lag_average_earnings.py @@ -12,8 +12,14 @@ def add_lagged_earnings( Add lagged average earnings to the economic assumptions. """ obr = parameters.gov.economic_assumptions.yoy_growth.obr + # Anchor the index at 1 on 1 January 2021 so it includes the 2022 + # growth (2021 earnings growth), the first listed (#2692). add_lagged_parameter( - obr, "average_earnings", "lagged_average_earnings", first_year=2022 + obr, + "average_earnings", + "lagged_average_earnings", + first_year=2022, + metadata={"index_base_year": 2021}, ) return parameters diff --git a/policyengine_uk/parameters/gov/economic_assumptions/lagged_series.py b/policyengine_uk/parameters/gov/economic_assumptions/lagged_series.py index cd55ef948f..b67f35afad 100644 --- a/policyengine_uk/parameters/gov/economic_assumptions/lagged_series.py +++ b/policyengine_uk/parameters/gov/economic_assumptions/lagged_series.py @@ -19,8 +19,13 @@ def add_lagged_parameter( source_name: str, lagged_name: str, first_year: int, + metadata: dict = None, ) -> Parameter: - """Add a one-year-lagged copy of ``source_name`` as a child of ``node``.""" + """Add a one-year-lagged copy of ``source_name`` as a child of ``node``. + + ``metadata`` is set on the lagged parameter (for example + ``index_base_year``). + """ source = getattr(node, source_name) lagged = Parameter( @@ -30,6 +35,7 @@ def add_lagged_parameter( f"{year}-01-01": source(year - 1) for year in lag_source_years(source, first_year) }, + **({"metadata": metadata} if metadata else {}), }, ) diff --git a/policyengine_uk/parameters/gov/economic_assumptions/yoy_growth.yaml b/policyengine_uk/parameters/gov/economic_assumptions/yoy_growth.yaml index a94a9b56a1..35dbf88953 100644 --- a/policyengine_uk/parameters/gov/economic_assumptions/yoy_growth.yaml +++ b/policyengine_uk/parameters/gov/economic_assumptions/yoy_growth.yaml @@ -411,6 +411,9 @@ obr: 2072-01-01: 0.0010010010010010895 2073-01-01: 0.0010010010010010895 metadata: + # Anchor the index at 1 on 1 January 2020 so it includes the + # 2021 growth, the first listed (#2692). + index_base_year: 2020 unit: /1 label: Petrol spending litre-proxy growth reference: @@ -478,6 +481,9 @@ obr: 2072-01-01: 0.0010010010010010895 2073-01-01: 0.0010010010010010895 metadata: + # Anchor the index at 1 on 1 January 2020 so it includes the + # 2021 growth, the first listed (#2692). + index_base_year: 2020 unit: /1 label: Diesel spending litre-proxy growth reference: @@ -854,6 +860,9 @@ obr: 2029-01-01: 0.0306 2030-01-01: 0.0315 metadata: + # Anchor the index at 1 on 1 January 2020 so it includes the + # 2021 growth, the first listed (#2692). + index_base_year: 2020 unit: /1 label: Per capita GDP growth reference: @@ -878,6 +887,9 @@ obr: 2029-01-01: 0.0364 2030-01-01: 0.0370 metadata: + # Anchor the index at 1 on 1 January 2020 so it includes the + # 2021 growth, the first listed (#2692). + index_base_year: 2020 unit: /1 label: Per capita mixed income growth reference: @@ -923,6 +935,9 @@ obr: 2029-01-01: 0.026 2030-01-01: 0.024 metadata: + # Anchor the index at 1 on 1 January 2020 so it includes the + # 2021 growth, the first listed (#2692). + index_base_year: 2020 unit: /1 label: House prices growth reference: @@ -949,6 +964,9 @@ obr: # figure; its 2030 Q1 rate is 5.40% and its 2029-30 rate 5.05%. 2030-01-01: 0.0553 metadata: + # Anchor the index at 1 on 1 January 2020 so it includes the + # 2021 growth, the first listed (#2692). + index_base_year: 2020 unit: /1 label: Mortgage interest growth reference: @@ -1098,6 +1116,9 @@ ons: 2072-01-01: -0.0010 2073-01-01: -0.0010 metadata: + # Anchor the index at 1 on 1 January 2020 so it includes the + # 2021 growth, the first listed (#2692). + index_base_year: 2020 unit: /1 label: Population growth reference: @@ -1694,6 +1715,9 @@ orr: 2072-01-01: 0.0010 # 1 / 0.9990 - 1 2073-01-01: 0.0010 # 1 / 0.9990 - 1 metadata: + # Anchor the index at 1 on 1 January 2020 so it includes the + # 2021 growth, the first listed (#2692). + index_base_year: 2020 unit: /1 label: Rail passenger journeys growth per person reference: diff --git a/policyengine_uk/tests/test_growth_index_first_year.py b/policyengine_uk/tests/test_growth_index_first_year.py new file mode 100644 index 0000000000..bcbaf210e0 --- /dev/null +++ b/policyengine_uk/tests/test_growth_index_first_year.py @@ -0,0 +1,104 @@ +"""Growth indices include their first listed year's growth (#2692). + +The index generator puts an index of 1 in a series' first listed year and +multiplies growth only from the next year, so the first listed growth was +never in the index. Each series below sets ``index_base_year`` to the year +before, so the index is 1 there and its first step is that growth. Only a +direct input dated before the first growth year read the dropped step. +Dataset projection reads ``yoy_growth`` directly, except for residential +property, which reads the five-decimal ``obr.house_prices`` index, so its +yearly ratios can move slightly. +""" + +import pytest + +from policyengine_uk import parameters + +YOY = "gov.economic_assumptions.yoy_growth." +INDICES = "gov.economic_assumptions.indices." + +# Series and their first listed growth year, from the #2692 table. +FIRST_GROWTH_YEAR = { + "obr.per_capita.gdp": 2021, + "obr.per_capita.mixed_income": 2021, + "obr.house_prices": 2021, + "obr.mortgage_interest": 2021, + "obr.petrol_spending_litre_proxy": 2021, + "obr.diesel_spending_litre_proxy": 2021, + "orr.per_capita.rail_passenger_journeys": 2021, + "ons.population": 2021, + # Generated: min(RPI in the previous year, 5%) from 2020. + "obr.private_pension_index": 2020, + # Generated: the previous year's earnings growth from 2022. + "obr.lagged_average_earnings": 2022, +} + +# Index levels are rounded to 5 decimal places as they cumulate, so a ratio +# of two levels matches the growth only to about 1e-5. +ROUNDING = 2e-5 + + +def growth(series, year): + return parameters.get_child(YOY + series)(f"{year}-01-01") + + +def index(series, year): + return parameters.get_child(INDICES + series)(f"{year}-01-01") + + +@pytest.mark.parametrize("series", FIRST_GROWTH_YEAR) +def test_index_includes_the_first_growth_year(series): + # For example rail: 2021 / 2020 = 2.5433, not 1. + first = FIRST_GROWTH_YEAR[series] + assert index(series, first - 1) == 1 + assert growth(series, first) != 0 + assert index(series, first) / index(series, first - 1) == pytest.approx( + 1 + growth(series, first), rel=ROUNDING + ) + + +@pytest.mark.parametrize("series", FIRST_GROWTH_YEAR) +def test_index_is_flat_before_its_base_year(series): + # Backdating carries the base year's 1 back, so no growth is invented + # before the first listed year. + first = FIRST_GROWTH_YEAR[series] + for year in range(2015, first): + assert index(series, year) == 1 + + +@pytest.mark.parametrize("series", FIRST_GROWTH_YEAR) +def test_later_steps_still_follow_their_growth(series): + first = FIRST_GROWTH_YEAR[series] + for year in range(first + 1, 2040): + assert index(series, year) / index(series, year - 1) == pytest.approx( + 1 + growth(series, year), rel=ROUNDING + ), year + + +def test_capital_gains_index_still_copies_gdp_per_person(): + # capital_gains_projection.py copies the per-person GDP index exactly. + for year in range(2015, 2040): + assert index("capital_gains", year) == index("obr.per_capita.gdp", year) + + +def test_direct_rail_input_dated_2020_grows_by_the_2021_step(): + # A 2020 input queried for 2021 is carried forward by the index ratio. + # Before #2692 the 2020 and 2021 levels were both 1, so it stayed at + # 100 rather than 100 x 2.5433. + from policyengine_uk import Simulation + + simulation = Simulation( + situation={ + "people": {"adult": {"age": {2020: 40, 2021: 41}}}, + "benunits": {"benunit": {"members": ["adult"]}}, + "households": { + "household": { + "members": ["adult"], + "rail_usage": {2020: 100.0}, + } + }, + } + ) + assert float(simulation.calculate("rail_usage", 2021)[0]) == pytest.approx( + 254.33, abs=0.01 + )