diff --git a/changelog.d/cms-long-range-glide.changed.md b/changelog.d/cms-long-range-glide.changed.md new file mode 100644 index 00000000000..f33182da0dc --- /dev/null +++ b/changelog.d/cms-long-range-glide.changed.md @@ -0,0 +1 @@ +Glide the CMS out-of-pocket spending and employer premium upraters from their last projected growth rates to the 2026 Medicare Trustees' long-range per capita health spending rates after 2034, instead of compounding the last projected rate to 2100. diff --git a/policyengine_us/parameters/calibration/gov/hhs/cms/esi_premium_per_enrollee.yaml b/policyengine_us/parameters/calibration/gov/hhs/cms/esi_premium_per_enrollee.yaml index 36951af5b5a..9302c17c389 100644 --- a/policyengine_us/parameters/calibration/gov/hhs/cms/esi_premium_per_enrollee.yaml +++ b/policyengine_us/parameters/calibration/gov/hhs/cms/esi_premium_per_enrollee.yaml @@ -41,6 +41,7 @@ values: 2032-01-01: 11_986 2033-01-01: 12_559 2034-01-01: 13_124 - # 2035 to 2100 grow at the 2033-2034 rate (13_124 / 12_559, 4.5% a year) - # and later years hold the 2100 value; see set_all_uprating_parameters in - # uprating_extensions.py. + # From 2035, growth glides from the 2033-2034 rate (13_124 / 12_559, 4.5% + # a year) to the Medicare Trustees' long-range per capita health spending + # rates (4.3% in 2050, 4.1% in 2100), and later years hold the 2100 value; + # see set_all_uprating_parameters in uprating_extensions.py. diff --git a/policyengine_us/parameters/uprating_extensions.py b/policyengine_us/parameters/uprating_extensions.py index 23ec45e9904..299eb2f4188 100644 --- a/policyengine_us/parameters/uprating_extensions.py +++ b/policyengine_us/parameters/uprating_extensions.py @@ -2,7 +2,7 @@ import math from decimal import ROUND_HALF_UP, Decimal -from typing import Optional, Tuple +from typing import Dict, Optional, Tuple from policyengine_us.model_api import * from policyengine_core.periods import Instant, instant @@ -21,6 +21,62 @@ "above_the_line_deductions", ) +# Long-range annual growth rates from the 2026 Medicare Trustees Report +# (transmitted 2026-06-09, sha256 +# ffa56b9137006872300b0346149eae1613d09a172b6ba118aad48e66dfc48fa8), as +# {year: rate} knots for extend_parameter_values(long_range_growth=...). The +# Trustees set the growth rates for years 11 through 25 of their projection +# (2036-2050) by interpolating between the rate at the end of the short-range +# period and the rate at the start of the last 50 years (file pages 24 and +# 180), and the long-range rates then decline gradually from 2050 to 2100 +# (file pages 23 and 173). They publish only the 2050 and 2100 rates, so the +# path between those two knots is linear here. +# +# Per capita overall health spending grows at GDP plus 0.7 percent (4.3 +# percent) in 2050, declining gradually to GDP plus 0.4 percent (4.1 percent) +# by 2100 (file page 173). The Trustees publish no long-range rate for +# out-of-pocket spending or private insurance (Table V.B5, file page 200, +# stops private health insurance in 2035), so this rate serves those series: +# the factors model behind it holds insurance coverage unchanged (file page +# 175), and the Trustees see only a small likelihood that one part of the +# health sector grows indefinitely at significantly faster rates than the +# others (file page 177). +HEALTH_SPENDING_PER_CAPITA_LONG_RANGE_GROWTH = {2050: 0.043, 2100: 0.041} + +# The Trustees' Medicare cost growth rates exclude the effects of the aging of +# the population and of changes in the sex and time-to-death mix of +# enrollees (file page 172, footnote 81). Spending per enrollee includes +# them: Table II.D1 (file page 29) puts the beneficiary demographic mix at +# -0.1 percent a year in 2051-2100 for each of Parts A, B and D. +MEDICARE_LONG_RANGE_DEMOGRAPHIC_MIX = -0.001 + + +def with_medicare_demographic_mix(cost_growth: Dict[int, float]) -> Dict[int, float]: + """Medicare spending per enrollee growth: the Trustees' cost growth rate + (excluding demographic impacts) compounded with the long-range + demographic mix.""" + return { + year: (1 + rate) * (1 + MEDICARE_LONG_RANGE_DEMOGRAPHIC_MIX) - 1 + for year, rate in cost_growth.items() + } + + +# Parts A, B and D combined: 3.8 percent in 2050, declining to 3.7 percent by +# 2100 (file pages 23 and 180). +MEDICARE_PER_ENROLLEE_LONG_RANGE_GROWTH = with_medicare_demographic_mix( + {2050: 0.038, 2100: 0.037} +) +# Part B: 3.8 percent in 2050, remaining at 3.8 percent by 2100 (file pages 23 +# and 180). +MEDICARE_PART_B_PER_ENROLLEE_LONG_RANGE_GROWTH = with_medicare_demographic_mix( + {2050: 0.038, 2100: 0.038} +) +# Part D: 4.1 percent in 2050, declining to 3.9 percent by 2100 (file pages 23 +# and 179). +MEDICARE_PART_D_PER_ENROLLEE_LONG_RANGE_GROWTH = with_medicare_demographic_mix( + {2050: 0.041, 2100: 0.039} +) + def get_irs_cpi(parameters: ParameterNode, year: int) -> float: """Calculate IRS CPI based on Chained CPI-U average from Sep to Aug.""" @@ -498,6 +554,39 @@ def extend_wa_millionaires_standard_deduction( ) +def long_range_growth_rate( + year: int, + last_projected_year: int, + short_range_growth: float, + long_range_growth: Dict[int, float], +) -> float: + """Annual growth rate for a year after the last projected year, on a path + that glides from the short-range rate to long-range rates. + + The knots are (last_projected_year, short_range_growth) and each + (year, rate) in long_range_growth. The rate moves linearly between + adjacent knots, as the Medicare Trustees interpolate between their + short-range and long-range rates, and holds the last knot's rate after + it. + """ + knots = sorted(long_range_growth.items()) + if not knots or knots[0][0] <= last_projected_year: + raise ValueError("Long-range growth knots must follow the last projected year.") + if year <= last_projected_year: + raise ValueError( + "Long-range growth applies only after the last projected year." + ) + previous_year, previous_rate = last_projected_year, short_range_growth + for knot_year, knot_rate in knots: + if year == knot_year: + return knot_rate + if year < knot_year: + share = (year - previous_year) / (knot_year - previous_year) + return previous_rate + share * (knot_rate - previous_rate) + previous_year, previous_rate = knot_year, knot_rate + return previous_rate + + def extend_parameter_values( parameter: Parameter, last_projected_year: int, @@ -505,6 +594,7 @@ def extend_parameter_values( period_month: int = 1, period_day: int = 1, growth_years: Optional[Tuple[int, int]] = None, + long_range_growth: Optional[Dict[int, float]] = None, ) -> None: """ Extend a parameter's values from last_projected_year to end_year using @@ -520,6 +610,11 @@ def extend_parameter_values( rate between, instead of the last two projected years — used when the final actual embeds a one-off level shift that should not compound in the long-run extension. + long_range_growth: Optional {year: annual growth rate} knots. When + given, growth moves from the measured short-range rate, placed + at last_projected_year, through the knots (see + long_range_growth_rate) instead of repeating the short-range + rate in every year. """ # Calculate the growth rate from the last two years of projections date_format = f"-{period_month:02d}-{period_day:02d}" @@ -533,7 +628,15 @@ def extend_parameter_values( # Apply growth rate for years beyond projections for year in range(last_projected_year + 1, end_year + 1): previous_value = parameter(f"{year - 1}{date_format}") - new_value = previous_value * growth_rate + if long_range_growth is None: + new_value = previous_value * growth_rate + else: + new_value = previous_value * ( + 1 + + long_range_growth_rate( + year, last_projected_year, growth_rate - 1, long_range_growth + ) + ) parameter.update(period=f"year:{year}{date_format}:1", value=new_value) # Set the final value for periods after the last year @@ -841,23 +944,28 @@ def set_all_uprating_parameters(parameters: ParameterNode) -> ParameterNode: # CMS per-capita out-of-pocket medical spending (January values, last # projection year 2035). Health expense inputs use this as their uprater. + # Growth glides from the 2034-2035 rate to the Medicare Trustees' + # long-range per capita health spending rates. extend_parameter_values( parameters.calibration.gov.hhs.cms.moop_per_capita, last_projected_year=2035, end_year=END_YEAR, period_month=1, period_day=1, + long_range_growth=HEALTH_SPENDING_PER_CAPITA_LONG_RANGE_GROWTH, ) # CMS employer-sponsored insurance premium per enrollee (January values, # last projection year 2034). Employer and pre-tax employee premium - # inputs use this as their uprater. + # inputs use this as their uprater. Growth glides from the 2033-2034 rate + # to the Medicare Trustees' long-range per capita health spending rates. extend_parameter_values( parameters.calibration.gov.hhs.cms.esi_premium_per_enrollee, last_projected_year=2034, end_year=END_YEAR, period_month=1, period_day=1, + long_range_growth=HEALTH_SPENDING_PER_CAPITA_LONG_RANGE_GROWTH, ) # Gross Social Security benefit inputs should age with gross Social diff --git a/policyengine_us/tests/core/test_esi_premium_uprating.py b/policyengine_us/tests/core/test_esi_premium_uprating.py index 88db168c410..cbadf456e6a 100644 --- a/policyengine_us/tests/core/test_esi_premium_uprating.py +++ b/policyengine_us/tests/core/test_esi_premium_uprating.py @@ -5,8 +5,9 @@ report ESI spending, ESI enrollment and ESI spending per enrollee for 2013 to 2034. ESI spending is premiums paid by employers and employees (NHEA methodology paper, file page 29), so spending per enrollee is the premium per -covered person. The parameter holds that row; 2035 to 2100 grow at its -2033-2034 rate. +covered person. The parameter holds that row; from 2035 its growth glides +from the 2033-2034 rate to the Medicare Trustees' long-range per capita health +spending rates (``tests/core/test_long_range_health_growth.py``). These tests read only the parameter tree. Engine uprating of the inputs (a household simulation and dataset extension) is covered in @@ -23,7 +24,8 @@ 3. Bounds and order: the index is positive in every year and strictly increasing, as CMS's series is in every year from 2013 to 2034; the extension keeps both through 2100. -4. Extension rule: each year from 2035 to 2100 grows by 13,124 / 12,559. +4. Extension rule: growth in 2035 starts from 13,124 / 12,559 and reaches + the Trustees' 4.3 percent in 2050 and 4.1 percent in 2100. """ import pytest @@ -130,10 +132,15 @@ def test_index_is_positive_and_strictly_increasing(year, gap): @given(st.integers(LAST_PROJECTED_YEAR + 1, END_YEAR)) -def test_extension_holds_the_last_projected_growth_rate(year): - last_growth = _index(LAST_PROJECTED_YEAR) / _index(LAST_PROJECTED_YEAR - 1) - assert last_growth == pytest.approx(13_124 / 12_559, rel=1e-12) - assert _index(year) / _index(year - 1) == pytest.approx(last_growth, rel=1e-12) +def test_extension_glides_from_the_last_projected_growth_rate(year): + last_growth = _index(LAST_PROJECTED_YEAR) / _index(LAST_PROJECTED_YEAR - 1) - 1 + assert last_growth == pytest.approx(13_124 / 12_559 - 1, rel=1e-12) + if year <= 2050: + share = (year - LAST_PROJECTED_YEAR) / (2050 - LAST_PROJECTED_YEAR) + expected = last_growth + share * (0.043 - last_growth) + else: + expected = 0.043 + (year - 2050) / 50 * (0.041 - 0.043) + assert _index(year) / _index(year - 1) - 1 == pytest.approx(expected, rel=1e-12) @pytest.mark.parametrize("variable", PREMIUM_INPUTS) diff --git a/policyengine_us/tests/core/test_long_range_health_growth.py b/policyengine_us/tests/core/test_long_range_health_growth.py new file mode 100644 index 00000000000..c63c2e52562 --- /dev/null +++ b/policyengine_us/tests/core/test_long_range_health_growth.py @@ -0,0 +1,355 @@ +"""Long-range growth for CMS health spending upraters. + +After a series' last projected year, ``extend_parameter_values`` with +``long_range_growth`` knots moves each year's growth linearly from the +measured short-range rate, placed at the last projected year, to the 2026 +Medicare Trustees' 2050 and 2100 rates. The Trustees interpolate years 11 +through 25 of their projection the same way (2026 Medicare Trustees Report, +file pages 24 and 180). + +Invariants: + +1. Knots: the rate in a knot year is that knot's rate exactly. +2. Betweenness: every rate lies between the two knots around it, so the path + never overshoots either end; after the last knot the rate holds. +3. Closed form: the rate in year y between knots (y0, g0) and (y1, g1) is + g0 + (g1 - g0)(y - y0) / (y1 - y0), and the value is the last projected + value times the product of (1 + rate). Checked against an independent + numpy implementation, for synthetic series and for the wired CMS series. +4. Constant knots: knots equal to the short-range rate reproduce the + compound path V_L (1 + g)^(y - L), and no knots leave the old rule intact. +5. Monotonicity: raising any knot's rate never lowers any extended value. +6. Homogeneity: scaling the projected values by c > 0 scales every extended + value by c, so the glide depends only on growth, not on the level. +7. Projected years are untouched, and years after the end year hold the end + year's value. +8. Trustees consistency: starting from the Trustees' 2035 per beneficiary + growth (Table V.D1), the Medicare per-enrollee knots reproduce the growth + in spending per enrollee implied by the Trustees' incurred expenditure + growth (Table V.B5) and enrollment (Table V.B3) in each long-range period, + and the Part B and Part D knots reproduce Table II.D1's total increase net + of beneficiary growth, within the tables' rounding. +""" + +import numpy as np +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st +from policyengine_core.parameters import ParameterNode + +from policyengine_us.parameters.uprating_extensions import ( + HEALTH_SPENDING_PER_CAPITA_LONG_RANGE_GROWTH, + MEDICARE_LONG_RANGE_DEMOGRAPHIC_MIX, + MEDICARE_PART_B_PER_ENROLLEE_LONG_RANGE_GROWTH, + MEDICARE_PART_D_PER_ENROLLEE_LONG_RANGE_GROWTH, + MEDICARE_PER_ENROLLEE_LONG_RANGE_GROWTH, + extend_parameter_values, + long_range_growth_rate, + with_medicare_demographic_mix, +) +from policyengine_us.system import system + +END_YEAR = 2100 + +rates = st.floats(-0.05, 0.15, allow_nan=False, allow_infinity=False) + + +@st.composite +def glide_cases(draw): + last = draw(st.integers(2020, 2060)) + knot_years = draw( + st.lists( + st.integers(last + 1, END_YEAR + 20), min_size=1, max_size=4, unique=True + ) + ) + knots = {year: draw(rates) for year in knot_years} + return last, draw(rates), knots + + +def _series(values): + # A node, so Parameter.update can reach a parent. + node = ParameterNode( + "node", + data={ + "series": { + "values": {f"{year}-01-01": value for year, value in values.items()} + } + }, + ) + return node.series + + +def _extend(last, short, knots, level=100.0, end_year=END_YEAR): + """Extend a two-point series whose last projected year grows at + ``short``.""" + series = _series({last - 1: level, last: level * (1 + short)}) + extend_parameter_values( + series, last, end_year, long_range_growth=knots if knots else None + ) + return series + + +def _value(series, year): + return series(f"{year}-01-01") + + +def _reference_path(last_value, last, short, knots, end_year=END_YEAR): + """Independent implementation: numpy interpolation and a cumulative + product.""" + years = np.arange(last + 1, end_year + 1) + knot_years = [last, *sorted(knots)] + knot_rates = [short, *(knots[year] for year in sorted(knots))] + growth = np.interp(years, knot_years, knot_rates) + return dict(zip(years.tolist(), last_value * np.cumprod(1 + growth))) + + +def _average_growth(series, start, end): + """Average annual growth from the end of ``start - 1`` to ``end``.""" + return (_value(series, end) / _value(series, start - 1)) ** ( + 1 / (end - start + 1) + ) - 1 + + +# 1-2. Knots and betweenness. + + +@given(glide_cases()) +def test_rate_in_a_knot_year_is_the_knot_rate(case): + last, short, knots = case + for year, rate in knots.items(): + assert long_range_growth_rate(year, last, short, knots) == rate + + +@given(glide_cases(), st.integers(1, 80)) +def test_rate_lies_between_the_surrounding_knots(case, offset): + last, short, knots = case + year = last + offset + points = [(last, short), *sorted(knots.items())] + rate = long_range_growth_rate(year, last, short, knots) + if year >= points[-1][0]: + assert rate == points[-1][1] + return + (_, before), (_, after) = next( + (left, right) + for left, right in zip(points, points[1:]) + if left[0] < year <= right[0] + ) + assert min(before, after) - 1e-15 <= rate <= max(before, after) + 1e-15 + + +@pytest.mark.parametrize("year", [2034, 2020]) +def test_rate_is_undefined_up_to_the_last_projected_year(year): + with pytest.raises(ValueError): + long_range_growth_rate(year, 2034, 0.05, {2050: 0.04}) + + +@pytest.mark.parametrize("knots", [{}, {2034: 0.04}, {2030: 0.04, 2050: 0.04}]) +def test_knots_must_follow_the_last_projected_year(knots): + with pytest.raises(ValueError): + long_range_growth_rate(2040, 2034, 0.05, knots) + + +# 3. Closed form. + + +def test_rate_on_the_first_segment_matches_the_closed_form(): + # 2034 at 5%, 2050 at 3%: each year moves 2 / 16 = 0.125 point. + for offset in range(1, 17): + rate = long_range_growth_rate(2034 + offset, 2034, 0.05, {2050: 0.03}) + assert rate == pytest.approx(0.05 - 0.00125 * offset, abs=1e-15) + + +@given(glide_cases(), st.floats(1, 1e6)) +@settings(max_examples=60, deadline=None) +def test_extension_matches_an_independent_implementation(case, level): + last, short, knots = case + series = _extend(last, short, knots, level) + reference = _reference_path(level * (1 + short), last, short, knots) + for year, expected in reference.items(): + assert _value(series, year) == pytest.approx(expected, rel=1e-12) + + +# 4. Constant knots and the old rule. + + +@given(st.integers(2020, 2060), rates) +@settings(max_examples=40, deadline=None) +def test_constant_knots_reproduce_compound_growth(last, short): + glided = _extend(last, short, {2050 if last < 2050 else last + 1: short}) + compounded = _extend(last, short, None) + for year in range(last + 1, END_YEAR + 1): + expected = 100 * (1 + short) ** (year - last + 1) + assert _value(glided, year) == pytest.approx(expected, rel=1e-12) + assert _value(compounded, year) == pytest.approx(expected, rel=1e-12) + + +# 5. Monotonicity. + + +@given(glide_cases(), st.data(), st.floats(1e-4, 0.05)) +@settings(max_examples=60, deadline=None) +def test_raising_a_knot_never_lowers_a_value(case, data, increase): + last, short, knots = case + raised_year = data.draw(st.sampled_from(sorted(knots))) + raised = {**knots, raised_year: knots[raised_year] + increase} + base = _extend(last, short, knots) + higher = _extend(last, short, raised) + for year in range(last + 1, END_YEAR + 1): + assert _value(higher, year) >= _value(base, year) * (1 - 1e-12) + if raised_year <= END_YEAR: + assert _value(higher, END_YEAR) > _value(base, END_YEAR) + + +# 6. Homogeneity. + + +@given(glide_cases(), st.floats(1e-3, 1e3)) +@settings(max_examples=40, deadline=None) +def test_scaling_the_level_scales_every_value(case, scale): + last, short, knots = case + base = _extend(last, short, knots) + scaled = _extend(last, short, knots, level=100 * scale) + for year in range(last - 1, END_YEAR + 30): + assert _value(scaled, year) == pytest.approx( + scale * _value(base, year), rel=1e-12 + ) + + +# 7. Projected years and the hold after the end year. + + +@given(glide_cases(), st.integers(1, 40)) +@settings(max_examples=40, deadline=None) +def test_projected_years_hold_and_later_years_freeze(case, later): + last, short, knots = case + end_year = min(last + later, END_YEAR) + series = _extend(last, short, knots, end_year=end_year) + assert _value(series, last - 1) == 100 + assert _value(series, last) == pytest.approx(100 * (1 + short), rel=1e-15) + for year in (end_year + 1, end_year + 25): + assert _value(series, year) == _value(series, end_year) + + +# 8. Trustees consistency. + +# Table V.D1 (file page 210): 2035 average percent change in per beneficiary +# costs, intermediate estimates. +TRUSTEES_2035_PER_BENEFICIARY_GROWTH = { + "total": 0.054, + "part_b": 0.067, + "part_d": 0.031, +} +# Table V.B5 (file page 200): average annual growth in incurred Medicare +# expenditures. +TRUSTEES_INCURRED_EXPENDITURE_GROWTH = { + (2036, 2050): 0.049, + (2051, 2075): 0.043, + (2076, 2100): 0.038, +} +# Table V.B3 (file page 196): beneficiaries with HI and/or SMI, thousands. +TRUSTEES_TOTAL_ENROLLMENT = {2035: 80_632, 2050: 85_413, 2075: 99_585, 2100: 103_133} +# Table II.D1 (file page 29): average annual percentage change in total +# incurred expenditures and in the number of beneficiaries, by Part. +TRUSTEES_COMPONENTS = { + "part_b": {(2036, 2050): (0.056, 0.004), (2051, 2100): (0.041, 0.004)}, + "part_d": {(2036, 2050): (0.039, 0.004), (2051, 2100): (0.043, 0.004)}, +} +# Every published rate is rounded to a tenth of a point. A period's implied +# per-enrollee growth carries two such roundings, and the glide's start rate +# and knots carry more; 0.1 point covers them for the long periods. +V_B5_TOLERANCE = 0.001 +# Table II.D1's per-beneficiary growth nets two rounded components. +II_D1_TOLERANCE = 0.0015 + + +def test_medicare_knots_compound_cost_growth_with_the_demographic_mix(): + assert MEDICARE_LONG_RANGE_DEMOGRAPHIC_MIX == -0.001 + assert MEDICARE_PER_ENROLLEE_LONG_RANGE_GROWTH == pytest.approx( + {2050: 1.038 * 0.999 - 1, 2100: 1.037 * 0.999 - 1}, abs=1e-15 + ) + assert with_medicare_demographic_mix({2050: 0.0}) == pytest.approx( + {2050: -0.001}, abs=1e-15 + ) + # The mix lowers growth by about a tenth of a point in every Part. + for knots in ( + MEDICARE_PER_ENROLLEE_LONG_RANGE_GROWTH, + MEDICARE_PART_B_PER_ENROLLEE_LONG_RANGE_GROWTH, + MEDICARE_PART_D_PER_ENROLLEE_LONG_RANGE_GROWTH, + ): + for rate in knots.values(): + assert 0.035 < rate < 0.041 + + +@pytest.mark.parametrize("period", TRUSTEES_INCURRED_EXPENDITURE_GROWTH) +def test_medicare_knots_reproduce_trustees_spending_per_enrollee(period): + start, end = period + enrollment_growth = ( + TRUSTEES_TOTAL_ENROLLMENT[end] / TRUSTEES_TOTAL_ENROLLMENT[start - 1] + ) ** (1 / (end - start + 1)) - 1 + implied = (1 + TRUSTEES_INCURRED_EXPENDITURE_GROWTH[period]) / ( + 1 + enrollment_growth + ) - 1 + series = _extend( + 2035, + TRUSTEES_2035_PER_BENEFICIARY_GROWTH["total"], + MEDICARE_PER_ENROLLEE_LONG_RANGE_GROWTH, + ) + assert _average_growth(series, start, end) == pytest.approx( + implied, abs=V_B5_TOLERANCE + ) + + +@pytest.mark.parametrize( + "part, knots", + [ + ("part_b", MEDICARE_PART_B_PER_ENROLLEE_LONG_RANGE_GROWTH), + ("part_d", MEDICARE_PART_D_PER_ENROLLEE_LONG_RANGE_GROWTH), + ], +) +def test_part_knots_reproduce_table_ii_d1(part, knots): + series = _extend(2035, TRUSTEES_2035_PER_BENEFICIARY_GROWTH[part], knots) + for (start, end), (total, beneficiaries) in TRUSTEES_COMPONENTS[part].items(): + implied = (1 + total) / (1 + beneficiaries) - 1 + assert _average_growth(series, start, end) == pytest.approx( + implied, abs=II_D1_TOLERANCE + ) + + +def test_repeating_the_2035_rate_misses_the_trustees_path(): + """The rule this replaces: 5.4 percent a year from 2036 runs a point and + more above every period's implied growth.""" + series = _extend(2035, TRUSTEES_2035_PER_BENEFICIARY_GROWTH["total"], None) + for (start, end), growth in TRUSTEES_INCURRED_EXPENDITURE_GROWTH.items(): + enrollment_growth = ( + TRUSTEES_TOTAL_ENROLLMENT[end] / TRUSTEES_TOTAL_ENROLLMENT[start - 1] + ) ** (1 / (end - start + 1)) - 1 + implied = (1 + growth) / (1 + enrollment_growth) - 1 + assert _average_growth(series, start, end) - implied > 0.009 + + +# Wiring of the CMS series in the tax-benefit system. + +PARAMETERS = system.parameters +WIRED_SERIES = [ + (PARAMETERS.calibration.gov.hhs.cms.moop_per_capita, 2035), + (PARAMETERS.calibration.gov.hhs.cms.esi_premium_per_enrollee, 2034), +] + + +@pytest.mark.parametrize("parameter, last", WIRED_SERIES) +def test_cms_series_glide_to_per_capita_health_spending(parameter, last): + short = _value(parameter, last) / _value(parameter, last - 1) - 1 + reference = _reference_path( + _value(parameter, last), + last, + short, + HEALTH_SPENDING_PER_CAPITA_LONG_RANGE_GROWTH, + ) + for year, expected in reference.items(): + assert _value(parameter, year) == pytest.approx(expected, rel=1e-12) + assert _value(parameter, 2150) == _value(parameter, END_YEAR) + # From 2050 the series grows at the Trustees' 4.3 to 4.1 percent. + for year in (2051, 2075, END_YEAR): + growth = _value(parameter, year) / _value(parameter, year - 1) - 1 + assert 0.041 - 1e-12 <= growth <= 0.043 + 1e-12 + assert _average_growth(parameter, 2051, END_YEAR) == pytest.approx(0.042, abs=1e-4) diff --git a/policyengine_us/tests/policy/baseline/parameters/test_uprating_extensions.py b/policyengine_us/tests/policy/baseline/parameters/test_uprating_extensions.py index e50fffb6cf2..286eea4355c 100644 --- a/policyengine_us/tests/policy/baseline/parameters/test_uprating_extensions.py +++ b/policyengine_us/tests/policy/baseline/parameters/test_uprating_extensions.py @@ -138,16 +138,23 @@ def test_cms_moop_per_capita_extends_to_2100(): assert parameter("2100-01-01") > parameter("2036-01-01") -def test_cms_esi_premium_per_enrollee_holds_last_projected_growth_rate(): +def test_cms_esi_premium_per_enrollee_glides_to_long_range_growth(): """Employer plan premium inputs keep growing after CMS's 2034 projection, - at the 2033-2034 growth rate.""" + gliding from the 2033-2034 growth rate to the Medicare Trustees' + long-range per capita health spending rates.""" parameter = PARAMETERS.calibration.gov.hhs.cms.esi_premium_per_enrollee - last_growth = parameter("2034-01-01") / parameter("2033-01-01") + last_growth = parameter("2034-01-01") / parameter("2033-01-01") - 1 + + def growth(year): + return parameter(f"{year}-01-01") / parameter(f"{year - 1}-01-01") - 1 assert parameter("2034-01-01") == 13_124 - for year in [2035, 2036, 2050, 2100]: - growth = parameter(f"{year}-01-01") / parameter(f"{year - 1}-01-01") - assert growth == pytest.approx(last_growth, rel=1e-12) + # 2035 is one sixteenth of the way from the 2034 rate to 4.3% in 2050. + assert growth(2035) == pytest.approx( + last_growth + (0.043 - last_growth) / 16, rel=1e-12 + ) + assert growth(2050) == pytest.approx(0.043, rel=1e-12) + assert growth(2100) == pytest.approx(0.041, rel=1e-12) assert parameter("2150-01-01") == parameter("2100-01-01")