diff --git a/changelog.d/medicare-part-b-premium-uprating.fixed.md b/changelog.d/medicare-part-b-premium-uprating.fixed.md new file mode 100644 index 00000000000..9fdda55b809 --- /dev/null +++ b/changelog.d/medicare-part-b-premium-uprating.fixed.md @@ -0,0 +1 @@ +Project Medicare Part A, B and D premiums, the Part A deductible, and the Part B and D income-related adjustment amounts and thresholds past CMS's last announced year, using the 2026 Medicare Trustees Report and the statutory inflation adjustment, instead of holding them at their 2026 values. diff --git a/changelog.d/premium-inputs-cms-uprating.changed.md b/changelog.d/premium-inputs-cms-uprating.changed.md new file mode 100644 index 00000000000..e20ed1c454b --- /dev/null +++ b/changelog.d/premium-inputs-cms-uprating.changed.md @@ -0,0 +1 @@ +Uprate premium inputs by premium series instead of CMS out-of-pocket spending per capita, which excludes premiums: reported and other health insurance premiums follow CMS's employer-sponsored insurance premium per enrollee (the series pre-tax premiums follow), directly supplied premiums follow a new CMS direct purchase premium per enrollee series, long-term care premiums follow a new CPI-U medical care series (BLS and CBO), and reported Medicare Part B premiums follow a new Part B standard premium series (Medicare Trustees Report). diff --git a/docs-quarto/methodology/spm-poverty.qmd b/docs-quarto/methodology/spm-poverty.qmd index b8562e63b9c..da7bc2f33ce 100644 --- a/docs-quarto/methodology/spm-poverty.qmd +++ b/docs-quarto/methodology/spm-poverty.qmd @@ -41,8 +41,8 @@ Benefit amounts are computed from statute for the eligible population. Whether a [`spm_unit_medical_out_of_pocket_expenses`](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/variables/household/income/spm_unit/spm_unit_medical_out_of_pocket_expenses.py) is health insurance premiums plus non-premium medical spending. -- Premiums the model computes from rules: Medicare Part A and Part B (including IRMAA), the Part D IRMAA surcharge, CHIP premiums in the states with encoded schedules, and Marketplace net premiums after the premium tax credit. Medicaid expansion premium schedules for Indiana, Michigan and Montana are encoded for reform analysis but are zero in the 2024 and 2025 baselines. Other reported premiums, including the employee share of employer plans, stay as reported in the survey. -- Non-premium spending ([`other_medical_expenses`](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/variables/household/expense/health/other_medical_expenses.py) and over-the-counter health expenses) is imported from the survey and uprated with CMS per-capita spending. No rule changes it. +- Premiums the model computes from rules: Medicare Part A and Part B (including IRMAA), the Part D IRMAA surcharge, CHIP premiums in the states with encoded schedules, and Marketplace net premiums after the premium tax credit. Medicaid expansion premium schedules for Indiana, Michigan and Montana are encoded for reform analysis but are zero in the 2024 and 2025 baselines. Other reported premiums, including the employee share of employer plans, stay as reported in the survey and are uprated with CMS's employer-sponsored insurance premium per enrollee. +- Non-premium spending ([`other_medical_expenses`](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/variables/household/expense/health/other_medical_expenses.py) and over-the-counter health expenses) is imported from the survey and uprated with CMS per-capita out-of-pocket spending. No rule changes it. The [MOOP decomposition page](moop-decomposition.qmd) records the design and the remaining double-count risks. @@ -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, over-the-counter health expenses and other health insurance premiums that enter SPM resources; physician services, inpatient, outpatient, emergency room, urgent care, ambulance, imaging, lab and prescription expenses; health insurance premiums in total, long-term and excluding Medicare Part B, and reported Medicare Part B premiums; and health savings account payroll contributions. 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. Employer-sponsored insurance premiums and pre-tax health insurance premiums, the employer's and the employee's shares of employer plans, 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), also a per-person series, which CMS projects to 2034; it grows at its 2033 to 2034 rate from 2035 to 2100 and holds the 2100 value after that. 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) 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; physician services, inpatient, outpatient, emergency room, urgent care, ambulance, imaging, lab and prescription expenses; and health savings account payroll contributions. 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. 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; one series for all three means a premium the data move from the reported inputs to the pre-tax input grows the same way. Health insurance premiums supplied directly, which the survey data hold only for self-employed people under 65 without employer coverage, follow [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), also projected to 2034. Long-term care insurance premiums, which no dataset fills and no CMS series measures, 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), the index for the deductible limits on those premiums (26 U.S.C. 213(d)(10)(B)), which CBO projects to 2036. 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; a premium with any income-related adjustment is a fixed multiple of it. 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) 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 new file mode 100644 index 00000000000..7e6b0bf1902 --- /dev/null +++ b/policyengine_us/parameters/calibration/gov/hhs/cms/direct_purchase_premium_per_enrollee.yaml @@ -0,0 +1,50 @@ +description: The Centers for Medicare and Medicaid Services (CMS) reports and projects this direct purchase insurance spending per enrollee, which is the premium per covered person for private plans bought outside an employer, before any premium tax credit. +metadata: + unit: currency-USD + label: CMS direct purchase insurance premium per enrollee + reference: + # Table17 Health Insurance Enrollment and Enrollment Growth Rates, + # "Spending per Enrollee ($)", "Direct Purchase" row: direct purchase + # spending divided by direct purchase enrollment. Its footnote 2: direct + # purchase "includes those with Medicare supplemental coverage and + # individually-purchased plans, including coverage purchased through the + # Marketplaces." ZIP Last-Modified 2026-09-03, sha256 + # 264f822618a5e1829138aea57cc6c8bf45bbaf072a72898fef5547f7ece6cd70, + # retrieved 2026-10-11 from + # https://www.cms.gov/data-research/statistics-trends-and-reports/national-health-expenditure-data/projected + - title: CMS National Health Expenditure Projections 2025-2034, Table 17 + href: https://www.cms.gov/files/zip/nhe-projections-tables.zip + # Direct purchase "includes premiums paid by individuals for policies that + # are not available through an employer or a union-based plan. This + # includes Medigap policies, Marketplace plans, and other directly purchased + # health insurance." + - title: "CMS National Health Expenditure Accounts: Methodology Paper, 2024" + href: https://www.cms.gov/files/document/definitions-sources-and-methods.pdf#page=29 +values: + # Historical. + 2013-01-01: 2_263 + 2014-01-01: 3_134 + 2015-01-01: 3_454 + 2016-01-01: 3_695 + 2017-01-01: 4_175 + 2018-01-01: 4_831 + 2019-01-01: 4_812 + 2020-01-01: 4_791 + 2021-01-01: 4_979 + 2022-01-01: 5_149 + 2023-01-01: 5_458 + 2024-01-01: 5_871 + # Projections. + 2025-01-01: 5_994 + 2026-01-01: 6_969 + 2027-01-01: 7_174 + 2028-01-01: 7_456 + 2029-01-01: 7_752 + 2030-01-01: 8_115 + 2031-01-01: 8_364 + 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. diff --git a/policyengine_us/parameters/calibration/gov/hhs/cms/medicare_part_a_deductible.yaml b/policyengine_us/parameters/calibration/gov/hhs/cms/medicare_part_a_deductible.yaml new file mode 100644 index 00000000000..9bfc9ee64b6 --- /dev/null +++ b/policyengine_us/parameters/calibration/gov/hhs/cms/medicare_part_a_deductible.yaml @@ -0,0 +1,42 @@ +description: The Centers for Medicare and Medicaid Services (CMS) sets, and the Medicare Trustees project, this Medicare Part A inpatient hospital deductible per benefit period. +metadata: + unit: currency-USD + period: year + label: Medicare Part A inpatient hospital deductible + reference: + # Table V.E1, "HI Cost-Sharing and Premium Amounts", "Inpatient hospital + # deductible" column (file page 212): historical amounts through 2026 and + # intermediate estimates for 2027-2035. Report transmitted 2026-06-09, + # retrieved 2026-10-11, sha256 + # ffa56b9137006872300b0346149eae1613d09a172b6ba118aad48e66dfc48fa8. + - title: 2026 Annual Report of the Boards of Trustees of the Federal Hospital Insurance and Federal Supplementary Medical Insurance Trust Funds, Table V.E1 + href: https://www.cms.gov/oact/tr/2026#page=212 + - title: 2026 Medicare Parts A & B Premiums and Deductibles + href: https://www.cms.gov/newsroom/fact-sheets/2026-medicare-parts-b-premiums-deductibles +values: + # Historical. + 2015-01-01: 1_260 + 2016-01-01: 1_288 + 2017-01-01: 1_316 + 2018-01-01: 1_340 + 2019-01-01: 1_364 + 2020-01-01: 1_408 + 2021-01-01: 1_484 + 2022-01-01: 1_556 + 2023-01-01: 1_600 + 2024-01-01: 1_632 + 2025-01-01: 1_676 + 2026-01-01: 1_736 + # Trustees' intermediate estimates. + 2027-01-01: 1_788 + 2028-01-01: 1_844 + 2029-01-01: 1_900 + 2030-01-01: 1_960 + 2031-01-01: 2_028 + 2032-01-01: 2_092 + 2033-01-01: 2_160 + 2034-01-01: 2_228 + 2035-01-01: 2_296 + # 2036 to 2100 grow at the 2034-2035 rate (2,296 / 2,228, 3.1% a year) 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/medicare_part_a_premium_per_enrollee.yaml b/policyengine_us/parameters/calibration/gov/hhs/cms/medicare_part_a_premium_per_enrollee.yaml new file mode 100644 index 00000000000..3d9df232ac0 --- /dev/null +++ b/policyengine_us/parameters/calibration/gov/hhs/cms/medicare_part_a_premium_per_enrollee.yaml @@ -0,0 +1,42 @@ +description: The Centers for Medicare and Medicaid Services (CMS) sets, and the Medicare Trustees project, this standard monthly Medicare Part A premium per enrollee who has fewer than 30 quarters of Medicare-covered employment. +metadata: + unit: currency-USD + period: month + label: Medicare Part A standard monthly premium per enrollee + reference: + # Table V.E1, "HI Cost-Sharing and Premium Amounts", "Monthly premium, + # Standard" column (file page 212): historical amounts through 2026 and + # intermediate estimates for 2027-2035. Report transmitted 2026-06-09, + # retrieved 2026-10-11, sha256 + # ffa56b9137006872300b0346149eae1613d09a172b6ba118aad48e66dfc48fa8. + - title: 2026 Annual Report of the Boards of Trustees of the Federal Hospital Insurance and Federal Supplementary Medical Insurance Trust Funds, Table V.E1 + href: https://www.cms.gov/oact/tr/2026#page=212 + - title: 2026 Medicare Parts A & B Premiums and Deductibles + href: https://www.cms.gov/newsroom/fact-sheets/2026-medicare-parts-b-premiums-deductibles +values: + # Historical. + 2015-01-01: 407 + 2016-01-01: 411 + 2017-01-01: 413 + 2018-01-01: 422 + 2019-01-01: 437 + 2020-01-01: 458 + 2021-01-01: 471 + 2022-01-01: 499 + 2023-01-01: 506 + 2024-01-01: 505 + 2025-01-01: 518 + 2026-01-01: 565 + # Trustees' intermediate estimates. + 2027-01-01: 600 + 2028-01-01: 629 + 2029-01-01: 660 + 2030-01-01: 694 + 2031-01-01: 730 + 2032-01-01: 767 + 2033-01-01: 812 + 2034-01-01: 862 + 2035-01-01: 900 + # 2036 to 2100 grow at the 2034-2035 rate (900 / 862, 4.4% a year) 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/medicare_part_b_premium_per_enrollee.yaml b/policyengine_us/parameters/calibration/gov/hhs/cms/medicare_part_b_premium_per_enrollee.yaml new file mode 100644 index 00000000000..01ee320a3e4 --- /dev/null +++ b/policyengine_us/parameters/calibration/gov/hhs/cms/medicare_part_b_premium_per_enrollee.yaml @@ -0,0 +1,47 @@ +description: The Centers for Medicare and Medicaid Services (CMS) sets, and the Medicare Trustees project, this standard monthly Medicare Part B premium per enrollee. +metadata: + unit: currency-USD + period: month + label: Medicare Part B standard monthly premium per enrollee + reference: + # Table III.C2, "Standard Part B Monthly Premium Rates, Actuarial Rates, and + # Premium Rates as a Percentage of Part B Cost" (file pages 89-90), for + # 2013-2026; Table V.E2, "SMI Cost-Sharing and Premium Amounts" (file page + # 213), intermediate estimates for 2027-2035. Report transmitted + # 2026-06-09, retrieved 2026-10-11, sha256 + # ffa56b9137006872300b0346149eae1613d09a172b6ba118aad48e66dfc48fa8. + - title: 2026 Annual Report of the Boards of Trustees of the Federal Hospital Insurance and Federal Supplementary Medical Insurance Trust Funds, Table V.E2 + href: https://www.cms.gov/oact/tr/2026#page=213 + - title: 2026 Annual Report of the Boards of Trustees of the Federal Hospital Insurance and Federal Supplementary Medical Insurance Trust Funds, Table III.C2 + href: https://www.cms.gov/oact/tr/2026#page=89 + - title: 2026 Medicare Parts A & B Premiums and Deductibles + href: https://www.cms.gov/newsroom/fact-sheets/2026-medicare-parts-b-premiums-deductibles +values: + # Historical. + 2013-01-01: 104.9 + 2014-01-01: 104.9 + 2015-01-01: 104.9 + 2016-01-01: 121.8 + 2017-01-01: 134 + 2018-01-01: 134 + 2019-01-01: 135.5 + 2020-01-01: 144.6 + 2021-01-01: 148.5 + 2022-01-01: 170.1 + 2023-01-01: 164.9 + 2024-01-01: 174.7 + 2025-01-01: 185 + 2026-01-01: 202.9 + # Trustees' intermediate estimates. + 2027-01-01: 209.5 + 2028-01-01: 224.5 + 2029-01-01: 238.5 + 2030-01-01: 255.5 + 2031-01-01: 272.1 + 2032-01-01: 290.2 + 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. diff --git a/policyengine_us/parameters/calibration/gov/hhs/cms/medicare_part_d_base_beneficiary_premium_per_enrollee.yaml b/policyengine_us/parameters/calibration/gov/hhs/cms/medicare_part_d_base_beneficiary_premium_per_enrollee.yaml new file mode 100644 index 00000000000..55c7f46e322 --- /dev/null +++ b/policyengine_us/parameters/calibration/gov/hhs/cms/medicare_part_d_base_beneficiary_premium_per_enrollee.yaml @@ -0,0 +1,44 @@ +description: The Centers for Medicare and Medicaid Services (CMS) sets, and the Medicare Trustees project, this Medicare Part D base beneficiary premium per enrollee. +metadata: + unit: currency-USD + period: month + label: Medicare Part D base beneficiary premium per enrollee + reference: + # Table V.E2, "SMI Cost-Sharing and Premium Amounts", "Part D, Base + # beneficiary premium" column (file page 213): historical amounts through + # 2026 and intermediate estimates for 2027-2035. The 2030 jump is the + # Inflation Reduction Act's reset of the base beneficiary premium percent + # to 20 percent (file page 154). Report transmitted 2026-06-09, retrieved + # 2026-10-11, sha256 + # ffa56b9137006872300b0346149eae1613d09a172b6ba118aad48e66dfc48fa8. + - title: 2026 Annual Report of the Boards of Trustees of the Federal Hospital Insurance and Federal Supplementary Medical Insurance Trust Funds, Table V.E2 + href: https://www.cms.gov/oact/tr/2026#page=213 + - title: 2026 Annual Report of the Boards of Trustees of the Federal Hospital Insurance and Federal Supplementary Medical Insurance Trust Funds, Part D premiums + href: https://www.cms.gov/oact/tr/2026#page=154 +values: + # Historical. + 2015-01-01: 33.13 + 2016-01-01: 34.10 + 2017-01-01: 35.63 + 2018-01-01: 35.02 + 2019-01-01: 33.19 + 2020-01-01: 32.74 + 2021-01-01: 33.06 + 2022-01-01: 33.37 + 2023-01-01: 32.74 + 2024-01-01: 34.70 + 2025-01-01: 36.78 + 2026-01-01: 38.99 + # Trustees' intermediate estimates. + 2027-01-01: 41.33 + 2028-01-01: 43.81 + 2029-01-01: 46.44 + 2030-01-01: 68.93 + 2031-01-01: 71.27 + 2032-01-01: 71.59 + 2033-01-01: 74.01 + 2034-01-01: 75.48 + 2035-01-01: 77.81 + # 2036 to 2100 grow at the 2034-2035 rate (77.81 / 75.48, 3.1% a year) and + # later years hold the 2100 value; see set_all_uprating_parameters in + # uprating_extensions.py. diff --git a/policyengine_us/parameters/gov/bls/cpi/cpi_u_medical_care.yaml b/policyengine_us/parameters/gov/bls/cpi/cpi_u_medical_care.yaml new file mode 100644 index 00000000000..965d853f813 --- /dev/null +++ b/policyengine_us/parameters/gov/bls/cpi/cpi_u_medical_care.yaml @@ -0,0 +1,47 @@ +description: The Bureau of Labor Statistics estimates this medical care component of the Consumer Price Index for All Urban Consumers, averaged over each calendar year. +values: + # BLS: the mean of the 12 published monthly indexes, series CUUR0000SAM + # (not seasonally adjusted), rounded to three decimals. + 2015-01-01: 446.752 + 2016-01-01: 463.675 + 2017-01-01: 475.322 + 2018-01-01: 484.707 + 2019-01-01: 498.413 + 2020-01-01: 518.875 + 2021-01-01: 525.276 + 2022-01-01: 546.553 + # CBO February 2026 forecast, calendar years: actual values (BLS data + # released as of December 3, 2025) for 2023 and 2024, projections after. + 2023-01-01: 549.081 + 2024-01-01: 563.841 + 2025-01-01: 581.279 + 2026-01-01: 600.332 + 2027-01-01: 617.675 + 2028-01-01: 636.697 + 2029-01-01: 656.371 + 2030-01-01: 676.782 + 2031-01-01: 697.848 + 2032-01-01: 719.546 + 2033-01-01: 741.904 + 2034-01-01: 764.927 + 2035-01-01: 788.642 + 2036-01-01: 813.07 + # 2037 to 2100 grow at the 2035-2036 rate (813.07 / 788.642, 3.1% a year) + # and later years hold the 2100 value; see set_all_uprating_parameters in + # uprating_extensions.py. +metadata: + unit: /1 + label: CPI-U medical care + economy: false + reference: + - title: BLS | CPI-U, U.S. city average, medical care, not seasonally adjusted (CUUR0000SAM) + href: https://data.bls.gov/timeseries/CUUR0000SAM + - title: CBO | Economic Projections | February 2026 + # Sheet "2. Calendar Year", row 59: "CPI-U, medical care", 1982-84=100. + # cbo.gov refuses automated downloads; the copy read is the Wayback + # capture of 2026-09-22 (sha256 + # ae8f4920702fabf8fb3136bc94a42c53466cff5e890dec22396d8dc49dc2f776), + # whose values match CBO's data repository, variable + # cpiu_medical_price_index in + # https://github.com/US-CBO/cbo-data/blob/284a95665f9f2f74ed1f482feb629b43fce323da/data/economic/economic_projections/calendar_2026-02.csv + href: https://www.cbo.gov/system/files/2026-02/51135-2026-02-Economic-Projections.xlsx diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_a/deductible.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_a/deductible.yaml index 89c12e5805c..2ba6e8c3e4f 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_a/deductible.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_a/deductible.yaml @@ -3,11 +3,24 @@ metadata: unit: currency-USD period: year label: Medicare Part A deductible + # After the last announced year, the deductible follows the Medicare + # Trustees' projected deductible, rounded to the nearest multiple of $4 as 42 + # U.S.C. 1395e(b)(1) requires. + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_a_deductible + rounding: + type: nearest + interval: 4 reference: + - title: 42 U.S.C. § 1395e(b)(1) + href: https://www.law.cornell.edu/uscode/text/42/1395e#b_1 - title: 2024 Medicare Parts A & B Premiums and Deductibles href: https://www.cms.gov/newsroom/fact-sheets/2024-medicare-parts-b-premiums-and-deductibles - title: 2025 Medicare Parts A & B Premiums and Deductibles href: https://www.cms.gov/newsroom/fact-sheets/2025-medicare-parts-b-premiums-and-deductibles + - title: 2026 Medicare Parts A & B Premiums and Deductibles + href: https://www.cms.gov/newsroom/fact-sheets/2026-medicare-parts-b-premiums-deductibles values: 2024-01-01: 1_632 2025-01-01: 1_676 + 2026-01-01: 1_736 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_a/full_premium.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_a/full_premium.yaml index a681c0151e4..fd6d44024c3 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_a/full_premium.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_a/full_premium.yaml @@ -3,7 +3,17 @@ metadata: unit: currency-USD period: month label: Medicare Part A full premium + # After the last announced year, the premium follows the Medicare Trustees' + # projected standard Part A premium, rounded to the nearest dollar as 42 + # U.S.C. 1395i-2(d)(2) requires. + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_a_premium_per_enrollee + rounding: + type: nearest + interval: 1 reference: + - title: 42 U.S.C. § 1395i-2(d) + href: https://www.law.cornell.edu/uscode/text/42/1395i-2#d - title: 2021 Medicare Parts A & B Premiums and Deductibles href: https://www.cms.gov/newsroom/press-releases/2021-medicare-part-b-premiums-remain-steady - title: 2022 Medicare Parts A & B Premiums and Deductibles diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_a/reduced_premium.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_a/reduced_premium.yaml index c124a354e27..5ea034beb2b 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_a/reduced_premium.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_a/reduced_premium.yaml @@ -3,7 +3,18 @@ metadata: unit: currency-USD period: month label: Medicare Part A reduced premium + # After the last announced year, the premium follows the Medicare Trustees' + # projected standard Part A premium, rounded to the nearest dollar as CMS + # publishes it. 42 U.S.C. 1395i-2(d)(4) sets it 45 percent below the + # standard premium, so the two grow together. + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_a_premium_per_enrollee + rounding: + type: nearest + interval: 1 reference: + - title: 42 U.S.C. § 1395i-2(d) + href: https://www.law.cornell.edu/uscode/text/42/1395i-2#d - title: 2022 Medicare Parts A & B Premiums and Deductibles href: https://www.cms.gov/newsroom/fact-sheets/2022-medicare-parts-b-premiums-deductibles-2022-medicare-part-d-income-related-monthly-adjustment - title: 2023 Medicare Parts A & B Premiums and Deductibles diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_b/base_premium.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_b/base_premium.yaml index 3f456752c45..d1b2c27e11f 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_b/base_premium.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_b/base_premium.yaml @@ -3,7 +3,17 @@ metadata: unit: currency-USD period: month label: Medicare Part B base premium + # After the last announced year, the premium follows the Medicare Trustees' + # projected standard premium, rounded to the nearest 10 cents as 42 U.S.C. + # 1395r(c) requires. + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 reference: + - title: 42 U.S.C. § 1395r(a)(3) and (c) + href: https://www.law.cornell.edu/uscode/text/42/1395r#a_3 - title: 2021 Medicare Parts A & B Premiums and Deductibles href: https://www.cms.gov/newsroom/press-releases/2021-medicare-part-b-premiums-remain-steady - title: 2022 Medicare Parts A & B Premiums and Deductibles diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/head_of_household.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/head_of_household.yaml index 3592b9a0e87..b8db3f41e8a 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/head_of_household.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/head_of_household.yaml @@ -5,7 +5,15 @@ metadata: amount_unit: currency-USD period: month label: Medicare Part B income brackets for head of household filers + # After the last announced year, each income-related adjustment amount + # follows the Medicare Trustees' projected standard Part B premium: 42 + # U.S.C. 1395r(i)(3)(A) makes each a fixed multiple of the same + # unsubsidized premium amount. The thresholds are indexed to CPI-U under + # 1395r(i)(5); see extend_medicare_irmaa_thresholds in + # uprating_extensions.py. reference: + - title: 42 U.S.C. § 1395r(i) + href: https://www.law.cornell.edu/uscode/text/42/1395r#i - title: 2024 Medicare Parts A & B Premiums and Deductibles href: https://www.cms.gov/newsroom/fact-sheets/2024-medicare-parts-b-premiums-and-deductibles - title: 2025 Medicare Parts A & B Premiums and Deductibles @@ -26,38 +34,73 @@ brackets: 2025-01-01: 106_001 2026-01-01: 109_001 amount: - 2024-01-01: 69.90 - 2025-01-01: 74 - 2026-01-01: 81.20 + values: + 2024-01-01: 69.90 + 2025-01-01: 74 + 2026-01-01: 81.20 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: 2024-01-01: 129_001 2025-01-01: 133_001 2026-01-01: 137_001 amount: - 2024-01-01: 174.70 - 2025-01-01: 185 - 2026-01-01: 202.90 + values: + 2024-01-01: 174.70 + 2025-01-01: 185 + 2026-01-01: 202.90 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: 2024-01-01: 161_001 2025-01-01: 167_001 2026-01-01: 171_001 amount: - 2024-01-01: 279.50 - 2025-01-01: 295.9 - 2026-01-01: 324.60 + values: + 2024-01-01: 279.50 + 2025-01-01: 295.9 + 2026-01-01: 324.60 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: 2024-01-01: 193_001 2025-01-01: 200_001 2026-01-01: 205_001 amount: - 2024-01-01: 384.30 - 2025-01-01: 406.9 - 2026-01-01: 446.30 + values: + 2024-01-01: 384.30 + 2025-01-01: 406.9 + 2026-01-01: 446.30 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: 2024-01-01: 500_000 2025-01-01: 500_000 2026-01-01: 500_000 amount: - 2024-01-01: 419.30 - 2025-01-01: 443.9 - 2026-01-01: 487 + values: + 2024-01-01: 419.30 + 2025-01-01: 443.9 + 2026-01-01: 487 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/joint.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/joint.yaml index ae22ca67c2e..13f3fef0744 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/joint.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/joint.yaml @@ -5,7 +5,15 @@ metadata: period: month type: single_amount label: Medicare Part B income brackets for joint filers + # After the last announced year, each income-related adjustment amount + # follows the Medicare Trustees' projected standard Part B premium: 42 + # U.S.C. 1395r(i)(3)(A) makes each a fixed multiple of the same + # unsubsidized premium amount. The thresholds are indexed to CPI-U under + # 1395r(i)(5); see extend_medicare_irmaa_thresholds in + # uprating_extensions.py. reference: + - title: 42 U.S.C. § 1395r(i) + href: https://www.law.cornell.edu/uscode/text/42/1395r#i - title: 2024 Medicare Parts A & B Premiums and Deductibles href: https://www.cms.gov/newsroom/fact-sheets/2024-medicare-parts-b-premiums-and-deductibles - title: 2025 Medicare Parts A & B Premiums and Deductibles @@ -26,38 +34,73 @@ brackets: 2025-01-01: 212_001 2026-01-01: 218_001 amount: - 2024-01-01: 69.90 - 2025-01-01: 74 - 2026-01-01: 81.20 + values: + 2024-01-01: 69.90 + 2025-01-01: 74 + 2026-01-01: 81.20 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: 2024-01-01: 258_001 2025-01-01: 266_001 2026-01-01: 274_001 amount: - 2024-01-01: 174.70 - 2025-01-01: 185 - 2026-01-01: 202.90 + values: + 2024-01-01: 174.70 + 2025-01-01: 185 + 2026-01-01: 202.90 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: 2024-01-01: 322_001 2025-01-01: 334_001 2026-01-01: 342_001 amount: - 2024-01-01: 279.50 - 2025-01-01: 295.9 - 2026-01-01: 324.60 + values: + 2024-01-01: 279.50 + 2025-01-01: 295.9 + 2026-01-01: 324.60 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: 2024-01-01: 386_001 2025-01-01: 400_001 2026-01-01: 410_001 amount: - 2024-01-01: 384.30 - 2025-01-01: 406.9 - 2026-01-01: 446.30 + values: + 2024-01-01: 384.30 + 2025-01-01: 406.9 + 2026-01-01: 446.30 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: 2024-01-01: 750_000 2025-01-01: 750_000 2026-01-01: 750_000 amount: - 2024-01-01: 419.30 - 2025-01-01: 443.9 - 2026-01-01: 487 + values: + 2024-01-01: 419.30 + 2025-01-01: 443.9 + 2026-01-01: 487 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/separate.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/separate.yaml index 00640fac5b1..0d0b39fc76a 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/separate.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/separate.yaml @@ -5,7 +5,15 @@ metadata: period: month type: single_amount label: Medicare Part B income brackets for married filing separately + # After the last announced year, each income-related adjustment amount + # follows the Medicare Trustees' projected standard Part B premium: 42 + # U.S.C. 1395r(i)(3)(A) makes each a fixed multiple of the same + # unsubsidized premium amount. The thresholds are indexed to CPI-U under + # 1395r(i)(5); see extend_medicare_irmaa_thresholds in + # uprating_extensions.py. reference: + - title: 42 U.S.C. § 1395r(i) + href: https://www.law.cornell.edu/uscode/text/42/1395r#i - title: 2024 Medicare Parts A & B Premiums and Deductibles href: https://www.cms.gov/newsroom/fact-sheets/2024-medicare-parts-b-premiums-and-deductibles - title: 2025 Medicare Parts A & B Premiums and Deductibles @@ -26,14 +34,28 @@ brackets: 2025-01-01: 106_001 2026-01-01: 109_001 amount: - 2024-01-01: 384.30 - 2025-01-01: 406.9 - 2026-01-01: 446.30 + values: + 2024-01-01: 384.30 + 2025-01-01: 406.9 + 2026-01-01: 446.30 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: 2024-01-01: 397_000 2025-01-01: 394_000 2026-01-01: 391_000 amount: - 2024-01-01: 419.30 - 2025-01-01: 443.9 - 2026-01-01: 487 + values: + 2024-01-01: 419.30 + 2025-01-01: 443.9 + 2026-01-01: 487 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/single.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/single.yaml index ee4af575421..602a1961220 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/single.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/single.yaml @@ -5,7 +5,15 @@ metadata: period: month type: single_amount label: Medicare Part B income brackets for single filers + # After the last announced year, each income-related adjustment amount + # follows the Medicare Trustees' projected standard Part B premium: 42 + # U.S.C. 1395r(i)(3)(A) makes each a fixed multiple of the same + # unsubsidized premium amount. The thresholds are indexed to CPI-U under + # 1395r(i)(5); see extend_medicare_irmaa_thresholds in + # uprating_extensions.py. reference: + - title: 42 U.S.C. § 1395r(i) + href: https://www.law.cornell.edu/uscode/text/42/1395r#i - title: 2024 Medicare Parts A & B Premiums and Deductibles href: https://www.cms.gov/newsroom/fact-sheets/2024-medicare-parts-b-premiums-and-deductibles - title: 2025 Medicare Parts A & B Premiums and Deductibles @@ -26,39 +34,73 @@ brackets: 2025-01-01: 106_001 2026-01-01: 109_001 amount: - 2024-01-01: 69.90 - 2025-01-01: 74 - 2026-01-01: 81.20 + values: + 2024-01-01: 69.90 + 2025-01-01: 74 + 2026-01-01: 81.20 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: 2024-01-01: 129_001 2025-01-01: 133_001 2026-01-01: 137_001 amount: - 2024-01-01: 174.70 - 2025-01-01: 185 - 2026-01-01: 202.90 + values: + 2024-01-01: 174.70 + 2025-01-01: 185 + 2026-01-01: 202.90 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: 2024-01-01: 161_001 2025-01-01: 167_001 2026-01-01: 171_001 amount: - 2024-01-01: 279.50 - 2025-01-01: 295.9 - 2026-01-01: 324.60 + values: + 2024-01-01: 279.50 + 2025-01-01: 295.9 + 2026-01-01: 324.60 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: 2024-01-01: 193_001 2025-01-01: 200_001 2026-01-01: 205_001 amount: - 2024-01-01: 384.30 - 2025-01-01: 406.9 - 2026-01-01: 446.30 + values: + 2024-01-01: 384.30 + 2025-01-01: 406.9 + 2026-01-01: 446.30 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: 2024-01-01: 500_000 2025-01-01: 500_000 2026-01-01: 500_000 amount: - 2024-01-01: 419.30 - 2025-01-01: 443.9 - 2026-01-01: 487 - + values: + 2024-01-01: 419.30 + 2025-01-01: 443.9 + 2026-01-01: 487 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/surviving_spouse.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/surviving_spouse.yaml index 25e1f04734c..e92b60b41c0 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/surviving_spouse.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/surviving_spouse.yaml @@ -5,7 +5,15 @@ metadata: type: single_amount period: month label: Medicare Part B income brackets for surviving spouse filers + # After the last announced year, each income-related adjustment amount + # follows the Medicare Trustees' projected standard Part B premium: 42 + # U.S.C. 1395r(i)(3)(A) makes each a fixed multiple of the same + # unsubsidized premium amount. The thresholds are indexed to CPI-U under + # 1395r(i)(5); see extend_medicare_irmaa_thresholds in + # uprating_extensions.py. reference: + - title: 42 U.S.C. § 1395r(i) + href: https://www.law.cornell.edu/uscode/text/42/1395r#i - title: 2024 Medicare Parts A & B Premiums and Deductibles href: https://www.cms.gov/newsroom/fact-sheets/2024-medicare-parts-b-premiums-and-deductibles - title: 2025 Medicare Parts A & B Premiums and Deductibles @@ -26,38 +34,73 @@ brackets: 2025-01-01: 106_001 2026-01-01: 109_001 amount: - 2024-01-01: 69.90 - 2025-01-01: 74 - 2026-01-01: 81.20 + values: + 2024-01-01: 69.90 + 2025-01-01: 74 + 2026-01-01: 81.20 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: 2024-01-01: 129_001 2025-01-01: 133_001 2026-01-01: 137_001 amount: - 2024-01-01: 174.70 - 2025-01-01: 185 - 2026-01-01: 202.90 + values: + 2024-01-01: 174.70 + 2025-01-01: 185 + 2026-01-01: 202.90 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: 2024-01-01: 161_001 2025-01-01: 167_001 2026-01-01: 171_001 amount: - 2024-01-01: 279.50 - 2025-01-01: 295.9 - 2026-01-01: 324.60 + values: + 2024-01-01: 279.50 + 2025-01-01: 295.9 + 2026-01-01: 324.60 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: 2024-01-01: 193_001 2025-01-01: 200_001 2026-01-01: 205_001 amount: - 2024-01-01: 384.30 - 2025-01-01: 406.9 - 2026-01-01: 446.30 + values: + 2024-01-01: 384.30 + 2025-01-01: 406.9 + 2026-01-01: 446.30 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: 2024-01-01: 500_000 2025-01-01: 500_000 2026-01-01: 500_000 amount: - 2024-01-01: 419.30 - 2025-01-01: 443.9 - 2026-01-01: 487 + values: + 2024-01-01: 419.30 + 2025-01-01: 443.9 + 2026-01-01: 487 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_d/base_beneficiary_premium_percent.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_d/base_beneficiary_premium_percent.yaml new file mode 100644 index 00000000000..f0bd8731fe3 --- /dev/null +++ b/policyengine_us/parameters/gov/hhs/medicare/part_d/base_beneficiary_premium_percent.yaml @@ -0,0 +1,23 @@ +description: The Centers for Medicare & Medicaid Services (CMS) uses this percent to set the Medicare Part D base beneficiary premium and to scale the Part D income-related monthly adjustment amount. +metadata: + unit: /1 + period: year + label: Medicare Part D base beneficiary premium percent + reference: + # (a)(3)(A) and (a)(7)(B)(i): 25.5 percent, or from 2030 the percent + # specified under (a)(9). (a)(9) sets that percent so the 2030 base + # beneficiary premium equals the lesser of the 2029 premium increased by 6 + # percent or the premium without (a)(9), but not below 20 percent. + - title: 42 U.S.C. § 1395w-113(a)(3), (a)(7)(B) and (a)(9) + href: https://www.law.cornell.edu/uscode/text/42/1395w-113#a + # Footnote 63: "Currently, we expect the percentage in 2030 will be set at + # 20 percent, causing a substantial premium increase." + - title: 2026 Annual Report of the Boards of Trustees of the Federal Hospital Insurance and Federal Supplementary Medical Insurance Trust Funds, footnote 63 + href: https://www.cms.gov/oact/tr/2026#page=113 + - title: 2026 Annual Report of the Boards of Trustees of the Federal Hospital Insurance and Federal Supplementary Medical Insurance Trust Funds, Part D premiums + href: https://www.cms.gov/oact/tr/2026#page=154 +values: + 2006-01-01: 0.255 + # The Trustees' expectation of the (a)(9) percent, which is also its floor; + # CMS determines it for 2030. + 2030-01-01: 0.2 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/head_of_household.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/head_of_household.yaml index c9c5403a23c..270075ab915 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/head_of_household.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/head_of_household.yaml @@ -5,6 +5,11 @@ metadata: period: month type: single_amount label: Medicare Part D IRMAA brackets for head of household filers + # After the last announced year, the thresholds are indexed to CPI-U under + # 42 U.S.C. 1395r(i)(5) and each adjustment amount is computed from the + # Medicare Trustees' projected base beneficiary premium under 42 U.S.C. + # 1395w-113(a)(7)(B); see extend_medicare_irmaa_thresholds and + # extend_medicare_part_d_irmaa_amounts in uprating_extensions.py. reference: - title: 42 U.S.C. § 1395w-113(a)(7) - Part D income-related premium adjustment. href: https://www.law.cornell.edu/uscode/text/42/1395w-113#a_7 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/joint.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/joint.yaml index 4837285dfdb..3f4c00fdd88 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/joint.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/joint.yaml @@ -5,6 +5,11 @@ metadata: period: month type: single_amount label: Medicare Part D IRMAA brackets for joint filers + # After the last announced year, the thresholds are indexed to CPI-U under + # 42 U.S.C. 1395r(i)(5) and each adjustment amount is computed from the + # Medicare Trustees' projected base beneficiary premium under 42 U.S.C. + # 1395w-113(a)(7)(B); see extend_medicare_irmaa_thresholds and + # extend_medicare_part_d_irmaa_amounts in uprating_extensions.py. reference: - title: 42 U.S.C. § 1395w-113(a)(7) - Part D income-related premium adjustment. href: https://www.law.cornell.edu/uscode/text/42/1395w-113#a_7 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/separate.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/separate.yaml index dfb4259c3c4..9c83356e797 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/separate.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/separate.yaml @@ -5,6 +5,11 @@ metadata: period: month type: single_amount label: Medicare Part D IRMAA brackets for married filing separately + # After the last announced year, the thresholds are indexed to CPI-U under + # 42 U.S.C. 1395r(i)(5) and each adjustment amount is computed from the + # Medicare Trustees' projected base beneficiary premium under 42 U.S.C. + # 1395w-113(a)(7)(B); see extend_medicare_irmaa_thresholds and + # extend_medicare_part_d_irmaa_amounts in uprating_extensions.py. reference: - title: 42 U.S.C. § 1395w-113(a)(7) - Part D income-related premium adjustment. href: https://www.law.cornell.edu/uscode/text/42/1395w-113#a_7 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/single.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/single.yaml index 7c79e99ee12..3aed61d03e2 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/single.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/single.yaml @@ -5,6 +5,11 @@ metadata: period: month type: single_amount label: Medicare Part D IRMAA brackets for single filers + # After the last announced year, the thresholds are indexed to CPI-U under + # 42 U.S.C. 1395r(i)(5) and each adjustment amount is computed from the + # Medicare Trustees' projected base beneficiary premium under 42 U.S.C. + # 1395w-113(a)(7)(B); see extend_medicare_irmaa_thresholds and + # extend_medicare_part_d_irmaa_amounts in uprating_extensions.py. reference: - title: 42 U.S.C. § 1395w-113(a)(7) - Part D income-related premium adjustment. href: https://www.law.cornell.edu/uscode/text/42/1395w-113#a_7 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/surviving_spouse.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/surviving_spouse.yaml index 749583c717b..cacf8ba4190 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/surviving_spouse.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/surviving_spouse.yaml @@ -5,6 +5,11 @@ metadata: period: month type: single_amount label: Medicare Part D IRMAA brackets for surviving spouse filers + # After the last announced year, the thresholds are indexed to CPI-U under + # 42 U.S.C. 1395r(i)(5) and each adjustment amount is computed from the + # Medicare Trustees' projected base beneficiary premium under 42 U.S.C. + # 1395w-113(a)(7)(B); see extend_medicare_irmaa_thresholds and + # extend_medicare_part_d_irmaa_amounts in uprating_extensions.py. reference: - title: 42 U.S.C. § 1395w-113(a)(7) - Part D income-related premium adjustment. href: https://www.law.cornell.edu/uscode/text/42/1395w-113#a_7 diff --git a/policyengine_us/parameters/uprating_extensions.py b/policyengine_us/parameters/uprating_extensions.py index 23ec45e9904..fda8c3c53dd 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 @@ -498,6 +498,215 @@ def extend_wa_millionaires_standard_deduction( ) +# 42 U.S.C. 1395r(i)(3)(C)(i)(III): the dollar amounts of the applicable +# percentage table for an unmarried individual, before the (5) inflation +# adjustment. The first is also the (2)(A) threshold amount. +MEDICARE_IRMAA_STATUTORY_AMOUNTS = (85_000, 107_000, 133_500, 160_000) +MEDICARE_IRMAA_STATUTORY_TOP_AMOUNT = 500_000 +# 1395r(i)(5)(A): for 2020 on, CPI-U for the 12 months ending August of the +# preceding year over the 12 months ending August 2018. (5)(C): the $500,000 +# amount is not adjusted before 2028, and from 2028 is adjusted against the 12 +# months ending August 2026. +MEDICARE_IRMAA_CPI_BASE_YEAR = 2018 +MEDICARE_IRMAA_FIRST_INDEXED_YEAR = 2020 +MEDICARE_IRMAA_TOP_CPI_BASE_YEAR = 2026 +MEDICARE_IRMAA_TOP_FIRST_INDEXED_YEAR = 2028 +# 1395r(i)(3)(C)(i)(III): the applicable percentage of each adjusted tier, by +# bracket of each filing status's scale (the first bracket pays no +# adjustment). A married individual filing separately who lives with the +# spouse pays an adjustment only above the unmarried threshold amount +# ((i)(1)), which is above where (C)(iii) starts the 80 percent tier, so that +# scale has only the 80 and 85 percent tiers. +MEDICARE_IRMAA_APPLICABLE_PERCENTAGES = { + "single": (0.35, 0.50, 0.65, 0.80, 0.85), + "joint": (0.35, 0.50, 0.65, 0.80, 0.85), + "head_of_household": (0.35, 0.50, 0.65, 0.80, 0.85), + "surviving_spouse": (0.35, 0.50, 0.65, 0.80, 0.85), + "separate": (0.80, 0.85), +} + + +def round_medicare_irmaa_amount(amount: float) -> float: + """Round an adjusted dollar amount to the nearest multiple of $1,000. + + 42 U.S.C. 1395r(i)(5)(B) names no rule for an amount midway between two + multiples; it rounds up here. + """ + return float( + (Decimal(repr(amount)) / 1_000).quantize(Decimal(1), rounding=ROUND_HALF_UP) + * 1_000 + ) + + +def get_medicare_irmaa_unmarried_amounts( + parameters: ParameterNode, year: int +) -> Tuple[Tuple[float, ...], float]: + """The unmarried dollar amounts of the IRMAA table for ``year``. + + Returns the four amounts above which the 35, 50, 65 and 80 percent tiers + begin, and the amount at which the 85 percent tier begins, each adjusted + under 42 U.S.C. 1395r(i)(5): increased by the percentage (if any) by which + average CPI-U for the 12 months ending August of the preceding year + exceeds its base-period average, then rounded to the nearest $1,000. + """ + if year < MEDICARE_IRMAA_FIRST_INDEXED_YEAR: + raise ValueError( + f"1395r(i)(5) adjusts against August 2018 only from " + f"{MEDICARE_IRMAA_FIRST_INDEXED_YEAR}; got {year}." + ) + window = get_cpi_u_average_for_12_months_ending_august(parameters, year - 1) + + def adjust(amount: int, base_year: int) -> float: + base = get_cpi_u_average_for_12_months_ending_august(parameters, base_year) + return round_medicare_irmaa_amount(amount * (1 + max(window / base - 1, 0))) + + lower = tuple( + adjust(amount, MEDICARE_IRMAA_CPI_BASE_YEAR) + for amount in MEDICARE_IRMAA_STATUTORY_AMOUNTS + ) + if year < MEDICARE_IRMAA_TOP_FIRST_INDEXED_YEAR: + top = float(MEDICARE_IRMAA_STATUTORY_TOP_AMOUNT) + else: + top = adjust( + MEDICARE_IRMAA_STATUTORY_TOP_AMOUNT, MEDICARE_IRMAA_TOP_CPI_BASE_YEAR + ) + return lower, top + + +def get_medicare_irmaa_thresholds( + lower: Tuple[float, ...], top: float +) -> Dict[str, Tuple[float, ...]]: + """Bracket thresholds of each filing status's IRMAA scale. + + The first four tiers cover income "more than" a dollar amount and the top + tier income of "at least" its amount (42 U.S.C. 1395r(i)(3)(C)(i)(III)). + A bracket applies from its threshold, so, as in the published years, the + first four thresholds sit $1 above their amounts. + """ + if lower[3] - lower[0] >= lower[0]: + raise ValueError( + "The married filing separately scale assumes its 80 percent tier " + "starts below the unmarried threshold amount." + ) + unmarried = (0, *(amount + 1 for amount in lower), top) + return { + "single": unmarried, + "head_of_household": unmarried, + "surviving_spouse": unmarried, + # (C)(ii): twice the unmarried amounts, but 150 percent of the + # $500,000 amount. + "joint": (0, *(2 * amount + 1 for amount in lower), 1.5 * top), + # (C)(iii): each amount less the unmarried threshold amount, with no + # adjustment at or below that threshold ((i)(1)). + "separate": (0, lower[0] + 1, top - lower[0]), + } + + +def extend_medicare_irmaa_thresholds(parameters: ParameterNode, end_year: int) -> None: + """Project the Medicare Part B and Part D IRMAA income thresholds. + + Part D uses the Part B thresholds (42 U.S.C. 1395w-113(a)(7)(A)). Each + year after the last CMS-published one is computed from the statutory + amounts (see ``get_medicare_irmaa_unmarried_amounts``) rather than + chained from the prior year's rounded values. CMS values encoded in the + YAML take precedence for their own years. + """ + medicare = parameters.gov.hhs.medicare + thresholds_by_year = {} + for program in (medicare.part_b, medicare.part_d): + for name, scale in program.irmaa.children.items(): + if name not in MEDICARE_IRMAA_APPLICABLE_PERCENTAGES: + raise ValueError(f"Unexpected Medicare IRMAA scale: {name}.") + for index, bracket in enumerate(scale.brackets): + threshold = bracket.threshold + first_projected_year = 1 + max( + int(value.instant_str[:4]) for value in threshold.values_list + ) + for year in range(first_projected_year, end_year + 1): + if year not in thresholds_by_year: + thresholds_by_year[year] = get_medicare_irmaa_thresholds( + *get_medicare_irmaa_unmarried_amounts(parameters, year) + ) + year_thresholds = thresholds_by_year[year][name] + if len(year_thresholds) != len(scale.brackets): + raise ValueError( + f"Expected {len(year_thresholds)} brackets in the " + f"{name} Medicare IRMAA scale; found " + f"{len(scale.brackets)}." + ) + threshold.update( + period=f"year:{year}-01-01:1", + value=float(year_thresholds[index]), + ) + threshold.update( + start=instant(f"{end_year}-01-01"), + value=threshold(f"{end_year}-01-01"), + ) + + +def get_medicare_part_d_irmaa_amount( + base_beneficiary_premium: float, + applicable_percentage: float, + base_percent: float, +) -> float: + """One Part D income-related monthly adjustment amount. + + 42 U.S.C. 1395w-113(a)(7)(B): the base beneficiary premium times the + applicable percentage less the base beneficiary premium percent, divided + by that percent. CMS publishes the amounts to the nearest 10 cents. + """ + premium = Decimal(repr(base_beneficiary_premium)) + applicable = Decimal(repr(applicable_percentage)) + base = Decimal(repr(base_percent)) + amount = premium * (applicable - base) / base + return float(amount.quantize(Decimal("0.1"), rounding=ROUND_HALF_UP)) + + +def extend_medicare_part_d_irmaa_amounts( + parameters: ParameterNode, end_year: int +) -> None: + """Project the Medicare Part D income-related monthly adjustment amounts. + + Each year after the last CMS-published one applies 42 U.S.C. + 1395w-113(a)(7)(B) to the Medicare Trustees' projected base beneficiary + premium. The base beneficiary premium percent falls from 25.5 percent to + the (a)(9) percent in 2030, which raises every amount; the Trustees + expect 20 percent. CMS values encoded in the YAML take precedence for + their own years. + """ + base_beneficiary_premium = parameters.calibration.gov.hhs.cms.medicare_part_d_base_beneficiary_premium_per_enrollee + part_d = parameters.gov.hhs.medicare.part_d + for name, scale in part_d.irmaa.children.items(): + applicable_percentages = MEDICARE_IRMAA_APPLICABLE_PERCENTAGES.get(name) + if applicable_percentages is None or len(applicable_percentages) != len( + scale.brackets[1:] + ): + raise ValueError( + f"Unexpected Medicare Part D IRMAA scale or bracket count: {name}." + ) + for bracket, applicable_percentage in zip( + scale.brackets[1:], applicable_percentages + ): + amount = bracket.amount + first_projected_year = 1 + max( + int(value.instant_str[:4]) for value in amount.values_list + ) + for year in range(first_projected_year, end_year + 1): + period = f"{year}-01-01" + amount.update( + period=f"year:{period}:1", + value=get_medicare_part_d_irmaa_amount( + base_beneficiary_premium(period), + applicable_percentage, + part_d.base_beneficiary_premium_percent(period), + ), + ) + amount.update( + start=instant(f"{end_year}-01-01"), + value=amount(f"{end_year}-01-01"), + ) + + def extend_parameter_values( parameter: Parameter, last_projected_year: int, @@ -822,6 +1031,17 @@ def set_all_uprating_parameters(parameters: ParameterNode) -> ParameterNode: period_day=1, ) + # CPI-U medical care (calendar-year values, last CBO projection year + # 2036). Long-term care insurance premium inputs use this as their + # uprater. + extend_parameter_values( + parameters.gov.bls.cpi.cpi_u_medical_care, + last_projected_year=2036, + end_year=END_YEAR, + period_month=1, + period_day=1, + ) + # Washington's millionaires tax deduction uses an odd-year October # adjustment schedule. Must run after the CPI-W extension above so the # determination dates are available through the projection horizon. @@ -850,8 +1070,8 @@ def set_all_uprating_parameters(parameters: ParameterNode) -> ParameterNode: ) # CMS employer-sponsored insurance premium per enrollee (January values, - # last projection year 2034). Employer and pre-tax employee premium - # inputs use this as their uprater. + # last projection year 2034). Employer and employee premium inputs use + # this as their uprater. extend_parameter_values( parameters.calibration.gov.hhs.cms.esi_premium_per_enrollee, last_projected_year=2034, @@ -860,6 +1080,56 @@ def set_all_uprating_parameters(parameters: ParameterNode) -> ParameterNode: period_day=1, ) + # CMS direct purchase insurance premium per enrollee (January values, last + # projection year 2034). Premiums for plans bought directly use this as + # their uprater. + 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, + ) + + # Medicare Part B standard monthly premium (January values, last Trustees + # projection year 2035). Reported Part B premium inputs use this as their + # uprater. + 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, + ) + + # Medicare Part A standard premium and inpatient hospital deductible, and + # Part D base beneficiary premium (January values, last Trustees + # projection year 2035). The Part A premiums and deductible use the first + # two as their upraters; the Part D income-related adjustment amounts are + # computed from the third below. + for medicare_series in ( + parameters.calibration.gov.hhs.cms.medicare_part_a_premium_per_enrollee, + parameters.calibration.gov.hhs.cms.medicare_part_a_deductible, + parameters.calibration.gov.hhs.cms.medicare_part_d_base_beneficiary_premium_per_enrollee, + ): + extend_parameter_values( + medicare_series, + last_projected_year=2035, + end_year=END_YEAR, + period_month=1, + period_day=1, + ) + + # Medicare IRMAA income thresholds follow 42 U.S.C. 1395r(i)(5)'s CPI-U + # schedule (12 months ending August), computed from the statutory + # amounts. Must run after the CPI-U extension above. + extend_medicare_irmaa_thresholds(parameters, end_year=END_YEAR) + + # Medicare Part D income-related adjustment amounts follow 42 U.S.C. + # 1395w-113(a)(7)(B) applied to the base beneficiary premium extended + # above. + extend_medicare_part_d_irmaa_amounts(parameters, end_year=END_YEAR) + # Gross Social Security benefit inputs should age with gross Social # Security benefits, not taxable Social Security income. extend_parameter_values( diff --git a/policyengine_us/programs.yaml b/policyengine_us/programs.yaml index 05cb1d13747..981187ff946 100644 --- a/policyengine_us/programs.yaml +++ b/policyengine_us/programs.yaml @@ -1116,6 +1116,9 @@ programs: Includes Part A (hospital) and Part B (medical) premiums with IRMAA adjustments; gross Part B premium calibration should use the enrollment-gated gross_medicare_part_b_premium_if_enrolled variable. + After CMS's last announced year, premiums and IRMAA amounts follow the + Medicare Trustees' projections and IRMAA thresholds the statutory CPI-U + indexing. # --- HUD programs --- - id: section_8 diff --git a/policyengine_us/tests/core/test_medicare_premium_projection.py b/policyengine_us/tests/core/test_medicare_premium_projection.py new file mode 100644 index 00000000000..bcf4f8aa5c1 --- /dev/null +++ b/policyengine_us/tests/core/test_medicare_premium_projection.py @@ -0,0 +1,577 @@ +"""Medicare premiums, deductible and IRMAA after CMS's last announced year. + +CMS announces each year's Part A and B premiums, Part A deductible and +income-related monthly adjustment amounts (IRMAA) in the autumn before. For +later years the model projects them: + +- The Part B standard premium and Part B IRMAA amounts uprate by the + Medicare Trustees' projected standard premium, rounded to 10 cents. +- The Part A premiums and deductible uprate by the Trustees' projections, + rounded to $1 and $4. +- The IRMAA income thresholds follow 42 U.S.C. 1395r(i)(5)'s CPI-U schedule. +- The Part D IRMAA amounts apply 42 U.S.C. 1395w-113(a)(7)(B) to the + Trustees' projected base beneficiary premium. + +These tests read the parameter tree, plus one small parameter tree built for +the precedence tests. Household calculations are in the YAML tests under +``tests/policy/baseline/gov/hhs/medicare``. + +When CMS announces a year, add its amounts to the YAML and move +``LAST_ANNOUNCED_YEAR`` forward. Later years then grow from the announced +amounts, so the Trustees rows pinned below for those years no longer apply +exactly; update them from the next Trustees Report. + +Invariants: + +1. Source agreement (differential, model against published figures): for + 2027-2035, the projections equal the 2026 Trustees Report's Part B + premium (Table V.E2), Part A premiums and deductible (Table V.E1) and + Part D IRMAA amounts (Table V.E4). The Part B IRMAA amounts are within + $0.30 of Table V.E3 through 2034 and $0.80 in 2035, where Table V.E3 + implies a $360.90 standard premium against Table V.E2's $360.60. +2. Statutory rounding: in every projected year, the Part B premium and IRMAA + amounts are multiples of 10 cents, the Part A premiums of $1 and the + deductible of $4. +3. Agreement with the uprater: the Part B premium equals the Trustees series + rounded to 10 cents in every year from 2021. +4. Proportionality: each Part B IRMAA amount stays at its 42 U.S.C. + 1395r(i)(3)(A) multiple of the standard premium, to within 0.1% plus + rounding, as in CMS's 2026 amounts. Each Part D amount equals + 1395w-113(a)(7)(B) applied to the base beneficiary premium. The reduced + Part A premium stays at 55% of the full premium (1395i-2(d)(4)), to + within 0.1% plus $1. CMS rounded 2026's $310.75 to $311, and uprating + from 2026 keeps that 0.04% excess, as it keeps the IRMAA amounts'. +5. Monotonicity: the Part B premium rises every year from 2023, and the Part + A premiums and deductible never fall from 2024. IRMAA thresholds never + fall from one year to the next and rise from tier to tier. Intended + exception: the married-filing-separately top threshold is $500,000 less + the rising unmarried threshold, so it falls each year until the $500,000 + amount is indexed in 2028. +6. Threshold structure: joint thresholds are twice the unmarried amounts + (150% for the top tier), married filing separately uses the unmarried + threshold and the top amount less that threshold, and Part D uses the Part + B thresholds. The top tier stays at $500,000 through 2027. +7. Indexing (differential): the statutory CPI-U schedule reproduces CMS's + published thresholds exactly for 2020-2024, where the model's CPI-U is + the published not seasonally adjusted index, and within one $1,000 step + for 2025-2026, where it is seasonally adjusted. +8. Precedence: a CMS amount added to the YAML for a year replaces the + projection for that year only; later years are still computed from the + statute rather than chained from it. +9. Extension: the new Trustees series grow at their last one-year rate past + 2035 and hold their 2100 value after 2100. +""" + +from types import SimpleNamespace + +import pytest +from hypothesis import given +from hypothesis import strategies as st +from policyengine_core.parameters import ParameterNode +from policyengine_core.parameters.operations.get_parameter import get_parameter + +from policyengine_us.parameters.uprating_extensions import ( + MEDICARE_IRMAA_APPLICABLE_PERCENTAGES, + MEDICARE_IRMAA_TOP_FIRST_INDEXED_YEAR, + extend_medicare_irmaa_thresholds, + extend_medicare_part_d_irmaa_amounts, + get_medicare_irmaa_thresholds, + get_medicare_irmaa_unmarried_amounts, + get_medicare_part_d_irmaa_amount, +) +from policyengine_us.system import system + +PARAMETERS = system.parameters +MEDICARE = PARAMETERS.gov.hhs.medicare +CMS = "calibration.gov.hhs.cms" +PART_B_SERIES = f"{CMS}.medicare_part_b_premium_per_enrollee" +PART_A_SERIES = f"{CMS}.medicare_part_a_premium_per_enrollee" +DEDUCTIBLE_SERIES = f"{CMS}.medicare_part_a_deductible" +PART_D_SERIES = f"{CMS}.medicare_part_d_base_beneficiary_premium_per_enrollee" +LAST_ANNOUNCED_YEAR = 2026 +LAST_TRUSTEES_YEAR = 2035 +END_YEAR = 2100 +FILING_STATUSES = ("single", "joint", "head_of_household", "surviving_spouse") +ALL_STATUSES = (*FILING_STATUSES, "separate") +PROJECTED_YEARS = st.integers(LAST_ANNOUNCED_YEAR + 1, END_YEAR + 20) +TRUSTEES_YEARS = range(LAST_ANNOUNCED_YEAR + 1, LAST_TRUSTEES_YEAR + 1) + + +def _at(parameter, year): + return parameter(f"{year}-01-01") + + +def _series(path, year): + return _at(get_parameter(PARAMETERS, path), year) + + +def _is_multiple(value, step): + return abs(value / step - round(value / step)) < 1e-6 + + +# 2026 Medicare Trustees Report (transmitted 2026-06-09, sha256 +# ffa56b91...8fa8), intermediate estimates for 2027-2035. +# Table V.E1 (file page 212): inpatient hospital deductible, standard and +# reduced monthly Part A premiums. +TRUSTEES_PART_A_DEDUCTIBLE = dict( + zip( + range(2026, 2036), + (1_736, 1_788, 1_844, 1_900, 1_960, 2_028, 2_092, 2_160, 2_228, 2_296), + ) +) +TRUSTEES_PART_A_FULL_PREMIUM = dict( + zip(TRUSTEES_YEARS, (600, 629, 660, 694, 730, 767, 812, 862, 900)) +) +TRUSTEES_PART_A_REDUCED_PREMIUM = dict( + zip(TRUSTEES_YEARS, (330, 346, 363, 382, 402, 422, 447, 474, 495)) +) +# Table V.E2 (file page 213): standard monthly Part B premium and Part D base +# beneficiary premium. +TRUSTEES_PART_B_PREMIUM = dict( + zip( + TRUSTEES_YEARS, + (209.5, 224.5, 238.5, 255.5, 272.1, 290.2, 313.6, 338.5, 360.6), + ) +) +TRUSTEES_PART_D_BASE_PREMIUM = dict( + zip( + TRUSTEES_YEARS, + (41.33, 43.81, 46.44, 68.93, 71.27, 71.59, 74.01, 75.48, 77.81), + ) +) +# Table V.E3 (file page 215): Part B income-related monthly adjustment +# amounts for the 35, 50, 65, 80 and 85 percent tiers. +TRUSTEES_PART_B_IRMAA = { + 2027: (83.70, 209.40, 335.10, 460.70, 502.60), + 2028: (89.80, 224.50, 359.20, 493.90, 538.80), + 2029: (95.30, 238.40, 381.50, 524.50, 572.20), + 2030: (102.10, 255.40, 408.70, 561.90, 613.00), + 2031: (108.80, 272.10, 435.40, 598.60, 653.00), + 2032: (116.00, 290.10, 464.20, 638.30, 696.30), + 2033: (125.40, 313.60, 501.80, 689.90, 752.60), + 2034: (135.30, 338.40, 541.50, 744.50, 812.20), + 2035: (144.40, 360.90, 577.40, 794.00, 866.20), +} +# Table V.E4 (file page 218): Part D income-related monthly adjustment +# amounts for the same tiers. +TRUSTEES_PART_D_IRMAA = { + 2027: (15.40, 39.70, 64.00, 88.30, 96.40), + 2028: (16.30, 42.10, 67.90, 93.60, 102.20), + 2029: (17.30, 44.60, 71.90, 99.30, 108.40), + 2030: (51.70, 103.40, 155.10, 206.80, 224.00), + 2031: (53.50, 106.90, 160.40, 213.80, 231.60), + 2032: (53.70, 107.40, 161.10, 214.80, 232.70), + 2033: (55.50, 111.00, 166.50, 222.00, 240.50), + 2034: (56.60, 113.20, 169.80, 226.40, 245.30), + 2035: (58.40, 116.70, 175.10, 233.40, 252.90), +} +# 42 U.S.C. 1395r(i)(3)(A): (applicable percentage - 25 points) x 200% of the +# aged actuarial rate, which is 4 x (applicable percentage - 25 points) times +# the standard premium of 50% of that rate. +PART_B_IRMAA_MULTIPLES = (0.4, 1.0, 1.6, 2.2, 2.4) +# CMS fact sheets "2020/2021/2022/2023 Medicare Parts A & B Premiums and +# Deductibles": the unmarried amounts above which the 35, 50, 65 and 80 +# percent tiers begin. 2024-2026 are the YAML's published values. +CMS_UNMARRIED_AMOUNTS = { + 2020: (87_000, 109_000, 136_000, 163_000), + 2021: (88_000, 111_000, 138_000, 165_000), + 2022: (91_000, 114_000, 142_000, 170_000), + 2023: (97_000, 123_000, 153_000, 183_000), + 2024: (103_000, 129_000, 161_000, 193_000), + 2025: (106_000, 133_000, 167_000, 200_000), + 2026: (109_000, 137_000, 171_000, 205_000), +} + + +def _brackets(program, status): + return getattr(program.irmaa, status).brackets + + +def _thresholds(program, status, year): + return [_at(b.threshold, year) for b in _brackets(program, status)] + + +# 1. Source agreement. + + +@pytest.mark.parametrize("year", TRUSTEES_YEARS) +def test_part_b_premium_is_the_trustees_projection(year): + assert _at(MEDICARE.part_b.base_premium, year) == pytest.approx( + TRUSTEES_PART_B_PREMIUM[year], abs=1e-9 + ) + + +@pytest.mark.parametrize("year", TRUSTEES_YEARS) +def test_part_a_premiums_are_the_trustees_projection(year): + assert _at(MEDICARE.part_a.full_premium, year) == pytest.approx( + TRUSTEES_PART_A_FULL_PREMIUM[year], abs=1e-9 + ) + assert _at(MEDICARE.part_a.reduced_premium, year) == pytest.approx( + TRUSTEES_PART_A_REDUCED_PREMIUM[year], abs=1e-9 + ) + + +@pytest.mark.parametrize("year", sorted(TRUSTEES_PART_A_DEDUCTIBLE)) +def test_part_a_deductible_is_the_trustees_figure(year): + """2026 is CMS's announced $1,736; later years are projections.""" + assert _at(MEDICARE.part_a.deductible, year) == pytest.approx( + TRUSTEES_PART_A_DEDUCTIBLE[year], abs=1e-9 + ) + + +@pytest.mark.parametrize("year", TRUSTEES_YEARS) +@pytest.mark.parametrize("status", FILING_STATUSES) +def test_part_b_irmaa_is_within_cents_of_the_trustees_projection(status, year): + tolerance = (0.80 if year == LAST_TRUSTEES_YEAR else 0.30) + 1e-9 + amounts = [_at(b.amount, year) for b in _brackets(MEDICARE.part_b, status)[1:]] + assert amounts == pytest.approx(TRUSTEES_PART_B_IRMAA[year], abs=tolerance) + + +@pytest.mark.parametrize("year", TRUSTEES_YEARS) +@pytest.mark.parametrize("status", FILING_STATUSES) +def test_part_d_irmaa_is_the_trustees_projection(status, year): + """Applying the statute to the Trustees' base beneficiary premium, with + the 20 percent base from 2030, reproduces their Table V.E4.""" + amounts = [_at(b.amount, year) for b in _brackets(MEDICARE.part_d, status)[1:]] + assert amounts == pytest.approx(TRUSTEES_PART_D_IRMAA[year], abs=1e-9) + + +@pytest.mark.parametrize("year", TRUSTEES_YEARS) +def test_married_filing_separately_gets_the_two_top_tiers(year): + for program in (MEDICARE.part_b, MEDICARE.part_d): + separate = [_at(b.amount, year) for b in _brackets(program, "separate")] + single = [_at(b.amount, year) for b in _brackets(program, "single")] + assert separate == [0, *single[-2:]] + + +@pytest.mark.parametrize("year", [2024, 2025, 2026]) +def test_part_d_formula_reproduces_cms_published_amounts(year): + """Differential check of the formula and its rounding against CMS's + published amounts, from CMS's published base beneficiary premiums.""" + base_premium = _series(PART_D_SERIES, year) + for status in ALL_STATUSES: + brackets = _brackets(MEDICARE.part_d, status)[1:] + for bracket, percentage in zip( + brackets, MEDICARE_IRMAA_APPLICABLE_PERCENTAGES[status] + ): + assert get_medicare_part_d_irmaa_amount( + base_premium, percentage, 0.255 + ) == pytest.approx(_at(bracket.amount, year), abs=1e-9) + + +@pytest.mark.parametrize( + "path, values", + [ + (PART_A_SERIES, TRUSTEES_PART_A_FULL_PREMIUM), + (DEDUCTIBLE_SERIES, TRUSTEES_PART_A_DEDUCTIBLE), + (PART_D_SERIES, TRUSTEES_PART_D_BASE_PREMIUM), + ], +) +def test_new_series_transcribe_the_trustees_tables(path, values): + for year, value in values.items(): + assert _series(path, year) == value + + +@pytest.mark.parametrize("year", range(2021, LAST_ANNOUNCED_YEAR + 1)) +def test_part_a_series_agrees_with_announced_premiums(year): + assert _series(PART_A_SERIES, year) == _at(MEDICARE.part_a.full_premium, year) + + +@pytest.mark.parametrize("year", range(2024, LAST_ANNOUNCED_YEAR + 1)) +def test_deductible_series_agrees_with_announced_deductibles(year): + assert _series(DEDUCTIBLE_SERIES, year) == _at(MEDICARE.part_a.deductible, year) + + +# 2. Statutory rounding. + + +@given(PROJECTED_YEARS) +def test_projected_amounts_are_rounded_as_published(year): + assert _is_multiple(_at(MEDICARE.part_b.base_premium, year), 0.1) + assert _is_multiple(_at(MEDICARE.part_a.full_premium, year), 1) + assert _is_multiple(_at(MEDICARE.part_a.reduced_premium, year), 1) + assert _is_multiple(_at(MEDICARE.part_a.deductible, year), 4) + for program in (MEDICARE.part_b, MEDICARE.part_d): + for status in ALL_STATUSES: + for bracket in _brackets(program, status): + assert _is_multiple(_at(bracket.amount, year), 0.1) + + +# 3. Agreement with the uprater. + + +@given(st.integers(2021, END_YEAR + 20)) +def test_part_b_premium_is_the_series_rounded_to_ten_cents(year): + series = _series(PART_B_SERIES, year) + premium = _at(MEDICARE.part_b.base_premium, year) + assert premium == pytest.approx(round(series / 0.1) * 0.1, abs=1e-9) + assert abs(premium - series) <= 0.05 + 1e-9 + + +# 4. Proportionality. + + +@given(PROJECTED_YEARS, st.sampled_from(FILING_STATUSES)) +def test_part_b_irmaa_keeps_its_statutory_multiple(year, status): + premium = _at(MEDICARE.part_b.base_premium, year) + for bracket, multiple in zip( + _brackets(MEDICARE.part_b, status)[1:], PART_B_IRMAA_MULTIPLES + ): + expected = multiple * premium + assert abs(_at(bracket.amount, year) - expected) <= 0.001 * expected + 0.1 + + +@given(PROJECTED_YEARS, st.sampled_from(ALL_STATUSES)) +def test_part_d_irmaa_follows_the_statute(year, status): + base_premium = _series(PART_D_SERIES, year) + base_percent = _at(MEDICARE.part_d.base_beneficiary_premium_percent, year) + for bracket, percentage in zip( + _brackets(MEDICARE.part_d, status)[1:], + MEDICARE_IRMAA_APPLICABLE_PERCENTAGES[status], + ): + assert _at(bracket.amount, year) == get_medicare_part_d_irmaa_amount( + base_premium, percentage, base_percent + ) + + +def test_part_d_base_percent_resets_in_2030(): + percent = MEDICARE.part_d.base_beneficiary_premium_percent + assert _at(percent, 2029) == 0.255 + assert _at(percent, 2030) == 0.2 + + +@given(st.integers(2024, END_YEAR + 20)) +def test_reduced_part_a_premium_is_55_percent_of_the_full_premium(year): + expected = 0.55 * _at(MEDICARE.part_a.full_premium, year) + reduced = _at(MEDICARE.part_a.reduced_premium, year) + assert abs(reduced - expected) <= 0.001 * expected + 1 + + +# 5. Monotonicity. + + +@given(st.integers(2023, END_YEAR - 1), st.integers(1, 80)) +def test_part_b_premium_rises_every_year_from_2023(year, gap): + later = min(year + gap, END_YEAR) + assert _at(MEDICARE.part_b.base_premium, year) < _at( + MEDICARE.part_b.base_premium, later + ) + + +@given(st.integers(2024, END_YEAR + 20), st.integers(1, 80)) +def test_part_a_amounts_never_fall_from_2024(year, gap): + for parameter in ( + MEDICARE.part_a.full_premium, + MEDICARE.part_a.reduced_premium, + MEDICARE.part_a.deductible, + ): + assert _at(parameter, year) <= _at(parameter, year + gap) + + +@given(st.integers(2024, END_YEAR + 20), st.integers(1, 80)) +def test_irmaa_thresholds_never_fall(year, gap): + for program in (MEDICARE.part_b, MEDICARE.part_d): + for status in ALL_STATUSES: + brackets = _brackets(program, status) + for bracket in brackets: + if ( + status == "separate" + and bracket is brackets[-1] + and year < MEDICARE_IRMAA_TOP_FIRST_INDEXED_YEAR + ): + continue # The intended exception; see the next test. + assert _at(bracket.threshold, year) <= _at( + bracket.threshold, year + gap + ) + + +@pytest.mark.parametrize("year", range(2025, MEDICARE_IRMAA_TOP_FIRST_INDEXED_YEAR)) +def test_separate_top_threshold_falls_until_2028(year): + """$500,000 less the unmarried threshold: $397,000 in 2024, $394,000 in + 2025 and $391,000 in 2026 as CMS published, then lower until 2028.""" + top = _brackets(MEDICARE.part_b, "separate")[-1].threshold + assert _at(top, year) < _at(top, year - 1) + single = _thresholds(MEDICARE.part_b, "single", year) + assert _at(top, year) == 500_000 - (single[1] - 1) + + +@given(st.integers(2024, END_YEAR + 20)) +def test_irmaa_tiers_rise_with_income_and_amount(year): + for program in (MEDICARE.part_b, MEDICARE.part_d): + for status in ALL_STATUSES: + brackets = _brackets(program, status) + thresholds = [_at(b.threshold, year) for b in brackets] + amounts = [_at(b.amount, year) for b in brackets] + assert thresholds == sorted(set(thresholds)) + assert amounts == sorted(set(amounts)) + + +# 6. Threshold structure. + + +@given(st.integers(2024, END_YEAR + 20)) +def test_thresholds_follow_the_filing_status_rules(year): + single = _thresholds(MEDICARE.part_b, "single", year) + lower, top = [t - 1 for t in single[1:5]], single[5] + assert _thresholds(MEDICARE.part_b, "head_of_household", year) == single + assert _thresholds(MEDICARE.part_b, "surviving_spouse", year) == single + assert _thresholds(MEDICARE.part_b, "joint", year) == [ + 0, + *(2 * amount + 1 for amount in lower), + 1.5 * top, + ] + assert _thresholds(MEDICARE.part_b, "separate", year) == [ + 0, + lower[0] + 1, + top - lower[0], + ] + for status in ALL_STATUSES: + assert _thresholds(MEDICARE.part_d, status, year) == _thresholds( + MEDICARE.part_b, status, year + ) + + +@given(st.integers(2024, END_YEAR + 20)) +def test_unmarried_amounts_are_whole_thousands(year): + """The first four thresholds sit $1 above a $1,000 multiple ("more + than" the amount); the top tier starts at a $1,000 multiple.""" + single = _thresholds(MEDICARE.part_b, "single", year) + assert all(_is_multiple(t - 1, 1_000) for t in single[1:5]) + assert _is_multiple(single[5], 1_000) + + +@pytest.mark.parametrize("year", range(2024, 2028)) +def test_top_tier_is_not_indexed_before_2028(year): + assert _thresholds(MEDICARE.part_b, "single", year)[5] == 500_000 + assert _thresholds(MEDICARE.part_b, "joint", year)[5] == 750_000 + + +def test_top_tier_is_indexed_from_2028(): + assert _thresholds(MEDICARE.part_b, "single", 2028)[5] > 500_000 + + +@pytest.mark.parametrize("year", range(2024, LAST_ANNOUNCED_YEAR + 1)) +def test_threshold_rules_reproduce_the_published_scales(year): + """Rebuilding each published year's scales from its unmarried amounts + reproduces every filing status's published thresholds.""" + lower = tuple(float(a) for a in CMS_UNMARRIED_AMOUNTS[year]) + rebuilt = get_medicare_irmaa_thresholds(lower, 500_000.0) + for status in ALL_STATUSES: + assert _thresholds(MEDICARE.part_b, status, year) == list(rebuilt[status]) + + +# 7. Indexing against CMS's published thresholds. + + +@pytest.mark.parametrize("year", sorted(CMS_UNMARRIED_AMOUNTS)) +def test_cpi_schedule_reproduces_published_thresholds(year): + lower, top = get_medicare_irmaa_unmarried_amounts(PARAMETERS, year) + published = CMS_UNMARRIED_AMOUNTS[year] + assert top == 500_000 + if year <= 2024: + assert lower == published + else: + # From January 2024 the model's CPI-U is seasonally adjusted, while + # CMS averages the published index; 2025's 12-month averages differ + # by about 0.05%, enough to move two amounts one rounding step. + assert all(abs(a - b) <= 1_000 for a, b in zip(lower, published)) + + +def test_cpi_schedule_starts_in_2020(): + with pytest.raises(ValueError): + get_medicare_irmaa_unmarried_amounts(PARAMETERS, 2019) + + +# 8. Precedence of published amounts. + + +def _medicare_tree_with_2027_values(): + """The IRMAA scales as loaded from YAML, with a 2027 value added to each + bracket, under a root that shares the system's CPI-U and Trustees + series.""" + data = {} + for program in ("part_b", "part_d"): + node = getattr(MEDICARE, program).irmaa + data[program] = {"irmaa": {}} + for status, scale in node.children.items(): + brackets = [] + for index, bracket in enumerate(scale.brackets): + entry = {} + for field in ("threshold", "amount"): + parameter = getattr(bracket, field) + values = { + v.instant_str: v.value + for v in parameter.values_list + if v.instant_str <= f"{LAST_ANNOUNCED_YEAR}-12-31" + } + # A stand-in for a CMS announcement: last year's value + # plus $1, which no projection produces. The first tier + # pays no adjustment in any year. + if field == "threshold" or index > 0: + last = values[f"{LAST_ANNOUNCED_YEAR}-01-01"] + values["2027-01-01"] = last + 1 + entry[field] = {"values": values} + brackets.append(entry) + data[program]["irmaa"][status] = { + "metadata": {"type": "single_amount"}, + "brackets": brackets, + } + data["part_d"]["base_beneficiary_premium_percent"] = { + "values": {"2006-01-01": 0.255, "2030-01-01": 0.2} + } + medicare = ParameterNode("gov.hhs.medicare", data=data) + root = SimpleNamespace( + gov=SimpleNamespace( + hhs=SimpleNamespace(medicare=medicare), bls=PARAMETERS.gov.bls + ), + calibration=PARAMETERS.calibration, + ) + return root, medicare + + +def test_published_values_take_precedence_and_later_years_follow_the_statute(): + root, medicare = _medicare_tree_with_2027_values() + extend_medicare_irmaa_thresholds(root, end_year=2030) + extend_medicare_part_d_irmaa_amounts(root, end_year=2030) + for program in ("part_b", "part_d"): + for status in ALL_STATUSES: + built = _brackets(getattr(medicare, program), status) + system_brackets = _brackets(getattr(MEDICARE, program), status) + for index, (bracket, system_bracket) in enumerate( + zip(built, system_brackets) + ): + assert _at(bracket.threshold, 2027) == ( + _at(bracket.threshold, LAST_ANNOUNCED_YEAR) + 1 + ) + if program == "part_d" and index > 0: + assert _at(bracket.amount, 2027) == ( + _at(bracket.amount, LAST_ANNOUNCED_YEAR) + 1 + ) + # 2028 on is computed from the statute, as in the system. + for year in (2028, 2030): + assert _at(bracket.threshold, year) == _at( + system_bracket.threshold, year + ) + if program == "part_d": + assert _at(bracket.amount, year) == _at( + system_bracket.amount, year + ) + + +# 9. Extension of the new series. + +NEW_SERIES = (PART_A_SERIES, DEDUCTIBLE_SERIES, PART_D_SERIES) + + +@given(st.sampled_from(NEW_SERIES), st.integers(LAST_TRUSTEES_YEAR + 1, END_YEAR)) +def test_new_series_extend_at_the_last_trustees_growth_rate(path, year): + last_growth = _series(path, LAST_TRUSTEES_YEAR) / _series( + path, LAST_TRUSTEES_YEAR - 1 + ) + growth = _series(path, year) / _series(path, year - 1) + assert growth == pytest.approx(last_growth, rel=1e-12) + + +@given(st.sampled_from(NEW_SERIES), st.integers(END_YEAR, END_YEAR + 100)) +def test_new_series_hold_their_2100_value(path, year): + assert _series(path, year) == _series(path, END_YEAR) diff --git a/policyengine_us/tests/core/test_per_capita_uprating.py b/policyengine_us/tests/core/test_per_capita_uprating.py index 99a30adf594..1a4ba4b7e0e 100644 --- a/policyengine_us/tests/core/test_per_capita_uprating.py +++ b/policyengine_us/tests/core/test_per_capita_uprating.py @@ -38,6 +38,8 @@ CBO_AGI = "calibration.gov.cbo.income_by_source.adjusted_gross_income" CMS_MOOP = "calibration.gov.hhs.cms.moop_per_capita" CMS_ESI = "calibration.gov.hhs.cms.esi_premium_per_enrollee" +CMS_DIRECT_PURCHASE = "calibration.gov.hhs.cms.direct_purchase_premium_per_enrollee" +CMS_PART_B = "calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee" CPI_U = "gov.bls.cpi.cpi_u" YEARS = (2025, 2030, 2035) BASE_YEAR = 2024 @@ -60,6 +62,8 @@ def _growth(path, year, parameters=None): ("calibration.gov.cbo.unemployment_compensation", True), (CMS_MOOP, False), (CMS_ESI, False), + (CMS_DIRECT_PURCHASE, False), + (CMS_PART_B, False), (CPI_U, False), (POPULATION_PATH, False), (per_capita_path(SOI_EMPLOYMENT), False), @@ -92,6 +96,8 @@ def test_no_variable_uprates_by_a_national_total(): ("miscellaneous_income", per_capita_path(CBO_AGI)), ("other_medical_expenses", CMS_MOOP), ("employer_sponsored_insurance_premiums", CMS_ESI), + ("health_insurance_premiums", CMS_DIRECT_PURCHASE), + ("medicare_part_b_premiums_reported", CMS_PART_B), ("child_support_expense", CPI_U), ("household_weight", POPULATION_PATH), ], @@ -404,7 +410,7 @@ def test_unclassified_calibration_path_is_rejected(): add_per_capita_uprating(_StubSystem("calibration.gov.aca.spending.state.AL")) -@pytest.mark.parametrize("path", [CMS_MOOP, CMS_ESI]) +@pytest.mark.parametrize("path", [CMS_MOOP, CMS_ESI, CMS_DIRECT_PURCHASE, CMS_PART_B]) def test_per_person_rates_are_classified(path): """CMS per-capita and per-enrollee series are per-person rates: neither national totals to divide nor unclassified paths to reject.""" diff --git a/policyengine_us/tests/core/test_premium_input_uprating.py b/policyengine_us/tests/core/test_premium_input_uprating.py new file mode 100644 index 00000000000..3420a5f6d9a --- /dev/null +++ b/policyengine_us/tests/core/test_premium_input_uprating.py @@ -0,0 +1,371 @@ +"""Premium inputs follow premium series, not CMS out-of-pocket spending. + +CMS defines out-of-pocket spending to exclude premiums (NHEA methodology +paper, 2024, file page 14), so no premium input uprates by +``moop_per_capita``. Each follows the series closest to what the data put in +it: + +- ``health_insurance_premiums_without_medicare_part_b`` carries the CPS ASEC + premium the person reported (``PHIP_VAL``), and + ``other_health_insurance_premiums`` is that premium less modeled CHIP, + Medicaid and Marketplace premiums. People with employer-sponsored coverage + pay most of these dollars, and Microcosm's pre-tax producer moves employees' + payroll-deducted premiums from these inputs to + ``pre_tax_health_insurance_premiums``. All three follow CMS's + employer-sponsored insurance (ESI) premium per enrollee, so that move leaves + the dollars' growth unchanged. +- ``health_insurance_premiums`` holds premiums of self-employed people under 65 + without employer-sponsored coverage, who buy plans directly. It follows + CMS's direct purchase premium per enrollee. +- ``long_term_health_insurance_premiums``: no dataset fills it and no CMS + series measures long-term care insurance premiums. 26 U.S.C. 213(d)(10)(B) + indexes the deductible limits on them by the medical care component of the + CPI, so it follows CPI-U medical care. +- ``medicare_part_b_premiums_reported`` follows the Part B standard premium: + a premium with any income-related adjustment is a fixed multiple of it + (42 U.S.C. 1395r(i)(3)). + +These tests read only the parameter tree. Engine uprating of the inputs is +covered in ``tests/microsimulation/test_premium_input_microsim_uprating.py``. + +Invariants: + +1. Transcription: each series equals its published source figure for figure, + and is consistent with the source's own components within rounding: CMS + direct purchase spending over enrollment; CBO's medical care CPI levels + against its percentage changes and against BLS; the Part B premium against + half the aged actuarial rate plus the repayment add-on. +2. Employee share: ESI spending less employer contributions, per ESI + enrollee, grows with the ESI index, so one index serves employees' + premiums whether paid pre-tax or post-tax. +3. Move neutrality: every input that holds employees' shares of ESI premiums + names the same series. +4. Agreement: the Part B series equals the model's Part B premium parameter + in every year that parameter holds. +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. +7. No premium input follows out-of-pocket spending. +""" + +from decimal import ROUND_HALF_UP, Decimal + +import pytest +from hypothesis import given +from hypothesis import strategies as st +from policyengine_core.parameters.operations.get_parameter import get_parameter + +from policyengine_us.system import system + +MOOP = "calibration.gov.hhs.cms.moop_per_capita" +ESI = "calibration.gov.hhs.cms.esi_premium_per_enrollee" +DIRECT_PURCHASE = "calibration.gov.hhs.cms.direct_purchase_premium_per_enrollee" +MEDICAL_CPI = "gov.bls.cpi.cpi_u_medical_care" +PART_B = "calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee" +SERIES = { + "employer_sponsored_insurance_premiums": ESI, + "pre_tax_health_insurance_premiums": ESI, + "health_insurance_premiums_without_medicare_part_b": ESI, + "other_health_insurance_premiums": ESI, + "health_insurance_premiums": DIRECT_PURCHASE, + "long_term_health_insurance_premiums": MEDICAL_CPI, + "medicare_part_b_premiums_reported": PART_B, +} +EMPLOYEE_ESI_INPUTS = ( + "pre_tax_health_insurance_premiums", + "health_insurance_premiums_without_medicare_part_b", + "other_health_insurance_premiums", +) +END_YEAR = 2100 +# First year, last published year and the year from which each index rises +# every year. +SPANS = { + DIRECT_PURCHASE: (2013, 2034, 2020), + MEDICAL_CPI: (2015, 2036, 2015), + PART_B: (2013, 2035, 2023), +} + + +def _index(path, year): + return get_parameter(system.parameters, path)(f"{year}-01-01") + + +# Wiring. + + +@pytest.mark.parametrize("variable, series", sorted(SERIES.items())) +def test_premium_input_follows_its_premium_series(variable, series): + assert system.variables[variable].uprating == series + + +def test_no_premium_input_follows_out_of_pocket_spending(): + """CMS out-of-pocket spending excludes premium payments, so a premium + input on it would follow a series that leaves out what it measures.""" + on_moop = sorted( + name + for name, variable in system.variables.items() + if variable.uprating == MOOP and "premium" in name + ) + assert on_moop == [] + + +def test_inputs_holding_employee_esi_shares_follow_one_series(): + """Microcosm's pre-tax producer moves an employee's payroll-deducted + premium out of the reported premium inputs into the pre-tax input. One + series for all of them keeps that move from changing the dollars' growth.""" + assert {system.variables[name].uprating for name in EMPLOYEE_ESI_INPUTS} == {ESI} + + +# Employer-sponsored insurance: CMS NHE projections tables (2025-2034 release, +# https://www.cms.gov/files/zip/nhe-projections-tables.zip, Last-Modified +# 2026-09-03, sha256 264f8226...cd70), Table 17 ESI spending and enrollment, +# and Table 16 employer contributions to private health insurance premiums by +# private business, the federal government and state and local governments. +ESI_SPENDING_BILLIONS = {2024: 1_429.1, 2026: 1_650.5, 2034: 2_343.1} +ESI_ENROLLMENT_MILLIONS = {2024: 178.6, 2026: 179.8, 2034: 178.5} +EMPLOYER_CONTRIBUTIONS_BILLIONS = { + 2024: 753 + 47 + 247, + 2026: 865 + 56 + 288, + 2034: 1_207 + 82 + 429, +} + + +@pytest.mark.parametrize("start, end", [(2024, 2034), (2026, 2034)]) +def test_employee_contributions_per_enrollee_grow_with_the_esi_index(start, end): + """ESI spending is premiums paid by employers and employees (NHEA + methodology paper, file page 29), so spending less employer contributions + is what employees, retirees and COBRA enrollees pay.""" + + def employee_share(year): + return 1 - EMPLOYER_CONTRIBUTIONS_BILLIONS[year] / ESI_SPENDING_BILLIONS[year] + + def per_enrollee(year): + employee = ESI_SPENDING_BILLIONS[year] - EMPLOYER_CONTRIBUTIONS_BILLIONS[year] + return employee / ESI_ENROLLMENT_MILLIONS[year] + + years = end - start + employee_growth = (per_enrollee(end) / per_enrollee(start)) ** (1 / years) + index_growth = (_index(ESI, end) / _index(ESI, start)) ** (1 / years) + assert employee_growth == pytest.approx(index_growth, abs=0.001) + # Employees pay 26.6% to 26.8% of ESI premiums in each year. + for year in (start, end): + assert 0.266 <= employee_share(year) <= 0.268 + + +# Direct purchase: the same tables, Table 17 "Direct Purchase" rows. +DIRECT_PURCHASE_YEARS = range(2013, 2035) +DIRECT_PURCHASE_SPENDING_PER_ENROLLEE = dict( + zip( + DIRECT_PURCHASE_YEARS, + ( + *(2_263, 3_134, 3_454, 3_695, 4_175, 4_831, 4_812, 4_791, 4_979), + *(5_149, 5_458, 5_871, 5_994, 6_969, 7_174, 7_456, 7_752, 8_115), + *(8_364, 8_743, 9_158, 9_589), + ), + ) +) +DIRECT_PURCHASE_SPENDING_BILLIONS = dict( + zip( + DIRECT_PURCHASE_YEARS, + ( + *(51.1, 81.3, 101.4, 110.2, 115.7, 129.7, 129.0, 131.5, 142.2), + *(154.7, 177.6, 215.5, 229.3, 240.6, 246.2, 246.8, 256.7, 270.3), + *(288.6, 303.5, 319.0, 335.3), + ), + ) +) +DIRECT_PURCHASE_ENROLLMENT_MILLIONS = dict( + zip( + DIRECT_PURCHASE_YEARS, + ( + *(22.6, 26.0, 29.4, 29.8, 27.7, 26.8, 26.8, 27.5, 28.6, 30.0), + *(32.5, 36.7, 38.3, 34.5, 34.3, 33.1, 33.1, 33.3, 34.5, 34.7), + *(34.8, 35.0), + ), + ) +) + + +@pytest.mark.parametrize("year", DIRECT_PURCHASE_YEARS) +def test_direct_purchase_index_is_table_17_spending_per_enrollee(year): + assert _index(DIRECT_PURCHASE, year) == DIRECT_PURCHASE_SPENDING_PER_ENROLLEE[year] + + +@pytest.mark.parametrize("year", DIRECT_PURCHASE_YEARS) +def test_direct_purchase_values_match_spending_over_enrollment(year): + """Checks the transcribed rows against each other, so one mistyped + spending, enrollment or per-enrollee figure fails.""" + spending = DIRECT_PURCHASE_SPENDING_BILLIONS[year] + enrollment = DIRECT_PURCHASE_ENROLLMENT_MILLIONS[year] + # Both components are rounded to one decimal and the published value to + # the dollar. + low = 1_000 * (spending - 0.05) / (enrollment + 0.05) - 0.5 + high = 1_000 * (spending + 0.05) / (enrollment - 0.05) + 0.5 + assert low <= DIRECT_PURCHASE_SPENDING_PER_ENROLLEE[year] <= high + + +def test_direct_purchase_index_dips_where_cms_reports_it(): + """CMS's direct purchase premium per enrollee falls in 2019 and 2020; + the parameter keeps CMS's figures rather than smoothing them.""" + assert _index(DIRECT_PURCHASE, 2019) < _index(DIRECT_PURCHASE, 2018) + assert _index(DIRECT_PURCHASE, 2020) < _index(DIRECT_PURCHASE, 2019) + + +# CPI-U medical care: BLS annual means to 2022 and CBO's February 2026 +# Economic Projections, sheet "2. Calendar Year", row 59 (level, 1982-84=100) +# and row 60 (percentage change, annual rate) from 2023. +CBO_MEDICAL_CPI = dict( + zip( + range(2023, 2037), + ( + *(549.081, 563.841, 581.279, 600.332, 617.675, 636.697, 656.371), + *(676.782, 697.848, 719.546, 741.904, 764.927, 788.642, 813.07), + ), + ) +) +CBO_MEDICAL_CPI_PERCENT_CHANGE = dict( + zip( + range(2023, 2037), + ( + *(0.463, 2.688, 3.092, 3.277, 2.888, 3.079, 3.09, 3.109, 3.112), + *(3.109, 3.107, 3.103, 3.1, 3.097), + ), + ) +) +# BLS series CUUR0000SAM, mean of the 12 published monthly indexes. +BLS_MEDICAL_CPI_ANNUAL_MEAN = {2022: 546.553, 2023: 549.084, 2024: 563.841} + + +@pytest.mark.parametrize("year", sorted(CBO_MEDICAL_CPI)) +def test_medical_cpi_is_cbo_calendar_year_series(year): + assert _index(MEDICAL_CPI, year) == CBO_MEDICAL_CPI[year] + + +@pytest.mark.parametrize("year", sorted(CBO_MEDICAL_CPI_PERCENT_CHANGE)) +def test_medical_cpi_levels_match_cbo_percent_changes(year): + """CBO publishes levels and percentage changes to three decimals, so each + change agrees with the levels within rounding. The 2023 change also + checks the BLS 2022 value that precedes CBO's series.""" + change = 100 * (_index(MEDICAL_CPI, year) / _index(MEDICAL_CPI, year - 1) - 1) + assert change == pytest.approx(CBO_MEDICAL_CPI_PERCENT_CHANGE[year], abs=0.001) + + +@pytest.mark.parametrize("year", [2022, 2023, 2024]) +def test_medical_cpi_matches_bls_where_bls_has_a_full_year(year): + assert _index(MEDICAL_CPI, year) == pytest.approx( + BLS_MEDICAL_CPI_ANNUAL_MEAN[year], abs=0.005 + ) + + +# Part B standard monthly premium: 2026 Medicare Trustees Report, Table III.C2 +# (file pages 89-90, 2013-2026, with the aged monthly actuarial rate) and +# Table V.E2 (file page 213, intermediate estimates for 2027-2035). +PART_B_STANDARD_PREMIUM = dict( + zip( + range(2013, 2036), + ( + *(104.9, 104.9, 104.9, 121.8, 134.0, 134.0, 135.5, 144.6, 148.5), + *(170.1, 164.9, 174.7, 185.0, 202.9, 209.5, 224.5, 238.5, 255.5), + *(272.1, 290.2, 313.6, 338.5, 360.6), + ), + ) +) +PART_B_AGED_ACTUARIAL_RATE = dict( + zip( + range(2013, 2027), + ( + *("209.80", "209.80", "209.80", "237.60", "261.90", "261.90"), + *("264.90", "283.20", "291.00", "334.20", "323.70", "343.40"), + *("368.10", "405.40"), + ), + ) +) +# 42 U.S.C. 1395r(a)(6): the add-on that repays the 2016 premium relief, $3.00 +# through 2024, $0.90 in 2025 and $0.20 in 2026 (2026 Trustees Report, +# footnote on file page 88). +PART_B_REPAYMENT_ADD_ON = { + **{year: "0" for year in range(2013, 2016)}, + **{year: "3.00" for year in range(2016, 2025)}, + 2025: "0.90", + 2026: "0.20", +} + + +@pytest.mark.parametrize("year", sorted(PART_B_STANDARD_PREMIUM)) +def test_part_b_index_is_the_trustees_standard_premium(year): + assert _index(PART_B, year) == PART_B_STANDARD_PREMIUM[year] + + +@pytest.mark.parametrize("year", sorted(PART_B_AGED_ACTUARIAL_RATE)) +def test_part_b_premium_is_half_the_aged_actuarial_rate_plus_the_add_on(year): + """42 U.S.C. 1395r(a)(3) sets the premium at 50% of the aged actuarial + rate, (a)(6) adds the repayment amount, and (c) rounds to the nearest + multiple of 10 cents.""" + premium = ( + Decimal(PART_B_AGED_ACTUARIAL_RATE[year]) / 2 + + Decimal(PART_B_REPAYMENT_ADD_ON[year]) + ).quantize(Decimal("0.1"), rounding=ROUND_HALF_UP) + assert float(premium) == _index(PART_B, year) + + +@pytest.mark.parametrize("year", range(2021, 2027)) +def test_part_b_index_agrees_with_the_model_part_b_premium(year): + """``gov.hhs.medicare.part_b.base_premium`` computes modeled Part B + premiums from the same announced amounts.""" + base_premium = get_parameter( + system.parameters, "gov.hhs.medicare.part_b.base_premium" + ) + assert _index(PART_B, year) == base_premium(f"{year}-01-01") + + +# Every index. + + +@given( + st.sampled_from((ESI, DIRECT_PURCHASE, MEDICAL_CPI, PART_B)), + st.integers(2015, END_YEAR + 50), +) +def test_each_index_is_positive(path, year): + assert _index(path, year) > 0 + + +@given(st.sampled_from(sorted(SPANS)), st.integers(0, END_YEAR), st.integers(1, 99)) +def test_each_index_strictly_increases_after_its_last_dip(path, offset, gap): + _, _, rises_from = SPANS[path] + year = rises_from + offset % (END_YEAR - rises_from) + later = min(year + gap, END_YEAR) + assert _index(path, year) < _index(path, later) + + +@given(st.sampled_from(sorted(SPANS)), 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) + last_growth = _index(path, last) / _index(path, last - 1) + growth = _index(path, year) / _index(path, year - 1) + assert growth == pytest.approx(last_growth, rel=1e-12) + + +@pytest.mark.parametrize( + "path, last_growth", + [ + (DIRECT_PURCHASE, 9_589 / 9_158), + (MEDICAL_CPI, 813.07 / 788.642), + (PART_B, 360.6 / 338.5), + ], +) +def test_last_published_growth_rates(path, last_growth): + _, last, _ = SPANS[path] + assert _index(path, last) / _index(path, last - 1) == pytest.approx( + last_growth, rel=1e-12 + ) + + +@given( + st.sampled_from((ESI, DIRECT_PURCHASE, MEDICAL_CPI, PART_B)), + st.integers(END_YEAR, END_YEAR + 100), +) +def test_each_index_holds_its_2100_value_after_2100(path, year): + assert _index(path, year) == _index(path, END_YEAR) diff --git a/policyengine_us/tests/core/test_uprating_series.py b/policyengine_us/tests/core/test_uprating_series.py index fb7d4e7aec6..ee243664cbe 100644 --- a/policyengine_us/tests/core/test_uprating_series.py +++ b/policyengine_us/tests/core/test_uprating_series.py @@ -1,7 +1,8 @@ """Each dollar input follows the series closest to what it measures: its own national series, the income source it is part of, prices for consumption -expenses and debts, CMS per-capita spending for health costs and CMS premiums -per enrollee for employer plan premiums.""" +expenses and debts, CMS per-capita out-of-pocket spending for health costs and +premiums per enrollee, or the prices that index them, for health insurance +premiums.""" from pathlib import Path @@ -20,7 +21,10 @@ DEFAULT = per_capita_path("calibration.gov.cbo.income_by_source.adjusted_gross_income") CMS = "calibration.gov.hhs.cms.moop_per_capita" ESI = "calibration.gov.hhs.cms.esi_premium_per_enrollee" +DIRECT_PURCHASE = "calibration.gov.hhs.cms.direct_purchase_premium_per_enrollee" CPI_U = "gov.bls.cpi.cpi_u" +MEDICAL_CPI = "gov.bls.cpi.cpi_u_medical_care" +PART_B = "calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee" COLA = "gov.ssa.uprating" EXPECTED = { @@ -60,12 +64,23 @@ "alimony_income": ALIMONY, "alimony_expense": ALIMONY, "unemployment_compensation": UNEMPLOYMENT, - # Health costs. - "health_insurance_premiums": CMS, - # Employer plan premiums: the employer's share and the employee's pre-tax - # share. + # Health costs paid out of pocket. + "other_medical_expenses": CMS, + "over_the_counter_health_expenses": CMS, + # Premiums for employer plans: the employer's share, the employee's + # pre-tax share, and the premiums people report, most of them paid by + # people with employer coverage. "employer_sponsored_insurance_premiums": ESI, "pre_tax_health_insurance_premiums": ESI, + "health_insurance_premiums_without_medicare_part_b": ESI, + "other_health_insurance_premiums": ESI, + # Premiums for plans bought directly. + "health_insurance_premiums": DIRECT_PURCHASE, + # Long-term care insurance premiums, whose deductible limits follow + # medical care prices. + "long_term_health_insurance_premiums": MEDICAL_CPI, + # Medicare Part B premiums. + "medicare_part_b_premiums_reported": PART_B, # Benefits adjusted by the Social Security cost-of-living index. "veterans_benefits": COLA, # Consumption expenses, and the interest on debts, follow prices. The diff --git a/policyengine_us/tests/microsimulation/test_premium_input_microsim_uprating.py b/policyengine_us/tests/microsimulation/test_premium_input_microsim_uprating.py new file mode 100644 index 00000000000..4ccd833ab36 --- /dev/null +++ b/policyengine_us/tests/microsimulation/test_premium_input_microsim_uprating.py @@ -0,0 +1,184 @@ +"""The engine ages premium inputs by their premium series, in a simulation and +in dataset extension. + +``tests/core/test_premium_input_uprating.py`` checks the series and that each +input names its series. This file checks what a parameter read cannot: that +holder uprating in a ``Simulation`` and ``extend_single_year_dataset`` resolve +the per-enrollee paths and the price index undivided by population and apply +them to these inputs. + +Invariants: + +1. Uprating: a premium entered for 2024 equals that amount times its index + ratio in any later year, so uprating is linear in the amount (Hypothesis, + one vectorized simulation per example). +2. Move neutrality: splitting an employee's premium in any proportion between + the pre-tax input and the reported premium inputs leaves the uprated total + unchanged in every later year (Hypothesis). This is what Microcosm's + pre-tax producer does when it routes a reported premium through payroll. +3. Dataset extension: each person's amount follows the index ratio, and the + weighted total grows by the index ratio times population growth, because + weights follow population. +""" + +from functools import lru_cache + +import pandas as pd +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st +from policyengine_core.parameters.operations.get_parameter import get_parameter + +from policyengine_us import Simulation +from policyengine_us.data.dataset_schema import USSingleYearDataset +from policyengine_us.data.economic_assumptions import extend_single_year_dataset +from policyengine_us.system import system +from policyengine_us.tools.per_capita_uprating import POPULATION_PATH + +ESI = "calibration.gov.hhs.cms.esi_premium_per_enrollee" +DIRECT_PURCHASE = "calibration.gov.hhs.cms.direct_purchase_premium_per_enrollee" +MEDICAL_CPI = "gov.bls.cpi.cpi_u_medical_care" +PART_B = "calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee" +PRE_TAX = "pre_tax_health_insurance_premiums" +REPORTED = "health_insurance_premiums_without_medicare_part_b" +OTHER = "other_health_insurance_premiums" +DIRECT = "health_insurance_premiums" +LONG_TERM_CARE = "long_term_health_insurance_premiums" +PART_B_REPORTED = "medicare_part_b_premiums_reported" +SERIES = { + PRE_TAX: ESI, + REPORTED: ESI, + OTHER: ESI, + DIRECT: DIRECT_PURCHASE, + LONG_TERM_CARE: MEDICAL_CPI, + PART_B_REPORTED: PART_B, +} +BASE_YEAR, END_YEAR = 2024, 2100 + +amounts = st.integers(0, 3_000_000).map(lambda cents: cents / 100) +later_years = st.lists(st.integers(BASE_YEAR + 1, END_YEAR), min_size=2, max_size=4) +SETTINGS = settings( + max_examples=3, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow], +) + + +def _ratio(path, year): + parameter = get_parameter(system.parameters, path) + return parameter(f"{year}-01-01") / parameter(f"{BASE_YEAR}-01-01") + + +def _simulation(people): + return Simulation( + situation={ + "people": people, + "households": {"household": {"members": list(people)}}, + } + ) + + +# Each example builds one simulation holding a batch of people. +@SETTINGS +@given( + st.lists(st.tuples(*(amounts,) * len(SERIES)), min_size=2, max_size=6), + later_years, +) +def test_simulation_uprates_premiums_by_their_index_ratio(rows, years): + people = { + f"person_{i}": { + "age": {BASE_YEAR: 40}, + **{variable: {BASE_YEAR: amount} for variable, amount in zip(SERIES, row)}, + } + for i, row in enumerate(rows) + } + simulation = _simulation(people) + for year in years: + for column, (variable, series) in enumerate(SERIES.items()): + values = simulation.calculate(variable, year) + for value, row in zip(values, rows): + assert value == pytest.approx( + row[column] * _ratio(series, year), rel=1e-6, abs=0.01 + ) + + +@SETTINGS +@given( + st.lists( + st.tuples(amounts, st.integers(0, 100), st.sampled_from((REPORTED, OTHER))), + min_size=2, + max_size=6, + ), + later_years, +) +def test_moving_a_premium_to_pre_tax_keeps_its_growth(rows, years): + """Each person's premium is split: ``percent`` of it pre-tax, the rest in + a reported premium input. Their uprated sum equals the whole premium + uprated, whatever the split.""" + people = {} + for i, (amount, percent, reported) in enumerate(rows): + pre_tax = amount * percent / 100 + people[f"person_{i}"] = { + "age": {BASE_YEAR: 40}, + PRE_TAX: {BASE_YEAR: pre_tax}, + reported: {BASE_YEAR: amount - pre_tax}, + } + simulation = _simulation(people) + for year in years: + totals = ( + simulation.calculate(PRE_TAX, year) + + simulation.calculate(REPORTED, year) + + simulation.calculate(OTHER, year) + ) + for total, (amount, _, _) in zip(totals, rows): + assert total == pytest.approx( + amount * _ratio(ESI, year), rel=1e-6, abs=0.01 + ) + + +@lru_cache(maxsize=1) +def _extended(): + person = pd.DataFrame( + { + "person_id": [1, 2, 3, 4], + "person_household_id": [1, 1, 2, 3], + PRE_TAX: [3_100.0, 0.0, 1_200.0, 0.0], + REPORTED: [1_400.0, 0.0, 6_300.0, 2_250.0], + OTHER: [1_400.0, 0.0, 5_100.0, 2_250.0], + DIRECT: [0.0, 0.0, 6_300.0, 0.0], + LONG_TERM_CARE: [0.0, 0.0, 0.0, 3_100.0], + PART_B_REPORTED: [0.0, 0.0, 0.0, 2_096.4], + "age": [40.0, 8.0, 55.0, 70.0], + } + ) + household = pd.DataFrame( + {"household_id": [1, 2, 3], "household_weight": [1_000.0, 2_500.0, 400.0]} + ) + dataset = USSingleYearDataset( + person=person, + household=household, + tax_unit=pd.DataFrame({"tax_unit_id": [1, 2, 3]}), + spm_unit=pd.DataFrame({"spm_unit_id": [1, 2, 3]}), + family=pd.DataFrame({"family_id": [1, 2, 3]}), + marital_unit=pd.DataFrame({"marital_unit_id": [1, 2, 3, 4]}), + time_period=BASE_YEAR, + ) + return extend_single_year_dataset(dataset, end_year=2036, system=system) + + +def _weighted_total(dataset, column): + weights = dataset.household.set_index("household_id")["household_weight"] + person_weights = dataset.person["person_household_id"].map(weights) + return float((dataset.person[column] * person_weights).sum()) + + +@pytest.mark.parametrize("year", [2025, 2026, 2034, 2036]) +@pytest.mark.parametrize("variable, series", sorted(SERIES.items())) +def test_dataset_extension_uprates_by_the_index(variable, series, year): + base, later = _extended().datasets[BASE_YEAR], _extended().datasets[year] + for before, after in zip(base.person[variable], later.person[variable]): + assert after == pytest.approx(before * _ratio(series, year), rel=1e-9) + assert _weighted_total(later, variable) / _weighted_total( + base, variable + ) == pytest.approx(_ratio(series, year) * _ratio(POPULATION_PATH, year), rel=1e-9) diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/base_part_a_premium.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/base_part_a_premium.yaml index 27e9423d9c3..6996012aa17 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/base_part_a_premium.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/base_part_a_premium.yaml @@ -69,3 +69,22 @@ medicare_quarters_of_coverage: 20 output: base_part_a_premium: 6_780 # $565 * 12 months + +# Projected years follow the Trustees' Table V.E1. +- name: 2027 projected reduced Part A premium. + period: 2027 + input: + age: 65 + is_medicare_eligible: true + medicare_quarters_of_coverage: 35 + output: + base_part_a_premium: 3_960 # $330 * 12 months + +- name: 2027 projected full Part A premium. + period: 2027 + input: + age: 65 + is_medicare_eligible: true + medicare_quarters_of_coverage: 20 + output: + base_part_a_premium: 7_200 # $600 * 12 months diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/base_part_b_premium.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/base_part_b_premium.yaml index 0711fd86c9f..a41f3dcd435 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/base_part_b_premium.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/base_part_b_premium.yaml @@ -37,3 +37,21 @@ is_medicare_eligible: true output: base_part_b_premium: 2_434.80 # $202.90 * 12 months + +# After CMS's last announced year (2026) the premium follows the 2026 Medicare +# Trustees Report's projected standard premium (Table V.E2). +- name: 2027 projected Part B premium. + period: 2027 + input: + age: 65 + is_medicare_eligible: true + output: + base_part_b_premium: 2_514 # $209.50 * 12 months + +- name: 2034 projected Part B premium. + period: 2034 + input: + age: 65 + is_medicare_eligible: true + output: + base_part_b_premium: 4_062 # $338.50 * 12 months diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/income_adjusted_part_d_premium_surcharge.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/income_adjusted_part_d_premium_surcharge.yaml index 66ce09857a6..e5bd78e1b1b 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/income_adjusted_part_d_premium_surcharge.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/income_adjusted_part_d_premium_surcharge.yaml @@ -129,3 +129,33 @@ medicare_irmaa_magi_two_years_prior: 500_000 output: income_adjusted_part_d_premium_surcharge: 1_029.6 + +# Projected years: 42 U.S.C. 1395w-113(a)(7)(B) applied to the Trustees' +# projected base beneficiary premium (Table V.E2). The base beneficiary +# premium percent falls from 25.5 to 20 percent in 2030, so the 35 percent +# tier's adjustment jumps from $46.44 * 9.5 / 25.5 = $17.30 to $68.93 * 15 / +# 20 = $51.70, as in the Trustees' Table V.E4. Joint 35 percent tier: $238,001 +# to $300,000 in 2029 and $242,001 to $306,000 in 2030. +- name: 2029 joint filers in the 35 percent tier. + period: 2029 + input: + age: 65 + filing_status: JOINT + adjusted_gross_income: + 2027: 270_000 + tax_exempt_interest_income: + 2027: 0 + output: + income_adjusted_part_d_premium_surcharge: 207.6 # $17.30 * 12 months + +- name: 2030 joint filers in the 35 percent tier, after the 20 percent reset. + period: 2030 + input: + age: 65 + filing_status: JOINT + adjusted_gross_income: + 2028: 270_000 + tax_exempt_interest_income: + 2028: 0 + output: + income_adjusted_part_d_premium_surcharge: 620.4 # $51.70 * 12 months diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_part_b_premium.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_part_b_premium.yaml index 5ad0e7b72d0..3309a1da570 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_part_b_premium.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_part_b_premium.yaml @@ -193,3 +193,64 @@ msp_asset_eligible: false output: medicare_part_b_premium: 8_278.8 + +# Projected years. The standard premium follows the Trustees' projection +# (Table V.E2), each income-related adjustment amount grows with it from +# CMS's 2026 amount, and the thresholds follow 42 U.S.C. 1395r(i)(5)'s CPI-U +# schedule: $112,000 / $141,000 / $176,000 / $211,000 / $500,000 unmarried in +# 2027, and twice that (but $888,000 at the top) jointly in 2034. +- name: 2027 single filer in the 50 percent tier. + period: 2027 + input: + age: 65 + filing_status: SINGLE + adjusted_gross_income: + 2025: 160_000 + tax_exempt_interest_income: + 2025: 0 + is_medicare_eligible: true + medicare_enrolled: true + msp_income_eligible: false + msp_asset_eligible: false + output: + # ($209.50 standard + $209.50 adjustment) * 12 months + medicare_part_b_premium: 5_028 + +- name: 2027 separate filer above the falling top threshold. + period: 2027 + input: + age: 65 + filing_status: SEPARATE + adjusted_gross_income: + 2025: 390_000 + tax_exempt_interest_income: + 2025: 0 + is_medicare_eligible: true + medicare_enrolled: true + msp_income_eligible: false + msp_asset_eligible: false + output: + # The top tier starts at $500,000 less the $112,000 unmarried threshold, + # $388,000 (CMS's 2026 figure was $391,000). The 85 percent tier's + # adjustment is $487.00 * 209.50 / 202.90 = $502.84, rounded to $502.80. + # ($209.50 + $502.80) * 12 months + medicare_part_b_premium: 8_547.6 + +- name: 2034 joint filers in the 65 percent tier. + period: 2034 + input: + age: 65 + filing_status: JOINT + adjusted_gross_income: + 2032: 450_000 + tax_exempt_interest_income: + 2032: 0 + is_medicare_eligible: true + medicare_enrolled: true + msp_income_eligible: false + msp_asset_eligible: false + output: + # Joint 65 percent tier: $418,001 to $500,000. Adjustment $324.60 * + # 338.50 / 202.90 = $541.53, rounded to $541.50. + # ($338.50 + $541.50) * 12 months + medicare_part_b_premium: 10_560 diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/msp_part_b_premium_coverage.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/msp_part_b_premium_coverage.yaml index 0c450945ee9..ef46dabc333 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/msp_part_b_premium_coverage.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/msp_part_b_premium_coverage.yaml @@ -27,3 +27,13 @@ base_part_b_premium: 2_220 output: msp_part_b_premium_coverage: 0 + +- name: MSP part B coverage pays the projected 2027 standard premium + period: 2027 + input: + is_medicare_eligible: true + medicare_enrolled: true + msp_income_eligible: true + msp_asset_eligible: true + output: + msp_part_b_premium_coverage: 2_514 # $209.50 * 12 months diff --git a/policyengine_us/tests/policy/baseline/parameters/test_uprating_extensions.py b/policyengine_us/tests/policy/baseline/parameters/test_uprating_extensions.py index e50fffb6cf2..961a542a5ee 100644 --- a/policyengine_us/tests/policy/baseline/parameters/test_uprating_extensions.py +++ b/policyengine_us/tests/policy/baseline/parameters/test_uprating_extensions.py @@ -151,6 +151,45 @@ def test_cms_esi_premium_per_enrollee_holds_last_projected_growth_rate(): assert parameter("2150-01-01") == parameter("2100-01-01") +def test_cms_direct_purchase_premium_per_enrollee_holds_last_projected_growth_rate(): + """Directly bought premium inputs keep growing after CMS's 2034 + projection, at the 2033-2034 growth rate.""" + parameter = PARAMETERS.calibration.gov.hhs.cms.direct_purchase_premium_per_enrollee + last_growth = parameter("2034-01-01") / parameter("2033-01-01") + + 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 parameter("2150-01-01") == parameter("2100-01-01") + + +def test_medicare_part_b_premium_holds_last_projected_growth_rate(): + """Reported Part B premium inputs keep growing after the Trustees' 2035 + projection, at the 2034-2035 growth rate.""" + parameter = PARAMETERS.calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + last_growth = parameter("2035-01-01") / parameter("2034-01-01") + + 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 parameter("2150-01-01") == parameter("2100-01-01") + + +def test_cpi_u_medical_care_holds_last_projected_growth_rate(): + """Long-term care premium inputs keep growing after CBO's 2036 + projection, at the 2035-2036 growth rate.""" + parameter = PARAMETERS.gov.bls.cpi.cpi_u_medical_care + last_growth = parameter("2036-01-01") / parameter("2035-01-01") + + assert parameter("2036-01-01") == 813.07 + for year in [2037, 2038, 2050, 2100]: + growth = parameter(f"{year}-01-01") / parameter(f"{year - 1}-01-01") + assert growth == pytest.approx(last_growth, rel=1e-12) + assert parameter("2150-01-01") == parameter("2100-01-01") + + def test_soi_income_upraters_extend_without_trustees_reform(): """SOI income upraters should inherit baseline long-run CBO extensions.""" soi = PARAMETERS.calibration.gov.irs.soi diff --git a/policyengine_us/variables/household/expense/health/health_insurance_premiums.py b/policyengine_us/variables/household/expense/health/health_insurance_premiums.py index 936031f7fc9..93107c1e06c 100644 --- a/policyengine_us/variables/household/expense/health/health_insurance_premiums.py +++ b/policyengine_us/variables/household/expense/health/health_insurance_premiums.py @@ -7,7 +7,9 @@ class health_insurance_premiums(Variable): label = "Health insurance premiums" unit = USD definition_period = YEAR - uprating = "calibration.gov.hhs.cms.moop_per_capita" + # Survey data put premiums here only for self-employed people under 65 + # without employer-sponsored coverage, who buy plans directly. + uprating = "calibration.gov.hhs.cms.direct_purchase_premium_per_enrollee" documentation = ( "Person-level health insurance premiums supplied directly as an input. " "SPM MOOP and statutory medical expense definitions use decomposed " diff --git a/policyengine_us/variables/household/expense/health/health_insurance_premiums_without_medicare_part_b.py b/policyengine_us/variables/household/expense/health/health_insurance_premiums_without_medicare_part_b.py index 92be6e51a96..e5ea67ae17a 100644 --- a/policyengine_us/variables/household/expense/health/health_insurance_premiums_without_medicare_part_b.py +++ b/policyengine_us/variables/household/expense/health/health_insurance_premiums_without_medicare_part_b.py @@ -7,4 +7,9 @@ class health_insurance_premiums_without_medicare_part_b(Variable): label = "Health insurance premiums without Medicare Part B premiums" unit = USD definition_period = YEAR - uprating = "calibration.gov.hhs.cms.moop_per_capita" + # Survey data carry the person's reported premiums here (CPS ASEC + # PHIP_VAL), most of them paid by people with employer-sponsored + # coverage. They follow the same premium index as + # pre_tax_health_insurance_premiums, so a premium moved between the two + # inputs grows the same way. + uprating = "calibration.gov.hhs.cms.esi_premium_per_enrollee" diff --git a/policyengine_us/variables/household/expense/health/long_term_health_insurance_premiums.py b/policyengine_us/variables/household/expense/health/long_term_health_insurance_premiums.py index 71a9825e4ef..5d91d8d3ff2 100644 --- a/policyengine_us/variables/household/expense/health/long_term_health_insurance_premiums.py +++ b/policyengine_us/variables/household/expense/health/long_term_health_insurance_premiums.py @@ -7,4 +7,7 @@ class long_term_health_insurance_premiums(Variable): label = "Long-term health insurance premiums" unit = USD definition_period = YEAR - uprating = "calibration.gov.hhs.cms.moop_per_capita" + # No dataset fills this input, and no CMS series measures long-term care + # insurance premiums. 26 U.S.C. 213(d)(10)(B) indexes the deductible + # limits on these premiums by the medical care component of the CPI. + uprating = "gov.bls.cpi.cpi_u_medical_care" diff --git a/policyengine_us/variables/household/expense/health/medicare_part_b_premiums_reported.py b/policyengine_us/variables/household/expense/health/medicare_part_b_premiums_reported.py index 500970c641d..c332c7bd9c2 100644 --- a/policyengine_us/variables/household/expense/health/medicare_part_b_premiums_reported.py +++ b/policyengine_us/variables/household/expense/health/medicare_part_b_premiums_reported.py @@ -7,4 +7,6 @@ class medicare_part_b_premiums_reported(Variable): label = "Medicare Part B premiums (reported)" definition_period = YEAR unit = USD - uprating = "calibration.gov.hhs.cms.moop_per_capita" + # A Part B premium, with any income-related adjustment, is a fixed + # multiple of the standard premium (42 U.S.C. 1395r(i)(3)). + uprating = "calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee" diff --git a/policyengine_us/variables/household/expense/health/other_health_insurance_premiums.py b/policyengine_us/variables/household/expense/health/other_health_insurance_premiums.py index 1f90a92ff02..160bf86ed5e 100644 --- a/policyengine_us/variables/household/expense/health/other_health_insurance_premiums.py +++ b/policyengine_us/variables/household/expense/health/other_health_insurance_premiums.py @@ -7,7 +7,10 @@ class other_health_insurance_premiums(Variable): label = "Other health insurance premiums" unit = USD definition_period = YEAR - uprating = "calibration.gov.hhs.cms.moop_per_capita" + # The reported premiums in + # health_insurance_premiums_without_medicare_part_b less modeled CHIP, + # Medicaid and Marketplace premiums, so the same premium index. + uprating = "calibration.gov.hhs.cms.esi_premium_per_enrollee" documentation = ( "Person-level health insurance premiums not otherwise represented by " "modeled Marketplace, CHIP, Medicaid, Medicare Part A or Part B "