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..1c3e5bb823e --- /dev/null +++ b/changelog.d/cms-long-range-glide.changed.md @@ -0,0 +1 @@ +Glide the CMS out-of-pocket spending, employer premium and direct purchase 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/docs-quarto/methodology/spm-poverty.qmd b/docs-quarto/methodology/spm-poverty.qmd index f0ea3e3acb4..e94c8b1e91a 100644 --- a/docs-quarto/methodology/spm-poverty.qmd +++ b/docs-quarto/methodology/spm-poverty.qmd @@ -50,7 +50,7 @@ The [MOOP decomposition page](moop-decomposition.qmd) records the design and the Population results use the Microcosm US dataset. The current default is Build P: 57,240 Current Population Survey Annual Social and Economic Supplement households with 166,321 people, whose weights are fitted to 5,659 administrative and demographic targets (IRS Statistics of Income by state and by income, Census population estimates by age, sex and state, CMS Medicaid and Marketplace enrollment, USDA SNAP participation and benefits, SSA SSI, TANF, state tax collections and BEA national accounts). Poverty rates are never a target. That is a rule of the calibration, not an accident: a model whose weights were fitted to survey-based poverty rates could not be checked against them. -2025 values are the 2024 survey values carried forward by the model's uprating parameters: each uprated input grows by the ratio of its uprating series between the two years. Household weights grow with total population, so an input that follows a national total grows by that total divided by population ([`per_capita_uprating.py`](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/tools/per_capita_uprating.py)); its weighted total then tracks the national total. The main income sources (employment, self-employment and partnership income, Social Security, pensions and retirement account distributions, capital gains, dividends, interest, rental and farm income, alimony received and paid, and unemployment compensation) follow [IRS Statistics of Income series](https://github.com/PolicyEngine/policyengine-us/tree/main/policyengine_us/parameters/calibration/gov/irs/soi). Each series holds IRS totals through its last recorded year and a CBO projection extends it from there, so the growth applied from 2024 to 2025 is CBO's, per capita. These health inputs follow [CMS's projection of per-capita out-of-pocket health spending](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/parameters/calibration/gov/hhs/cms/moop_per_capita.yaml), which is already a per-person series: the other medical expenses and over-the-counter health expenses that enter SPM resources; and physician services, inpatient, outpatient, emergency room, urgent care, ambulance, imaging, lab and prescription expenses. CMS counts premiums outside out-of-pocket spending, so premium inputs follow premium series instead, each a per-person series that grows at its last published one-year rate to 2100 and holds the 2100 value after that. These expenses, assets and debts follow [CPI-U](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/parameters/gov/bls/cpi/cpi_u.yaml): rent, child care and other care costs, child support paid and received, utilities, phone and broadband, tuition, homeowners insurance and association fees, transportation costs, real estate taxes and the energy subsidy; the person-level home mortgage interest input, student loan interest, investment interest expense and vehicle loan interest; bank account, stock and bond assets and the value of the primary residence; the value, equity and debt of business assets, rental property and other real estate; and vehicle equity and debt, the auto loan balance and net worth. These pay and wage-replacement inputs follow the IRS employment income series: hourly wages, tips, overtime premiums, military and government pay, the W-2 wages of qualified businesses, desired 401(k), 403(b) and IRA contributions, workers' compensation and disability benefits. Desired self-employed pension contributions follow the IRS self-employment income series. Health savings account payroll contributions, which the Microcosm data do not fill, follow [the HSA contribution limit for family coverage](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/parameters/gov/irs/gross_income/health_savings_account_contributions/limit/family.yaml) in 26 U.S.C. 223(b)(2). Section 223(g) indexes that limit to chained CPI-U, and after the IRS's last published year the model computes it from the statute, so a contribution at the limit stays at the limit. Employer-sponsored insurance premiums, pre-tax health insurance premiums and the premiums people report paying (health insurance premiums excluding Medicare Part B, and the other health insurance premiums that enter SPM resources) follow [CMS's employer-sponsored insurance premium per enrollee](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/parameters/calibration/gov/hhs/cms/esi_premium_per_enrollee.yaml), which CMS projects to 2034. These are the employer's share of employer plans, the employee's share paid through payroll, and the premiums survey respondents report, most of which people with employer coverage pay; with one series for all three, a premium the data move from the reported inputs to the pre-tax input keeps growing the same way. Health insurance premiums supplied directly, which the survey data hold only for self-employed people under 65 without employer coverage, follow the closest CMS series for that group, [CMS's direct purchase premium per enrollee](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/parameters/calibration/gov/hhs/cms/direct_purchase_premium_per_enrollee.yaml) (individually bought plans, Marketplace plans and Medigap), also projected to 2034. Long-term care insurance premiums, which no dataset fills and for which CMS's health expenditure projections have no premium series, follow [the medical care component of CPI-U](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/parameters/gov/bls/cpi/cpi_u_medical_care.yaml) by calendar year, which CBO projects to 2036. It stands in for the index that sets the deductible limits on those premiums, the chained CPI-U medical care component for August of the preceding year (26 U.S.C. 213(d)(10)(B)), which the CBO and CMS projections do not provide. Reported Medicare Part B premiums follow [the Part B standard premium](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/parameters/calibration/gov/hhs/cms/medicare_part_b_premium_per_enrollee.yaml), which the Medicare Trustees project to 2035; within an income tier, a premium with its income-related adjustment is a fixed multiple of the standard premium before rounding. The series is a growth proxy, not a forecast of each person's premium: hold-harmless limits and Medicare Advantage premium reductions move some premiums differently. Veterans' benefits follow the [Social Security cost-of-living index](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/parameters/gov/ssa/uprating.yaml). Every other uprated dollar input follows [CBO's projection of total adjusted gross income](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/parameters/calibration/gov/cbo/income_by_source.yaml), the default that [`default_uprating.py`](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/tools/default_uprating.py) assigns, per capita like every national total. Inputs on the default include tax units' first and second home mortgage interest and balances, home equity, personal property, SPM unit assets, mortgage payments, charitable donations, casualty losses, educator expenses, tax preparation fees, energy efficiency and clean energy property expenditures, state unemployment insurance wage and earnings inputs, employer payroll totals and local earnings and wage tax inputs. Household weights follow the model's [total population series](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/parameters/calibration/gov/census/populations/total.yaml), which takes its values from CBO's demographic outlook; person and SPM unit weights read the household weight. Inputs without an uprating parameter (ages, take-up indicators, tenure, geography, and federal student loan balances, which `default_uprating.py` exempts from the default) carry over unchanged, so household composition is held at its 2024 state. The population is projected, not resampled. +2025 values are the 2024 survey values carried forward by the model's uprating parameters: each uprated input grows by the ratio of its uprating series between the two years. Household weights grow with total population, so an input that follows a national total grows by that total divided by population ([`per_capita_uprating.py`](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/tools/per_capita_uprating.py)); its weighted total then tracks the national total. The main income sources (employment, self-employment and partnership income, Social Security, pensions and retirement account distributions, capital gains, dividends, interest, rental and farm income, alimony received and paid, and unemployment compensation) follow [IRS Statistics of Income series](https://github.com/PolicyEngine/policyengine-us/tree/main/policyengine_us/parameters/calibration/gov/irs/soi). Each series holds IRS totals through its last recorded year and a CBO projection extends it from there, so the growth applied from 2024 to 2025 is CBO's, per capita. These health inputs follow [CMS's projection of per-capita out-of-pocket health spending](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/parameters/calibration/gov/hhs/cms/moop_per_capita.yaml), which is already a per-person series: the other medical expenses and over-the-counter health expenses that enter SPM resources; and physician services, inpatient, outpatient, emergency room, urgent care, ambulance, imaging, lab and prescription expenses. After CMS's last projected year, the out-of-pocket series glides from its last projected growth rate to the Medicare Trustees' long-range growth in per capita health spending, 4.3 percent in 2050 and 4.1 percent in 2100, and holds the 2100 value after that. CMS counts premiums outside out-of-pocket spending, so premium inputs follow premium series instead, each a per-person series that holds its 2100 value after 2100. The employer-sponsored insurance and direct purchase premium series glide to the same Trustees rates after their last projected year; the other premium series grow at their last published one-year rates to 2100. These expenses, assets and debts follow [CPI-U](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/parameters/gov/bls/cpi/cpi_u.yaml): rent, child care and other care costs, child support paid and received, utilities, phone and broadband, tuition, homeowners insurance and association fees, transportation costs, real estate taxes and the energy subsidy; the person-level home mortgage interest input, student loan interest, investment interest expense and vehicle loan interest; bank account, stock and bond assets and the value of the primary residence; the value, equity and debt of business assets, rental property and other real estate; and vehicle equity and debt, the auto loan balance and net worth. These pay and wage-replacement inputs follow the IRS employment income series: hourly wages, tips, overtime premiums, military and government pay, the W-2 wages of qualified businesses, desired 401(k), 403(b) and IRA contributions, workers' compensation and disability benefits. Desired self-employed pension contributions follow the IRS self-employment income series. Health savings account payroll contributions, which the Microcosm data do not fill, follow [the HSA contribution limit for family coverage](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/parameters/gov/irs/gross_income/health_savings_account_contributions/limit/family.yaml) in 26 U.S.C. 223(b)(2). Section 223(g) indexes that limit to chained CPI-U, and after the IRS's last published year the model computes it from the statute, so a contribution at the limit stays at the limit. Employer-sponsored insurance premiums, pre-tax health insurance premiums and the premiums people report paying (health insurance premiums excluding Medicare Part B, and the other health insurance premiums that enter SPM resources) follow [CMS's employer-sponsored insurance premium per enrollee](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/parameters/calibration/gov/hhs/cms/esi_premium_per_enrollee.yaml), which CMS projects to 2034. These are the employer's share of employer plans, the employee's share paid through payroll, and the premiums survey respondents report, most of which people with employer coverage pay; with one series for all three, a premium the data move from the reported inputs to the pre-tax input keeps growing the same way. Health insurance premiums supplied directly, which the survey data hold only for self-employed people under 65 without employer coverage, follow the closest CMS series for that group, [CMS's direct purchase premium per enrollee](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/parameters/calibration/gov/hhs/cms/direct_purchase_premium_per_enrollee.yaml) (individually bought plans, Marketplace plans and Medigap), also projected to 2034. Long-term care insurance premiums, which no dataset fills and for which CMS's health expenditure projections have no premium series, follow [the medical care component of CPI-U](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/parameters/gov/bls/cpi/cpi_u_medical_care.yaml) by calendar year, which CBO projects to 2036. It stands in for the index that sets the deductible limits on those premiums, the chained CPI-U medical care component for August of the preceding year (26 U.S.C. 213(d)(10)(B)), which the CBO and CMS projections do not provide. Reported Medicare Part B premiums follow [the Part B standard premium](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/parameters/calibration/gov/hhs/cms/medicare_part_b_premium_per_enrollee.yaml), which the Medicare Trustees project to 2035; within an income tier, a premium with its income-related adjustment is a fixed multiple of the standard premium before rounding. The series is a growth proxy, not a forecast of each person's premium: hold-harmless limits and Medicare Advantage premium reductions move some premiums differently. Veterans' benefits follow the [Social Security cost-of-living index](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/parameters/gov/ssa/uprating.yaml). Every other uprated dollar input follows [CBO's projection of total adjusted gross income](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/parameters/calibration/gov/cbo/income_by_source.yaml), the default that [`default_uprating.py`](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/tools/default_uprating.py) assigns, per capita like every national total. Inputs on the default include tax units' first and second home mortgage interest and balances, home equity, personal property, SPM unit assets, mortgage payments, charitable donations, casualty losses, educator expenses, tax preparation fees, energy efficiency and clean energy property expenditures, state unemployment insurance wage and earnings inputs, employer payroll totals and local earnings and wage tax inputs. Household weights follow the model's [total population series](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/parameters/calibration/gov/census/populations/total.yaml), which takes its values from CBO's demographic outlook; person and SPM unit weights read the household weight. Inputs without an uprating parameter (ages, take-up indicators, tenure, geography, and federal student loan balances, which `default_uprating.py` exempts from the default) carry over unchanged, so household composition is held at its 2024 state. The population is projected, not resampled. ## What the model does not do diff --git a/policyengine_us/parameters/calibration/gov/hhs/cms/direct_purchase_premium_per_enrollee.yaml b/policyengine_us/parameters/calibration/gov/hhs/cms/direct_purchase_premium_per_enrollee.yaml index 7e6b0bf1902..599892097df 100644 --- a/policyengine_us/parameters/calibration/gov/hhs/cms/direct_purchase_premium_per_enrollee.yaml +++ b/policyengine_us/parameters/calibration/gov/hhs/cms/direct_purchase_premium_per_enrollee.yaml @@ -45,6 +45,7 @@ values: 2032-01-01: 8_743 2033-01-01: 9_158 2034-01-01: 9_589 - # 2035 to 2100 grow at the 2033-2034 rate (9_589 / 9_158, 4.7% 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 (9_589 / 9_158, 4.7% 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/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/calibration/gov/hhs/cms/moop_per_capita.yaml b/policyengine_us/parameters/calibration/gov/hhs/cms/moop_per_capita.yaml index 0b9aaac29c2..887dac50c9d 100644 --- a/policyengine_us/parameters/calibration/gov/hhs/cms/moop_per_capita.yaml +++ b/policyengine_us/parameters/calibration/gov/hhs/cms/moop_per_capita.yaml @@ -46,6 +46,7 @@ values: 2032-01-01: 2_314 2033-01-01: 2_412 2034-01-01: 2_512 - # 2035 to 2100 grow at the 2033-2034 rate (2_512 / 2_412, 4.1% 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 (2_512 / 2_412, 4.1% 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 3e3950b835e..bc2d910824c 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,67 @@ "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. That is an approximation: the +# Trustees' own path is not a straight line, as their income-technology +# elasticity declines at a slowing pace and their price elasticity follows a +# non-linear path between 2050 and 2100 (file page 175). +# +# 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). The Trustees extend CMS's national health +# expenditure projections past 2033 by the same long-range methods (Table +# V.B5, note 1, file page 200). +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: """Average chained CPI-U from August of ``year - 1`` through July of ``year``. @@ -716,6 +777,38 @@ 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 Medicare Trustees interpolate + between their short-range and long-range rates; this path is linear + between adjacent knots, 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, @@ -723,10 +816,14 @@ 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 - the growth rate from the last two years of projections. + Extend a parameter's values from last_projected_year to end_year. The + short-range rate is the growth between growth_years (by default the last + two projected years). It repeats in every year or, given + long_range_growth, glides through the knots. Years after end_year hold + its value. Args: parameter: The parameter to extend @@ -738,8 +835,13 @@ 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 + # Measure the short-range growth rate between growth_years date_format = f"-{period_month:02d}-{period_day:02d}" if growth_years is None: growth_years = (last_projected_year - 1, last_projected_year) @@ -751,7 +853,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 @@ -1101,36 +1211,44 @@ def set_all_uprating_parameters(parameters: ParameterNode) -> ParameterNode: # CMS per-capita out-of-pocket medical spending (January values, last # projection year 2034). Health expense inputs use this as their uprater. - # Each year from 2035 to 2100 grows at CMS's 2033-2034 rate, and later - # years hold the 2100 value. + # Growth glides from CMS's 2033-2034 rate to the Medicare Trustees' + # long-range per capita health spending rates, and later years hold the + # 2100 value. extend_parameter_values( parameters.calibration.gov.hhs.cms.moop_per_capita, last_projected_year=2034, 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 employee premium inputs use - # this as their uprater. + # this as their uprater. Growth glides from CMS's 2033-2034 rate to the + # Medicare Trustees' long-range per capita health spending rates, and + # later years hold the 2100 value. 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, ) # CMS direct purchase insurance premium per enrollee (January values, last # projection year 2034). Premiums for plans bought directly use this as - # their uprater. + # their uprater. Like the ESI premium, growth glides from CMS's 2033-2034 + # rate to the Medicare Trustees' long-range per capita health spending + # rates, and later years hold the 2100 value. extend_parameter_values( parameters.calibration.gov.hhs.cms.direct_purchase_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, ) # Medicare Part B standard monthly premium (January values, last Trustees diff --git a/policyengine_us/tests/core/test_cms_moop_uprating.py b/policyengine_us/tests/core/test_cms_moop_uprating.py index 014139b9bba..e1a211a71d2 100644 --- a/policyengine_us/tests/core/test_cms_moop_uprating.py +++ b/policyengine_us/tests/core/test_cms_moop_uprating.py @@ -5,8 +5,10 @@ release) report out-of-pocket spending per capita for 2018 to 2034: historical estimates through 2024 and projections from 2025. The parameter holds that column. 2015 to 2017 come from CMS's historical accounts by the rule that -reproduces Table 3's 2018-2024 values, and 2035 to 2100 grow at the 2033-2034 -rate. +reproduces Table 3's 2018-2024 values. From 2035, growth glides from the +2033-2034 rate to the 2026 Medicare Trustees' long-range per capita health +spending rates: 4.3 percent in 2050 and 4.1 percent in 2100 (Trustees Report, +file page 173). These tests read only the parameter tree and the parameter's YAML file. @@ -27,14 +29,18 @@ 4. Bounds and order: the series is positive in every year. It is strictly increasing over 2015-2019 and 2020-2100; its one decline, 2019 to 2020 (1,269 to 1,239), is CMS's and is intended. -5. Extension rule: each year from 2035 to 2100 grows by 2,512 / 2,412, so the - value equals the closed form 2,512 * (2,512 / 2,412) ** (year - 2034); later - years hold the 2100 value. +5. Extension rule: with g = 2,512 / 2,412 - 1, the 2033-2034 growth rate, + growth moves in a straight line from g at 2034 to 4.3 percent in 2050, then + to 4.1 percent in 2100, so 2035 grows by g + (0.043 - g) / 16. Each value + is 2,512 times the product of one plus each year's rate since 2034, checked + against an independent np.interp and np.prod. Later years hold the 2100 + value. """ import math from pathlib import Path +import numpy as np import pytest import yaml from hypothesis import given @@ -142,7 +148,10 @@ ), ) ) -EXTENSION_GROWTH = 2_512 / 2_412 +# 2033-2034 growth, from which the extension glides to the Trustees' +# long-range per capita health spending rates (file page 173). +LAST_PROJECTED_GROWTH = 2_512 / 2_412 - 1 +TRUSTEES_KNOTS = {2050: 0.043, END_YEAR: 0.041} def _index(year): @@ -242,11 +251,27 @@ def test_the_only_decline_is_cms_2019_to_2020(): assert (_index(2019), _index(2020)) == (1_269, 1_239) +def _growth(year): + return _index(year) / _index(year - 1) - 1 + + +def test_extension_glides_from_the_last_projected_rate_to_the_trustees_rates(): + g = LAST_PROJECTED_GROWTH + # 2035 is one sixteenth of the way from the 2034 rate to 4.3% in 2050. + assert _growth(2035) == pytest.approx(g + (0.043 - g) / 16, rel=1e-12) + assert _growth(2050) == pytest.approx(0.043, rel=1e-12) + assert _growth(END_YEAR) == pytest.approx(0.041, rel=1e-12) + + @given(st.integers(LAST_PROJECTED_YEAR + 1, END_YEAR)) -def test_extension_holds_the_last_projected_growth_rate(year): - growth = _index(year) / _index(year - 1) - assert growth == pytest.approx(EXTENSION_GROWTH, rel=1e-12) - closed_form = 2_512 * EXTENSION_GROWTH ** (year - LAST_PROJECTED_YEAR) +def test_extension_matches_an_independent_closed_form(year): + years = np.arange(LAST_PROJECTED_YEAR + 1, year + 1) + rates = np.interp( + years, + [LAST_PROJECTED_YEAR, *TRUSTEES_KNOTS], + [LAST_PROJECTED_GROWTH, *TRUSTEES_KNOTS.values()], + ) + closed_form = 2_512 * np.prod(1 + rates) assert _index(year) == pytest.approx(closed_form, rel=1e-12) 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..2be3c546b1b --- /dev/null +++ b/policyengine_us/tests/core/test_long_range_health_growth.py @@ -0,0 +1,356 @@ +"""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 between the short-range and long-range rates +(2026 Medicare Trustees Report, file pages 24 and 180); this path is linear. + +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, 2034), + (PARAMETERS.calibration.gov.hhs.cms.esi_premium_per_enrollee, 2034), + (PARAMETERS.calibration.gov.hhs.cms.direct_purchase_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/core/test_premium_input_uprating.py b/policyengine_us/tests/core/test_premium_input_uprating.py index 6431e6684fd..58ab42f1de5 100644 --- a/policyengine_us/tests/core/test_premium_input_uprating.py +++ b/policyengine_us/tests/core/test_premium_input_uprating.py @@ -55,8 +55,12 @@ 5. Bounds and order: each index is positive in every year, and strictly increasing from its last published dip (direct purchase from 2020, Part B from 2023, medical care CPI throughout) to 2100. -6. Extension rule: past its source's last year, each index grows at its last - published one-year rate to 2100 and holds its 2100 value after. +6. Extension rule: past its source's last year, the medical care CPI and + Part B indexes grow at their last published one-year rates to 2100. The + ESI and direct purchase indexes instead glide to the Medicare Trustees' + long-range per capita health spending rates (tested in + test_long_range_health_growth.py, test_esi_premium_uprating.py and + test_uprating_extensions.py). Every index holds its 2100 value after. 7. No premium input follows out-of-pocket spending. """ @@ -394,7 +398,11 @@ def test_each_index_strictly_increases_after_its_last_dip(path, offset, gap): assert _index(path, year) < _index(path, later) -@given(st.sampled_from(sorted(SPANS)), st.integers(0, END_YEAR)) +# Indexes that repeat their last published growth rate after their last year. +CONSTANT_EXTENSIONS = sorted({MEDICAL_CPI, PART_B}) + + +@given(st.sampled_from(CONSTANT_EXTENSIONS), st.integers(0, END_YEAR)) def test_each_extension_holds_the_last_published_growth_rate(path, offset): _, last, _ = SPANS[path] year = last + 1 + offset % (END_YEAR - last) 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 1b1dd84fc72..aab6438e4ca 100644 --- a/policyengine_us/tests/policy/baseline/parameters/test_uprating_extensions.py +++ b/policyengine_us/tests/policy/baseline/parameters/test_uprating_extensions.py @@ -140,43 +140,70 @@ def test_social_security_benefit_inputs_use_gross_benefit_uprater(): assert system.variables[variable_name].uprating is None -def test_cms_moop_per_capita_extends_to_2100(): +def test_cms_moop_per_capita_glides_to_long_range_growth(): """Health expense input upraters keep growing after CMS's 2034 - projection, at the 2033-2034 growth rate, then hold the 2100 value.""" + projection, gliding from the 2033-2034 growth rate to the Medicare + Trustees' long-range per capita health spending rates, then hold the 2100 + value.""" parameter = PARAMETERS.calibration.gov.hhs.cms.moop_per_capita - 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") == 2_512 - assert last_growth == pytest.approx(2_512 / 2_412, rel=1e-12) - 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) + assert last_growth == pytest.approx(2_512 / 2_412 - 1, 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") -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") -def test_cms_direct_purchase_premium_per_enrollee_holds_last_projected_growth_rate(): +def test_cms_direct_purchase_premium_per_enrollee_glides_to_long_range_growth(): """Directly bought premium inputs keep growing after CMS's 2034 - projection, at the 2033-2034 growth rate.""" + projection, gliding from the 2033-2034 growth rate to the Medicare + Trustees' long-range per capita health spending rates, as the ESI + premium does.""" parameter = PARAMETERS.calibration.gov.hhs.cms.direct_purchase_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") == 9_589 - 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) + assert last_growth == pytest.approx(9_589 / 9_158 - 1, rel=1e-12) + # 2035 is one sixteenth of the way from the 2034 rate to 4.3% in 2050: + # 0.0470627 + (0.043 - 0.0470627) / 16 = 0.0468088, so 2035 is + # 9,589 * 1.0468088 = 10,037.85 (hand computed). + assert growth(2035) == pytest.approx( + last_growth + (0.043 - last_growth) / 16, rel=1e-12 + ) + assert parameter("2035-01-01") == pytest.approx(10_037.85, abs=0.01) + 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")