diff --git a/changelog.d/medicare-long-range-glide.changed.md b/changelog.d/medicare-long-range-glide.changed.md new file mode 100644 index 00000000000..61e197ebcf9 --- /dev/null +++ b/changelog.d/medicare-long-range-glide.changed.md @@ -0,0 +1 @@ +Glide the Medicare Part B premium and Medicare spending per enrollee upraters from their last projected growth rates to the 2026 Medicare Trustees' long-range growth in Part B and total Medicare spending per enrollee, 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 e94c8b1e91a..0db4bb5e860 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. 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. +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 Medicare Part B standard premium glides to the Trustees' long-range growth in Part B spending per enrollee, 3.7 percent a year from 2050, because the Secretary of Health and Human Services sets the standard premium to cover about 25 percent of average expenditures for beneficiaries aged 65 and over. The long-term care premium series grows at its last published one-year rate 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/medicare_part_b_premium_per_enrollee.yaml b/policyengine_us/parameters/calibration/gov/hhs/cms/medicare_part_b_premium_per_enrollee.yaml index c64cc20f92b..01e8d1d9089 100644 --- a/policyengine_us/parameters/calibration/gov/hhs/cms/medicare_part_b_premium_per_enrollee.yaml +++ b/policyengine_us/parameters/calibration/gov/hhs/cms/medicare_part_b_premium_per_enrollee.yaml @@ -40,6 +40,11 @@ values: 2033-01-01: 313.6 2034-01-01: 338.5 2035-01-01: 360.6 - # 2036 to 2100 grow at the 2034-2035 rate (360.6 / 338.5, 6.5% a year) and - # later years hold the 2100 value; see set_all_uprating_parameters in - # uprating_extensions.py. + # After 2035, growth glides from the 2034-2035 rate (360.6 / 338.5, 6.5%) + # to the Trustees' long-range growth in Part B spending per enrollee, 3.7% + # a year from 2050 (Part B cost growth of 3.8% compounded with a -0.1% + # demographic mix, file pages 29 and 180), because the Secretary of Health + # and Human Services sets the standard premium to cover about 25% of + # average expenditures for beneficiaries aged 65 and over (42 U.S.C. + # 1395r(a); file page 97). Later years hold the 2100 value; see + # set_all_uprating_parameters in uprating_extensions.py. diff --git a/policyengine_us/parameters/calibration/gov/hhs/medicare/per_capita_cost.yaml b/policyengine_us/parameters/calibration/gov/hhs/medicare/per_capita_cost.yaml index 48ab04c9c07..d942e1a27be 100644 --- a/policyengine_us/parameters/calibration/gov/hhs/medicare/per_capita_cost.yaml +++ b/policyengine_us/parameters/calibration/gov/hhs/medicare/per_capita_cost.yaml @@ -58,7 +58,10 @@ values: 2032-01-01: 26_552 2033-01-01: 28_277 2034-01-01: 30_129 - # 2035 to 2100 grow at the 2031-2032 rate (26_552 / 25_185, 5.4% a year), + # After 2034, growth glides from the 2031-2032 rate (26_552 / 25_185, 5.4%), # not the 2033-2034 rate, which carries the end of Medicare sequestration - # on August 31, 2033; later years hold the 2100 value. See + # on August 31, 2033, to the 2026 Medicare Trustees' long-range growth in + # Medicare spending per enrollee: 3.7% in 2050, declining to 3.6% by 2100 + # (cost growth of 3.8% and 3.7% compounded with a -0.1% demographic mix, + # file pages 29 and 180). 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 bc2d910824c..a3360063dad 100644 --- a/policyengine_us/parameters/uprating_extensions.py +++ b/policyengine_us/parameters/uprating_extensions.py @@ -2,7 +2,8 @@ import math from decimal import ROUND_HALF_UP, Decimal -from typing import Dict, Optional, Tuple +from types import MappingProxyType +from typing import Mapping, Optional, Tuple from policyengine_us.model_api import * from policyengine_core.periods import Instant, instant @@ -30,10 +31,13 @@ # 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). +# glide runs in a straight line between those two knots. The Trustees' own +# path bends, 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). Each series also starts its glide from its own last +# projected year and growth rate, which for CMS's 2034 projections comes a +# year before the Trustees' 2035 start. MappingProxyType keeps importers from +# changing the knots. # # 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) @@ -41,12 +45,14 @@ # 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 +# 175), and the Trustees expect "the drivers of health spending will be +# similar for the overall health sector and for the Medicare program" over +# the long range (file page 176). 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} +HEALTH_SPENDING_PER_CAPITA_LONG_RANGE_GROWTH = MappingProxyType( + {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 @@ -56,14 +62,18 @@ MEDICARE_LONG_RANGE_DEMOGRAPHIC_MIX = -0.001 -def with_medicare_demographic_mix(cost_growth: Dict[int, float]) -> Dict[int, float]: +def with_medicare_demographic_mix( + cost_growth: Mapping[int, float], +) -> Mapping[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() - } + return MappingProxyType( + { + 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 @@ -781,7 +791,7 @@ def long_range_growth_rate( year: int, last_projected_year: int, short_range_growth: float, - long_range_growth: Dict[int, float], + long_range_growth: Mapping[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. @@ -789,11 +799,16 @@ def long_range_growth_rate( 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. + between adjacent knots, and holds the last knot's rate after it. It + rejects a NaN or infinite rate and any rate at or below -1, which would + make a year's growth factor zero or negative. """ 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.") + rates = [short_range_growth, *(rate for _, rate in knots)] + if not all(math.isfinite(rate) and rate > -1 for rate in rates): + raise ValueError(f"Growth rates must exceed -1 and stay finite; got {rates}.") if year <= last_projected_year: raise ValueError( "Long-range growth applies only after the last projected year." @@ -816,7 +831,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, + long_range_growth: Optional[Mapping[int, float]] = None, ) -> None: """ Extend a parameter's values from last_projected_year to end_year. The @@ -831,21 +846,27 @@ def extend_parameter_values( end_year: The year to extend values through period_month: The month for the period (default 1 for January) period_day: The day for the period (default 1) - growth_years: Optional (earlier, later) pair to measure the growth - 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 + growth_years: Optional (earlier, later) pair of consecutive years, no + later than last_projected_year, to measure the short-range rate + between, instead of the last two projected years. Callers pass + it 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. With + knots, growth moves from the measured short-range rate in + last_projected_year through the knots (see long_range_growth_rate) instead of repeating the short-range - rate in every year. + rate every year. """ # 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) earlier_year, later_year = growth_years + if later_year != earlier_year + 1 or later_year > last_projected_year: + raise ValueError( + "growth_years must name consecutive years ending no later than " + f"{last_projected_year}; got {growth_years}." + ) second_to_last_value = parameter(f"{earlier_year}{date_format}") last_value = parameter(f"{later_year}{date_format}") growth_rate = last_value / second_to_last_value @@ -1253,13 +1274,19 @@ def set_all_uprating_parameters(parameters: ParameterNode) -> ParameterNode: # Medicare Part B standard monthly premium (January values, last Trustees # projection year 2035). Reported Part B premium inputs use this as their - # uprater. + # uprater. The Secretary of Health and Human Services sets the standard + # premium to cover about 25 percent of average expenditures for + # beneficiaries aged 65 and over (42 U.S.C. 1395r(a); 2026 Medicare + # Trustees Report, file page 97), so growth glides from the 2034-2035 rate + # to the Trustees' long-range growth in Part B spending per enrollee, and + # later years hold the 2100 value. extend_parameter_values( parameters.calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee, last_projected_year=2035, end_year=END_YEAR, period_month=1, period_day=1, + long_range_growth=MEDICARE_PART_B_PER_ENROLLEE_LONG_RANGE_GROWTH, ) # Gross Social Security benefit inputs should age with gross Social @@ -1288,8 +1315,9 @@ def set_all_uprating_parameters(parameters: ParameterNode) -> ParameterNode: # Medicare Trustees Report, file page 209); its end lifts 2033 and 2034 # growth, and the Trustees' per-beneficiary growth falls from 6.6 # percent in 2034 to 5.4 percent in 2035 (Table V.D1, file page 210). - # So each year from 2035 to 2100 grows at the 2031-2032 rate, and later - # years hold the 2100 value. + # So growth glides from the 2031-2032 rate in 2034 to the Trustees' + # long-range growth in Medicare spending per enrollee, and later years hold + # the 2100 value. extend_parameter_values( parameters.calibration.gov.hhs.medicare.per_capita_cost, last_projected_year=2034, @@ -1297,6 +1325,7 @@ def set_all_uprating_parameters(parameters: ParameterNode) -> ParameterNode: period_month=1, period_day=1, growth_years=(2031, 2032), + long_range_growth=MEDICARE_PER_ENROLLEE_LONG_RANGE_GROWTH, ) # CBO income-by-source aggregates are used directly and as anchors for diff --git a/policyengine_us/tests/core/test_long_range_health_growth.py b/policyengine_us/tests/core/test_long_range_health_growth.py index 2be3c546b1b..c77a480d4e5 100644 --- a/policyengine_us/tests/core/test_long_range_health_growth.py +++ b/policyengine_us/tests/core/test_long_range_health_growth.py @@ -15,7 +15,7 @@ 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. + numpy implementation, for synthetic series and for every wired 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. @@ -23,20 +23,35 @@ 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. +8. Growth years: with growth_years=(y - 1, y), the short-range rate is + V_y / V_(y-1) - 1, and the glide still starts from it at the last + projected year, whatever the growth between y and that year. +9. Trustees consistency: Medicare spending per enrollee, which starts its + glide at its 2031-2032 growth in 2034, reproduces 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. Starting + from the Trustees' 2035 per beneficiary growth (Table V.D1), the Part B + and Part D knots reproduce Table II.D1's total increase net of + beneficiary growth. The Part B premium's glide matches II.D1's Part B + growth per beneficiary in 2051-2100, where the per capita health + spending knots would overshoot it. +10. Guards: long_range_growth_rate rejects any rate that would make a growth + factor zero, negative or not finite; extend_parameter_values rejects + growth_years other than consecutive years ending by the last projected + year, before it changes any value; and the module's knots reject + assignment. """ +import ast +import inspect + 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 import uprating_extensions from policyengine_us.parameters.uprating_extensions import ( HEALTH_SPENDING_PER_CAPITA_LONG_RANGE_GROWTH, MEDICARE_LONG_RANGE_DEMOGRAPHIC_MIX, @@ -50,6 +65,9 @@ from policyengine_us.system import system END_YEAR = 2100 +PARAMETERS = system.parameters +PER_CAPITA_COST = PARAMETERS.calibration.gov.hhs.medicare.per_capita_cost +PART_B_PREMIUM = PARAMETERS.calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee rates = st.floats(-0.05, 0.15, allow_nan=False, allow_infinity=False) @@ -149,6 +167,53 @@ def test_knots_must_follow_the_last_projected_year(knots): long_range_growth_rate(2040, 2034, 0.05, knots) +# 10. Guards. + +bad_rates = st.one_of( + st.floats(max_value=-1), st.sampled_from([float("nan"), float("inf")]) +) + + +@given(glide_cases(), bad_rates, st.booleans()) +def test_rates_must_keep_every_growth_factor_positive_and_finite( + case, bad, short_is_bad +): + last, short, knots = case + if short_is_bad: + short = bad + else: + # The last knot, which the first extended year never reaches. + knots = {**knots, max(knots): bad} + with pytest.raises(ValueError): + long_range_growth_rate(last + 1, last, short, knots) + + +@pytest.mark.parametrize( + "knots", + [ + HEALTH_SPENDING_PER_CAPITA_LONG_RANGE_GROWTH, + MEDICARE_PER_ENROLLEE_LONG_RANGE_GROWTH, + MEDICARE_PART_B_PER_ENROLLEE_LONG_RANGE_GROWTH, + MEDICARE_PART_D_PER_ENROLLEE_LONG_RANGE_GROWTH, + ], +) +def test_importers_cannot_change_the_module_knots(knots): + with pytest.raises(TypeError): + knots[2050] = 0.0 + assert sorted(knots) == [2050, 2100] + + +@pytest.mark.parametrize("growth_years", [(2030, 2032), (2032, 2031), (2034, 2035)]) +def test_growth_years_name_consecutive_projected_years(growth_years): + series = _series( + {year: 100.0 * 1.05 ** (year - 2030) for year in range(2030, 2035)} + ) + with pytest.raises(ValueError): + extend_parameter_values(series, 2034, END_YEAR, growth_years=growth_years) + # Nothing extended: later years still read the 2034 value. + assert _value(series, 2040) == _value(series, 2034) + + # 3. Closed form. @@ -230,15 +295,67 @@ def test_projected_years_hold_and_later_years_freeze(case, later): assert _value(series, year) == _value(series, end_year) -# 8. Trustees consistency. +# 8. Growth years. + + +def test_growth_years_set_the_rate_the_glide_starts_from(): + """2031-2032 grows 5 percent and 2033-2034 faster; the glide starts from 5 + percent at 2034 and reaches the 3 percent knot in 2050.""" + series = _series({2031: 100.0, 2032: 105.0, 2033: 112.0, 2034: 120.0}) + extend_parameter_values( + series, + 2034, + END_YEAR, + growth_years=(2031, 2032), + long_range_growth={2050: 0.03}, + ) + # Hand computed: 2035 is one sixteenth of the way from 5% to 3%, so it + # grows 0.05 - 0.02 / 16 = 4.875%, to 120 * 1.04875 = 125.85, and 2036 + # grows 4.75%, to 125.85 * 1.0475 = 131.827875. + assert _value(series, 2035) == pytest.approx(125.85, rel=1e-12) + assert _value(series, 2036) == pytest.approx(131.827875, rel=1e-12) + for year in (2051, END_YEAR): + growth = _value(series, year) / _value(series, year - 1) + assert growth == pytest.approx(1.03, rel=1e-12) + projected = [_value(series, year) for year in range(2031, 2035)] + assert projected == [100, 105, 112, 120] + + +@st.composite +def growth_year_cases(draw): + """A series whose last projected year comes zero to five years after its + growth years, with arbitrary growth in between.""" + last, _, knots = draw(glide_cases()) + later = draw(st.integers(last - 5, last)) + values = {later - 1: draw(st.floats(1, 1e6))} + for year in range(later, last + 1): + values[year] = values[year - 1] * (1 + draw(rates)) + return last, later, values, knots + + +@given(growth_year_cases()) +@settings(max_examples=60, deadline=None) +def test_growth_years_extension_matches_an_independent_implementation(case): + last, later, values, knots = case + series = _series(values) + extend_parameter_values( + series, + last, + END_YEAR, + growth_years=(later - 1, later), + long_range_growth=knots, + ) + short = values[later] / values[later - 1] - 1 + reference = _reference_path(values[last], last, short, knots) + for year, expected in reference.items(): + assert _value(series, year) == pytest.approx(expected, rel=1e-12) + + +# 9. 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, -} +TRUSTEES_2035_PER_BENEFICIARY_GROWTH = {"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 = { @@ -254,11 +371,17 @@ def test_projected_years_hold_and_later_years_freeze(case, later): "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. +# Table V.B5 rounds expenditure growth to a tenth of a point, while Table V.B3 +# gives enrollment in thousands, so a period's implied per-enrollee growth +# carries one such rounding. The glide's start rate and rounded knots, and +# its straight line where the Trustees' path is not one, 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. +# Table II.D1's per-beneficiary growth nets two components rounded to a tenth +# of a point, and the knots apply 2051-2100's demographic mix to 2036-2050 as +# well. That mix is -0.1 for Part B in both periods, but -0.2 for Part D in +# 2036-2050, so the Part D glide runs 0.09 point above II.D1 there (0.12 point +# with Table V.B3's Part D enrollment). II_D1_TOLERANCE = 0.0015 @@ -280,22 +403,26 @@ def test_medicare_knots_compound_cost_growth_with_the_demographic_mix(): assert 0.035 < rate < 0.041 -@pytest.mark.parametrize("period", TRUSTEES_INCURRED_EXPENDITURE_GROWTH) -def test_medicare_knots_reproduce_trustees_spending_per_enrollee(period): +def _implied_spending_per_enrollee_growth(period): + """Table V.B5's incurred expenditure growth net of Table V.B3's enrollment + growth over a 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]) / ( + return (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("period", TRUSTEES_INCURRED_EXPENDITURE_GROWTH) +def test_medicare_spending_per_enrollee_reproduces_the_trustees_path(period): + """The wired series starts its glide in 2034, a year before the Trustees' + 2035, from its 2031-2032 growth of 5.43 percent rather than Table V.D1's + 5.4 percent in 2035; it misses by at most 0.05 point.""" + start, end = period + assert _average_growth(PER_CAPITA_COST, start, end) == pytest.approx( + _implied_spending_per_enrollee_growth(period), abs=V_B5_TOLERANCE ) @@ -315,42 +442,100 @@ def test_part_knots_reproduce_table_ii_d1(part, knots): ) -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 +def test_repeating_the_2031_2032_rate_misses_the_trustees_path(): + """The rule this replaces: 5.43 percent a year from 2035 runs 0.9 point + and more above every period's implied growth.""" + short = _value(PER_CAPITA_COST, 2032) / _value(PER_CAPITA_COST, 2031) - 1 + for period in TRUSTEES_INCURRED_EXPENDITURE_GROWTH: + assert short - _implied_spending_per_enrollee_growth(period) > 0.009 + + +def _part_b_per_beneficiary_growth(period): + total, beneficiaries = TRUSTEES_COMPONENTS["part_b"][period] + return (1 + total) / (1 + beneficiaries) - 1 + + +def test_part_b_premium_glide_tracks_table_ii_d1(): + """The Secretary of Health and Human Services sets the standard premium + to cover about 25 percent of average expenditures for beneficiaries aged + 65 and over (42 U.S.C. 1395r(a); file page 97), so the premium glides to + the Part B knots. In 2051-2100 it grows within 0.02 point of + Table II.D1's Part B growth per beneficiary, 3.685 percent; the per + capita health spending knots would run 0.5 point above it. In 2036-2050 + it runs 0.16 point below II.D1's 5.18 percent: half from its start at + the premium's 6.53 percent 2034-2035 growth, below Table V.D1's 6.7 + percent Part B growth in 2035, and half from the straight-line glide + (test_part_knots_reproduce_table_ii_d1 starts from 6.7 percent).""" + late = _part_b_per_beneficiary_growth((2051, 2100)) + assert _average_growth(PART_B_PREMIUM, 2051, END_YEAR) == pytest.approx( + late, abs=0.0002 + ) + short = _value(PART_B_PREMIUM, 2035) / _value(PART_B_PREMIUM, 2034) - 1 + per_capita = _extend(2035, short, HEALTH_SPENDING_PER_CAPITA_LONG_RANGE_GROWTH) + assert _average_growth(per_capita, 2051, END_YEAR) - late > 0.005 + early = _part_b_per_beneficiary_growth((2036, 2050)) + assert _average_growth(PART_B_PREMIUM, 2036, 2050) == pytest.approx( + early, abs=0.002 + ) -# Wiring of the CMS series in the tax-benefit system. +# Wiring in the tax-benefit system: (series, last projected year, growth +# years, long-range knots). -PARAMETERS = system.parameters +CMS = PARAMETERS.calibration.gov.hhs.cms 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), + (CMS.moop_per_capita, 2034, None, HEALTH_SPENDING_PER_CAPITA_LONG_RANGE_GROWTH), + ( + CMS.esi_premium_per_enrollee, + 2034, + None, + HEALTH_SPENDING_PER_CAPITA_LONG_RANGE_GROWTH, + ), + ( + CMS.direct_purchase_premium_per_enrollee, + 2034, + None, + HEALTH_SPENDING_PER_CAPITA_LONG_RANGE_GROWTH, + ), + (PART_B_PREMIUM, 2035, None, MEDICARE_PART_B_PER_ENROLLEE_LONG_RANGE_GROWTH), + (PER_CAPITA_COST, 2034, (2031, 2032), MEDICARE_PER_ENROLLEE_LONG_RANGE_GROWTH), ] -@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, - ) +@pytest.mark.parametrize( + "parameter, last, growth_years, knots", + WIRED_SERIES, + ids=[series.name for series, *_ in WIRED_SERIES], +) +def test_wired_series_glide_to_their_long_range_rates( + parameter, last, growth_years, knots +): + earlier, later = growth_years or (last - 1, last) + short = _value(parameter, later) / _value(parameter, earlier) - 1 + reference = _reference_path(_value(parameter, last), last, short, knots) 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): + # From 2050 each year grows at a rate between the 2050 and 2100 knots. + low, high = sorted((knots[2050], knots[2100])) + for year in range(2051, END_YEAR + 1): 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) + assert low - 1e-12 <= growth <= high + 1e-12 + assert _average_growth(parameter, 2051, END_YEAR) == pytest.approx( + (low + high) / 2, abs=1e-4 + ) + + +def test_every_glided_series_is_checked_here(): + """Each series that set_all_uprating_parameters extends with + long_range_growth appears in WIRED_SERIES, so the differential check above + covers it.""" + tree = ast.parse(inspect.getsource(uprating_extensions)) + glided = { + ast.unparse(call.args[0]).removeprefix("parameters.") + for call in ast.walk(tree) + if isinstance(call, ast.Call) + and getattr(call.func, "id", None) == "extend_parameter_values" + and any(keyword.arg == "long_range_growth" for keyword in call.keywords) + } + assert glided == {series.name for series, *_ in WIRED_SERIES} diff --git a/policyengine_us/tests/core/test_medicare_per_capita_cost.py b/policyengine_us/tests/core/test_medicare_per_capita_cost.py index d3d47ca3079..e5707755985 100644 --- a/policyengine_us/tests/core/test_medicare_per_capita_cost.py +++ b/policyengine_us/tests/core/test_medicare_per_capita_cost.py @@ -7,9 +7,10 @@ program administration; enrollment counts Part A and/or Part B enrollees in person-years; both exclude the territories (NHEA methodology paper, file pages 15, 23 and 34). 2013-2024 are historical and 2025-2034 projected, from the -projections prepared for the 2026 Medicare Trustees Report. 2035 to 2100 grow -at the 2031-2032 rate, because the end of sequestration on August 31, 2033 -lifts 2033 and 2034 growth. +projections prepared for the 2026 Medicare Trustees Report. After 2034, growth +glides from the 2031-2032 rate, because the end of sequestration on August 31, +2033 lifts 2033 and 2034 growth, to the Trustees' long-range growth in +Medicare spending per enrollee (``MEDICARE_PER_ENROLLEE_LONG_RANGE_GROWTH``). ``medicare_gross_cost`` returns the series for each enrolled person, ``medicare_cost`` subtracts the person's Part A and gross Part B premiums, and @@ -27,7 +28,9 @@ 3. Bounds and order: the series is positive and strictly increasing from 2013 to 2100, and holds the 2100 value afterwards. 4. Extension rule: each year from 2035 to 2100 equals the 2034 value times - (26,552 / 25,185) ** (year - 2034), the closed form of the iterative + the product of (1 + r) over the years since 2034, where r moves in a + straight line from g = 26,552 / 25,185 - 1 in 2034 to 1.038 * 0.999 - 1 in + 2050 and 1.037 * 0.999 - 1 in 2100, the closed form of the iterative extension; the 2035 value is within 0.1% of the 2034 value grown at the Trustees' 2034-2035 per beneficiary growth. 5. Calendar years: every date within a year reads the January 1 value. @@ -38,6 +41,8 @@ the series over 12 times the QMB rate. A person not enrolled gets zero. """ +import math + import pytest from hypothesis import given, settings from hypothesis import strategies as st @@ -149,18 +154,43 @@ def test_series_is_positive_and_strictly_increasing(year): assert 0 < _value(year) < _value(year + 1) -@pytest.mark.parametrize("year", range(LAST_PROJECTED_YEAR + 1, END_YEAR + 1)) -def test_extension_is_the_closed_form_of_the_2031_2032_growth(year): +def _glide_rate(year): + """The extension's growth rate in a year after 2034: a straight line from + the 2031-2032 rate in 2034 to the Trustees' long-range growth in Medicare + spending per enrollee in 2050 and 2100 (cost growth of 3.8 and 3.7 + percent compounded with a -0.1 percent demographic mix).""" earlier, later = GROWTH_YEARS - growth = ( - MEDICARE_SPENDING_PER_ENROLLEE[later] - / (MEDICARE_SPENDING_PER_ENROLLEE[earlier]) - ) - expected = MEDICARE_SPENDING_PER_ENROLLEE[LAST_PROJECTED_YEAR] * growth ** ( - year - LAST_PROJECTED_YEAR + spending = MEDICARE_SPENDING_PER_ENROLLEE + start = spending[later] / spending[earlier] - 1 + in_2050 = 1.038 * 0.999 - 1 + in_2100 = 1.037 * 0.999 - 1 + if year <= 2050: + share = (year - LAST_PROJECTED_YEAR) / (2050 - LAST_PROJECTED_YEAR) + return start + (in_2050 - start) * share + return in_2050 + (in_2100 - in_2050) * (year - 2050) / (END_YEAR - 2050) + + +@pytest.mark.parametrize("year", range(LAST_PROJECTED_YEAR + 1, END_YEAR + 1)) +def test_extension_is_the_closed_form_of_the_glide(year): + expected = MEDICARE_SPENDING_PER_ENROLLEE[LAST_PROJECTED_YEAR] * math.prod( + 1 + _glide_rate(y) for y in range(LAST_PROJECTED_YEAR + 1, year + 1) ) assert _value(year) == pytest.approx(expected, rel=1e-9) - assert _value(year) / _value(year - 1) == pytest.approx(growth, rel=1e-12) + assert _value(year) / _value(year - 1) - 1 == pytest.approx( + _glide_rate(year), rel=1e-9 + ) + + +@pytest.mark.parametrize( + "year, expected", + # Hand computed from 30,129 at a 5.42783% rate in 2034: 2035 grows + # 5.31961%, 2050 3.6962%, 2100 3.5963%. + [(2035, 31_731.74), (2050, 60_994.64), (2100, 365_393.86)], +) +def test_extension_levels(year, expected): + """The constant 2031-2032 rate gave $70,189 in 2050 and $986,356 in + 2100.""" + assert _value(year) == pytest.approx(expected, abs=0.01) @settings(deadline=None) diff --git a/policyengine_us/tests/core/test_premium_input_uprating.py b/policyengine_us/tests/core/test_premium_input_uprating.py index 58ab42f1de5..6385807832d 100644 --- a/policyengine_us/tests/core/test_premium_input_uprating.py +++ b/policyengine_us/tests/core/test_premium_input_uprating.py @@ -55,10 +55,11 @@ 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, 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 +6. Extension rule: past its source's last year, the medical care CPI index + grows at its last published one-year rate to 2100. The ESI and direct + purchase indexes instead glide to the Medicare Trustees' long-range per + capita health spending rates, and the Part B index to their long-range + growth in Part B spending per enrollee (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. @@ -399,7 +400,7 @@ def test_each_index_strictly_increases_after_its_last_dip(path, offset, gap): # Indexes that repeat their last published growth rate after their last year. -CONSTANT_EXTENSIONS = sorted({MEDICAL_CPI, PART_B}) +CONSTANT_EXTENSIONS = [MEDICAL_CPI] @given(st.sampled_from(CONSTANT_EXTENSIONS), st.integers(0, END_YEAR)) 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 aab6438e4ca..8fda2fcd57b 100644 --- a/policyengine_us/tests/policy/baseline/parameters/test_uprating_extensions.py +++ b/policyengine_us/tests/policy/baseline/parameters/test_uprating_extensions.py @@ -43,11 +43,6 @@ def test_all_uprating_factors_extend_to_2100(): ("CPI-U", parameters.gov.bls.cpi.cpi_u, "-02-01"), ("Chained CPI-U", parameters.gov.bls.cpi.c_cpi_u, "-02-01"), ("CPI-W", parameters.gov.bls.cpi.cpi_w, "-02-01"), - ( - "CMS Medicare spending per enrollee", - parameters.calibration.gov.hhs.medicare.per_capita_cost, - "-01-01", - ), ] for name, param, date_suffix in uprating_params: @@ -207,16 +202,58 @@ def growth(year): assert parameter("2150-01-01") == parameter("2100-01-01") -def test_medicare_part_b_premium_holds_last_projected_growth_rate(): +def test_medicare_part_b_premium_glides_to_long_range_part_b_growth(): """Reported Part B premium inputs keep growing after the Trustees' 2035 - projection, at the 2034-2035 growth rate.""" + projection, gliding from the 2034-2035 growth rate to the Trustees' + long-range growth in Part B spending per enrollee: Part B cost growth of + 3.8% in 2050 and 2100, compounded with a -0.1% demographic mix.""" parameter = PARAMETERS.calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee - last_growth = parameter("2035-01-01") / parameter("2034-01-01") + last_growth = parameter("2035-01-01") / parameter("2034-01-01") - 1 + long_range = 1.038 * 0.999 - 1 + + def growth(year): + return parameter(f"{year}-01-01") / parameter(f"{year - 1}-01-01") - 1 assert parameter("2035-01-01") == 360.6 - for year in [2036, 2037, 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(360.6 / 338.5 - 1, rel=1e-12) + # 2036 is one fifteenth of the way from the 2035 rate to 3.6962% in 2050: + # 0.0652880 + (0.036962 - 0.0652880) / 15 = 0.0633996, so 2036 is + # 360.6 * 1.0633996 = 383.46 (hand computed). + assert growth(2036) == pytest.approx( + last_growth + (long_range - last_growth) / 15, rel=1e-12 + ) + assert parameter("2036-01-01") == pytest.approx(383.46, abs=0.01) + for year in [2050, 2051, 2075, 2100]: + assert growth(year) == pytest.approx(long_range, rel=1e-12) + # The 2034-2035 rate every year gave $931 in 2050 and $21,998 in 2100. + assert parameter("2050-01-01") == pytest.approx(751.26, abs=0.01) + assert parameter("2100-01-01") == pytest.approx(4_612.46, abs=0.01) + assert parameter("2150-01-01") == parameter("2100-01-01") + + +def test_medicare_per_capita_cost_glides_to_long_range_growth(): + """Medicare spending per enrollee keeps growing after CMS's 2034 + projection, gliding from the 2031-2032 growth rate, which skips the end + of sequestration in 2033, to the Medicare Trustees' long-range growth in + Medicare spending per enrollee: cost growth of 3.8% in 2050 and 3.7% in + 2100, compounded with a -0.1% demographic mix.""" + parameter = PARAMETERS.calibration.gov.hhs.medicare.per_capita_cost + short_range = parameter("2032-01-01") / parameter("2031-01-01") - 1 + + def growth(year): + return parameter(f"{year}-01-01") / parameter(f"{year - 1}-01-01") - 1 + + assert parameter("2034-01-01") == 30_129 + assert short_range == pytest.approx(26_552 / 25_185 - 1, rel=1e-12) + # 2035 is one sixteenth of the way from the 2031-2032 rate to 3.6962% in + # 2050: 0.0542783 + (0.036962 - 0.0542783) / 16 = 0.0531961, so 2035 is + # 30,129 * 1.0531961 = 31,731.74 (hand computed). + assert growth(2035) == pytest.approx( + short_range + (1.038 * 0.999 - 1 - short_range) / 16, rel=1e-12 + ) + assert parameter("2035-01-01") == pytest.approx(31_731.74, abs=0.01) + assert growth(2050) == pytest.approx(1.038 * 0.999 - 1, rel=1e-12) + assert growth(2100) == pytest.approx(1.037 * 0.999 - 1, rel=1e-12) assert parameter("2150-01-01") == parameter("2100-01-01")