From 432d5ee02529b2f40a9be315f42f160891756277 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 12:02:47 -0400 Subject: [PATCH 1/4] Project Medicare premiums and IRMAA past the last announced year The Part B standard premium, Part A premiums and deductible, and the Part B and Part D income-related adjustment amounts and thresholds held their 2026 values in every later year. - Part B standard premium and Part B IRMAA amounts uprate by the Trustees' projected standard premium (calibration series from #10085), rounded to 10 cents (42 U.S.C. 1395r(c)). - Part A full and reduced premiums uprate by the Trustees' projected Part A premium (Table V.E1), rounded to $1; the Part A deductible gains its 2026 value ($1,736) and uprates by the Trustees' projected deductible, rounded to $4 (1395e(b)(1)). - IRMAA thresholds (Parts B and D) follow 1395r(i)(5)'s CPI-U schedule, computed from the statutory amounts; the $500,000 tier is indexed from 2028. - Part D IRMAA amounts follow 1395w-113(a)(7)(B) applied to the Trustees' projected base beneficiary premium, with the (a)(9) percent of 20% from 2030; this reproduces Trustees Table V.E4 for 2027-2035. Co-Authored-By: Claude Opus 5.5 --- .../medicare-part-b-premium-uprating.fixed.md | 1 + .../hhs/cms/medicare_part_a_deductible.yaml | 42 +++ .../medicare_part_a_premium_per_enrollee.yaml | 42 +++ ...base_beneficiary_premium_per_enrollee.yaml | 44 ++++ .../gov/hhs/medicare/part_a/deductible.yaml | 13 + .../gov/hhs/medicare/part_a/full_premium.yaml | 10 + .../hhs/medicare/part_a/reduced_premium.yaml | 11 + .../gov/hhs/medicare/part_b/base_premium.yaml | 10 + .../part_b/irmaa/head_of_household.yaml | 73 ++++-- .../gov/hhs/medicare/part_b/irmaa/joint.yaml | 73 ++++-- .../hhs/medicare/part_b/irmaa/separate.yaml | 34 ++- .../gov/hhs/medicare/part_b/irmaa/single.yaml | 74 ++++-- .../part_b/irmaa/surviving_spouse.yaml | 73 ++++-- .../base_beneficiary_premium_percent.yaml | 23 ++ .../part_d/irmaa/head_of_household.yaml | 5 + .../gov/hhs/medicare/part_d/irmaa/joint.yaml | 5 + .../hhs/medicare/part_d/irmaa/separate.yaml | 5 + .../gov/hhs/medicare/part_d/irmaa/single.yaml | 5 + .../part_d/irmaa/surviving_spouse.yaml | 5 + .../parameters/uprating_extensions.py | 239 +++++++++++++++++- 20 files changed, 719 insertions(+), 68 deletions(-) create mode 100644 changelog.d/medicare-part-b-premium-uprating.fixed.md create mode 100644 policyengine_us/parameters/calibration/gov/hhs/cms/medicare_part_a_deductible.yaml create mode 100644 policyengine_us/parameters/calibration/gov/hhs/cms/medicare_part_a_premium_per_enrollee.yaml create mode 100644 policyengine_us/parameters/calibration/gov/hhs/cms/medicare_part_d_base_beneficiary_premium_per_enrollee.yaml create mode 100644 policyengine_us/parameters/gov/hhs/medicare/part_d/base_beneficiary_premium_percent.yaml 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/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_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/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 8e1142c6419..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, @@ -893,6 +1102,34 @@ def set_all_uprating_parameters(parameters: ParameterNode) -> ParameterNode: 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( From a9658c3354d2857726e88f46be96e828abd1182d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 12:30:39 -0400 Subject: [PATCH 2/4] Test Medicare premium and IRMAA projections Property tests (Hypothesis) for statutory rounding, proportionality, monotonicity and threshold structure in every projected year; differential tests against the 2026 Trustees Report Tables V.E1-V.E4 and CMS's published 2020-2026 IRMAA thresholds; a precedence test showing a CMS amount added to the YAML replaces only its own year; and YAML household cases for 2027-2034. Co-Authored-By: Claude Opus 5.5 --- .../core/test_medicare_premium_projection.py | 572 ++++++++++++++++++ .../eligibility/base_part_a_premium.yaml | 19 + .../eligibility/base_part_b_premium.yaml | 18 + ...ome_adjusted_part_d_premium_surcharge.yaml | 30 + .../eligibility/medicare_part_b_premium.yaml | 61 ++ .../msp_part_b_premium_coverage.yaml | 10 + 6 files changed, 710 insertions(+) create mode 100644 policyengine_us/tests/core/test_medicare_premium_projection.py 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..bc9d205d074 --- /dev/null +++ b/policyengine_us/tests/core/test_medicare_premium_projection.py @@ -0,0 +1,572 @@ +"""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``. + +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/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 From 16eafbad730713cf20e8e87649762c2b54eaf82a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 12:37:13 -0400 Subject: [PATCH 3/4] Say how to update the projection tests when CMS announces a year Co-Authored-By: Claude Opus 5.5 --- .../tests/core/test_medicare_premium_projection.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/policyengine_us/tests/core/test_medicare_premium_projection.py b/policyengine_us/tests/core/test_medicare_premium_projection.py index bc9d205d074..bcf4f8aa5c1 100644 --- a/policyengine_us/tests/core/test_medicare_premium_projection.py +++ b/policyengine_us/tests/core/test_medicare_premium_projection.py @@ -16,6 +16,11 @@ 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 From ce75b4abb8b7c6c4f2cb3e7f774dae6007f762de Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 12:41:06 -0400 Subject: [PATCH 4/4] Note Medicare's projected years in the program registry Co-Authored-By: Claude Opus 5.5 --- policyengine_us/programs.yaml | 3 +++ 1 file changed, 3 insertions(+) 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