From d4cae008c99b6bf69ba9eeb4989d6ff140a0eb5c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 16:53:26 -0400 Subject: [PATCH 1/3] Scale Medicare per-enrollee spending to enrollees outside nursing homes per_capita_cost (CMS NHE Table 17) averages over every Part A and/or B enrollee, including nursing home residents, whom the CPS-based microdata exclude. Add calibration.gov.hhs.medicare.noninstitutionalized_spending_ratio (0.971, from MedPAC's 2022 fee-for-service spending for long-stay nursing home residents vs other beneficiaries and their 1.7% share) and apply it in medicare_gross_cost, medicare_cost, qmb_cost_sharing and, before premiums and after the Part D low-income subsidy, cbo_medicare_benefit. Co-Authored-By: Claude Opus 5.5 --- ...edicare-noninstitutional-spending.fixed.md | 1 + .../noninstitutionalized_spending_ratio.yaml | 48 +++++++++++++++++++ .../medicare/costs/cbo_medicare_benefit.py | 25 +++++----- .../gov/hhs/medicare/costs/medicare_cost.py | 15 +++--- .../hhs/medicare/costs/medicare_gross_cost.py | 13 +++-- .../savings_programs/qmb_cost_sharing.py | 7 ++- 6 files changed, 84 insertions(+), 25 deletions(-) create mode 100644 changelog.d/medicare-noninstitutional-spending.fixed.md create mode 100644 policyengine_us/parameters/calibration/gov/hhs/medicare/noninstitutionalized_spending_ratio.yaml diff --git a/changelog.d/medicare-noninstitutional-spending.fixed.md b/changelog.d/medicare-noninstitutional-spending.fixed.md new file mode 100644 index 00000000000..6d446f54b84 --- /dev/null +++ b/changelog.d/medicare-noninstitutional-spending.fixed.md @@ -0,0 +1 @@ +Scale Medicare spending per enrollee to enrollees who do not live in nursing homes in medicare_gross_cost, medicare_cost, cbo_medicare_benefit and qmb_cost_sharing, since the survey data exclude nursing home residents. diff --git a/policyengine_us/parameters/calibration/gov/hhs/medicare/noninstitutionalized_spending_ratio.yaml b/policyengine_us/parameters/calibration/gov/hhs/medicare/noninstitutionalized_spending_ratio.yaml new file mode 100644 index 00000000000..bbeaeec6434 --- /dev/null +++ b/policyengine_us/parameters/calibration/gov/hhs/medicare/noninstitutionalized_spending_ratio.yaml @@ -0,0 +1,48 @@ +description: PolicyEngine multiplies Medicare spending per enrollee by this ratio of Medicare spending per enrollee who is not a long-stay nursing home resident to Medicare spending per enrollee, because the household survey data exclude people living in nursing homes. +metadata: + unit: /1 + period: year + label: Medicare noninstitutionalized spending ratio + reference: + # Medicare spending per enrollee (per_capita_cost) averages over every + # enrollee, including nursing home residents, whom the Current Population + # Survey does not sample. In 2022, among beneficiaries in fee-for-service + # Medicare, annual Part A and Part B program spending averaged $31,200 for + # long-stay nursing home residents and about $11,300 for other + # beneficiaries (June 2025, file page 9), and long-stay residents were 1.7 + # percent of beneficiaries (October 2024, file page 5; 1.7 percent again in + # 2023, June 2025, file page 8). A long-stay resident had been in a nursing + # home for more than 90 days in at least one month of the year. So spending + # per other beneficiary relative to spending per beneficiary is + # 1 / (1 + 0.017 * (31_200 / 11_300 - 1)) = 0.971. + # + # The ratio is measured for fee-for-service Parts A and B and applied to + # all Medicare spending per enrollee, including Medicare Advantage, Part D + # and administration. With fee-for-service enrollment shares instead (62 + # percent of long-stay residents, 53 percent of others, July 2023, file + # page 13) it is 0.966; pricing Medicare Advantage at the long-stay + # residents' risk score ratio (2.12 / 1.01, October 2024, file page 5) gives + # about 0.975. The long-stay share was 2.6 percent in 2013 (June 2025, file + # page 8), but relative spending is published for 2022 only, so the 2022 + # ratio applies to every year from 2013, the first year of per_capita_cost. + # + # Nursing homes are the facilities the survey excludes. The Medicare + # Current Beneficiary Survey's long-term care facilities also include + # assisted living and other residential care, whose residents the survey + # samples; excluding all of them gives a larger gap (community residents' + # benefit payments per capita were 0.91 to 0.93 of the average in + # 2006-2013, Table 4.1 over Table 1.1). + - title: Medicare Payment Advisory Commission, Report to the Congress, June 2025, Chapter 5, Medicare beneficiaries in nursing homes + href: https://www.medpac.gov/wp-content/uploads/2025/06/Jun25_Ch5_MedPAC_Report_To_Congress_SEC.pdf#page=9 + - title: Medicare Payment Advisory Commission, Medicare beneficiaries in nursing homes, October 2024 public meeting + href: https://www.medpac.gov/wp-content/uploads/2023/10/Beneficiaries-in-Nursing-Homes-MedPAC-Oct-2024-SEC.pdf#page=5 + - title: Centers for Medicare and Medicaid Services, Characteristics and Perceptions of the Medicare Population, 2013, Table 4.1 + href: https://www.cms.gov/research-statistics-data-and-systems/research/mcbs/downloads/2013_characteristics_and_perceptions.zip + # CBO likewise removes institutionalized enrollees' benefits before + # assigning Medicare's average cost to survey respondents (file page 46), + # with the institutional share held at its 2012 level from 2013 (file page + # 52). + - title: Congressional Budget Office, Working Paper 2018-07, How CBO Adjusts for Survey Underreporting of Transfer Income in Its Distributional Analyses + href: https://www.cbo.gov/system/files/2018-07/54234-workingpaper.pdf#page=46 +values: + 2013-01-01: 0.971 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 index 1a4e1d96df5..f7987a8b683 100644 --- a/policyengine_us/variables/gov/hhs/medicare/costs/cbo_medicare_benefit.py +++ b/policyengine_us/variables/gov/hhs/medicare/costs/cbo_medicare_benefit.py @@ -9,11 +9,13 @@ class cbo_medicare_benefit(Variable): "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." + "behalf of enrollees, excluding costs for institutionalized " + "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 scales Medicare spending per enrollee net of the " + "low-income subsidy to enrollees who do not live in nursing homes, " + "then subtracts per-enrollee premium income, rather than the " + "person's own premiums." ) unit = USD definition_period = YEAR @@ -21,6 +23,7 @@ class cbo_medicare_benefit(Variable): "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", + "https://www.cbo.gov/system/files/2018-07/54234-workingpaper.pdf#page=52", ) defined_for = "medicare_enrolled" @@ -28,9 +31,9 @@ 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) + # CBO removes institutionalized enrollees' share of Medicare benefits + # other than the low-income subsidy, which it counts separately. + noninstitutionalized_spending = ( + p.per_capita_cost - p.part_d_low_income_subsidy_per_enrollee + ) * p.noninstitutionalized_spending_ratio + return max_(noninstitutionalized_spending - total_premiums, 0) 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..6ed57bb6f04 100644 --- a/policyengine_us/variables/gov/hhs/medicare/costs/medicare_cost.py +++ b/policyengine_us/variables/gov/hhs/medicare/costs/medicare_cost.py @@ -6,11 +6,11 @@ class medicare_cost(Variable): entity = Person label = "Medicare cost" documentation = ( - "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." + "Annual value of Medicare benefits received. Calculated as Medicare " + "spending per enrollee who does not live in a nursing home, 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 @@ -22,8 +22,9 @@ class medicare_cost(Variable): def formula(person, period, parameters): p = parameters(period).calibration.gov.hhs.medicare - # Total Medicare spending on behalf of beneficiary - per_capita_spending = p.per_capita_cost + # Medicare spending on behalf of an enrollee who does not live in a + # nursing home, the population the household survey data cover. + per_capita_spending = p.per_capita_cost * p.noninstitutionalized_spending_ratio # Premium offsets to Medicare program cost. Use the enrollment-gated # gross Part B premium before MSP offsets so MSP support does not 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 1baeeaf188a..ee250bebe94 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 @@ -9,10 +9,12 @@ class medicare_gross_cost(Variable): "Annual Medicare spending on behalf of the beneficiary before " "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." + "low-income subsidy, scaled to enrollees who do not live in nursing " + "homes, the population the household survey data cover. 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 @@ -23,4 +25,5 @@ class medicare_gross_cost(Variable): defined_for = "medicare_enrolled" def formula(person, period, parameters): - return parameters(period).calibration.gov.hhs.medicare.per_capita_cost + p = parameters(period).calibration.gov.hhs.medicare + return p.per_capita_cost * p.noninstitutionalized_spending_ratio diff --git a/policyengine_us/variables/gov/hhs/medicare/savings_programs/qmb_cost_sharing.py b/policyengine_us/variables/gov/hhs/medicare/savings_programs/qmb_cost_sharing.py index d8e70729ad9..2016834a1df 100644 --- a/policyengine_us/variables/gov/hhs/medicare/savings_programs/qmb_cost_sharing.py +++ b/policyengine_us/variables/gov/hhs/medicare/savings_programs/qmb_cost_sharing.py @@ -18,14 +18,17 @@ class qmb_cost_sharing(Variable): "First-pass monthly value of QMB coverage for Medicare deductibles, " "coinsurance, and copayments. The model lacks beneficiary-level " "Medicare claims, so this approximates cost sharing as a parameterized " - "share of average Medicare per-capita spending." + "share of average Medicare spending per enrollee who does not live " + "in a nursing home." ) def formula(person, period, parameters): category = person("msp_category", period) p = parameters(period).gov.hhs.medicare.savings_programs + medicare = parameters(period).calibration.gov.hhs.medicare monthly_medicare_cost = ( - parameters(period).calibration.gov.hhs.medicare.per_capita_cost + medicare.per_capita_cost + * medicare.noninstitutionalized_spending_ratio / MONTHS_IN_YEAR ) return ( From 50c85ff60c8e1bb8c7f2d39511d9a2ba20806846 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 17:31:00 -0400 Subject: [PATCH 2/3] Test the noninstitutionalized Medicare spending ratio and its consumers New test_medicare_noninstitutionalized_spending_ratio.py: the stored value is the MedPAC formula, constant and below one from 2013; the documented fee-for-service and Medicare Advantage variants bracket it; the MCBS facility definition (2006-2013 Characteristics and Perceptions tables) gives a larger gap every year; and, across reformed ratios, every consumer matches a reference implementation, stays within [0, gross], does not fall when the ratio rises, and reproduces the unscaled formulas at a ratio of one. Hand-computed YAML expectations and the core consumer tests move to the scaled spending. Co-Authored-By: Claude Opus 5.5 --- .../core/test_cbo_medicare_benefit_series.py | 23 +- .../core/test_medicare_cost_part_d_premium.py | 14 +- ...are_noninstitutionalized_spending_ratio.py | 277 ++++++++++++++++++ .../core/test_medicare_per_capita_cost.py | 32 +- .../medicare/costs/cbo_medicare_benefit.yaml | 26 +- .../gov/hhs/medicare/costs/medicare_cost.yaml | 49 ++-- .../medicare/costs/medicare_gross_cost.yaml | 21 +- .../medicare/savings_programs/msp_cost.yaml | 4 +- .../msp_federal_and_state_cost.yaml | 6 +- .../savings_programs/qmb_cost_sharing.yaml | 5 +- .../hhs/medicare/test_part_b_msp_offset.py | 14 +- .../household/cbo_household_income.yaml | 21 +- 12 files changed, 407 insertions(+), 85 deletions(-) create mode 100644 policyengine_us/tests/core/test_medicare_noninstitutionalized_spending_ratio.py diff --git a/policyengine_us/tests/core/test_cbo_medicare_benefit_series.py b/policyengine_us/tests/core/test_cbo_medicare_benefit_series.py index c3da92714a1..385af0bc399 100644 --- a/policyengine_us/tests/core/test_cbo_medicare_benefit_series.py +++ b/policyengine_us/tests/core/test_cbo_medicare_benefit_series.py @@ -3,10 +3,12 @@ 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): +spending per enrollee (``calibration.gov.hhs.medicare.per_capita_cost``) less +the low-income subsidy, scales it to enrollees outside nursing homes +(``noninstitutionalized_spending_ratio``), and subtracts the premium series. +All four series under ``calibration.gov.hhs.medicare`` are aggregates 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. @@ -34,8 +36,10 @@ 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. +5. Bounds: every series is positive, and net Medicare spending per enrollee, + (spending - low-income subsidy) x the noninstitutionalized ratio - + premiums, stays between 0 and scaled 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. @@ -57,6 +61,7 @@ "lis": f"{MEDICARE}.part_d_low_income_subsidy_per_enrollee", } SPENDING = f"{MEDICARE}.per_capita_cost" +RATIO = f"{MEDICARE}.noninstitutionalized_spending_ratio" FIRST_YEAR, LAST_PROJECTED_YEAR, END_YEAR = 2015, 2035, 2100 YEARS = range(FIRST_YEAR, LAST_PROJECTED_YEAR + 1) @@ -193,9 +198,11 @@ def test_part_b_premium_income_includes_income_related_premiums(): @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) + ratio = _read(RATIO, f"{year}-01-01") + premiums = sum(_value(series, year) for series in SERIES if series != "lis") + net = (_spending(year) - _value("lis", year)) * ratio - premiums assert all(_value(series, year) > 0 for series in SERIES) - assert 0 < _spending(year) - deductions < _spending(year) + assert 0 < net < _spending(year) * ratio @pytest.mark.parametrize("series", SERIES) 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..9ba27f78c89 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,9 +1,10 @@ """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: +per_capita_cost``, scaled by ``noninstitutionalized_spending_ratio`` to +enrollees outside nursing homes) 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) @@ -35,7 +36,7 @@ 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``], + 0) with gross = per_capita_cost x the ratio, 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. """ @@ -226,7 +227,10 @@ 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") + date = f"{year}-01-01" + gross = _read("calibration.gov.hhs.medicare.per_capita_cost", date) * _read( + "calibration.gov.hhs.medicare.noninstitutionalized_spending_ratio", date + ) part_d = _share(year) * (_premium(year) + irmaa) return max(gross - part_a - part_b - part_d, 0) diff --git a/policyengine_us/tests/core/test_medicare_noninstitutionalized_spending_ratio.py b/policyengine_us/tests/core/test_medicare_noninstitutionalized_spending_ratio.py new file mode 100644 index 00000000000..5fac42ce69a --- /dev/null +++ b/policyengine_us/tests/core/test_medicare_noninstitutionalized_spending_ratio.py @@ -0,0 +1,277 @@ +"""The ratio that scales Medicare spending per enrollee to enrollees outside +nursing homes. + +``calibration.gov.hhs.medicare.per_capita_cost`` averages Medicare spending +over every enrollee, including nursing home residents, whom the household +survey data do not sample. ``noninstitutionalized_spending_ratio`` is +spending per enrollee who is not a long-stay nursing home resident over +spending per enrollee, from MedPAC's 2022 fee-for-service Part A and B +spending ($31,200 for long-stay residents, about $11,300 for others; June +2025 report, chapter 5, file page 9) and the long-stay share of beneficiaries +(1.7 percent; October 2024 slides, file page 5): + + ratio = 1 / (1 + share x (long-stay spending / other spending - 1)) + +``medicare_gross_cost``, ``medicare_cost`` and ``qmb_cost_sharing`` multiply +``per_capita_cost`` by it; ``cbo_medicare_benefit`` multiplies spending less +the Part D low-income subsidy by it, then subtracts premium income per +enrollee. + +Invariants: + +1. Transcription: the stored value is the formula applied to MedPAC's + published figures, rounded to three decimals. +2. Bounds and constancy: the ratio is strictly between 0 and 1 and is the + same on every date from 2013, the first year of ``per_capita_cost``. +3. Sensitivity: the alternatives documented in the parameter file bracket + the stored value: fee-for-service enrollment shares (62 percent of + long-stay residents, 53 percent of others) give a lower ratio, and pricing + Medicare Advantage at the CMS-HCC risk score ratio (2.12 / 1.01) gives a + higher one. +4. Definition order: excluding every resident of a Medicare Current + Beneficiary Survey long-term care facility, which includes assisted living, + gives a lower ratio in every year of the published 2006-2013 tables + (community-only over all-beneficiary benefit payments per capita), so the + nursing home ratio is the smaller of the two corrections. +5. Consumers (Hypothesis, differential): for any year, any premiums and a + ratio set by a reform (0.5, 0.8, 0.95 or 1), each consumer equals a + reference implementation; + gross spending is proportional to the ratio; ``medicare_cost`` and + ``cbo_medicare_benefit`` lie in [0, ``medicare_gross_cost``] and never fall + when the ratio rises; with the ratio at 1 every consumer reproduces the + unscaled formulas. A person not enrolled gets zero. +""" + +from functools import lru_cache + +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st +from policyengine_core.parameters.operations.get_parameter import get_parameter +from policyengine_core.reforms import Reform + +from policyengine_us import Simulation +from policyengine_us.system import CountryTaxBenefitSystem, system + +MEDICARE = "calibration.gov.hhs.medicare" +RATIO = f"{MEDICARE}.noninstitutionalized_spending_ratio" +FIRST_YEAR, END_YEAR = 2013, 2100 + +# MedPAC, June 2025 Report to the Congress, chapter 5 (file pages 8, 9 and +# 13), and October 2024 meeting slides (file page 5). +LONG_STAY_SPENDING, OTHER_SPENDING = 31_200, 11_300 +LONG_STAY_SHARE = 0.017 +LONG_STAY_FFS_SHARE, OTHER_FFS_SHARE = 0.62, 0.53 +LONG_STAY_RISK_SCORE, OTHER_RISK_SCORE = 2.12, 1.01 + +# MCBS Characteristics and Perceptions of the Medicare Population, all +# beneficiaries: Table 1.1 beneficiaries (thousands) and Table 4.1 benefit +# payments ($ millions), total and community only. +MCBS = { + 2006: (43_867, 41_599, 305_066, 267_344), + 2007: (45_046, 42_867, 307_372, 271_509), + 2008: (46_100, 43_906, 320_032, 282_887), + 2009: (47_371, 45_053, 335_081, 289_843), + 2010: (48_454, 46_270, 365_098, 322_150), + 2011: (50_186, 47_969, 374_252, 330_907), + 2012: (51_723, 49_816, 338_395, 299_144), + 2013: (53_574, 51_568, 352_943, 312_626), +} + + +def _ratio(share, relative): + return 1 / (1 + share * (relative - 1)) + + +def _read(name, date): + return get_parameter(system.parameters, name)(date) + + +def _stored(year): + return _read(RATIO, f"{year}-01-01") + + +def test_value_is_the_medpac_formula(): + expected = _ratio(LONG_STAY_SHARE, LONG_STAY_SPENDING / OTHER_SPENDING) + assert _stored(2022) == round(expected, 3) == 0.971 + + +@given(st.integers(FIRST_YEAR, END_YEAR + 100), st.integers(1, 12)) +def test_ratio_is_a_constant_share_below_one(year, month): + value = _read(RATIO, f"{year}-{month:02d}-01") + assert 0 < value < 1 + assert value == _stored(2022) + + +def test_documented_alternatives_bracket_the_value(): + relative = LONG_STAY_SPENDING / OTHER_SPENDING + ffs_share = ( + LONG_STAY_SHARE + * LONG_STAY_FFS_SHARE + / ( + LONG_STAY_SHARE * LONG_STAY_FFS_SHARE + + (1 - LONG_STAY_SHARE) * OTHER_FFS_SHARE + ) + ) + ffs_ratio = _ratio(ffs_share, relative) + # Price Medicare Advantage enrollees at the risk score ratio, with other + # beneficiaries' spending the same in both programs. + groups = { + "long_stay_ffs": (LONG_STAY_SHARE * LONG_STAY_FFS_SHARE, relative), + "long_stay_ma": ( + LONG_STAY_SHARE * (1 - LONG_STAY_FFS_SHARE), + LONG_STAY_RISK_SCORE / OTHER_RISK_SCORE, + ), + "other": (1 - LONG_STAY_SHARE, 1), + } + average = sum(count * cost for count, cost in groups.values()) + mixed_ratio = 1 / average + assert round(ffs_ratio, 3) == 0.966 + assert round(mixed_ratio, 3) == 0.975 + assert ffs_ratio < _stored(2022) < mixed_ratio + + +@pytest.mark.parametrize("year", MCBS) +def test_mcbs_facility_definition_gives_a_larger_gap(year): + total_people, community_people, total_pay, community_pay = MCBS[year] + mcbs_ratio = (community_pay / community_people) / (total_pay / total_people) + assert 0.9 < mcbs_ratio < _stored(2022) + + +# Reformed systems take seconds to build, so the property tests draw the +# ratio from a few values and build each system once. +RATIOS = (0.5, 0.8, 0.95, 1) + + +@lru_cache(maxsize=None) +def _system(ratio): + if ratio is None: + return system + reform = Reform.from_dict( + {RATIO: {f"{FIRST_YEAR}-01-01.{END_YEAR}-12-31": ratio}}, + country_id="us", + ) + return CountryTaxBenefitSystem(reform=reform) + + +def _simulation(year, people, ratio=None): + names = list(people) + return Simulation( + tax_benefit_system=_system(ratio), + 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 _person(year, enrolled, part_a=0, part_b=0): + return { + "age": {year: 70}, + "is_medicare_eligible": {year: True}, + "medicare_enrolled": {year: enrolled}, + "base_part_a_premium": {year: part_a}, + "gross_medicare_part_b_premium": {year: part_b}, + "income_adjusted_part_d_premium_surcharge": {year: 0}, + "msp_category": {f"{year}-01": "QMB"}, + } + + +def _reference(year, ratio, part_a, part_b): + p = system.parameters(f"{year}-01-01").calibration.gov.hhs.medicare + spending = p.per_capita_cost + premiums = p.premium_income_per_enrollee + part_d = p.part_d.enrollment_share * p.part_d.premiums_per_enrollee + rate = system.parameters( + f"{year}-01-01" + ).gov.hhs.medicare.savings_programs.qmb.cost_sharing_rate + return { + "medicare_gross_cost": spending * ratio, + "medicare_cost": max(spending * ratio - part_a - part_b - part_d, 0), + "cbo_medicare_benefit": max( + (spending - p.part_d_low_income_subsidy_per_enrollee) * ratio + - (premiums.part_a + premiums.part_b + premiums.part_d), + 0, + ), + "qmb_cost_sharing": spending * ratio / 12 * rate, + } + + +def _calculate(simulation, year): + return { + name: simulation.calculate( + name, f"{year}-01" if name == "qmb_cost_sharing" else year + ) + for name in ( + "medicare_gross_cost", + "medicare_cost", + "cbo_medicare_benefit", + "qmb_cost_sharing", + ) + } + + +premiums = st.floats(0, 30_000, allow_nan=False) + + +@settings(max_examples=12, deadline=None, suppress_health_check=[HealthCheck.too_slow]) +@given( + st.integers(2015, END_YEAR), + st.sampled_from(RATIOS), + st.sampled_from(RATIOS), + st.floats(0, 12_000, allow_nan=False), + premiums, +) +def test_consumers_match_reference_for_any_ratio(year, one, other, part_a, part_b): + low, high = sorted((one, other)) + results = {} + for value in (low, high): + simulation = _simulation( + year, + { + "enrolled": _person(year, True, part_a, part_b), + "not_enrolled": _person(year, False), + }, + ratio=value, + ) + results[value] = _calculate(simulation, year) + reference = _reference(year, value, part_a, part_b) + for name, values in results[value].items(): + # Variables are float32, so compare to single precision. + assert values[0] == pytest.approx(reference[name], rel=1e-6, abs=0.01) + if name != "qmb_cost_sharing": + assert values[1] == 0 + gross = results[value]["medicare_gross_cost"][0] + for name in ("medicare_cost", "cbo_medicare_benefit"): + assert 0 <= results[value][name][0] <= gross + for name in results[low]: + assert results[low][name][0] <= results[high][name][0] + 0.01 + + +@settings(max_examples=5, deadline=None, suppress_health_check=[HealthCheck.too_slow]) +@given(st.integers(2015, END_YEAR), premiums) +def test_ratio_of_one_reproduces_unscaled_formulas(year, part_b): + unscaled = _calculate( + _simulation(year, {"enrolled": _person(year, True, 0, part_b)}, ratio=1), + year, + ) + baseline = _calculate( + _simulation(year, {"enrolled": _person(year, True, 0, part_b)}), year + ) + p = system.parameters(f"{year}-01-01").calibration.gov.hhs.medicare + assert unscaled["medicare_gross_cost"][0] == pytest.approx( + p.per_capita_cost, rel=1e-6 + ) + # Gross spending scales exactly with the ratio. + assert baseline["medicare_gross_cost"][0] == pytest.approx( + unscaled["medicare_gross_cost"][0] * _stored(year), rel=1e-6 + ) + assert baseline["qmb_cost_sharing"][0] == pytest.approx( + unscaled["qmb_cost_sharing"][0] * _stored(year), rel=1e-6 + ) 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 9e37cb5d1e3..76c44d1bbb7 100644 --- a/policyengine_us/tests/core/test_medicare_per_capita_cost.py +++ b/policyengine_us/tests/core/test_medicare_per_capita_cost.py @@ -11,10 +11,13 @@ 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, 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. +``medicare_gross_cost`` returns the series times +``noninstitutionalized_spending_ratio`` (enrollees outside nursing homes, +tested in test_medicare_noninstitutionalized_spending_ratio.py) for each +enrolled person, ``medicare_cost`` subtracts the person's Part A, gross Part +B and expected Part D premiums from that (tested in +test_medicare_cost_part_d_premium.py), and ``qmb_cost_sharing`` pays QMB +enrollees a share of it each month. Invariants: @@ -33,11 +36,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, 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. + series times the noninstitutionalized ratio; ``medicare_cost`` equals that + 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 scaled series over 12 + times the QMB rate. A person not enrolled gets zero. """ import pytest @@ -49,6 +52,7 @@ from policyengine_us.system import system SERIES = "calibration.gov.hhs.medicare.per_capita_cost" +RATIO = "calibration.gov.hhs.medicare.noninstitutionalized_spending_ratio" FIRST_YEAR, LAST_PROJECTED_YEAR, END_YEAR = 2013, 2034, 2100 YEARS = range(FIRST_YEAR, LAST_PROJECTED_YEAR + 1) GROWTH_YEARS = (2031, 2032) @@ -110,6 +114,10 @@ def _value(year): return _series(f"{year}-01-01") +def _scaled(year): + return _value(year) * get_parameter(system.parameters, RATIO)(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] @@ -214,10 +222,10 @@ def test_medicare_cost_nets_premiums_from_the_series( 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) + assert gross == pytest.approx(_scaled(year), rel=1e-6) 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) + expected = max(_scaled(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 @@ -246,5 +254,5 @@ def test_qmb_cost_sharing_is_the_qmb_rate_of_monthly_spending(year, month): ).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 + _scaled(year) / 12 * rate, rel=1e-6 ) 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 index f3e1652a12b..0908efc1a77 100644 --- 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 @@ -1,7 +1,9 @@ -# 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. +# cbo_medicare_benefit = (Medicare spending per enrollee - Part D low-income +# subsidy per Medicare enrollee) x the ratio of spending per enrollee outside +# nursing homes to spending per enrollee (0.971) - premium income per Medicare +# enrollee (Parts A, B and D), the same for every enrollee. +# 2025: ($17,751 - $251.12) x 0.971 - $82.26 - $2,169.44 - $215.04 +# = $16,992.38 - $2,466.74 = $14,525.64. - name: Case 1, enrolled person gets the average net of premiums and the low-income subsidy. period: 2025 @@ -11,8 +13,8 @@ is_medicare_eligible: true medicare_enrolled: true output: - medicare_gross_cost: 17_751 - cbo_medicare_benefit: 15_033.14 + medicare_gross_cost: 17_236.22 + cbo_medicare_benefit: 14_525.64 # 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 @@ -76,7 +78,7 @@ not_enrolled, ] output: - cbo_medicare_benefit: [15_033.14, 15_033.14, 15_033.14, 15_033.14, 0] + cbo_medicare_benefit: [14_525.64, 14_525.64, 14_525.64, 14_525.64, 0] - name: Case 3, first year of the series. period: 2015 @@ -86,8 +88,8 @@ 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 + # ($11,929 - $460.52) x 0.971 - $57.57 - $1,214.54 - $224.86. + cbo_medicare_benefit: 9_638.92 - name: Case 4, last Trustees projection year. period: 2035 @@ -97,9 +99,9 @@ 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 + # ($31,764.35, the 2034 CMS value grown at the 2031-2032 rate, - $431.59) + # x 0.971 - $156.27 - $4,652 - $809.85. + cbo_medicare_benefit: 24_805.99 - name: Case 5, not enrolled. period: 2025 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..cce9be572ff 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,8 @@ -# 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. +# Spending is CMS Medicare spending per enrollee times the ratio of spending +# per enrollee outside nursing homes to spending per enrollee (0.971); 2025: +# $17,751 x 0.971 = $17,236.22. 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 @@ -14,8 +16,8 @@ tax_exempt_interest_income: 2023: 0 output: - # $17,751 spending - $2,220 Part B premium - $180.41 Part D (2025) - medicare_cost: 15_350.59 + # $17,236.22 spending - $2,220 Part B premium - $180.41 Part D (2025) + medicare_cost: 14_835.81 - name: unit test 2 - enrolled, pays full Part A premium period: 2025 @@ -29,8 +31,8 @@ tax_exempt_interest_income: 2023: 0 output: - # $17,751 - $6,216 Part A - $2,220 Part B - $180.41 Part D (2025) - medicare_cost: 9_134.59 + # $17,236.22 - $6,216 Part A - $2,220 Part B - $180.41 Part D (2025) + medicare_cost: 8_619.81 - name: unit test 3 - enrolled with IRMAA period: 2025 @@ -45,9 +47,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,236.22 - $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: 8_665.67 - name: unit test 4 - eligible but not enrolled period: 2025 @@ -70,8 +72,9 @@ tax_exempt_interest_income: 2019: 0 output: - # $14,293 - $1,782 Part B (2021: $148.50*12) - 0.7811 x $306.15 Part D - medicare_cost: 12_271.87 + # $14,293 x 0.971 = $13,878.50 - $1,782 Part B (2021: $148.50*12) + # - 0.7811 x $306.15 Part D + medicare_cost: 11_857.37 - name: unit test 6 - direct Part B premium input period: 2025 @@ -82,8 +85,8 @@ 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 + # $17,236.22 spending - $3,000 Part B premium - $180.41 Part D + medicare_cost: 14_055.81 - name: unit test 7 - Part D IRMAA in the 50 percent tier period: 2025 @@ -100,8 +103,9 @@ 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,236.22 - $2,220 Part B - 0.8191 x ($220.25 + $423.60) = $527.38 + # Part D + medicare_cost: 14_488.84 - name: unit test 8 - projected year period: 2027 @@ -113,8 +117,9 @@ 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 + # $20,096 x 0.971 = $19,513.22 - $2,500 Part B - 0.8255 x $373.53 = + # $308.35 Part D (2027) + medicare_cost: 16_704.87 - name: unit test 9 - Inflation Reduction Act premium reset year, with IRMAA period: 2030 @@ -126,8 +131,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 x 0.971 = $23,099.12 - $3,000 Part B - 0.8382 x ($637.06 + + # $1,000) = $1,372.18 Part D + medicare_cost: 18_726.94 - name: unit test 10 - premiums above spending floor the value at zero period: 2025 @@ -141,6 +147,7 @@ - name: unit test 11 - gross cost does not subtract the Part D premium period: 2025 + absolute_error_margin: 0.01 input: age: 65 is_medicare_eligible: true @@ -148,4 +155,4 @@ gross_medicare_part_b_premium: 2_220 income_adjusted_part_d_premium_surcharge: 1_029.6 output: - medicare_gross_cost: 17_751 + medicare_gross_cost: 17_236.22 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 15eb8725e7d..e59831f759d 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 @@ -1,5 +1,10 @@ +# medicare_gross_cost = CMS Medicare spending per enrollee x the ratio of +# spending per enrollee outside nursing homes to spending per enrollee (0.971). +# 2025: $17,751 x 0.971 = $17,236.22. + - name: unit test 1 - enrolled, premium-free Part A gets full gross Medicare value period: 2025 + absolute_error_margin: 0.01 input: age: 65 is_medicare_eligible: true @@ -10,11 +15,11 @@ tax_exempt_interest_income: 2023: 0 output: - # CMS Medicare spending per enrollee, 2025. - medicare_gross_cost: 17_751 + medicare_gross_cost: 17_236.22 - name: unit test 2 - premiums do not reduce gross Medicare value period: 2025 + absolute_error_margin: 0.01 input: age: 65 is_medicare_eligible: true @@ -26,5 +31,13 @@ 2023: 0 filing_status: SINGLE output: - # CMS Medicare spending per enrollee, 2025. - medicare_gross_cost: 17_751 + medicare_gross_cost: 17_236.22 + +- name: unit test 3 - not enrolled + period: 2025 + input: + age: 65 + is_medicare_eligible: true + medicare_enrolled: false + output: + medicare_gross_cost: 0 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 55be058ba49..9cb12d59fa3 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: $18,983 * 20% = $3,796.60. - msp_cost: 6_231.4 + # QMB cost sharing: $18,983 * 0.971 * 20% = $3,686.50. + msp_cost: 6_121.3 - 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 0c526fe9c29..610ba12f6d1 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 $6,231.40. - msp_federal_cost: 3_115.7 - msp_state_cost: 3_115.7 + # CA regular FMAP is 50%; total MSP cost is $6,121.30. + msp_federal_cost: 3_060.65 + msp_state_cost: 3_060.65 - 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 1fa5134a84f..695e184b496 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,9 @@ input: msp_category: QMB output: - # $18,983 annual Medicare spending per enrollee (2026) / 12 * 20%. - qmb_cost_sharing: 316.38 + # $18,983 annual Medicare spending per enrollee (2026) x 0.971 (enrollees + # outside nursing homes) / 12 * 20%. + qmb_cost_sharing: 307.21 - 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 b15943e5203..5de6f86d4a3 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,9 +87,10 @@ 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 - # - 0.8191 x $220.25 expected Part D premium. - assert sim.calculate("medicare_cost", PERIOD)[0] == pytest.approx(15_350.59) + # $17,751 CMS Medicare spending per enrollee (2025) x 0.971 for enrollees + # outside nursing homes - $2,220 gross Part B - 0.8191 x $220.25 expected + # Part D premium. + assert sim.calculate("medicare_cost", PERIOD)[0] == pytest.approx(14_835.81) def test_medicare_part_b_premium_is_zero_when_not_enrolled(): @@ -345,9 +346,10 @@ 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 - # - 0.8191 x $220.25 expected Part D premium. - assert sim.calculate("medicare_cost", PERIOD)[0] == pytest.approx(13_130.59) + # $17,751 CMS Medicare spending per enrollee (2025) x 0.971 for enrollees + # outside nursing homes - $4,440 gross Part B - 0.8191 x $220.25 expected + # Part D premium. + assert sim.calculate("medicare_cost", PERIOD)[0] == pytest.approx(12_615.81) 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 d52fdf17f34..1aca1ce94ce 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 @@ -34,16 +34,17 @@ workers_compensation: 1_000 output: cbo_household_market_income: 49_102 - # CMS Medicare spending per enrollee, 2025. - medicare_gross_cost: 17_751 - # $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 + # CMS Medicare spending per enrollee, 2025, x 0.971 for enrollees outside + # nursing homes: $17,751 x 0.971. + medicare_gross_cost: 17_236.22 + # ($17,751 less the $251.12 Part D low-income subsidy per Medicare + # enrollee) x 0.971, less 2025 premium income per Medicare enrollee + # ($82.26 Part A, $2,169.44 Part B, $215.04 Part D). + cbo_medicare_benefit: 14_525.64 + # $12,000 + $14,525.64 + $2,000 + $1,000. + cbo_household_social_insurance_benefits: 29_525.64 + cbo_household_income_before_transfers_and_taxes: 78_627.64 + cbo_household_income: 78_627.64 - name: CBO means-tested transfers include only the listed programs period: 2026 From f449aa2d0aead638a9947c74af41df3663b3ade9 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 17:37:48 -0400 Subject: [PATCH 3/3] Cite the CPS sample frame for the nursing home exclusion BLS's CPS design page says institutional group quarters such as nursing homes have never been in the CPS sample; state that, rather than asserting that assisted living residents are sampled. Co-Authored-By: Claude Opus 5.5 --- .../medicare/noninstitutionalized_spending_ratio.yaml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/policyengine_us/parameters/calibration/gov/hhs/medicare/noninstitutionalized_spending_ratio.yaml b/policyengine_us/parameters/calibration/gov/hhs/medicare/noninstitutionalized_spending_ratio.yaml index bbeaeec6434..3f4a37d0ac1 100644 --- a/policyengine_us/parameters/calibration/gov/hhs/medicare/noninstitutionalized_spending_ratio.yaml +++ b/policyengine_us/parameters/calibration/gov/hhs/medicare/noninstitutionalized_spending_ratio.yaml @@ -26,16 +26,19 @@ metadata: # page 8), but relative spending is published for 2022 only, so the 2022 # ratio applies to every year from 2013, the first year of per_capita_cost. # - # Nursing homes are the facilities the survey excludes. The Medicare - # Current Beneficiary Survey's long-term care facilities also include - # assisted living and other residential care, whose residents the survey - # samples; excluding all of them gives a larger gap (community residents' + # The Current Population Survey has never sampled institutional group + # quarters such as nursing homes. The Medicare Current Beneficiary Survey's + # long-term care facilities also include assisted living, retirement homes + # and personal care homes, which are not institutional group quarters; + # excluding all of their residents gives a larger gap (community residents' # benefit payments per capita were 0.91 to 0.93 of the average in # 2006-2013, Table 4.1 over Table 1.1). - title: Medicare Payment Advisory Commission, Report to the Congress, June 2025, Chapter 5, Medicare beneficiaries in nursing homes href: https://www.medpac.gov/wp-content/uploads/2025/06/Jun25_Ch5_MedPAC_Report_To_Congress_SEC.pdf#page=9 - title: Medicare Payment Advisory Commission, Medicare beneficiaries in nursing homes, October 2024 public meeting href: https://www.medpac.gov/wp-content/uploads/2023/10/Beneficiaries-in-Nursing-Homes-MedPAC-Oct-2024-SEC.pdf#page=5 + - title: U.S. Bureau of Labor Statistics, Handbook of Methods, Current Population Survey, Design + href: https://www.bls.gov/opub/hom/cps/design.htm - title: Centers for Medicare and Medicaid Services, Characteristics and Perceptions of the Medicare Population, 2013, Table 4.1 href: https://www.cms.gov/research-statistics-data-and-systems/research/mcbs/downloads/2013_characteristics_and_perceptions.zip # CBO likewise removes institutionalized enrollees' benefits before