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..958f4bf71d5 --- /dev/null +++ b/changelog.d/medicare-cost-part-d-premiums.fixed.md @@ -0,0 +1 @@ +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/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..f9d3062bdbb --- /dev/null +++ b/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d/enrollment_share.yaml @@ -0,0 +1,53 @@ +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 + 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 + # 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, 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 + 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..dea0b330731 --- /dev/null +++ b/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d/premiums_per_enrollee.yaml @@ -0,0 +1,68 @@ +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 + 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 + # 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 + # 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. + # 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 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, 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 +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 154). + 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 3e3950b835e..37caa4f6256 100644 --- a/policyengine_us/parameters/uprating_extensions.py +++ b/policyengine_us/parameters/uprating_extensions.py @@ -1181,6 +1181,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_cost_part_d_premium.py b/policyengine_us/tests/core/test_medicare_cost_part_d_premium.py new file mode 100644 index 00000000000..f20823b4b4d --- /dev/null +++ b/policyengine_us/tests/core/test_medicare_cost_part_d_premium.py @@ -0,0 +1,260 @@ +"""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, 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 + +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 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. + +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. +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 +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}, + } + + +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_bounds_and_irmaa_slope(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) + 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] + # 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/core/test_medicare_per_capita_cost.py b/policyengine_us/tests/core/test_medicare_per_capita_cost.py index d3d47ca3079..41a82587d2e 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 @@ -208,13 +210,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..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 @@ -1,5 +1,11 @@ +# 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 + absolute_error_margin: 0.01 input: age: 65 is_medicare_eligible: true @@ -9,10 +15,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 +30,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 +46,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 - $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 @@ -49,6 +61,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 +71,122 @@ 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: + # $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 - $180.41 Part D - $423.60 Part D IRMAA + medicare_cost: 14_926.99 + +- 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 = $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 + 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 + +# 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 +# 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: - medicare_cost: 14_751 # $17,751 spending - $3,000 Part B premium + 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/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..eb48c24cd7e 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,50 @@ 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. " - "This represents the net benefit value of Medicare coverage." + "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, 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. 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 - 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 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) + total_premiums = ( + part_a_premium + part_b_premium + average_part_d_premium + part_d_irmaa + ) # Net benefit = spending - premiums return max_(per_capita_spending - total_premiums, 0)