From c5ab5b5d70166c4cf483b7e8ab890779529b4069 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 15:50:17 -0400 Subject: [PATCH 1/2] Count Medicare net of premiums in CBO household income CBO's standard household income counts Medicare at the government's average cost per enrollee, net of premiums paid by enrollees, and counts the Part D low-income subsidy separately as a means-tested transfer (CBO, January 2026, publication 61910; Distribution of Household Income in 2021, Appendix A; Habib 2018, Appendix B). The default list used medicare_gross_cost, which is gross of premiums and includes the subsidy. Add cbo_medicare_benefit: Medicare spending per enrollee less per-enrollee Part A, B and D premium income and the Part D low-income subsidy per Medicare enrollee, from the 2026 Medicare Trustees Report. Every enrollee gets the same value, as CBO assigns the average. Fix the medicare_gross_cost docstring, which said gross matched CBO. Co-Authored-By: Claude Opus 5.5 --- .../cbo-medicare-net-of-premiums.fixed.md | 1 + ...art_d_low_income_subsidy_per_enrollee.yaml | 54 +++++++++++++++++ .../premium_income_per_enrollee/part_a.yaml | 47 +++++++++++++++ .../premium_income_per_enrollee/part_b.yaml | 60 +++++++++++++++++++ .../premium_income_per_enrollee/part_d.yaml | 57 ++++++++++++++++++ .../cbo_social_insurance_benefits.yaml | 15 ++++- .../household/cbo_household_income.yaml | 13 ++-- .../medicare/costs/cbo_medicare_benefit.py | 36 +++++++++++ .../hhs/medicare/costs/medicare_gross_cost.py | 13 +++- ...cbo_household_social_insurance_benefits.py | 4 +- 10 files changed, 290 insertions(+), 10 deletions(-) create mode 100644 changelog.d/cbo-medicare-net-of-premiums.fixed.md create mode 100644 policyengine_us/parameters/calibration/gov/hhs/medicare/part_d_low_income_subsidy_per_enrollee.yaml create mode 100644 policyengine_us/parameters/calibration/gov/hhs/medicare/premium_income_per_enrollee/part_a.yaml create mode 100644 policyengine_us/parameters/calibration/gov/hhs/medicare/premium_income_per_enrollee/part_b.yaml create mode 100644 policyengine_us/parameters/calibration/gov/hhs/medicare/premium_income_per_enrollee/part_d.yaml create mode 100644 policyengine_us/variables/gov/hhs/medicare/costs/cbo_medicare_benefit.py diff --git a/changelog.d/cbo-medicare-net-of-premiums.fixed.md b/changelog.d/cbo-medicare-net-of-premiums.fixed.md new file mode 100644 index 00000000000..31b17d9bb0c --- /dev/null +++ b/changelog.d/cbo-medicare-net-of-premiums.fixed.md @@ -0,0 +1 @@ +The default CBO household income now counts Medicare as CBO does, at the average cost per enrollee net of premiums and excluding the Part D low-income subsidy, through a new `cbo_medicare_benefit` variable instead of gross Medicare spending. diff --git a/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d_low_income_subsidy_per_enrollee.yaml b/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d_low_income_subsidy_per_enrollee.yaml new file mode 100644 index 00000000000..6e84c8d8cac --- /dev/null +++ b/policyengine_us/parameters/calibration/gov/hhs/medicare/part_d_low_income_subsidy_per_enrollee.yaml @@ -0,0 +1,54 @@ +description: The Medicare Trustees report and project these Part D low-income subsidy payments per Medicare enrollee. +metadata: + unit: currency-USD + period: year + label: Medicare Part D low-income subsidy per enrollee + # From 2036, each value is the 2035 value times the growth in Medicare + # spending per enrollee, so the subsidy keeps its 2035 share of spending. + uprating: calibration.gov.hhs.medicare.per_capita_cost + reference: + # Each value is Table IV.B10's "Low-income subsidy" payments ($ billions, + # incurred basis) over Table V.B3's total enrollment (file page 196, + # thousands), people with Part A and/or Part B coverage, so it is per + # Medicare enrollee, not per subsidy recipient. 2016 to 2035 come from the + # 2026 report's Table IV.B10 (file page 159); 2015, which that table no + # longer shows, from the 2025 report's (file page 159). The two reports + # agree on 2016 to 2023. 2026 Medicare Trustees Report, transmitted + # 2026-06-09, retrieved 2026-10-11, sha256 + # ffa56b9137006872300b0346149eae1613d09a172b6ba118aad48e66dfc48fa8; 2025 + # report sha256 + # 1b3de7a4faeee5ad42059e10740603182b716a5fa7b53b30d360b45b9e474d95. + - title: 2026 Annual Report of the Boards of Trustees of the Federal Hospital Insurance and Federal Supplementary Medical Insurance Trust Funds, Table IV.B10 + href: https://www.cms.gov/oact/tr/2026#page=159 + - title: 2025 Annual Report of the Boards of Trustees of the Federal Hospital Insurance and Federal Supplementary Medical Insurance Trust Funds, Table IV.B10 + href: https://www.cms.gov/oact/tr/2025#page=159 + - 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: 460.52 + 2016-01-01: 462.57 + 2017-01-01: 465.21 + 2018-01-01: 474.84 + 2019-01-01: 482.65 + 2020-01-01: 524.75 + 2021-01-01: 547.05 + 2022-01-01: 604.61 + 2023-01-01: 663.6 + 2024-01-01: 605.29 + # 2025 is the first year of the full Inflation Reduction Act benefit + # redesign (file page 119). The subsidy per low-income enrollee fell from + # $2,833 to $1,226 while the direct subsidy per enrollee rose from $383 to + # $1,869 (Table IV.B9, file page 158). + 2025-01-01: 251.12 + # Trustees' intermediate estimates. + 2026-01-01: 255.86 + 2027-01-01: 283.45 + 2028-01-01: 298.54 + 2029-01-01: 324.78 + 2030-01-01: 377.29 + 2031-01-01: 396.02 + 2032-01-01: 404.71 + 2033-01-01: 409.83 + 2034-01-01: 417.15 + 2035-01-01: 431.59 diff --git a/policyengine_us/parameters/calibration/gov/hhs/medicare/premium_income_per_enrollee/part_a.yaml b/policyengine_us/parameters/calibration/gov/hhs/medicare/premium_income_per_enrollee/part_a.yaml new file mode 100644 index 00000000000..5049d213595 --- /dev/null +++ b/policyengine_us/parameters/calibration/gov/hhs/medicare/premium_income_per_enrollee/part_a.yaml @@ -0,0 +1,47 @@ +description: The Medicare Trustees report and project this Part A premium income per Medicare enrollee, which counts premiums paid by or on behalf of people without premium-free Part A who enroll voluntarily. +metadata: + unit: currency-USD + period: year + label: Medicare Part A premium income per enrollee + # From 2036, each value is the 2035 value times the growth in Medicare + # spending per enrollee, so premiums keep their 2035 share of spending. + uprating: calibration.gov.hhs.medicare.per_capita_cost + reference: + # Each value is Table III.B4's "Premiums from voluntary enrollees" (file + # page 64, $ billions, cash basis) over Table V.B3's total enrollment + # (file page 196, thousands), which footnote 2 defines as the "Number of + # beneficiaries with HI and/or SMI coverage". The Part A premium is paid + # "by or on behalf of" these enrollees (glossary, file page 257), so state + # Medicaid buy-in payments are included. 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 III.B4 + href: https://www.cms.gov/oact/tr/2026#page=64 + - 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, glossary, Part A premium + href: https://www.cms.gov/oact/tr/2026#page=257 +values: + # Historical. + 2015-01-01: 57.57 + 2016-01-01: 57.82 + 2017-01-01: 59.64 + 2018-01-01: 59.98 + 2019-01-01: 63.38 + 2020-01-01: 63.61 + 2021-01-01: 65.65 + 2022-01-01: 69.05 + 2023-01-01: 70.56 + 2024-01-01: 70.52 + 2025-01-01: 82.26 + # Trustees' intermediate estimates. + 2026-01-01: 97.54 + 2027-01-01: 102.32 + 2028-01-01: 106.72 + 2029-01-01: 112.68 + 2030-01-01: 117.5 + 2031-01-01: 125.13 + 2032-01-01: 131.5 + 2033-01-01: 140.4 + 2034-01-01: 149.07 + 2035-01-01: 156.27 diff --git a/policyengine_us/parameters/calibration/gov/hhs/medicare/premium_income_per_enrollee/part_b.yaml b/policyengine_us/parameters/calibration/gov/hhs/medicare/premium_income_per_enrollee/part_b.yaml new file mode 100644 index 00000000000..e70f8535fae --- /dev/null +++ b/policyengine_us/parameters/calibration/gov/hhs/medicare/premium_income_per_enrollee/part_b.yaml @@ -0,0 +1,60 @@ +description: The Medicare Trustees report and project this Part B premium income per Medicare enrollee, which counts premiums paid by or on behalf of enrollees, including income-related premiums and premiums that state Medicaid programs pay. +metadata: + unit: currency-USD + period: year + label: Medicare Part B premium income per enrollee + # From 2036, each value is the 2035 value times the growth in Medicare + # spending per enrollee, so premiums keep their 2035 share of spending. + uprating: calibration.gov.hhs.medicare.per_capita_cost + reference: + # Each value is premium income over Table V.B3's total enrollment (file + # page 196, thousands), people with Part A and/or Part B coverage. + # - 2015 to 2026: Table III.C7 (file page 102), "Premium income", $ + # millions, incurred basis. + # - 2027 to 2035: Table III.C4 (file page 96), "Premium income", $ + # billions, cash basis. Footnote 7 credits premiums withheld from the + # benefits due January 3, 2027 on December 31, 2026; their amount is + # III.C4's 2026 cash value less III.C7's 2026 incurred value (174.8 - + # 173.228 = 1.572), which is added back to 2027. Cash and incurred + # premium income agree in every year from 2017 to 2025 except 2020 and + # 2021, the other footnote 7 years. + # Premium income is "premium payments by (or on behalf of) enrollees" + # (file page 88). It includes income-related premiums: in 2019 the + # standard premium, $135.50 a month, times 56.019 million Part B enrollees + # plus Table V.E3's $8.4 billion of income-related premiums (file page + # 208) is $99.5 billion, against $99.4 billion of premium income. + # 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 III.C7 + href: https://www.cms.gov/oact/tr/2026#page=102 + - title: 2026 Annual Report of the Boards of Trustees of the Federal Hospital Insurance and Federal Supplementary Medical Insurance Trust Funds, Table III.C4 + href: https://www.cms.gov/oact/tr/2026#page=96 + - 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 B premium income + href: https://www.cms.gov/oact/tr/2026#page=88 +values: + # Historical. + 2015-01-01: 1_214.54 + 2016-01-01: 1_296.34 + 2017-01-01: 1_389.19 + 2018-01-01: 1_554.68 + 2019-01-01: 1_615.55 + 2020-01-01: 1_729.23 + 2021-01-01: 1_772.6 + 2022-01-01: 2_009.35 + 2023-01-01: 1_969.28 + 2024-01-01: 2_054.43 + 2025-01-01: 2_169.44 + # Trustees' intermediate estimates. + 2026-01-01: 2_448.69 + 2027-01-01: 2_549.32 + 2028-01-01: 2_755.79 + 2029-01-01: 2_946.87 + 2030-01-01: 3_178.93 + 2031-01-01: 3_409.4 + 2032-01-01: 3_662.85 + 2033-01-01: 3_985.68 + 2034-01-01: 4_334.37 + 2035-01-01: 4_652 diff --git a/policyengine_us/parameters/calibration/gov/hhs/medicare/premium_income_per_enrollee/part_d.yaml b/policyengine_us/parameters/calibration/gov/hhs/medicare/premium_income_per_enrollee/part_d.yaml new file mode 100644 index 00000000000..1c8af7fff7f --- /dev/null +++ b/policyengine_us/parameters/calibration/gov/hhs/medicare/premium_income_per_enrollee/part_d.yaml @@ -0,0 +1,57 @@ +description: The Medicare Trustees report and project this Part D premium income per Medicare enrollee, which counts premiums withheld from benefits, paid to the federal government or paid directly to plans, including income-related premiums. +metadata: + unit: currency-USD + period: year + label: Medicare Part D premium income per enrollee + # From 2036, each value is the 2035 value times the growth in Medicare + # spending per enrollee, so premiums keep their 2035 share of spending. + uprating: calibration.gov.hhs.medicare.per_capita_cost + reference: + # Each value is Table III.D3's "Premium income" (file page 116, $ + # billions, cash basis) over Table V.B3's total enrollment (file page 196, + # thousands), people with Part A and/or Part B coverage. 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." 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, which are moved to 2016, 2021 and 2027. Premium income includes + # income-related premiums, which are deposited into the Part D account + # (footnote 73, file page 151). So each value equals the Part D enrollment + # share times calibration.gov.hhs.medicare.part_d.premiums_per_enrollee, + # plus Table V.E4's aggregate income-related premiums (file page 218) per + # Medicare enrollee. 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 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.B3 + href: https://www.cms.gov/oact/tr/2026#page=196 +values: + # Historical. + 2015-01-01: 224.86 + 2016-01-01: 245.3 + 2017-01-01: 264.13 + 2018-01-01: 264.91 + 2019-01-01: 255.14 + 2020-01-01: 249.65 + 2021-01-01: 267.27 + 2022-01-01: 270.08 + 2023-01-01: 274.75 + 2024-01-01: 282.08 + # 2025 premiums fell with the voluntary Part D premium demonstration for + # stand-alone prescription drug plans (file page 109). + 2025-01-01: 215.04 + # 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: 281.3 + 2027-01-01: 356.74 + 2028-01-01: 389.05 + 2029-01-01: 404.32 + 2030-01-01: 674.95 + 2031-01-01: 708.2 + 2032-01-01: 720.06 + 2033-01-01: 752.61 + 2034-01-01: 776.68 + 2035-01-01: 809.85 diff --git a/policyengine_us/parameters/gov/household/cbo_social_insurance_benefits.yaml b/policyengine_us/parameters/gov/household/cbo_social_insurance_benefits.yaml index ac597fb7156..550ea17a0f1 100644 --- a/policyengine_us/parameters/gov/household/cbo_social_insurance_benefits.yaml +++ b/policyengine_us/parameters/gov/household/cbo_social_insurance_benefits.yaml @@ -2,7 +2,7 @@ description: Social insurance benefits included in CBO household income. values: 0000-01-01: - social_security - - medicare_gross_cost + - cbo_medicare_benefit - unemployment_compensation - workers_compensation @@ -10,3 +10,16 @@ metadata: unit: list period: year label: CBO social insurance benefits + reference: + # "Social insurance benefits consist of benefits provided through Social + # Security (Old Age, Survivors, and Disability Insurance); Medicare + # (measured as the average cost to the government of providing those + # benefits, net of offsetting receipts); regular unemployment insurance + # ...; and workers' compensation." (Appendix A, note 3.) + - title: Congressional Budget Office, The Distribution of Household Income in 2021, Appendix A + href: https://www.cbo.gov/system/files/2024-09/60341-income.pdf#page=27 + # "The measure of Medicare benefits in The Distribution of Household + # Income includes the federal government's average cost per enrollee of + # providing Medicare, net of premiums paid by enrollees." + - title: Congressional Budget Office, Sensitivity of the Distribution of Household Income to the Treatment of Health Insurance From 1979 to 2021 + href: https://www.cbo.gov/publication/61910 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 07d9f4f1d8e..d52fdf17f34 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 @@ -14,6 +14,7 @@ - name: CBO before-tax income adds social insurance and alias matches period: 2025 + absolute_error_margin: 0.01 input: employment_income: 40_000 employer_sponsored_insurance_premiums: 6_000 @@ -35,10 +36,14 @@ cbo_household_market_income: 49_102 # 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 + # $17,751 less 2025 premium income per Medicare enrollee ($82.26 Part A, + # $2,169.44 Part B, $215.04 Part D) and the $251.12 Part D low-income + # subsidy per Medicare enrollee. + cbo_medicare_benefit: 15_033.14 + # $12,000 + $15,033.14 + $2,000 + $1,000. + cbo_household_social_insurance_benefits: 30_033.14 + cbo_household_income_before_transfers_and_taxes: 79_135.14 + cbo_household_income: 79_135.14 - name: CBO means-tested transfers include only the listed programs period: 2026 diff --git a/policyengine_us/variables/gov/hhs/medicare/costs/cbo_medicare_benefit.py b/policyengine_us/variables/gov/hhs/medicare/costs/cbo_medicare_benefit.py new file mode 100644 index 00000000000..1a4e1d96df5 --- /dev/null +++ b/policyengine_us/variables/gov/hhs/medicare/costs/cbo_medicare_benefit.py @@ -0,0 +1,36 @@ +from policyengine_us.model_api import * + + +class cbo_medicare_benefit(Variable): + value_type = float + entity = Person + label = "CBO Medicare benefit" + documentation = ( + "Medicare benefit as the Congressional Budget Office counts it in " + "household income: the government's average cost per enrollee of " + "providing Medicare Parts A, B and D, net of premiums paid by or on " + "behalf of enrollees. CBO assigns the same average to every " + "enrollee and counts the Part D low-income subsidy separately as a " + "means-tested transfer, so this subtracts per-enrollee averages of " + "premium income and of the low-income subsidy from Medicare " + "spending per enrollee, rather than the person's own premiums." + ) + unit = USD + definition_period = YEAR + reference = ( + "https://www.cbo.gov/publication/61910", + "https://www.cbo.gov/system/files/2024-09/60341-income.pdf#page=27", + "https://www.cbo.gov/system/files/2018-07/54234-workingpaper.pdf#page=46", + ) + defined_for = "medicare_enrolled" + + def formula(person, period, parameters): + p = parameters(period).calibration.gov.hhs.medicare + premiums = p.premium_income_per_enrollee + total_premiums = premiums.part_a + premiums.part_b + premiums.part_d + net = ( + p.per_capita_cost + - total_premiums + - p.part_d_low_income_subsidy_per_enrollee + ) + return max_(net, 0) diff --git a/policyengine_us/variables/gov/hhs/medicare/costs/medicare_gross_cost.py b/policyengine_us/variables/gov/hhs/medicare/costs/medicare_gross_cost.py index 48533e92fb4..1baeeaf188a 100644 --- a/policyengine_us/variables/gov/hhs/medicare/costs/medicare_gross_cost.py +++ b/policyengine_us/variables/gov/hhs/medicare/costs/medicare_gross_cost.py @@ -7,12 +7,19 @@ class medicare_gross_cost(Variable): label = "Gross Medicare cost" documentation = ( "Annual Medicare spending on behalf of the beneficiary before " - "subtracting beneficiary premiums. This matches the gross Medicare " - "benefit concept used in the CBO household-income framework." + "subtracting beneficiary premiums: CMS Medicare spending per " + "enrollee, which covers Parts A through D, including the Part D " + "low-income subsidy. CBO's standard household income counts " + "Medicare net of premiums instead (cbo_medicare_benefit); CBO adds " + "the premiums back only in an expanded sensitivity measure, which " + "it calls gross Medicare spending." ) 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.cbo.gov/publication/61910", + ) defined_for = "medicare_enrolled" def formula(person, period, parameters): diff --git a/policyengine_us/variables/household/income/household/cbo_household_social_insurance_benefits.py b/policyengine_us/variables/household/income/household/cbo_household_social_insurance_benefits.py index b39c09e5db5..82ecc087d38 100644 --- a/policyengine_us/variables/household/income/household/cbo_household_social_insurance_benefits.py +++ b/policyengine_us/variables/household/income/household/cbo_household_social_insurance_benefits.py @@ -7,8 +7,8 @@ class cbo_household_social_insurance_benefits(Variable): label = "CBO household social insurance benefits" documentation = ( "Social insurance benefits included in CBO household income: Social " - "Security, gross Medicare benefits, unemployment insurance, and " - "workers' compensation." + "Security, Medicare benefits net of enrollee premiums, unemployment " + "insurance, and workers' compensation." ) definition_period = YEAR unit = USD From 3d8bb6d64327a418f56ea674d1690206ad1d20cf Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 16:15:40 -0400 Subject: [PATCH 2/2] Test cbo_medicare_benefit and its Trustees series YAML: the 2025, 2015 and 2035 values, and five people with different premiums (Part A premium, income-related premiums, under 65, not enrolled) who all get the same average. Python, parameters only: transcription and conservation against the Trustees cells, the Part D series against #10111's share x premium plus income-related premiums, Part B premium income including income-related premiums, bounds through 2200, the post-2035 extension keeping each series' share of spending, and January reads. Co-Authored-By: Claude Opus 5.5 --- .../premium_income_per_enrollee/part_b.yaml | 7 +- .../core/test_cbo_medicare_benefit_series.py | 220 ++++++++++++++++++ .../medicare/costs/cbo_medicare_benefit.yaml | 111 +++++++++ 3 files changed, 335 insertions(+), 3 deletions(-) create mode 100644 policyengine_us/tests/core/test_cbo_medicare_benefit_series.py create mode 100644 policyengine_us/tests/policy/baseline/gov/hhs/medicare/costs/cbo_medicare_benefit.yaml diff --git a/policyengine_us/parameters/calibration/gov/hhs/medicare/premium_income_per_enrollee/part_b.yaml b/policyengine_us/parameters/calibration/gov/hhs/medicare/premium_income_per_enrollee/part_b.yaml index e70f8535fae..53b67c103aa 100644 --- a/policyengine_us/parameters/calibration/gov/hhs/medicare/premium_income_per_enrollee/part_b.yaml +++ b/policyengine_us/parameters/calibration/gov/hhs/medicare/premium_income_per_enrollee/part_b.yaml @@ -20,9 +20,10 @@ metadata: # 2021, the other footnote 7 years. # Premium income is "premium payments by (or on behalf of) enrollees" # (file page 88). It includes income-related premiums: in 2019 the - # standard premium, $135.50 a month, times 56.019 million Part B enrollees - # plus Table V.E3's $8.4 billion of income-related premiums (file page - # 208) is $99.5 billion, against $99.4 billion of premium income. + # standard premium, $135.50 a month (Table III.C2, file page 89), times + # 56.019 million Part B enrollees plus Table V.E3's $8.4 billion of + # income-related premiums (file page 215) is $99.5 billion, against $99.4 + # billion of premium income. # 2026 Medicare Trustees Report, transmitted 2026-06-09, retrieved # 2026-10-11, sha256 # ffa56b9137006872300b0346149eae1613d09a172b6ba118aad48e66dfc48fa8. diff --git a/policyengine_us/tests/core/test_cbo_medicare_benefit_series.py b/policyengine_us/tests/core/test_cbo_medicare_benefit_series.py new file mode 100644 index 00000000000..c3da92714a1 --- /dev/null +++ b/policyengine_us/tests/core/test_cbo_medicare_benefit_series.py @@ -0,0 +1,220 @@ +"""The per-enrollee series that ``cbo_medicare_benefit`` subtracts. + +CBO counts Medicare in household income at the government's average cost +per enrollee, net of premiums paid by enrollees, and counts the Part D +low-income subsidy separately. ``cbo_medicare_benefit`` takes CMS Medicare +spending per enrollee (``calibration.gov.hhs.medicare.per_capita_cost``) and +subtracts four series under ``calibration.gov.hhs.medicare``, each an +aggregate from the 2026 Medicare Trustees Report over Table V.B3's total +enrollment (people with Part A and/or Part B): + +- ``premium_income_per_enrollee.part_a``: Table III.B4 premiums from + voluntary enrollees. +- ``premium_income_per_enrollee.part_b``: Table III.C7 incurred premium + income through 2026, then Table III.C4 cash premium income, with the + December 31, 2026 credit moved back to 2027. +- ``premium_income_per_enrollee.part_d``: Table III.D3 premium income with + footnote 6's December 31 credits moved back a year. +- ``part_d_low_income_subsidy_per_enrollee``: Table IV.B10 low-income + subsidy (the 2025 report for 2015). + +These tests read parameters only; the YAML tests cover the variable. + +Invariants: + +1. Transcription: every value equals its formula applied to the Trustees' + published cells, rounded as stored. +2. Conservation: each series times total enrollment returns its published + aggregate, within the cells' rounding. +3. Differential: the Part D series equals the Part D enrollment share times + ``part_d.premiums_per_enrollee`` (which excludes income-related premiums, + per Part D enrollee) plus Table V.E4's income-related premiums per + Medicare enrollee: two derivations from different cells agree. +4. Part B premium income includes income-related premiums: in 2019 the + standard premium times Part B enrollment plus Table V.E3's aggregate + income-related premiums reproduces it within 0.5 percent, while the + standard premium alone falls more than 5 percent short. +5. Bounds: every series is positive, and net Medicare spending per enrollee + stays between 0 and gross spending, in every year from 2015 to 2200. +6. Extension: from 2036 to 2100 each series grows with Medicare spending + per enrollee, so each keeps its 2035 share of spending; later years hold + the 2100 value. +7. Calendar years: every date in a year reads the January 1 value. +""" + +import pytest +from hypothesis import given +from hypothesis import strategies as st +from policyengine_core.parameters.operations.get_parameter import get_parameter + +from policyengine_us.system import system + +MEDICARE = "calibration.gov.hhs.medicare" +SERIES = { + "part_a": f"{MEDICARE}.premium_income_per_enrollee.part_a", + "part_b": f"{MEDICARE}.premium_income_per_enrollee.part_b", + "part_d": f"{MEDICARE}.premium_income_per_enrollee.part_d", + "lis": f"{MEDICARE}.part_d_low_income_subsidy_per_enrollee", +} +SPENDING = f"{MEDICARE}.per_capita_cost" +FIRST_YEAR, LAST_PROJECTED_YEAR, END_YEAR = 2015, 2035, 2100 +YEARS = range(FIRST_YEAR, LAST_PROJECTED_YEAR + 1) + + +def _cells(*values): + return dict(zip(YEARS, values)) + + +# 2026 Medicare Trustees Report (sha256 ffa56b91...8fa8), file pages. +# Table V.B3 (page 196), total enrollment, thousands. +TOTAL_ENROLLMENT = _cells( + *(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), +) +# Table III.B4 (page 64), premiums from voluntary enrollees, $ billions. +PART_A_PREMIUMS = _cells( + *(3.2, 3.3, 3.5, 3.6, 3.9, 4.0, 4.2, 4.5, 4.7, 4.8, 5.7), + *(6.9, 7.4, 7.9, 8.5, 9.0, 9.7, 10.3, 11.1, 11.9, 12.6), +) +# Table III.C7 (page 102), premium income, incurred basis, $ millions. +PART_B_INCURRED = dict( + zip( + range(2015, 2027), + ( + *(67_515, 73_986, 81_522, 93_312, 99_413, 108_746, 113_411), + *(130_941, 131_166, 139_837, 150_318, 173_228), + ), + ) +) +# Table III.C4 (page 96), premium income, cash basis, $ billions. +PART_B_CASH = _cells( + *(69.4, 72.1, 81.5, 93.3, 99.4, 111.2, 111.0, 130.9, 131.2, 139.8, 150.3), + *(174.8, 182.8, 204.0, 222.3, 243.5, 264.3, 286.9, 315.1, 346.0, 375.1), +) +# Table III.D3 (page 116), premium income, cash basis, $ billions, and +# footnote 6's December 31 credits (page 117). +PART_D_CASH = _cells( + *(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), +) +PART_D_DECEMBER_31_CREDIT = {2015: 0.2, 2020: 0.1, 2026: 0.2} +# Table IV.B10 (page 159), low-income subsidy, $ billions; 2015 from the +# 2025 report's Table IV.B10 (page 159). +LOW_INCOME_SUBSIDY = _cells( + *(25.6, 26.4, 27.3, 28.5, 29.7, 33.0, 35.0, 39.4, 44.2, 41.2, 17.4), + *(18.1, 20.5, 22.1, 24.5, 28.9, 30.7, 31.7, 32.4, 33.3, 34.8), +) +# Table V.E4 (page 218), Part D income-related premiums, $ billions. +PART_D_IRMAA = _cells( + *(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), +) +# 2019: Table III.C2 (page 89) standard monthly premium, Table V.B3 Part B +# enrollment (thousands), Table V.E3 (page 215) income-related premiums +# ($ billions). +STANDARD_PART_B_PREMIUM_2019 = 135.50 +PART_B_ENROLLMENT_2019 = 56_019 +PART_B_IRMAA_2019 = 8.4 + + +def _part_b_billions(year): + if year in PART_B_INCURRED: + return PART_B_INCURRED[year] / 1e3 + # III.C4 footnote 7: January 2027 withholdings credited December 31, 2026. + december_31_credit = PART_B_CASH[2026] - PART_B_INCURRED[2026] / 1e3 + return PART_B_CASH[year] + (december_31_credit if year == 2027 else 0) + + +def _part_d_billions(year): + return ( + PART_D_CASH[year] + - PART_D_DECEMBER_31_CREDIT.get(year, 0) + + PART_D_DECEMBER_31_CREDIT.get(year - 1, 0) + ) + + +AGGREGATES = { + "part_a": PART_A_PREMIUMS.get, + "part_b": _part_b_billions, + "part_d": _part_d_billions, + "lis": LOW_INCOME_SUBSIDY.get, +} + + +def _read(name, date): + return get_parameter(system.parameters, name)(date) + + +def _value(series, year): + return _read(SERIES[series], f"{year}-01-01") + + +def _spending(year): + return _read(SPENDING, f"{year}-01-01") + + +def _per_enrollee(billions, year): + return billions * 1e9 / (TOTAL_ENROLLMENT[year] * 1e3) + + +@pytest.mark.parametrize("series", SERIES) +@pytest.mark.parametrize("year", YEARS) +def test_series_is_the_trustees_aggregate_per_medicare_enrollee(series, year): + expected = _per_enrollee(AGGREGATES[series](year), year) + assert _value(series, year) == round(expected, 2) + + +@pytest.mark.parametrize("series", SERIES) +@pytest.mark.parametrize("year", YEARS) +def test_series_times_enrollment_returns_the_aggregate(series, year): + rebuilt = _value(series, year) * TOTAL_ENROLLMENT[year] * 1e3 / 1e9 + # Stored to the cent; enrollment of at most 81 million moves the + # aggregate by at most $0.0005 billion. + assert rebuilt == pytest.approx(AGGREGATES[series](year), abs=1e-3) + + +@pytest.mark.parametrize("year", YEARS) +def test_part_d_series_matches_share_times_premium_plus_irmaa(year): + share = _read(f"{MEDICARE}.part_d.enrollment_share", f"{year}-01-01") + premium = _read(f"{MEDICARE}.part_d.premiums_per_enrollee", f"{year}-01-01") + irmaa = _per_enrollee(PART_D_IRMAA[year], year) + # The share is stored to four decimals and the premium to the cent, so + # the product can be off by up to about $0.05. + assert share * premium + irmaa == pytest.approx(_value("part_d", year), abs=0.1) + + +def test_part_b_premium_income_includes_income_related_premiums(): + standard = STANDARD_PART_B_PREMIUM_2019 * 12 * PART_B_ENROLLMENT_2019 * 1e3 / 1e9 + published = _part_b_billions(2019) + assert standard + PART_B_IRMAA_2019 == pytest.approx(published, rel=0.005) + assert standard < published * 0.95 + + +@given(st.integers(FIRST_YEAR, END_YEAR + 100)) +def test_series_are_positive_and_net_spending_is_between_zero_and_gross(year): + deductions = sum(_value(series, year) for series in SERIES) + assert all(_value(series, year) > 0 for series in SERIES) + assert 0 < _spending(year) - deductions < _spending(year) + + +@pytest.mark.parametrize("series", SERIES) +def test_extension_keeps_the_2035_share_of_spending(series): + share_2035 = _value(series, LAST_PROJECTED_YEAR) / _spending(LAST_PROJECTED_YEAR) + for year in range(LAST_PROJECTED_YEAR + 1, END_YEAR + 1): + assert _value(series, year) / _spending(year) == pytest.approx( + share_2035, rel=1e-9 + ) + + +@given(st.integers(END_YEAR, END_YEAR + 100)) +def test_series_hold_the_2100_values_afterwards(year): + for series in SERIES: + assert _value(series, year) == _value(series, END_YEAR) + + +@given(st.integers(FIRST_YEAR, END_YEAR), 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}" + for series in SERIES: + assert _read(SERIES[series], date) == _value(series, year) diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/costs/cbo_medicare_benefit.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/costs/cbo_medicare_benefit.yaml new file mode 100644 index 00000000000..f3e1652a12b --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/costs/cbo_medicare_benefit.yaml @@ -0,0 +1,111 @@ +# cbo_medicare_benefit = Medicare spending per enrollee - premium income per +# Medicare enrollee (Parts A, B and D) - Part D low-income subsidy per +# Medicare enrollee, the same for every enrollee. +# 2025: $17,751 - $82.26 - $2,169.44 - $215.04 - $251.12 = $15,033.14. + +- name: Case 1, enrolled person gets the average net of premiums and the low-income subsidy. + period: 2025 + absolute_error_margin: 0.01 + input: + age: 70 + is_medicare_eligible: true + medicare_enrolled: true + output: + medicare_gross_cost: 17_751 + cbo_medicare_benefit: 15_033.14 + +# CBO assigns the average to every enrollee, so the person with 20 quarters +# of coverage (a $6,216 Part A premium in 2025) and the person paying the +# income-related premiums get the same value as the others, though each one's +# medicare_cost differs. +- name: Case 2, every enrollee gets the same value whatever premiums they pay. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + premium_free_part_a: + age: 70 + is_medicare_eligible: true + medicare_enrolled: true + medicare_quarters_of_coverage: 40 + pays_part_a_premium: + age: 70 + is_medicare_eligible: true + medicare_enrolled: true + medicare_quarters_of_coverage: 20 + pays_irmaa: + age: 70 + is_medicare_eligible: true + medicare_enrolled: true + medicare_quarters_of_coverage: 40 + disabled_under_65: + age: 50 + is_medicare_eligible: true + medicare_enrolled: true + medicare_quarters_of_coverage: 40 + not_enrolled: + age: 70 + is_medicare_eligible: true + medicare_enrolled: false + tax_units: + tax_unit: + members: [premium_free_part_a] + adjusted_gross_income: + 2023: 30_000 + tax_unit_2: + members: [pays_part_a_premium] + adjusted_gross_income: + 2023: 30_000 + tax_unit_3: + members: [pays_irmaa] + # IRMAA uses income from two years before. + adjusted_gross_income: + 2023: 1_000_000 + tax_unit_4: + members: [disabled_under_65] + tax_unit_5: + members: [not_enrolled] + households: + household: + members: + [ + premium_free_part_a, + pays_part_a_premium, + pays_irmaa, + disabled_under_65, + not_enrolled, + ] + output: + cbo_medicare_benefit: [15_033.14, 15_033.14, 15_033.14, 15_033.14, 0] + +- name: Case 3, first year of the series. + period: 2015 + absolute_error_margin: 0.01 + input: + age: 70 + is_medicare_eligible: true + medicare_enrolled: true + output: + # $11,929 - $57.57 - $1,214.54 - $224.86 - $460.52. + cbo_medicare_benefit: 9_971.51 + +- name: Case 4, last Trustees projection year. + period: 2035 + absolute_error_margin: 0.01 + input: + age: 70 + is_medicare_eligible: true + medicare_enrolled: true + output: + # $31,764.35 (the 2034 CMS value grown at the 2031-2032 rate) - $156.27 + # - $4,652 - $809.85 - $431.59. + cbo_medicare_benefit: 25_714.64 + +- name: Case 5, not enrolled. + period: 2025 + input: + age: 70 + is_medicare_eligible: true + medicare_enrolled: false + output: + cbo_medicare_benefit: 0