From 1d369d99ca950069b3ee9acf6fe7110f2facf035 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 12:59:59 -0400 Subject: [PATCH 1/8] Use CMS Medicare spending per enrollee for the Medicare per capita cost Replace KFF's 2021 traditional-Medicare figure and an unsourced $14,500 2025 estimate (held flat for every later year) with CMS National Health Expenditure Table 17's Medicare spending per enrollee: historical 2013-2024, projected 2025-2034, extended to 2100 at the 2031-2032 rate so the end of sequestration in 2033 does not compound. Co-Authored-By: Claude Opus 5.5 --- .../medicare-per-enrollee-cms.fixed.md | 1 + .../gov/hhs/medicare/per_capita_cost.yaml | 68 ++++++++++++++++--- .../parameters/uprating_extensions.py | 17 +++++ 3 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 changelog.d/medicare-per-enrollee-cms.fixed.md diff --git a/changelog.d/medicare-per-enrollee-cms.fixed.md b/changelog.d/medicare-per-enrollee-cms.fixed.md new file mode 100644 index 00000000000..04369fcfd3f --- /dev/null +++ b/changelog.d/medicare-per-enrollee-cms.fixed.md @@ -0,0 +1 @@ +Value Medicare enrollees at CMS's Medicare spending per enrollee (National Health Expenditure projections, 2013-2034, extended to 2100) instead of a $14,500 estimate held flat from 2025. diff --git a/policyengine_us/parameters/calibration/gov/hhs/medicare/per_capita_cost.yaml b/policyengine_us/parameters/calibration/gov/hhs/medicare/per_capita_cost.yaml index 3e92c3e491c..756e0272c4a 100644 --- a/policyengine_us/parameters/calibration/gov/hhs/medicare/per_capita_cost.yaml +++ b/policyengine_us/parameters/calibration/gov/hhs/medicare/per_capita_cost.yaml @@ -1,12 +1,64 @@ -description: Average Medicare spending per enrolled beneficiary (national average). Represents the annual value of Medicare coverage including Part A and Part B. -values: - 2021-01-01: 11_080 # Traditional Medicare (Part A and/or Part B) per KFF - 2025-01-01: 14_500 # Estimated - update with official Centers for Medicare & Medicaid Services (CMS) data when available +description: The Centers for Medicare and Medicaid Services (CMS) reports and projects this Medicare spending per enrollee, which is all Medicare spending, including payments to Medicare Advantage plans, Part D and program administration, divided by average enrollment in Part A, Part B or both. metadata: - label: Medicare per capita cost unit: currency-USD period: year + label: CMS Medicare spending per enrollee reference: - - title: KFF Medicare Spending Per Beneficiary (2021) - href: https://www.kff.org/medicare/state-indicator/per-beneficiary/ - - title: Note - 2024 value is estimated and should be updated with official Centers for Medicare & Medicaid Services (CMS) data + # Table17 Health Insurance Enrollment and Enrollment Growth Rates, + # "Spending per Enrollee ($)", "Medicare" row: Medicare spending (the same + # row as Table03's Medicare source of funds) divided by Medicare + # enrollment. Historical for 2013-2024, projected for 2025-2034. 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 + # Medicare enrollment is Part A and/or B enrollment by "a person-year + # methodology, which sums the number of months each beneficiary is enrolled + # during the year and divides by 12", with U.S. territories removed. + - title: "CMS National Health Expenditure Accounts: Methodology Paper, 2024, Medicare enrollment" + href: https://www.cms.gov/files/document/definitions-sources-and-methods.pdf#page=34 + # Medicare spending covers Parts A, B, C (Medicare Advantage) and D (file + # page 15) and, as program spending, federal administration and the + # non-medical expenditures of Part C and Part D private plans (file page + # 23). Premiums finance part of that spending rather than reduce it: the + # sponsor analysis counts SMI premiums as household spending on Medicare + # (file page 30). + - title: "CMS National Health Expenditure Accounts: Methodology Paper, 2024, Medicare spending" + href: https://www.cms.gov/files/document/definitions-sources-and-methods.pdf#page=23 + # "Projections of Medicare spending and enrollment used in the NHE + # projections were generated for the most recent Trustees Report", here + # the 2026 report. Last Updated June 24, 2026, sha256 + # ce62fe2c236cc96ce287027784b141584ddf39e62c5fd8ddc3e771a014944817. + - title: "Projections of National Health Expenditures and Enrollment: Methodology and Model Specification" + href: https://www.cms.gov/research-statistics-data-and-systems/statistics-trends-and-reports/nationalhealthexpenddata/downloads/projectionsmethodology.pdf#page=7 +values: + # Historical. + 2013-01-01: 11_483 + 2014-01-01: 11_683 + 2015-01-01: 11_929 + 2016-01-01: 12_119 + 2017-01-01: 12_319 + 2018-01-01: 12_795 + 2019-01-01: 13_366 + 2020-01-01: 13_547 + 2021-01-01: 14_293 + 2022-01-01: 14_921 + 2023-01-01: 15_915 + 2024-01-01: 16_779 + # Projections. + 2025-01-01: 17_751 + 2026-01-01: 18_983 + 2027-01-01: 20_096 + 2028-01-01: 21_310 + 2029-01-01: 22_543 + 2030-01-01: 23_789 + 2031-01-01: 25_185 + 2032-01-01: 26_552 + 2033-01-01: 28_277 + 2034-01-01: 30_129 + # 2035 to 2100 grow at the 2031-2032 rate (26_552 / 25_185, 5.4% a year), + # not the 2033-2034 rate, which carries the end of Medicare sequestration + # on August 31, 2033; later years hold the 2100 value. See + # set_all_uprating_parameters in uprating_extensions.py. diff --git a/policyengine_us/parameters/uprating_extensions.py b/policyengine_us/parameters/uprating_extensions.py index 23ec45e9904..df50047bd66 100644 --- a/policyengine_us/parameters/uprating_extensions.py +++ b/policyengine_us/parameters/uprating_extensions.py @@ -880,6 +880,23 @@ def set_all_uprating_parameters(parameters: ParameterNode) -> ParameterNode: period_day=1, ) + # CMS Medicare spending per enrollee (January values, last projection + # year 2034), which Medicare cost variables read directly. Sequestration + # cuts Medicare benefit payments 2 percent through August 31, 2033 (2026 + # Medicare Trustees Report, file page 62); its end lifts 2033 and 2034 + # growth, and the Trustees' per-beneficiary growth falls from 6.6 + # percent in 2034 to 5.4 percent in 2035 (Table V.D1, file page 210). + # So each year from 2035 to 2100 grows at the 2031-2032 rate, and later + # years hold the 2100 value. + extend_parameter_values( + parameters.calibration.gov.hhs.medicare.per_capita_cost, + last_projected_year=2034, + end_year=END_YEAR, + period_month=1, + period_day=1, + growth_years=(2031, 2032), + ) + # CBO income-by-source aggregates are used directly and as anchors for # SOI-based income upraters. Extending them in the baseline path keeps # long-run nominal data aging independent of scenario-specific reforms. From cdfe759d6fad3d2a226b2f6de05fb5d9cbf953de Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 13:18:16 -0400 Subject: [PATCH 2/8] Test the Medicare spending per enrollee series and its consumers Transcription checks against NHE Table 17 (and its spending over enrollment), a cross-source level check against the Trustees Report's per beneficiary cost, Hypothesis properties for order, the 2035-2100 closed form and calendar-year steps, and identities for medicare_gross_cost, medicare_cost and qmb_cost_sharing. Existing expectations move from $14,500/$11,080 to the CMS values. Co-Authored-By: Claude Opus 5.5 --- .../core/test_medicare_per_capita_cost.py | 245 ++++++++++++++++++ .../gov/hhs/medicare/costs/medicare_cost.yaml | 10 +- .../medicare/costs/medicare_gross_cost.yaml | 6 +- .../medicare/savings_programs/msp_cost.yaml | 4 +- .../msp_federal_and_state_cost.yaml | 6 +- .../savings_programs/qmb_cost_sharing.yaml | 4 +- .../hhs/medicare/test_part_b_msp_offset.py | 6 +- .../household/cbo_household_income.yaml | 10 +- .../parameters/test_uprating_extensions.py | 5 + 9 files changed, 276 insertions(+), 20 deletions(-) create mode 100644 policyengine_us/tests/core/test_medicare_per_capita_cost.py diff --git a/policyengine_us/tests/core/test_medicare_per_capita_cost.py b/policyengine_us/tests/core/test_medicare_per_capita_cost.py new file mode 100644 index 00000000000..986236c9d7d --- /dev/null +++ b/policyengine_us/tests/core/test_medicare_per_capita_cost.py @@ -0,0 +1,245 @@ +"""The CMS Medicare spending per enrollee series that values Medicare coverage. + +``calibration.gov.hhs.medicare.per_capita_cost`` holds the "Medicare" row of +"Spending per Enrollee ($)" in CMS's National Health Expenditure projections, +Table 17 (2025-2034 release): Medicare spending over Medicare enrollment. +Spending covers every Part, including payments to Medicare Advantage plans and +program administration; enrollment counts Part A and/or Part B enrollees in +person-years; both exclude the territories (NHEA methodology paper, file pages +15, 23 and 34). 2013-2024 are historical and 2025-2034 projected, from the +projections prepared for the 2026 Medicare Trustees Report. 2035 to 2100 grow +at the 2031-2032 rate, because the end of sequestration on August 31, 2033 +lifts 2033 and 2034 growth. + +``medicare_gross_cost`` returns the series for each enrolled person, +``medicare_cost`` subtracts the person's Part A and gross Part B premiums, and +``qmb_cost_sharing`` pays QMB enrollees a share of it each month. + +Invariants: + +1. Transcription: every value is Table 17's published dollar figure, and is + consistent with the same table's Medicare spending over Medicare enrollment + within rounding. +2. Cross-source level: the Trustees Report's per beneficiary cost (Table V.D1) + runs 5% to 8% above the series in every year from 2015 to 2034. V.D1 sums + per-Part averages (its footnote 2), while Table II.B1 counts 63.4 million + Part B and 56.8 million Part D enrollees among 69.3 million in 2025. +3. Bounds and order: the series is positive and strictly increasing from 2013 + to 2100, and holds the 2100 value afterwards. +4. Extension rule: each year from 2035 to 2100 equals the 2034 value times + (26,552 / 25,185) ** (year - 2034), the closed form of the iterative + extension; the 2035 value is within 0.1% of the 2034 value grown at the + Trustees' 2034-2035 per beneficiary growth. +5. Calendar years: every date within a year reads the January 1 value. +6. Consumers: for an enrolled person, ``medicare_gross_cost`` equals the + series; ``medicare_cost`` equals the series less the Part A premium and the + gross Part B premium, floored at zero, so never exceeds + ``medicare_gross_cost``; a QMB enrollee's monthly ``qmb_cost_sharing`` is + the series over 12 times the QMB rate. A person not enrolled gets zero. +""" + +import pytest +from hypothesis import 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.system import system + +SERIES = "calibration.gov.hhs.medicare.per_capita_cost" +FIRST_YEAR, LAST_PROJECTED_YEAR, END_YEAR = 2013, 2034, 2100 +YEARS = range(FIRST_YEAR, LAST_PROJECTED_YEAR + 1) +GROWTH_YEARS = (2031, 2032) + +# CMS NHE projections tables, Table 17, "Medicare" rows +# (https://www.cms.gov/files/zip/nhe-projections-tables.zip, Last-Modified +# 2026-09-03, sha256 264f8226...cd70). +MEDICARE_SPENDING_PER_ENROLLEE = dict( + zip( + YEARS, + ( + *(11_483, 11_683, 11_929, 12_119, 12_319, 12_795, 13_366, 13_547), + *(14_293, 14_921, 15_915, 16_779, 17_751, 18_983, 20_096, 21_310), + *(22_543, 23_789, 25_185, 26_552, 28_277, 30_129), + ), + ) +) +MEDICARE_SPENDING_BILLIONS = dict( + zip( + YEARS, + ( + *(588.9, 617.3, 647.7, 675.7, 704.9, 751.6, 805.2, 833.8, 894.6), + *(951.7, 1037.3, 1118.0, 1204.0, 1314.6, 1422.7, 1544.2, 1664.6), + *(1783.7, 1911.1, 2035.8, 2188.3, 2354.3), + ), + ) +) +MEDICARE_ENROLLMENT_MILLIONS = dict( + zip( + YEARS, + ( + *(51.3, 52.8, 54.3, 55.8, 57.2, 58.7, 60.2, 61.6, 62.6, 63.8), + *(65.2, 66.6, 67.8, 69.2, 70.8, 72.5, 73.8, 75.0, 75.9, 76.7), + *(77.4, 78.1), + ), + ) +) +# 2026 Medicare Trustees Report, Table V.D1 (file page 210), "Total" average +# per beneficiary cost: historical through 2025, intermediate estimates after. +TRUSTEES_PER_BENEFICIARY_COST = dict( + zip( + range(2015, 2036), + ( + *(12_734, 12_922, 13_127, 13_604, 14_224, 14_401, 15_170, 15_820), + *(16_809, 17_837, 18_894, 20_430, 21_497, 22_783, 24_076, 25_401), + *(26_826, 28_169, 29_989, 31_967, 33_687), + ), + ) +) + +years = st.integers(FIRST_YEAR, END_YEAR) + + +def _series(date): + return get_parameter(system.parameters, SERIES)(date) + + +def _value(year): + return _series(f"{year}-01-01") + + +@pytest.mark.parametrize("year", YEARS) +def test_series_is_table_17_medicare_spending_per_enrollee(year): + assert _value(year) == MEDICARE_SPENDING_PER_ENROLLEE[year] + + +@pytest.mark.parametrize("year", YEARS) +def test_published_values_match_spending_over_enrollment(year): + """Checks the transcribed rows against each other, so one mistyped + spending, enrollment or per-enrollee figure fails.""" + spending = MEDICARE_SPENDING_BILLIONS[year] + enrollment = MEDICARE_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 <= MEDICARE_SPENDING_PER_ENROLLEE[year] <= high + + +@pytest.mark.parametrize("year", range(2015, LAST_PROJECTED_YEAR + 1)) +def test_trustees_per_beneficiary_cost_runs_5_to_8_percent_above(year): + ratio = TRUSTEES_PER_BENEFICIARY_COST[year] / _value(year) + assert 1.05 <= ratio <= 1.08 + + +def test_2035_extension_follows_the_trustees_2035_growth(): + trustees_growth = ( + TRUSTEES_PER_BENEFICIARY_COST[2035] / TRUSTEES_PER_BENEFICIARY_COST[2034] + ) + assert _value(2035) == pytest.approx( + _value(LAST_PROJECTED_YEAR) * trustees_growth, rel=0.001 + ) + + +@given(years, st.integers(1, END_YEAR - FIRST_YEAR)) +def test_series_is_positive_and_strictly_increasing(year, gap): + later = min(year + gap, END_YEAR) + if later == year: + return + assert 0 < _value(year) < _value(later) + + +@given(st.integers(LAST_PROJECTED_YEAR + 1, END_YEAR)) +def test_extension_is_the_closed_form_of_the_2031_2032_growth(year): + earlier, later = GROWTH_YEARS + growth = ( + MEDICARE_SPENDING_PER_ENROLLEE[later] + / (MEDICARE_SPENDING_PER_ENROLLEE[earlier]) + ) + expected = MEDICARE_SPENDING_PER_ENROLLEE[LAST_PROJECTED_YEAR] * growth ** ( + year - LAST_PROJECTED_YEAR + ) + assert _value(year) == pytest.approx(expected, rel=1e-9) + assert _value(year) / _value(year - 1) == pytest.approx(growth, rel=1e-12) + + +@given(st.integers(END_YEAR, END_YEAR + 100)) +def test_series_holds_the_2100_value_afterwards(year): + assert _value(year) == _value(END_YEAR) + + +@given(years, st.integers(1, 12), st.integers(1, 28)) +def test_every_date_in_a_year_reads_the_january_value(year, month, day): + assert _series(f"{year}-{month:02d}-{day:02d}") == _value(year) + + +def _person_simulation(year, person): + members = ["person"] + return Simulation( + situation={ + "people": {"person": person}, + "tax_units": {"tax_unit": {"members": members}}, + "spm_units": {"spm_unit": {"members": members}}, + "families": {"family": {"members": members}}, + "marital_units": {"marital_unit": {"members": members}}, + "households": { + "household": {"members": members, "state_code": {year: "CA"}} + }, + } + ) + + +@settings(max_examples=20, deadline=None) +@given( + st.integers(2015, END_YEAR), + st.floats(0, 12_000, allow_nan=False), + st.floats(0, 40_000, allow_nan=False), +) +def test_medicare_cost_nets_premiums_from_the_series( + year, part_a_premium, part_b_premium +): + simulation = _person_simulation( + year, + { + "age": {year: 70}, + "is_medicare_eligible": {year: True}, + "medicare_enrolled": {year: True}, + "base_part_a_premium": {year: part_a_premium}, + "gross_medicare_part_b_premium": {year: part_b_premium}, + }, + ) + gross = simulation.calculate("medicare_gross_cost", year)[0] + net = simulation.calculate("medicare_cost", year)[0] + # Variables are float32, so compare to single precision. + assert gross == pytest.approx(_value(year), rel=1e-6) + expected = max(_value(year) - part_a_premium - part_b_premium, 0) + assert net == pytest.approx(expected, rel=1e-6, abs=0.01) + assert net <= gross + + +@settings(max_examples=10, deadline=None) +@given(st.integers(2015, END_YEAR)) +def test_person_not_enrolled_has_no_medicare_cost(year): + simulation = _person_simulation( + year, + { + "age": {year: 70}, + "is_medicare_eligible": {year: True}, + "medicare_enrolled": {year: False}, + }, + ) + assert simulation.calculate("medicare_gross_cost", year)[0] == 0 + assert simulation.calculate("medicare_cost", year)[0] == 0 + + +@settings(max_examples=10, deadline=None) +@given(st.integers(2015, END_YEAR), st.integers(1, 12)) +def test_qmb_cost_sharing_is_the_qmb_rate_of_monthly_spending(year, month): + period = f"{year}-{month:02d}" + rate = system.parameters( + period + ).gov.hhs.medicare.savings_programs.qmb.cost_sharing_rate + simulation = _person_simulation(year, {"msp_category": {period: "QMB"}}) + assert simulation.calculate("qmb_cost_sharing", period)[0] == pytest.approx( + _value(year) / 12 * rate, rel=1e-6 + ) diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/costs/medicare_cost.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/costs/medicare_cost.yaml index 5cb855c4319..b0fb091fce4 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/costs/medicare_cost.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/costs/medicare_cost.yaml @@ -9,7 +9,7 @@ tax_exempt_interest_income: 2023: 0 output: - medicare_cost: 12_280 # $14,500 spending - $2,220 Part B premium (2025) + medicare_cost: 15_531 # $17,751 spending - $2,220 Part B premium (2025) - name: unit test 2 - enrolled, pays full Part A premium period: 2025 @@ -22,7 +22,7 @@ tax_exempt_interest_income: 2023: 0 output: - medicare_cost: 6_064 # $14,500 - $6,216 Part A - $2,220 Part B (2025) + medicare_cost: 9_315 # $17,751 - $6,216 Part A - $2,220 Part B (2025) - name: unit test 3 - enrolled with IRMAA period: 2025 @@ -36,7 +36,7 @@ 2023: 0 filing_status: SINGLE output: - medicare_cost: 6_953.2 # $14,500 - $7,546.80 Part B with IRMAA (2025) + medicare_cost: 10_204.2 # $17,751 - $7,546.80 Part B with IRMAA (2025) - name: unit test 4 - eligible but not enrolled period: 2025 @@ -58,7 +58,7 @@ tax_exempt_interest_income: 2019: 0 output: - medicare_cost: 9_298 # $11,080 - $1,782 Part B (2021: $148.50*12) + medicare_cost: 12_511 # $14,293 - $1,782 Part B (2021: $148.50*12) - name: unit test 6 - direct Part B premium input period: 2025 @@ -68,4 +68,4 @@ medicare_quarters_of_coverage: 40 gross_medicare_part_b_premium: 3_000 output: - medicare_cost: 11_500 # $14,500 spending - $3,000 Part B premium + medicare_cost: 14_751 # $17,751 spending - $3,000 Part B premium diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/costs/medicare_gross_cost.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/costs/medicare_gross_cost.yaml index cba1201e13c..15eb8725e7d 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/costs/medicare_gross_cost.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/costs/medicare_gross_cost.yaml @@ -10,7 +10,8 @@ tax_exempt_interest_income: 2023: 0 output: - medicare_gross_cost: 14_500 + # CMS Medicare spending per enrollee, 2025. + medicare_gross_cost: 17_751 - name: unit test 2 - premiums do not reduce gross Medicare value period: 2025 @@ -25,4 +26,5 @@ 2023: 0 filing_status: SINGLE output: - medicare_gross_cost: 14_500 + # CMS Medicare spending per enrollee, 2025. + medicare_gross_cost: 17_751 diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/msp_cost.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/msp_cost.yaml index 1e9cdaec98a..55be058ba49 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/msp_cost.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/msp_cost.yaml @@ -11,8 +11,8 @@ base_part_b_premium: 2_434.8 output: # Part B premium: $202.90 * 12 = $2,434.80. - # QMB cost sharing: $14,500 * 20% = $2,900. - msp_cost: 5_334.8 + # QMB cost sharing: $18,983 * 20% = $3,796.60. + msp_cost: 6_231.4 - name: Case 2, SLMB MSP-only beneficiary receives Part B premium value. period: 2026 diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/msp_federal_and_state_cost.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/msp_federal_and_state_cost.yaml index da73487521b..0c526fe9c29 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/msp_federal_and_state_cost.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/msp_federal_and_state_cost.yaml @@ -10,9 +10,9 @@ base_part_a_premium: 0 base_part_b_premium: 2_434.8 output: - # CA regular FMAP is 50%; total MSP cost is $5,334.80. - msp_federal_cost: 2_667.4 - msp_state_cost: 2_667.4 + # CA regular FMAP is 50%; total MSP cost is $6,231.40. + msp_federal_cost: 3_115.7 + msp_state_cost: 3_115.7 - name: Case 2, QI cost is federally financed within allocation. period: 2026 diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/qmb_cost_sharing.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/qmb_cost_sharing.yaml index 4a812377fd3..1fa5134a84f 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/qmb_cost_sharing.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/qmb_cost_sharing.yaml @@ -4,8 +4,8 @@ input: msp_category: QMB output: - # $14,500 annual Medicare per-capita cost / 12 * 20%. - qmb_cost_sharing: 241.67 + # $18,983 annual Medicare spending per enrollee (2026) / 12 * 20%. + qmb_cost_sharing: 316.38 - name: Case 2, non-QMB MSP categories do not receive QMB cost sharing. period: 2026-01 diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/test_part_b_msp_offset.py b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/test_part_b_msp_offset.py index 611c0bf7592..29ecb083de7 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/test_part_b_msp_offset.py +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/test_part_b_msp_offset.py @@ -87,7 +87,8 @@ def test_medicare_cost_uses_gross_part_b_before_msp_offset(): ) assert sim.calculate("medicare_part_b_premium", PERIOD)[0] == pytest.approx(0) - assert sim.calculate("medicare_cost", PERIOD)[0] == pytest.approx(12_280) + # $17,751 CMS Medicare spending per enrollee (2025) - $2,220 gross Part B. + assert sim.calculate("medicare_cost", PERIOD)[0] == pytest.approx(15_531) def test_medicare_part_b_premium_is_zero_when_not_enrolled(): @@ -343,7 +344,8 @@ def test_gross_medicare_part_b_premium_if_enrolled_preserves_irmaa(): assert sim.calculate("gross_medicare_part_b_premium_if_enrolled", PERIOD)[ 0 ] == pytest.approx(4_440) - assert sim.calculate("medicare_cost", PERIOD)[0] == pytest.approx(10_060) + # $17,751 CMS Medicare spending per enrollee (2025) - $4,440 gross Part B. + assert sim.calculate("medicare_cost", PERIOD)[0] == pytest.approx(13_311) def test_medicare_irmaa_magi_two_years_prior_falls_back_to_lagged_income(): diff --git a/policyengine_us/tests/policy/baseline/household/income/household/cbo_household_income.yaml b/policyengine_us/tests/policy/baseline/household/income/household/cbo_household_income.yaml index 275f2797e57..07d9f4f1d8e 100644 --- a/policyengine_us/tests/policy/baseline/household/income/household/cbo_household_income.yaml +++ b/policyengine_us/tests/policy/baseline/household/income/household/cbo_household_income.yaml @@ -33,10 +33,12 @@ workers_compensation: 1_000 output: cbo_household_market_income: 49_102 - medicare_gross_cost: 14_500 - cbo_household_social_insurance_benefits: 29_500 - cbo_household_income_before_transfers_and_taxes: 78_602 - cbo_household_income: 78_602 + # CMS Medicare spending per enrollee, 2025. + medicare_gross_cost: 17_751 + # $12,000 + $17,751 + $2,000 + $1,000. + cbo_household_social_insurance_benefits: 32_751 + cbo_household_income_before_transfers_and_taxes: 81_853 + cbo_household_income: 81_853 - name: CBO means-tested transfers include only the listed programs period: 2026 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..901e665cd28 100644 --- a/policyengine_us/tests/policy/baseline/parameters/test_uprating_extensions.py +++ b/policyengine_us/tests/policy/baseline/parameters/test_uprating_extensions.py @@ -39,6 +39,11 @@ def test_all_uprating_factors_extend_to_2100(): ("CPI-U", parameters.gov.bls.cpi.cpi_u, "-02-01"), ("Chained CPI-U", parameters.gov.bls.cpi.c_cpi_u, "-02-01"), ("CPI-W", parameters.gov.bls.cpi.cpi_w, "-02-01"), + ( + "CMS Medicare spending per enrollee", + parameters.calibration.gov.hhs.medicare.per_capita_cost, + "-01-01", + ), ] for name, param, date_suffix in uprating_params: From 33a94b4172f459d3e803c37bfae472f8d3e7e65d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 13:30:10 -0400 Subject: [PATCH 3/8] Update the QMB federal and state benefit cost test to the CMS series Co-Authored-By: Claude Opus 5.5 --- .../income/person/federal_and_state_benefit_cost.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/household/income/person/federal_and_state_benefit_cost.yaml b/policyengine_us/tests/policy/baseline/household/income/person/federal_and_state_benefit_cost.yaml index cdc30cc0523..11dedad24db 100644 --- a/policyengine_us/tests/policy/baseline/household/income/person/federal_and_state_benefit_cost.yaml +++ b/policyengine_us/tests/policy/baseline/household/income/person/federal_and_state_benefit_cost.yaml @@ -84,5 +84,7 @@ base_part_a_premium: 0 base_part_b_premium: 2_434.8 output: - federal_benefit_cost: 2_667.4 - state_benefit_cost: 2_667.4 + # MSP cost: $2,434.80 Part B premium + 20% of $18,983 CMS Medicare + # spending per enrollee (2026) = $6,231.40, split at CA's 50% FMAP. + federal_benefit_cost: 3_115.7 + state_benefit_cost: 3_115.7 From 712abd368a2466948d7a829d6ecb95883c5d1b53 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 14:54:06 -0400 Subject: [PATCH 4/8] Subtract the expected Part D premium from medicare_cost CMS Medicare spending per enrollee (calibration.gov.hhs.medicare.per_capita_cost, PolicyEngine/policyengine-us#10107) includes Part D, which enrollee premiums partly finance, but medicare_cost subtracted only Part A and Part B premiums. It now also subtracts the Part D enrollment share times (Part D premiums per Part D enrollee + the person's Part D IRMAA), both new calibration series from the 2026 Medicare Trustees Report (Tables III.D3, V.E4 and V.B3). Co-Authored-By: Claude Opus 5.5 --- .../medicare-cost-part-d-premiums.fixed.md | 1 + .../hhs/medicare/part_d/enrollment_share.yaml | 53 +++++++++++ .../part_d/premiums_per_enrollee.yaml | 73 +++++++++++++++ .../parameters/uprating_extensions.py | 14 +++ .../core/test_medicare_per_capita_cost.py | 17 ++-- .../gov/hhs/medicare/costs/medicare_cost.yaml | 90 +++++++++++++++++-- .../hhs/medicare/test_part_b_msp_offset.py | 10 ++- .../gov/hhs/medicare/costs/medicare_cost.py | 24 +++-- 8 files changed, 261 insertions(+), 21 deletions(-) create mode 100644 changelog.d/medicare-cost-part-d-premiums.fixed.md create mode 100644 policyengine_us/parameters/calibration/gov/hhs/medicare/part_d/enrollment_share.yaml create mode 100644 policyengine_us/parameters/calibration/gov/hhs/medicare/part_d/premiums_per_enrollee.yaml diff --git a/changelog.d/medicare-cost-part-d-premiums.fixed.md b/changelog.d/medicare-cost-part-d-premiums.fixed.md new file mode 100644 index 00000000000..b7989573aaa --- /dev/null +++ b/changelog.d/medicare-cost-part-d-premiums.fixed.md @@ -0,0 +1 @@ +Subtract the expected Medicare Part D premium, the Part D enrollment share times Part D premiums per Part D enrollee plus the person's Part D IRMAA, from `medicare_cost`, whose CMS spending per enrollee includes Part D. diff --git a/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d/enrollment_share.yaml b/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d/enrollment_share.yaml new file mode 100644 index 00000000000..6cf55361039 --- /dev/null +++ b/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d/enrollment_share.yaml @@ -0,0 +1,53 @@ +description: The Medicare Trustees report and project this share of Medicare enrollees, people with Part A or Part B coverage, who are enrolled in Part D. +metadata: + unit: /1 + period: year + label: Medicare Part D enrollment share + reference: + # Table V.B3, "Medicare Enrollment": Part D enrollment over total + # enrollment, which footnote 2 defines as the "Number of beneficiaries with + # HI and/or SMI coverage". Part D enrollment includes retiree drug subsidy + # beneficiaries (it equals the total of Table IV.B7, file page 153). + # Annual through 2035, then every fifth year through 2100; each value holds + # until the next. 2026 Medicare Trustees 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.B3 + href: https://www.cms.gov/oact/tr/2026#page=196 +values: + # Historical. + 2015-01-01: 0.7517 + 2016-01-01: 0.7569 + 2017-01-01: 0.7579 + 2018-01-01: 0.763 + 2019-01-01: 0.7667 + 2020-01-01: 0.7743 + 2021-01-01: 0.7811 + 2022-01-01: 0.7885 + 2023-01-01: 0.7941 + 2024-01-01: 0.8117 + 2025-01-01: 0.8191 + # Trustees' intermediate estimates. + 2026-01-01: 0.8195 + 2027-01-01: 0.8255 + 2028-01-01: 0.8301 + 2029-01-01: 0.8348 + 2030-01-01: 0.8382 + 2031-01-01: 0.8414 + 2032-01-01: 0.844 + 2033-01-01: 0.8462 + 2034-01-01: 0.8476 + 2035-01-01: 0.8485 + 2040-01-01: 0.8531 + 2045-01-01: 0.8551 + 2050-01-01: 0.8543 + 2055-01-01: 0.8534 + 2060-01-01: 0.8537 + 2065-01-01: 0.8555 + 2070-01-01: 0.8566 + 2075-01-01: 0.8575 + 2080-01-01: 0.8593 + 2085-01-01: 0.8609 + 2090-01-01: 0.863 + 2095-01-01: 0.8638 + 2100-01-01: 0.8639 diff --git a/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d/premiums_per_enrollee.yaml b/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d/premiums_per_enrollee.yaml new file mode 100644 index 00000000000..871164280c8 --- /dev/null +++ b/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d/premiums_per_enrollee.yaml @@ -0,0 +1,73 @@ +description: The Medicare Trustees report and project these Part D premiums from enrollees per Part D enrollee, excluding the income-related monthly adjustment amount. +metadata: + unit: currency-USD + period: year + label: Medicare Part D premiums per Part D enrollee + reference: + # Each value is (premium income - aggregate income-related premiums) / + # Part D enrollment, from the 2026 Medicare Trustees Report (transmitted + # 2026-06-09, retrieved 2026-10-11, sha256 + # ffa56b9137006872300b0346149eae1613d09a172b6ba118aad48e66dfc48fa8): + # - Premium income, Table III.D3 (file page 116), in billions. Footnote 1: + # "Premiums include both amounts withheld from Social Security benefits or + # other Federal payments, direct payments to the Federal government, and + # those paid directly to Part D plans." The table is on a cash basis, and + # footnote 6 (file page 117) credits one January's withheld premiums on + # December 31 of the year before: about $0.2 billion in 2015, $0.1 billion + # in 2020 and an estimated $0.2 billion in 2026. Those amounts are moved + # back to 2016, 2021 and 2027. + # - Income-related premiums, Table V.E4 (file page 218), "Aggregate + # premiums", the amount paid "in excess of the Part D plan premium". They + # are part of premium income: they are withheld from benefits (42 U.S.C. + # 1395w-113(c)(4)) and deposited into the Part D account (footnote 73, + # file page 151). medicare_cost subtracts each person's own adjustment + # amount instead. + # - Part D enrollment, Table V.B3 (file page 196), in thousands. It equals + # the total of Table IV.B7 (file page 153), which includes retiree drug + # subsidy beneficiaries. + # Low-income subsidy enrollees pay reduced premiums or none (file page 219); + # Medicare's low-income subsidy payments are expenditures of the Part D + # account, not enrollee premiums (file page 114). So the average is below + # the base beneficiary premium. + - title: 2026 Annual Report of the Boards of Trustees of the Federal Hospital Insurance and Federal Supplementary Medical Insurance Trust Funds, Table III.D3 + href: https://www.cms.gov/oact/tr/2026#page=116 + - title: 2026 Annual Report of the Boards of Trustees of the Federal Hospital Insurance and Federal Supplementary Medical Insurance Trust Funds, Table V.E4 + href: https://www.cms.gov/oact/tr/2026#page=218 + - title: 2026 Annual Report of the Boards of Trustees of the Federal Hospital Insurance and Federal Supplementary Medical Insurance Trust Funds, Table V.B3 + href: https://www.cms.gov/oact/tr/2026#page=196 + - title: 2026 Annual Report of the Boards of Trustees of the Federal Hospital Insurance and Federal Supplementary Medical Insurance Trust Funds, Part D revenue and expenditures + href: https://www.cms.gov/oact/tr/2026#page=109 + - title: 42 U.S.C. § 1395w-113(c)(4) - Collection of monthly adjustment amount + href: https://www.law.cornell.edu/uscode/text/42/1395w-113#c_4 +values: + # Historical. + 2015-01-01: 277.6 + 2016-01-01: 300.94 + 2017-01-01: 321.53 + 2018-01-01: 316.61 + 2019-01-01: 298.86 + 2020-01-01: 285.44 + 2021-01-01: 306.15 + 2022-01-01: 309.42 + 2023-01-01: 304.39 + 2024-01-01: 307.7 + # 2025 premiums fell with the voluntary Part D premium demonstration for + # stand-alone prescription drug plans (file page 109). + 2025-01-01: 220.25 + # Trustees' intermediate estimates. The 2030 jump is the Inflation + # Reduction Act's reset of the base beneficiary premium percentage + # (file page 152). + 2026-01-01: 289.79 + 2027-01-01: 373.53 + 2028-01-01: 403.59 + 2029-01-01: 411.27 + 2030-01-01: 637.06 + 2031-01-01: 659.27 + 2032-01-01: 662.52 + 2033-01-01: 684.61 + 2034-01-01: 699.07 + 2035-01-01: 722.05 + # 2036 to 2100 grow at the 2034-2035 rate (722.05 / 699.07, 3.3% a year), + # near the Trustees' 3.1% growth in Part D cost per beneficiary in 2035 + # (Table V.D1, file page 210); later years hold the 2100 value. See + # set_all_uprating_parameters in uprating_extensions.py. diff --git a/policyengine_us/parameters/uprating_extensions.py b/policyengine_us/parameters/uprating_extensions.py index df50047bd66..e02e95eacc8 100644 --- a/policyengine_us/parameters/uprating_extensions.py +++ b/policyengine_us/parameters/uprating_extensions.py @@ -897,6 +897,20 @@ def set_all_uprating_parameters(parameters: ParameterNode) -> ParameterNode: growth_years=(2031, 2032), ) + # Medicare Part D premiums per Part D enrollee (January values, last + # projection year 2035), which medicare_cost reads directly. Each year + # from 2036 to 2100 grows at the 2034-2035 rate, near the Trustees' 3.1 + # percent growth in Part D cost per beneficiary in 2035 (Table V.D1, file + # page 210); later years hold the 2100 value. The Part D enrollment share + # needs no extension: the Trustees project enrollment through 2100. + extend_parameter_values( + parameters.calibration.gov.hhs.medicare.part_d.premiums_per_enrollee, + last_projected_year=2035, + end_year=END_YEAR, + period_month=1, + period_day=1, + ) + # CBO income-by-source aggregates are used directly and as anchors for # SOI-based income upraters. Extending them in the baseline path keeps # long-run nominal data aging independent of scenario-specific reforms. diff --git a/policyengine_us/tests/core/test_medicare_per_capita_cost.py b/policyengine_us/tests/core/test_medicare_per_capita_cost.py index 986236c9d7d..9e37cb5d1e3 100644 --- a/policyengine_us/tests/core/test_medicare_per_capita_cost.py +++ b/policyengine_us/tests/core/test_medicare_per_capita_cost.py @@ -12,7 +12,8 @@ lifts 2033 and 2034 growth. ``medicare_gross_cost`` returns the series for each enrolled person, -``medicare_cost`` subtracts the person's Part A and gross Part B premiums, and +``medicare_cost`` subtracts the person's Part A, gross Part B and expected +Part D premiums (tested in test_medicare_cost_part_d_premium.py), and ``qmb_cost_sharing`` pays QMB enrollees a share of it each month. Invariants: @@ -32,10 +33,11 @@ Trustees' 2034-2035 per beneficiary growth. 5. Calendar years: every date within a year reads the January 1 value. 6. Consumers: for an enrolled person, ``medicare_gross_cost`` equals the - series; ``medicare_cost`` equals the series less the Part A premium and the - gross Part B premium, floored at zero, so never exceeds - ``medicare_gross_cost``; a QMB enrollee's monthly ``qmb_cost_sharing`` is - the series over 12 times the QMB rate. A person not enrolled gets zero. + series; ``medicare_cost`` equals the series less the Part A premium, the + gross Part B premium and the expected Part D premium, floored at zero, so + never exceeds ``medicare_gross_cost``; a QMB enrollee's monthly + ``qmb_cost_sharing`` is the series over 12 times the QMB rate. A person + not enrolled gets zero. """ import pytest @@ -206,13 +208,16 @@ def test_medicare_cost_nets_premiums_from_the_series( "medicare_enrolled": {year: True}, "base_part_a_premium": {year: part_a_premium}, "gross_medicare_part_b_premium": {year: part_b_premium}, + "income_adjusted_part_d_premium_surcharge": {year: 0}, }, ) gross = simulation.calculate("medicare_gross_cost", year)[0] net = simulation.calculate("medicare_cost", year)[0] # Variables are float32, so compare to single precision. assert gross == pytest.approx(_value(year), rel=1e-6) - expected = max(_value(year) - part_a_premium - part_b_premium, 0) + part_d = system.parameters(f"{year}-01-01").calibration.gov.hhs.medicare.part_d + part_d_premium = part_d.enrollment_share * part_d.premiums_per_enrollee + expected = max(_value(year) - part_a_premium - part_b_premium - part_d_premium, 0) assert net == pytest.approx(expected, rel=1e-6, abs=0.01) assert net <= gross diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/costs/medicare_cost.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/costs/medicare_cost.yaml index b0fb091fce4..504cd704414 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/costs/medicare_cost.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/costs/medicare_cost.yaml @@ -1,5 +1,10 @@ +# The expected Part D premium is the Part D enrollment share times (Part D +# premiums per Part D enrollee + the person's Part D IRMAA). 2025: 0.8191 x +# $220.25 = $180.41 without IRMAA. + - name: unit test 1 - enrolled, premium-free Part A period: 2025 + absolute_error_margin: 0.01 input: age: 65 is_medicare_eligible: true @@ -9,10 +14,12 @@ tax_exempt_interest_income: 2023: 0 output: - medicare_cost: 15_531 # $17,751 spending - $2,220 Part B premium (2025) + # $17,751 spending - $2,220 Part B premium - $180.41 Part D (2025) + medicare_cost: 15_350.59 - name: unit test 2 - enrolled, pays full Part A premium period: 2025 + absolute_error_margin: 0.01 input: age: 65 is_medicare_eligible: true @@ -22,10 +29,12 @@ tax_exempt_interest_income: 2023: 0 output: - medicare_cost: 9_315 # $17,751 - $6,216 Part A - $2,220 Part B (2025) + # $17,751 - $6,216 Part A - $2,220 Part B - $180.41 Part D (2025) + medicare_cost: 9_134.59 - name: unit test 3 - enrolled with IRMAA period: 2025 + absolute_error_margin: 0.01 input: age: 65 is_medicare_eligible: true @@ -36,7 +45,9 @@ 2023: 0 filing_status: SINGLE output: - medicare_cost: 10_204.2 # $17,751 - $7,546.80 Part B with IRMAA (2025) + # $17,751 - $7,546.80 Part B with IRMAA - 0.8191 x ($220.25 + $85.80 x 12 + # Part D IRMAA) = $1,023.75 Part D (2025) + medicare_cost: 9_180.45 - name: unit test 4 - eligible but not enrolled period: 2025 @@ -49,6 +60,7 @@ - name: unit test 5 - 2021 data, premium-free period: 2021 + absolute_error_margin: 0.01 input: age: 70 is_medicare_eligible: true @@ -58,14 +70,82 @@ tax_exempt_interest_income: 2019: 0 output: - medicare_cost: 12_511 # $14,293 - $1,782 Part B (2021: $148.50*12) + # $14,293 - $1,782 Part B (2021: $148.50*12) - 0.7811 x $306.15 Part D + medicare_cost: 12_271.87 - name: unit test 6 - direct Part B premium input period: 2025 + absolute_error_margin: 0.01 input: age: 65 is_medicare_eligible: true medicare_quarters_of_coverage: 40 gross_medicare_part_b_premium: 3_000 output: - medicare_cost: 14_751 # $17,751 spending - $3,000 Part B premium + # $17,751 spending - $3,000 Part B premium - $180.41 Part D + medicare_cost: 14_570.59 + +- name: unit test 7 - Part D IRMAA in the 50 percent tier + period: 2025 + absolute_error_margin: 0.01 + input: + age: 65 + is_medicare_eligible: true + medicare_quarters_of_coverage: 40 + filing_status: SINGLE + adjusted_gross_income: + 2023: 150_000 # Single, $133,001 to $167,000: $35.30 a month in 2025 + tax_exempt_interest_income: + 2023: 0 + gross_medicare_part_b_premium: 2_220 + output: + income_adjusted_part_d_premium_surcharge: 423.6 + # $17,751 - $2,220 Part B - 0.8191 x ($220.25 + $423.60) = $527.38 Part D + medicare_cost: 15_003.62 + +- name: unit test 8 - projected year + period: 2027 + absolute_error_margin: 0.01 + input: + age: 67 + is_medicare_eligible: true + medicare_quarters_of_coverage: 40 + gross_medicare_part_b_premium: 2_500 + income_adjusted_part_d_premium_surcharge: 0 + output: + # $20,096 - $2,500 Part B - 0.8255 x $373.53 = $308.35 Part D (2027) + medicare_cost: 17_287.65 + +- name: unit test 9 - Inflation Reduction Act premium reset year, with IRMAA + period: 2030 + absolute_error_margin: 0.01 + input: + age: 70 + is_medicare_eligible: true + medicare_quarters_of_coverage: 40 + gross_medicare_part_b_premium: 3_000 + income_adjusted_part_d_premium_surcharge: 1_000 + output: + # $23,789 - $3,000 Part B - 0.8382 x ($637.06 + $1,000) = $1,372.18 Part D + medicare_cost: 19_416.82 + +- name: unit test 10 - premiums above spending floor the value at zero + period: 2025 + input: + age: 65 + is_medicare_eligible: true + medicare_quarters_of_coverage: 40 + gross_medicare_part_b_premium: 30_000 + output: + medicare_cost: 0 + +- name: unit test 11 - gross cost does not subtract the Part D premium + period: 2025 + input: + age: 65 + is_medicare_eligible: true + medicare_quarters_of_coverage: 40 + gross_medicare_part_b_premium: 2_220 + income_adjusted_part_d_premium_surcharge: 1_029.6 + output: + medicare_gross_cost: 17_751 diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/test_part_b_msp_offset.py b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/test_part_b_msp_offset.py index 29ecb083de7..b15943e5203 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/test_part_b_msp_offset.py +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/test_part_b_msp_offset.py @@ -87,8 +87,9 @@ def test_medicare_cost_uses_gross_part_b_before_msp_offset(): ) assert sim.calculate("medicare_part_b_premium", PERIOD)[0] == pytest.approx(0) - # $17,751 CMS Medicare spending per enrollee (2025) - $2,220 gross Part B. - assert sim.calculate("medicare_cost", PERIOD)[0] == pytest.approx(15_531) + # $17,751 CMS Medicare spending per enrollee (2025) - $2,220 gross Part B + # - 0.8191 x $220.25 expected Part D premium. + assert sim.calculate("medicare_cost", PERIOD)[0] == pytest.approx(15_350.59) def test_medicare_part_b_premium_is_zero_when_not_enrolled(): @@ -344,8 +345,9 @@ def test_gross_medicare_part_b_premium_if_enrolled_preserves_irmaa(): assert sim.calculate("gross_medicare_part_b_premium_if_enrolled", PERIOD)[ 0 ] == pytest.approx(4_440) - # $17,751 CMS Medicare spending per enrollee (2025) - $4,440 gross Part B. - assert sim.calculate("medicare_cost", PERIOD)[0] == pytest.approx(13_311) + # $17,751 CMS Medicare spending per enrollee (2025) - $4,440 gross Part B + # - 0.8191 x $220.25 expected Part D premium. + assert sim.calculate("medicare_cost", PERIOD)[0] == pytest.approx(13_130.59) def test_medicare_irmaa_magi_two_years_prior_falls_back_to_lagged_income(): diff --git a/policyengine_us/variables/gov/hhs/medicare/costs/medicare_cost.py b/policyengine_us/variables/gov/hhs/medicare/costs/medicare_cost.py index 29da4f02627..d2b7e45b123 100644 --- a/policyengine_us/variables/gov/hhs/medicare/costs/medicare_cost.py +++ b/policyengine_us/variables/gov/hhs/medicare/costs/medicare_cost.py @@ -7,26 +7,38 @@ class medicare_cost(Variable): label = "Medicare cost" documentation = ( "Annual value of Medicare benefits received. Calculated as total " - "Medicare spending on behalf of the beneficiary minus premiums paid. " + "Medicare spending on behalf of the beneficiary, which covers Parts A " + "through D, minus the premiums that finance it: the Part A premium, " + "the gross Part B premium and the expected Part D premium. " "This represents the net benefit value of Medicare coverage." ) unit = USD definition_period = YEAR - reference = "https://www.cms.gov/about-cms/what-we-do/medicare" + reference = ( + "https://www.cms.gov/about-cms/what-we-do/medicare", + "https://www.cms.gov/oact/tr/2026#page=116", + ) defined_for = "medicare_enrolled" def formula(person, period, parameters): + p = parameters(period).calibration.gov.hhs.medicare # Total Medicare spending on behalf of beneficiary - per_capita_spending = parameters( - period - ).calibration.gov.hhs.medicare.per_capita_cost + per_capita_spending = p.per_capita_cost # Premium offsets to Medicare program cost. Use the enrollment-gated # gross Part B premium before MSP offsets so MSP support does not # inflate Medicare's value. part_a_premium = person("base_part_a_premium", period) part_b_premium = person("gross_medicare_part_b_premium_if_enrolled", period) - total_premiums = part_a_premium + part_b_premium + # The spending includes Part D, which enrollee premiums partly finance. + # No variable records Part D enrollment, so subtract the expected + # Part D premium: the Part D enrollment share times the premium per + # Part D enrollee plus the person's income-related adjustment. + part_d_irmaa = person("income_adjusted_part_d_premium_surcharge", period) + part_d_premium = p.part_d.enrollment_share * ( + p.part_d.premiums_per_enrollee + part_d_irmaa + ) + total_premiums = part_a_premium + part_b_premium + part_d_premium # Net benefit = spending - premiums return max_(per_capita_spending - total_premiums, 0) From 4024d2ec08b7b970bfcea89d5873753f6773b790 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 15:09:29 -0400 Subject: [PATCH 5/8] Test the Part D premium series and medicare_cost's Part D offset Transcription from the Trustees' cells, conservation back to Table III.D3 premium income, bounds, the 2036-2100 extension's closed form, and a Hypothesis differential test of medicare_cost against a reference implementation (0 <= net <= gross, non-increasing in Part D IRMAA). Co-Authored-By: Claude Opus 5.5 --- .../core/test_medicare_cost_part_d_premium.py | 261 ++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 policyengine_us/tests/core/test_medicare_cost_part_d_premium.py diff --git a/policyengine_us/tests/core/test_medicare_cost_part_d_premium.py b/policyengine_us/tests/core/test_medicare_cost_part_d_premium.py new file mode 100644 index 00000000000..a9dc443b21d --- /dev/null +++ b/policyengine_us/tests/core/test_medicare_cost_part_d_premium.py @@ -0,0 +1,261 @@ +"""The expected Part D premium that ``medicare_cost`` subtracts. + +CMS Medicare spending per enrollee (``calibration.gov.hhs.medicare. +per_capita_cost``) covers Parts A through D, gross of the premiums that +finance it. No variable records Part D enrollment, so ``medicare_cost`` +subtracts an expected Part D premium: + + enrollment_share x (premiums_per_enrollee + Part D IRMAA) + +from two series under ``calibration.gov.hhs.medicare.part_d``, both from the +2026 Medicare Trustees Report: + +- ``premiums_per_enrollee``: Table III.D3 premium income (footnote 6's + December 31 credits moved back a year) less Table V.E4's aggregate + income-related premiums, over Table V.B3's Part D enrollment. +- ``enrollment_share``: Table V.B3's Part D enrollment over its total, people + with Part A and/or Part B coverage. + +Invariants: + +1. Transcription: every value equals its formula applied to the Trustees' + published cells, rounded as stored. +2. Conservation: premiums per Part D enrollee times Part D enrollment, plus + aggregate IRMAA, returns Table III.D3's premium income in every year from + 2015 to 2035, within the tables' rounding. +3. Bounds: the share lies strictly between 0 and 1 and premiums are + positive in every year from 2015 to 2200. The monthly premium per Part D + enrollee is below the Trustees' average paid premium in 2025 and 2026, + since low-income subsidy enrollees pay less or nothing. +4. Extension: each year from 2036 to 2100 equals the 2035 value times + (722.05 / 699.07) ** (year - 2035), and later years hold the 2100 value. + That growth is within half a point of the Trustees' 2035 growth in Part D + cost per beneficiary (Table V.D1). +5. Calendar years: every date in a year reads the January 1 value; between + the Trustees' five-year enrollment points the share holds the earlier one. +6. Consumer (Hypothesis, differential): ``medicare_cost`` equals a reference + implementation, max(gross - Part A - Part B - share x (premium + IRMAA), + 0), for any year and premiums; it lies in [0, ``medicare_gross_cost``], + which does not change with the Part D premium; and it does not rise with + the person's Part D IRMAA. +""" + +import pytest +from hypothesis import 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.system import system + +PREMIUM = "calibration.gov.hhs.medicare.part_d.premiums_per_enrollee" +SHARE = "calibration.gov.hhs.medicare.part_d.enrollment_share" +FIRST_YEAR, LAST_PROJECTED_YEAR, END_YEAR = 2015, 2035, 2100 +PROJECTED_YEARS = range(FIRST_YEAR, LAST_PROJECTED_YEAR + 1) + +# 2026 Medicare Trustees Report (sha256 ffa56b91...8fa8). +# Table III.D3 (file page 116), premium income, $ billions, cash basis. +PREMIUM_INCOME = dict( + zip( + PROJECTED_YEARS, + ( + *(12.7, 13.8, 15.5, 15.9, 15.7, 15.8, 17.0, 17.6, 18.3, 19.2, 14.9), + *(20.1, 25.6, 28.8, 30.5, 51.7, 54.9, 56.4, 59.5, 62.0, 65.3), + ), + ) +) +# Footnote 6 (file page 117): premiums withheld from January benefits that +# were credited on December 31 of the year before, $ billions. +DECEMBER_31_CREDIT = {2015: 0.2, 2020: 0.1, 2026: 0.2} +# Table V.E4 (file page 218), aggregate income-related premiums, $ billions. +IRMAA_AGGREGATE = dict( + zip( + PROJECTED_YEARS, + ( + *(0.9, 1.0, 1.2, 1.4, 1.6, 1.8, 1.8, 1.7, 2.2, 2.2, 2.4, 3.1, 3.5), + *(4.0, 4.6, 10.8, 11.9, 12.6, 13.7, 14.7, 15.9), + ), + ) +) +# Table V.B3 (file page 196), enrollment in thousands: annual through 2035, +# then every fifth year. +ENROLLMENT_YEARS = (*PROJECTED_YEARS, *range(2040, END_YEAR + 1, 5)) +PART_D_ENROLLMENT = dict( + zip( + ENROLLMENT_YEARS, + ( + *(41_786, 43_198, 44_475, 45_798, 47_179, 48_696, 49_976, 51_386), + *(52_893, 55_249, 56_754, 57_973, 59_700, 61_448, 62_975, 64_201), + *(65_224, 66_111, 66_899, 67_661, 68_416, 70_471, 71_528, 72_969), + *(75_043, 77_612, 80_085, 82_775, 85_396, 87_208, 88_382, 88_728), + *(88_845, 89_095), + ), + ) +) +TOTAL_ENROLLMENT = dict( + zip( + ENROLLMENT_YEARS, + ( + *(55_589, 57_073, 58_683, 60_020, 61_535, 62_887, 63_980, 65_166), + *(66_606, 68_066, 69_289, 70_743, 72_322, 74_026, 75_436, 76_598), + *(77_521, 78_327, 79_058, 79_827, 80_632, 82_606, 83_648, 85_413), + *(87_929, 90_916, 93_612, 96_634, 99_585, 101_485, 102_662), + *(102_816, 102_848, 103_133), + ), + ) +) +# Average paid premium per month (file page 217). +AVERAGE_PAID_PREMIUM = {2025: 23.95, 2026: 27.22} +# Table V.D1 (file page 210), Part D average per beneficiary cost. +PART_D_COST_PER_BENEFICIARY = {2034: 4_919, 2035: 5_074} + +years = st.integers(FIRST_YEAR, END_YEAR) + + +def _read(name, date): + return get_parameter(system.parameters, name)(date) + + +def _premium(year): + return _read(PREMIUM, f"{year}-01-01") + + +def _share(year): + return _read(SHARE, f"{year}-01-01") + + +def _accrual_premium_income(year): + return ( + PREMIUM_INCOME[year] + - DECEMBER_31_CREDIT.get(year, 0) + + DECEMBER_31_CREDIT.get(year - 1, 0) + ) + + +@pytest.mark.parametrize("year", PROJECTED_YEARS) +def test_premium_is_trustees_premium_income_net_of_irmaa_per_part_d_enrollee(year): + billions = _accrual_premium_income(year) - IRMAA_AGGREGATE[year] + expected = billions * 1e9 / (PART_D_ENROLLMENT[year] * 1e3) + assert _premium(year) == round(expected, 2) + + +@pytest.mark.parametrize("year", ENROLLMENT_YEARS) +def test_share_is_trustees_part_d_over_total_enrollment(year): + assert _share(year) == round(PART_D_ENROLLMENT[year] / TOTAL_ENROLLMENT[year], 4) + + +@pytest.mark.parametrize("year", PROJECTED_YEARS) +def test_premiums_and_irmaa_return_trustees_premium_income(year): + """Conservation: the series, scaled back up by enrollment, plus IRMAA + gives the published premium income, within rounding of the cells.""" + rebuilt = _premium(year) * PART_D_ENROLLMENT[year] * 1e3 / 1e9 + rebuilt += IRMAA_AGGREGATE[year] + # Each cell is rounded to $0.1 billion, the stored value to the cent. + assert rebuilt == pytest.approx(_accrual_premium_income(year), abs=1e-3) + assert abs(rebuilt - PREMIUM_INCOME[year]) <= 0.2 + 1e-3 + + +@pytest.mark.parametrize("year", sorted(AVERAGE_PAID_PREMIUM)) +def test_monthly_premium_is_below_the_average_paid_premium(year): + assert _premium(year) / 12 < AVERAGE_PAID_PREMIUM[year] + + +@given(st.integers(FIRST_YEAR, END_YEAR + 100)) +def test_share_and_premium_are_bounded(year): + assert 0 < _share(year) < 1 + assert _premium(year) > 0 + + +def test_extension_rate_follows_trustees_part_d_cost_growth(): + growth = _premium(LAST_PROJECTED_YEAR + 1) / _premium(LAST_PROJECTED_YEAR) + trustees = PART_D_COST_PER_BENEFICIARY[2035] / PART_D_COST_PER_BENEFICIARY[2034] + assert abs(growth - trustees) < 0.005 + + +@given(st.integers(LAST_PROJECTED_YEAR + 1, END_YEAR)) +def test_extension_is_the_closed_form_of_the_2034_2035_growth(year): + growth = _premium(LAST_PROJECTED_YEAR) / _premium(LAST_PROJECTED_YEAR - 1) + expected = _premium(LAST_PROJECTED_YEAR) * growth ** (year - LAST_PROJECTED_YEAR) + assert _premium(year) == pytest.approx(expected, rel=1e-9) + + +@given(st.integers(END_YEAR, END_YEAR + 100)) +def test_series_hold_the_2100_values_afterwards(year): + assert _premium(year) == _premium(END_YEAR) + assert _share(year) == _share(END_YEAR) + + +@given(years, st.integers(1, 12), st.integers(1, 28)) +def test_every_date_in_a_year_reads_the_january_value(year, month, day): + date = f"{year}-{month:02d}-{day:02d}" + assert _read(PREMIUM, date) == _premium(year) + assert _read(SHARE, date) == _share(year) + + +@given(st.integers(LAST_PROJECTED_YEAR, END_YEAR)) +def test_share_holds_the_latest_trustees_point(year): + point = max(y for y in ENROLLMENT_YEARS if y <= year) + assert _share(year) == _share(point) + + +def _simulation(year, people): + names = list(people) + return Simulation( + situation={ + "people": people, + "tax_units": {f"tax_unit_{name}": {"members": [name]} for name in names}, + "spm_units": {"spm_unit": {"members": names}}, + "families": {"family": {"members": names}}, + "marital_units": { + f"marital_unit_{name}": {"members": [name]} for name in names + }, + "households": {"household": {"members": names, "state_code": {year: "CA"}}}, + } + ) + + +def _enrollee(year, part_a, part_b, irmaa): + return { + "age": {year: 70}, + "is_medicare_eligible": {year: True}, + "medicare_enrolled": {year: True}, + "base_part_a_premium": {year: part_a}, + "gross_medicare_part_b_premium": {year: part_b}, + "income_adjusted_part_d_premium_surcharge": {year: irmaa}, + } + + +def _reference_medicare_cost(year, part_a, part_b, irmaa): + gross = _read("calibration.gov.hhs.medicare.per_capita_cost", f"{year}-01-01") + part_d = _share(year) * (_premium(year) + irmaa) + return max(gross - part_a - part_b - part_d, 0) + + +premiums = st.floats(0, 40_000, allow_nan=False) +irmaas = st.floats(0, 12_000, allow_nan=False) + + +@settings(max_examples=25, deadline=None) +@given(years, st.floats(0, 12_000, allow_nan=False), premiums, irmaas, irmaas) +def test_medicare_cost_matches_reference_and_bounds( + year, part_a, part_b, irmaa, more_irmaa +): + low, high = sorted((irmaa, irmaa + more_irmaa)) + simulation = _simulation( + year, + { + "low": _enrollee(year, part_a, part_b, low), + "high": _enrollee(year, part_a, part_b, high), + }, + ) + net = simulation.calculate("medicare_cost", year) + gross = simulation.calculate("medicare_gross_cost", year) + # Variables are float32, so compare to single precision. + for value, surcharge in zip(net, (low, high)): + expected = _reference_medicare_cost(year, part_a, part_b, surcharge) + assert value == pytest.approx(expected, rel=1e-6, abs=0.01) + assert (net >= 0).all() + assert (net <= gross).all() + # The gross cost subtracts no premium, so both people have the same one. + assert gross[0] == gross[1] + assert net[1] <= net[0] From 1d7e9f97f5fbd21336c9702b5f4793ac00376dd4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 00:01:41 +0000 Subject: [PATCH 6/8] Subtract the person's Part D IRMAA from medicare_cost in full #10115 made income_adjusted_part_d_premium_surcharge zero for anyone not enrolled in Part D (medicare_part_d_enrolled). Multiplying it by the Part D enrollment share then counted non-enrollment twice once take-up is modeled, and charged an enrolled person only 82 percent of the adjustment they pay. The share now scales only the average Part D premium, which matches spending per enrollee averaged over every Medicare enrollee; the person's own adjustment amount is subtracted as computed, as the Part B one is. Co-Authored-By: Claude Opus 5.5 --- .../medicare-cost-part-d-premiums.fixed.md | 2 +- .../core/test_medicare_cost_part_d_premium.py | 35 ++++++++++----- .../gov/hhs/medicare/costs/medicare_cost.yaml | 44 ++++++++++++++----- .../gov/hhs/medicare/costs/medicare_cost.py | 27 ++++++++---- 4 files changed, 76 insertions(+), 32 deletions(-) diff --git a/changelog.d/medicare-cost-part-d-premiums.fixed.md b/changelog.d/medicare-cost-part-d-premiums.fixed.md index b7989573aaa..958f4bf71d5 100644 --- a/changelog.d/medicare-cost-part-d-premiums.fixed.md +++ b/changelog.d/medicare-cost-part-d-premiums.fixed.md @@ -1 +1 @@ -Subtract the expected Medicare Part D premium, the Part D enrollment share times Part D premiums per Part D enrollee plus the person's Part D IRMAA, from `medicare_cost`, whose CMS spending per enrollee includes Part D. +Subtract Medicare Part D premiums from `medicare_cost`, whose CMS spending per enrollee includes Part D: the average Part D premium per Medicare enrollee from the 2026 Medicare Trustees Report, plus the person's own Part D income-related monthly adjustment amount. diff --git a/policyengine_us/tests/core/test_medicare_cost_part_d_premium.py b/policyengine_us/tests/core/test_medicare_cost_part_d_premium.py index a9dc443b21d..527a3e85644 100644 --- a/policyengine_us/tests/core/test_medicare_cost_part_d_premium.py +++ b/policyengine_us/tests/core/test_medicare_cost_part_d_premium.py @@ -1,14 +1,18 @@ -"""The expected Part D premium that ``medicare_cost`` subtracts. +"""The Part D premium that ``medicare_cost`` subtracts. CMS Medicare spending per enrollee (``calibration.gov.hhs.medicare. -per_capita_cost``) covers Parts A through D, gross of the premiums that -finance it. No variable records Part D enrollment, so ``medicare_cost`` -subtracts an expected Part D premium: +per_capita_cost``) covers Parts A through D, averaged over every Medicare +enrollee, gross of the premiums that finance it. ``medicare_cost`` subtracts +the Part D premium averaged the same way, plus the person's own +income-related monthly adjustment amount (IRMAA): - enrollment_share x (premiums_per_enrollee + Part D IRMAA) + enrollment_share x premiums_per_enrollee + Part D IRMAA -from two series under ``calibration.gov.hhs.medicare.part_d``, both from the -2026 Medicare Trustees Report: +The IRMAA is subtracted in full: ``income_adjusted_part_d_premium_surcharge`` +is already zero for anyone not enrolled in Part D, so scaling it by the share +would count non-enrollment twice. The two series are under +``calibration.gov.hhs.medicare.part_d``, both from the 2026 Medicare Trustees +Report: - ``premiums_per_enrollee``: Table III.D3 premium income (footnote 6's December 31 credits moved back a year) less Table V.E4's aggregate @@ -34,10 +38,12 @@ 5. Calendar years: every date in a year reads the January 1 value; between the Trustees' five-year enrollment points the share holds the earlier one. 6. Consumer (Hypothesis, differential): ``medicare_cost`` equals a reference - implementation, max(gross - Part A - Part B - share x (premium + IRMAA), - 0), for any year and premiums; it lies in [0, ``medicare_gross_cost``], - which does not change with the Part D premium; and it does not rise with - the person's Part D IRMAA. + implementation, max(gross - Part A - Part B - share x premium - IRMAA, 0), + for any year and premiums; it lies in [0, ``medicare_gross_cost``], which + does not change with the Part D premium. +7. Accounting (Hypothesis): the person's Part D IRMAA lowers + ``medicare_cost`` dollar for dollar until the zero floor, as it raises + what the person pays; the enrollment share does not scale it. """ import pytest @@ -227,7 +233,7 @@ def _enrollee(year, part_a, part_b, irmaa): def _reference_medicare_cost(year, part_a, part_b, irmaa): gross = _read("calibration.gov.hhs.medicare.per_capita_cost", f"{year}-01-01") - part_d = _share(year) * (_premium(year) + irmaa) + part_d = _share(year) * _premium(year) + irmaa return max(gross - part_a - part_b - part_d, 0) @@ -259,3 +265,8 @@ def test_medicare_cost_matches_reference_and_bounds( # The gross cost subtracts no premium, so both people have the same one. assert gross[0] == gross[1] assert net[1] <= net[0] + # Accounting: above the zero floor, more IRMAA lowers the value dollar for + # dollar, so the enrollment share does not scale it. Float32 values near + # the 2100 spending level carry about 0.1 of rounding. + if net[1] > 0: + assert net[0] - net[1] == pytest.approx(high - low, abs=0.02 + 1e-6 * gross[0]) diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/costs/medicare_cost.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/costs/medicare_cost.yaml index 504cd704414..52d793f2cd9 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/costs/medicare_cost.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/costs/medicare_cost.yaml @@ -1,6 +1,7 @@ -# The expected Part D premium is the Part D enrollment share times (Part D -# premiums per Part D enrollee + the person's Part D IRMAA). 2025: 0.8191 x -# $220.25 = $180.41 without IRMAA. +# The Part D premium is the average per Medicare enrollee, the Part D +# enrollment share times Part D premiums per Part D enrollee (2025: 0.8191 x +# $220.25 = $180.41), plus the person's own Part D IRMAA. Sources: 2026 +# Medicare Trustees Report, Tables III.D3, V.B3 and V.E4. - name: unit test 1 - enrolled, premium-free Part A period: 2025 @@ -45,9 +46,9 @@ 2023: 0 filing_status: SINGLE output: - # $17,751 - $7,546.80 Part B with IRMAA - 0.8191 x ($220.25 + $85.80 x 12 - # Part D IRMAA) = $1,023.75 Part D (2025) - medicare_cost: 9_180.45 + # $17,751 - $7,546.80 Part B with IRMAA - $180.41 Part D - $85.80 x 12 + # = $1,029.60 Part D IRMAA (2025) + medicare_cost: 8_994.19 - name: unit test 4 - eligible but not enrolled period: 2025 @@ -100,8 +101,8 @@ gross_medicare_part_b_premium: 2_220 output: income_adjusted_part_d_premium_surcharge: 423.6 - # $17,751 - $2,220 Part B - 0.8191 x ($220.25 + $423.60) = $527.38 Part D - medicare_cost: 15_003.62 + # $17,751 - $2,220 Part B - $180.41 Part D - $423.60 Part D IRMAA + medicare_cost: 14_926.99 - name: unit test 8 - projected year period: 2027 @@ -126,8 +127,9 @@ gross_medicare_part_b_premium: 3_000 income_adjusted_part_d_premium_surcharge: 1_000 output: - # $23,789 - $3,000 Part B - 0.8382 x ($637.06 + $1,000) = $1,372.18 Part D - medicare_cost: 19_416.82 + # $23,789 - $3,000 Part B - 0.8382 x $637.06 = $533.98 Part D - $1,000 + # Part D IRMAA (2030) + medicare_cost: 19_255.02 - name: unit test 10 - premiums above spending floor the value at zero period: 2025 @@ -149,3 +151,25 @@ income_adjusted_part_d_premium_surcharge: 1_029.6 output: medicare_gross_cost: 17_751 + +# A Medicare enrollee whose drug coverage is not Part D owes no Part D IRMAA +# (income_adjusted_part_d_premium_surcharge is defined for +# medicare_part_d_enrolled), but still has the average Part D premium +# subtracted, because the spending averages Part D over every enrollee. +- name: unit test 12 - high income, not enrolled in Part D + period: 2025 + absolute_error_margin: 0.01 + input: + age: 65 + is_medicare_eligible: true + medicare_quarters_of_coverage: 40 + takes_up_medicare_part_d_if_eligible: false + adjusted_gross_income: + 2023: 500_000 # IRMAA uses income from 2 years prior + tax_exempt_interest_income: + 2023: 0 + filing_status: SINGLE + output: + income_adjusted_part_d_premium_surcharge: 0 + # $17,751 - $7,546.80 Part B with IRMAA - $180.41 Part D (2025) + medicare_cost: 10_023.79 diff --git a/policyengine_us/variables/gov/hhs/medicare/costs/medicare_cost.py b/policyengine_us/variables/gov/hhs/medicare/costs/medicare_cost.py index d2b7e45b123..ed734b03c9a 100644 --- a/policyengine_us/variables/gov/hhs/medicare/costs/medicare_cost.py +++ b/policyengine_us/variables/gov/hhs/medicare/costs/medicare_cost.py @@ -9,8 +9,10 @@ class medicare_cost(Variable): "Annual value of Medicare benefits received. Calculated as total " "Medicare spending on behalf of the beneficiary, which covers Parts A " "through D, minus the premiums that finance it: the Part A premium, " - "the gross Part B premium and the expected Part D premium. " - "This represents the net benefit value of Medicare coverage." + "the gross Part B premium, the average Part D premium per Medicare " + "enrollee, and the person's own Part D income-related monthly " + "adjustment amount. This represents the net benefit value of " + "Medicare coverage." ) unit = USD definition_period = YEAR @@ -30,15 +32,22 @@ def formula(person, period, parameters): # inflate Medicare's value. part_a_premium = person("base_part_a_premium", period) part_b_premium = person("gross_medicare_part_b_premium_if_enrolled", period) - # The spending includes Part D, which enrollee premiums partly finance. - # No variable records Part D enrollment, so subtract the expected - # Part D premium: the Part D enrollment share times the premium per - # Part D enrollee plus the person's income-related adjustment. + # The spending includes Part D averaged over every Medicare enrollee, + # and enrollee premiums finance part of it. Subtract the Part D + # premium averaged the same way: the Part D enrollment share times + # premiums per Part D enrollee, which exclude the income-related + # monthly adjustment amount. + average_part_d_premium = ( + p.part_d.enrollment_share * p.part_d.premiums_per_enrollee + ) + # The adjustment amount is the person's own, and is zero for anyone + # not enrolled in Part D (medicare_part_d_enrolled), so it is + # subtracted in full: scaling it by the enrollment share would count + # non-enrollment twice. part_d_irmaa = person("income_adjusted_part_d_premium_surcharge", period) - part_d_premium = p.part_d.enrollment_share * ( - p.part_d.premiums_per_enrollee + part_d_irmaa + total_premiums = ( + part_a_premium + part_b_premium + average_part_d_premium + part_d_irmaa ) - total_premiums = part_a_premium + part_b_premium + part_d_premium # Net benefit = spending - premiums return max_(per_capita_spending - total_premiums, 0) From 437e8e59930bf19617915ba6710f9b6aa308817c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 00:06:43 +0000 Subject: [PATCH 7/8] Cite the 2026 Trustees Report once per file and fix a page cite The Part D series listed the report in several reference entries that differed only by #page=; CLAUDE.md asks for one entry per source with the pages in its title. The 2030 base beneficiary premium reset is on file page 154, not 152, and file page 219 says low-income subsidy enrollees pay substantially reduced premiums, not none. The December 31 credits move to the following year, not back a year. Co-Authored-By: Claude Opus 5.5 --- .../hhs/medicare/part_d/enrollment_share.yaml | 6 +++--- .../medicare/part_d/premiums_per_enrollee.yaml | 17 ++++++----------- .../core/test_medicare_cost_part_d_premium.py | 2 +- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d/enrollment_share.yaml b/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d/enrollment_share.yaml index 6cf55361039..510c5097b88 100644 --- a/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d/enrollment_share.yaml +++ b/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d/enrollment_share.yaml @@ -1,4 +1,4 @@ -description: The Medicare Trustees report and project this share of Medicare enrollees, people with Part A or Part B coverage, who are enrolled in Part D. +description: The Medicare Trustees' reported and projected enrollment gives this share of Medicare enrollees, people with Part A or Part B coverage, who are enrolled in Part D. metadata: unit: /1 period: year @@ -12,8 +12,8 @@ metadata: # until the next. 2026 Medicare Trustees 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.B3 - href: https://www.cms.gov/oact/tr/2026#page=196 + - title: "2026 Annual Report of the Boards of Trustees of the Federal Hospital Insurance and Federal Supplementary Medical Insurance Trust Funds, Tables IV.B7 and V.B3 #page 153,196" + href: https://www.cms.gov/oact/tr/2026#page=153 values: # Historical. 2015-01-01: 0.7517 diff --git a/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d/premiums_per_enrollee.yaml b/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d/premiums_per_enrollee.yaml index 871164280c8..88d0d93b5b5 100644 --- a/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d/premiums_per_enrollee.yaml +++ b/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d/premiums_per_enrollee.yaml @@ -1,4 +1,4 @@ -description: The Medicare Trustees report and project these Part D premiums from enrollees per Part D enrollee, excluding the income-related monthly adjustment amount. +description: The Medicare Trustees' reported and projected Part D premium income and enrollment give these Part D premiums from enrollees per Part D enrollee, excluding the income-related monthly adjustment amount. metadata: unit: currency-USD period: year @@ -15,7 +15,7 @@ metadata: # footnote 6 (file page 117) credits one January's withheld premiums on # December 31 of the year before: about $0.2 billion in 2015, $0.1 billion # in 2020 and an estimated $0.2 billion in 2026. Those amounts are moved - # back to 2016, 2021 and 2027. + # to the following year: 2016, 2021 and 2027. # - Income-related premiums, Table V.E4 (file page 218), "Aggregate # premiums", the amount paid "in excess of the Part D plan premium". They # are part of premium income: they are withheld from benefits (42 U.S.C. @@ -25,17 +25,12 @@ metadata: # - Part D enrollment, Table V.B3 (file page 196), in thousands. It equals # the total of Table IV.B7 (file page 153), which includes retiree drug # subsidy beneficiaries. - # Low-income subsidy enrollees pay reduced premiums or none (file page 219); + # Low-income subsidy enrollees pay substantially reduced premiums (file + # page 219); # Medicare's low-income subsidy payments are expenditures of the Part D # account, not enrollee premiums (file page 114). So the average is below # the base beneficiary premium. - - title: 2026 Annual Report of the Boards of Trustees of the Federal Hospital Insurance and Federal Supplementary Medical Insurance Trust Funds, Table III.D3 - href: https://www.cms.gov/oact/tr/2026#page=116 - - title: 2026 Annual Report of the Boards of Trustees of the Federal Hospital Insurance and Federal Supplementary Medical Insurance Trust Funds, Table V.E4 - href: https://www.cms.gov/oact/tr/2026#page=218 - - title: 2026 Annual Report of the Boards of Trustees of the Federal Hospital Insurance and Federal Supplementary Medical Insurance Trust Funds, Table V.B3 - href: https://www.cms.gov/oact/tr/2026#page=196 - - title: 2026 Annual Report of the Boards of Trustees of the Federal Hospital Insurance and Federal Supplementary Medical Insurance Trust Funds, Part D revenue and expenditures + - title: "2026 Annual Report of the Boards of Trustees of the Federal Hospital Insurance and Federal Supplementary Medical Insurance Trust Funds, Tables III.D1, III.D3, V.B3, V.D1 and V.E4 #page 109,114,116-117,151,154,196,210,218-219" href: https://www.cms.gov/oact/tr/2026#page=109 - title: 42 U.S.C. § 1395w-113(c)(4) - Collection of monthly adjustment amount href: https://www.law.cornell.edu/uscode/text/42/1395w-113#c_4 @@ -56,7 +51,7 @@ values: 2025-01-01: 220.25 # Trustees' intermediate estimates. The 2030 jump is the Inflation # Reduction Act's reset of the base beneficiary premium percentage - # (file page 152). + # (file page 154). 2026-01-01: 289.79 2027-01-01: 373.53 2028-01-01: 403.59 diff --git a/policyengine_us/tests/core/test_medicare_cost_part_d_premium.py b/policyengine_us/tests/core/test_medicare_cost_part_d_premium.py index 527a3e85644..6c2379bcd05 100644 --- a/policyengine_us/tests/core/test_medicare_cost_part_d_premium.py +++ b/policyengine_us/tests/core/test_medicare_cost_part_d_premium.py @@ -15,7 +15,7 @@ Report: - ``premiums_per_enrollee``: Table III.D3 premium income (footnote 6's - December 31 credits moved back a year) less Table V.E4's aggregate + December 31 credits moved to the following year) less Table V.E4's aggregate income-related premiums, over Table V.B3's Part D enrollment. - ``enrollment_share``: Table V.B3's Part D enrollment over its total, people with Part A and/or Part B coverage. From 1b77458c7ac4ee43765696ab7db1d16be871ff82 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 5 Nov 2026 11:29:51 -0500 Subject: [PATCH 8/8] Name the Part D average's limits and pin its extension - Say in `medicare_cost`'s documentation that every enrollee nets the same average premium. - Name retiree drug subsidy beneficiaries in both labels, and add page 153 to the premium reference. - Drop the test's reference equality, keeping the bounds and the IRMAA slope. - Add a 2036 case that pins where the premium's extension starts. Co-Authored-By: Claude Opus 5.5 --- .../hhs/medicare/part_d/enrollment_share.yaml | 2 +- .../medicare/part_d/premiums_per_enrollee.yaml | 4 ++-- .../core/test_medicare_cost_part_d_premium.py | 14 +------------- .../gov/hhs/medicare/costs/medicare_cost.yaml | 17 +++++++++++++++++ .../gov/hhs/medicare/costs/medicare_cost.py | 5 ++++- 5 files changed, 25 insertions(+), 17 deletions(-) diff --git a/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d/enrollment_share.yaml b/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d/enrollment_share.yaml index 510c5097b88..f9d3062bdbb 100644 --- a/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d/enrollment_share.yaml +++ b/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d/enrollment_share.yaml @@ -2,7 +2,7 @@ description: The Medicare Trustees' reported and projected enrollment gives this metadata: unit: /1 period: year - label: Medicare Part D enrollment share + label: Medicare Part D enrollment share, including retiree drug subsidy beneficiaries reference: # Table V.B3, "Medicare Enrollment": Part D enrollment over total # enrollment, which footnote 2 defines as the "Number of beneficiaries with diff --git a/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d/premiums_per_enrollee.yaml b/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d/premiums_per_enrollee.yaml index 88d0d93b5b5..dea0b330731 100644 --- a/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d/premiums_per_enrollee.yaml +++ b/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d/premiums_per_enrollee.yaml @@ -2,7 +2,7 @@ description: The Medicare Trustees' reported and projected Part D premium income metadata: unit: currency-USD period: year - label: Medicare Part D premiums per Part D enrollee + label: Medicare Part D premiums per Part D enrollee, counting retiree drug subsidy beneficiaries as enrollees reference: # Each value is (premium income - aggregate income-related premiums) / # Part D enrollment, from the 2026 Medicare Trustees Report (transmitted @@ -30,7 +30,7 @@ metadata: # Medicare's low-income subsidy payments are expenditures of the Part D # account, not enrollee premiums (file page 114). So the average is below # the base beneficiary premium. - - title: "2026 Annual Report of the Boards of Trustees of the Federal Hospital Insurance and Federal Supplementary Medical Insurance Trust Funds, Tables III.D1, III.D3, V.B3, V.D1 and V.E4 #page 109,114,116-117,151,154,196,210,218-219" + - title: "2026 Annual Report of the Boards of Trustees of the Federal Hospital Insurance and Federal Supplementary Medical Insurance Trust Funds, Tables III.D1, III.D3, IV.B7, V.B3, V.D1 and V.E4 #page 109,114,116-117,151,153,154,196,210,218-219" href: https://www.cms.gov/oact/tr/2026#page=109 - title: 42 U.S.C. § 1395w-113(c)(4) - Collection of monthly adjustment amount href: https://www.law.cornell.edu/uscode/text/42/1395w-113#c_4 diff --git a/policyengine_us/tests/core/test_medicare_cost_part_d_premium.py b/policyengine_us/tests/core/test_medicare_cost_part_d_premium.py index 6c2379bcd05..f20823b4b4d 100644 --- a/policyengine_us/tests/core/test_medicare_cost_part_d_premium.py +++ b/policyengine_us/tests/core/test_medicare_cost_part_d_premium.py @@ -231,21 +231,13 @@ def _enrollee(year, part_a, part_b, irmaa): } -def _reference_medicare_cost(year, part_a, part_b, irmaa): - gross = _read("calibration.gov.hhs.medicare.per_capita_cost", f"{year}-01-01") - part_d = _share(year) * _premium(year) + irmaa - return max(gross - part_a - part_b - part_d, 0) - - premiums = st.floats(0, 40_000, allow_nan=False) irmaas = st.floats(0, 12_000, allow_nan=False) @settings(max_examples=25, deadline=None) @given(years, st.floats(0, 12_000, allow_nan=False), premiums, irmaas, irmaas) -def test_medicare_cost_matches_reference_and_bounds( - year, part_a, part_b, irmaa, more_irmaa -): +def test_medicare_cost_bounds_and_irmaa_slope(year, part_a, part_b, irmaa, more_irmaa): low, high = sorted((irmaa, irmaa + more_irmaa)) simulation = _simulation( year, @@ -256,10 +248,6 @@ def test_medicare_cost_matches_reference_and_bounds( ) net = simulation.calculate("medicare_cost", year) gross = simulation.calculate("medicare_gross_cost", year) - # Variables are float32, so compare to single precision. - for value, surcharge in zip(net, (low, high)): - expected = _reference_medicare_cost(year, part_a, part_b, surcharge) - assert value == pytest.approx(expected, rel=1e-6, abs=0.01) assert (net >= 0).all() assert (net <= gross).all() # The gross cost subtracts no premium, so both people have the same one. diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/costs/medicare_cost.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/costs/medicare_cost.yaml index 52d793f2cd9..73dceb0fd0d 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/costs/medicare_cost.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/costs/medicare_cost.yaml @@ -152,6 +152,23 @@ output: medicare_gross_cost: 17_751 +# From 2036 the premium grows at its 2034-2035 rate, and the share holds its +# 2035 value until 2040. +- name: unit test 13 - the premium extension starts after 2035 + period: 2036 + absolute_error_margin: 0.01 + input: + age: 65 + is_medicare_eligible: true + medicare_quarters_of_coverage: 40 + gross_medicare_part_b_premium: 3_000 + income_adjusted_part_d_premium_surcharge: 0 + output: + # Premium 722.05 x 722.05 / 699.07 = 745.79, times the 2035 share 0.8485 + # gives 632.80. 33,488.47 gross - 3,000 Part B - 632.80 Part D = 29,855.67. + medicare_gross_cost: 33_488.47 + medicare_cost: 29_855.67 + # A Medicare enrollee whose drug coverage is not Part D owes no Part D IRMAA # (income_adjusted_part_d_premium_surcharge is defined for # medicare_part_d_enrolled), but still has the average Part D premium diff --git a/policyengine_us/variables/gov/hhs/medicare/costs/medicare_cost.py b/policyengine_us/variables/gov/hhs/medicare/costs/medicare_cost.py index ed734b03c9a..eb48c24cd7e 100644 --- a/policyengine_us/variables/gov/hhs/medicare/costs/medicare_cost.py +++ b/policyengine_us/variables/gov/hhs/medicare/costs/medicare_cost.py @@ -12,7 +12,10 @@ class medicare_cost(Variable): "the gross Part B premium, the average Part D premium per Medicare " "enrollee, and the person's own Part D income-related monthly " "adjustment amount. This represents the net benefit value of " - "Medicare coverage." + "Medicare coverage. Every enrollee nets the same average Part D " + "premium, including low-income subsidy enrollees and people whose " + "drug coverage isn't Part D, so that term doesn't measure what a " + "given person pays." ) unit = USD definition_period = YEAR