Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/medicare-per-enrollee-cms.fixed.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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 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). 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 (file page 34). sha256
# 9c5755666847a78103016f04da937326744071a9a6736f4e77e91ce9e7af8629.
- title: "CMS National Health Expenditure Accounts: Methodology Paper, 2024 #page 15,23,30,34"
href: https://www.cms.gov/files/document/definitions-sources-methods.pdf#page=15
# "Projections of Medicare spending and enrollment used in the NHE
# projections were generated for the most recent Trustees Report" (file
# page 7); file page 6 names the 2026 report for the economic assumptions.
# Last Updated June 24, 2026, sha256
# ce62fe2c236cc96ce287027784b141584ddf39e62c5fd8ddc3e771a014944817.
- title: "Projections of National Health Expenditures and Enrollment: Methodology and Model Specification #page 6-7"
href: https://www.cms.gov/research-statistics-data-and-systems/statistics-trends-and-reports/nationalhealthexpenddata/downloads/projectionsmethodology.pdf#page=6
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.
17 changes: 17 additions & 0 deletions policyengine_us/parameters/uprating_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 expenditures 2 percent through August 31, 2033 (2026
# Medicare Trustees Report, file page 209); its end lifts 2033 and 2034
# growth, and the Trustees' per-beneficiary growth falls from 6.6
# percent in 2034 to 5.4 percent in 2035 (Table V.D1, file page 210).
# So each year from 2035 to 2100 grows at the 2031-2032 rate, and later
# years hold the 2100 value.
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.
Expand Down
247 changes: 247 additions & 0 deletions policyengine_us/tests/core/test_medicare_per_capita_cost.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
"""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
)


# The year domains below are finite and small, so they are checked
# exhaustively rather than sampled.
@pytest.mark.parametrize("year", range(FIRST_YEAR, END_YEAR))
def test_series_is_positive_and_strictly_increasing(year):
"""Adjacent years, so strict order holds for every pair of years."""
assert 0 < _value(year) < _value(year + 1)


@pytest.mark.parametrize("year", range(LAST_PROJECTED_YEAR + 1, END_YEAR + 1))
def test_extension_is_the_closed_form_of_the_2031_2032_growth(year):
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)


@settings(deadline=None)
@given(st.integers(END_YEAR, END_YEAR + 1_000))
def test_series_holds_the_2100_value_afterwards(year):
assert _value(year) == _value(END_YEAR)


@settings(deadline=None)
@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
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Loading
Loading