Repository navigation
Conversation
Replace KFF's 2021 traditional-Medicare figure and an unsourced $14,500 2025 estimate (held flat for every later year) with CMS National Health Expenditure Table 17's Medicare spending per enrollee: historical 2013-2024, projected 2025-2034, extended to 2100 at the 2031-2032 rate so the end of sequestration in 2033 does not compound. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Transcription checks against NHE Table 17 (and its spending over enrollment), a cross-source level check against the Trustees Report's per beneficiary cost, Hypothesis properties for order, the 2035-2100 closed form and calendar-year steps, and identities for medicare_gross_cost, medicare_cost and qmb_cost_sharing. Existing expectations move from $14,500/$11,080 to the CMS values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CMS Medicare spending per enrollee (calibration.gov.hhs.medicare.per_capita_cost, #10107) includes Part D, which enrollee premiums partly finance, but medicare_cost subtracted only Part A and Part B premiums. It now also subtracts the Part D enrollment share times (Part D premiums per Part D enrollee + the person's Part D IRMAA), both new calibration series from the 2026 Medicare Trustees Report (Tables III.D3, V.E4 and V.B3). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Transcription from the Trustees' cells, conservation back to Table III.D3 premium income, bounds, the 2036-2100 extension's closed form, and a Hypothesis differential test of medicare_cost against a reference implementation (0 <= net <= gross, non-increasing in Part D IRMAA). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Heads-up from #10115, which charges This PR subtracts Options, after both merge:
With the default input the two PRs give the same numbers today: |
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 <noreply@anthropic.com>
|
Picking this up for the US hub. I'll post a hand-off comment here when it's ready. Generated by Claude Code |
The branch was stacked on #10107, which has since merged to main in revised form, so its shared lines (the per capita cost series, its extension and its tests) take main's version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#10115 made income_adjusted_part_d_premium_surcharge zero for anyone not enrolled in Part D (medicare_part_d_enrolled). Multiplying it by the Part D enrollment share then counted non-enrollment twice once take-up is modeled, and charged an enrolled person only 82 percent of the adjustment they pay. The share now scales only the average Part D premium, which matches spending per enrollee averaged over every Medicare enrollee; the person's own adjustment amount is subtracted as computed, as the Part B one is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Part D series listed the report in several reference entries that differed only by #page=; CLAUDE.md asks for one entry per source with the pages in its title. The 2030 base beneficiary premium reset is on file page 154, not 152, and file page 219 says low-income subsidy enrollees pay substantially reduced premiums, not none. The December 31 credits move to the following year, not back a year. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Picking this up for the US hub, continuing an earlier session's work on this PR. I'll post a hand-off comment here when it's ready. Generated by Claude Code |
|
Ready for hub review Head: This continues the earlier session's work: What I did
No reviews or inline comments exist on this PR. Definition followed
Hand computationsEvery
Tests run (local, one file per process)
The one failure is the same network-blocked test that fails on I also ran this branch merged with #10103: Partner tests. No file under Mutants (one process each, code restored)
CINone has run. CI skips drafts. Judgment calls
Sources I could not read in this sessionThis environment's network policy refused cms.gov, cbo.gov, law.cornell.edu and web.archive.org. I could not re-read the Trustees tables behind the two series. The conservation and transcription tests check internal consistency against the cells the earlier session recorded, not against the report. Interactions with #10103 and #10118
Generated by Claude Code |
|
Hub review in progress Reviewing head
Mutants are running. Generated by Claude Code |
|
Ready for hub review The head hasn't moved since The PR makes three methodology choices that no source settles, so METHODOLOGY_CHOICE is yes:
The first two are consistent with the gross series: FindingsP2-1. The IRMAA term counts on Part D take-up that the data may not set (check before landing)
Failure scenario: unless the microdata assign take-up, every high-income enrollee pays the Part D IRMAA in the model. The model's aggregate Part D IRMAA then exceeds the Trustees' aggregate that the average premium excludes, and On main the Part D IRMAA isn't subtracted at all, so this PR makes the gap reachable. Within the model's own rules the treatment is right: an enrollee pays the surcharge and a non-enrollee doesn't. The open question is the data. In the impact run, compare the weighted sum of P2-2. The brief's "quote the published definition" step isn't done, and the definition sentence leaves out the Part B premiumPR body, "Which definition this follows". The brief asked each PR to name the published definition it follows and quote it. The body names CBO's household-income measure (CBO, January 2026, publication 61910) but quotes nothing, and the hand-off says cbo.gov was unreachable. I couldn't reach cbo.gov either (proxy 403), so nobody has read the source behind the claim that "its concept matches" CBO's. The body also says "Where this PR differs is that it subtracts the person's own Part A premium and IRMAA, not averages", but the code also subtracts the person's own gross Part B premium (
P3-1. "Part D enrollee" means two different populations
The PR's own comment says Table V.B3's Part D enrollment "includes retiree drug subsidy beneficiaries" (it equals Table IV.B7's total). The model's A later reader could still go wrong by using
P3-2. The hand-off's mutant table overstates the YAML kill for the extension mutantThe hand-off says "Extend the premium series from 2034 instead of 2035 | 3 failed | killed". The Python file kills it, but the YAML file doesn't: no P3-3. The Hypothesis consumer test re-implements the formula
Cost, measured locally on this VM rather than in Linux CI: the whole file took 19.3 s inside pytest, 71 s wall including Mutants (one process each, on the working tree, restored after each;
|
| Mutant | test_medicare_cost_part_d_premium.py |
medicare_cost.yaml |
|---|---|---|
| M1 (requested): scale the IRMAA by the enrollment share | 1 failed (test_medicare_cost_matches_reference_and_bounds) |
3 failed |
| M2: drop the share (premium per Part D enrollee for everyone) | 1 failed (same test) | 9 failed |
| M3: extend the premium from 2034 instead of 2035 | 3 failed (test_premium_is_…[2035], test_premiums_and_irmaa_return_…[2035], test_extension_rate_follows_…) |
0 failed (12 passed): survives |
| M4 (mine): drop the IRMAA term | 1 failed (consumer test) | 3 failed |
M5 (mine): subtract the average premium only for medicare_part_d_enrolled |
0 failed (85 passed): survives | 1 failed |
| M6 (mine): remove the zero floor | 1 failed (consumer test) | 1 failed |
The YAML runner's failure lines carry no case names. I inferred them from which cases have the input each mutant touches:
- M1 and M4: the three cases with nonzero Part D IRMAA (unit tests 3, 7 and 9).
- M5: unit test 12, the only non-enrollee.
- M6: unit test 10, the floor case.
M5 is the alternative reading of methodology choice 1, and only YAML unit test 12 pins the PR's reading. That's enough, but it is the only guard.
Command per mutant: python3 mut.py <name>, then uv run pytest -q -p no:cacheprovider policyengine_us/tests/core/test_medicare_cost_part_d_premium.py, then uv run python -m policyengine_core.scripts.policyengine_command test policyengine_us/tests/policy/baseline/gov/hhs/medicare/costs/medicare_cost.yaml -c policyengine_us, then git checkout -- policyengine_us.
Checked and sound
-
The defect reproduces at main (
6145ad4513). Main'smedicare_costsubtracts no Part D premium. One enrollee with premium-free Part A and standard Part B, main against head:Year Main Head Difference Hand value 2015 10,147.00 9,938.33 208.67 0.7517 × 277.60 2020 11,765.00 11,543.98 221.02 0.7743 × 285.44 2025 15,531.00 15,350.59 180.41 0.8191 × 220.25 2026 16,548.20 16,310.72 237.48 0.8195 × 289.79 2030 21,354.20 20,820.22 533.98 0.8382 × 637.06 2035 29,329.55 28,716.89 612.66 0.8485 × 722.05 2036 31,053.67 30,420.87 632.80 0.8485 × 722.05 × (722.05/699.07) 2050 67,754.23 66,752.21 1,002.0 0.8543 × 722.05 × 1.03287^15 2101 983,920.69 978,814.94 5,105.75 0.8639 × the 2100 premium, held Every difference matches the hand value.
-
The definition at the head (
medicare_cost.py:35-50).medicare_cost= max(per_capita_cost−base_part_a_premium−gross_medicare_part_b_premium_if_enrolled−enrollment_share × premiums_per_enrollee−income_adjusted_part_d_premium_surcharge, 0). That matches the body. The IRMAA is subtracted in full and only the base premium is scaled. -
Gating.
income_adjusted_part_d_premium_surchargeisdefined_for = "medicare_part_d_enrolled". That variable isdefined_for = "medicare_enrolled"andadds = ["takes_up_medicare_part_d_if_eligible"]. Witness (2025, single, $500k AGI): the surcharge is $1,029.60 with take-up and $0 without, andmedicare_costmoves by exactly $1,029.60. A non-enrollee pays no Part D IRMAA. -
YAML hand values. I recomputed every changed case before reading the test comments:
- Case 1: 17,751 − 2,220 − 180.41 = 15,350.59.
- Case 2: 17,751 − 6,216 − 2,220 − 180.41 = 9,134.59.
- Case 3: 17,751 − 7,546.80 − 180.41 − 85.80×12 = 8,994.19.
- Case 5: 14,293 − 1,782 − 0.7811×306.15 = 12,271.87.
- Case 6: 17,751 − 3,000 − 180.41 = 14,570.59.
- Case 7: 17,751 − 2,220 − 180.41 − 35.30×12 = 14,926.99.
- Case 8: 20,096 − 2,500 − 0.8255×373.53 = 17,287.65.
- Case 9: 23,789 − 3,000 − 0.8382×637.06 − 1,000 = 19,255.02.
- Case 12: 17,751 − 7,546.80 − 180.41 = 10,023.79.
All match the expected outputs. The
test_part_b_msp_offset.pyvalues (15,350.59 and 4,440 → 13,130.59) match too. -
Bounds.
medicare_costcan't go negative because of themax_(…, 0)floor (main has it too), and it can't exceedmedicare_gross_cost. With formula-derived premiums the floor never binds. My fully loaded case has full Part A premium (0 quarters), single filer, $5M AGI two years prior, so top-tier Part B and Part D IRMAA. At the head,medicare_costis 2,369.24 in 2024 and 2,778.19 in 2025 (17,751 − 6,216 − 7,546.80 − 1,029.60 − 180.41 = 2,778.19). With Project Medicare premiums and IRMAA past the last announced year #10103 merged, its smallest value from 2024 to 2045 is 1,813.42 in 2030 (23,789 − 8,328 − 10,425.60 − 2,688 − 533.98).So premiums exceed spending only through direct input, as in YAML unit test 10 (Part B $30,000 → 0). I think the floor is right for a value-of-coverage measure; letting it go negative would only matter for aggregate premium accounting, and that path is unreachable in 2024–2045.
-
Extension.
uprating_extensions.pyextendspremiums_per_enrolleefromlast_projected_year=2035toEND_YEAR. The 2036, 2050 and 2101 values above follow the closed form 722.05 × (722.05/699.07)^(y−2035), held after 2100. The share holds each five-year Trustees point, so 2036 reads 0.8485 and 2050 reads 0.8543. -
Internal consistency of the series. The stored values reproduce the PR's own transcription of the cells. For example, 2025 is (14.9 − 2.4) × 10⁹ / 56,754,000 = 220.25; 2026 is (20.1 − 0.2 − 3.1) × 10⁹ / 57,973,000 = 289.79; and the 2025 share is 56,754 / 69,289 = 0.8191. The transcription and conservation tests (85 passed) check exactly this.
-
Tests run (head, one file per process; times are local wall clock including
uv runstart-up):File Result Time Peak RSS tests/core/test_medicare_cost_part_d_premium.py85 passed 71 s 1.19 GB tests/core/test_medicare_per_capita_cost.py223 passed 87 s 1.60 GB tests/policy/baseline/gov/hhs/medicare/test_part_b_msp_offset.py14 passed 112 s 2.11 GB tests/policy/baseline/gov/hhs/medicare/costs/medicare_cost.yaml12 passed 128 s 2.30 GB -
Readers. Nothing in
policyengine_us/readsmedicare_costbesides its own tests.monthly_medicare_costinqmb_cost_sharing.pyis an unrelated local name. Onlymedicare_costanduprating_extensions.pyread the two new parameters. At baseline the diff movesmedicare_costfor every Medicare enrollee, by −share × premium − own Part D IRMAA, and nothing else in this repo. The population check: the aggregate fall inmedicare_costshould be close to Part D premium income (Table III.D3, $14.9 billion in 2025 per the PR's transcription) scaled to the model's enrollee count, plus the model's aggregate Part D IRMAA (see P2-1). -
Count Medicare net of premiums in CBO household income #10118. Nothing in this PR depends on it. Count Medicare net of premiums in CBO household income #10118's
cbo_medicare_benefitreads its ownpremium_income_per_enrolleeseries, not this PR'spart_dparameters. Count Medicare net of premiums in CBO household income #10118's head (3d8bb6d643) sits on this branch's pre-merge commit4024d2ec08, whosemedicare_coststill scales the IRMAA by the share. When it rebases it should take this branch's version. Its trial merge conflicts only incbo_social_insurance_benefits.yaml. -
Project Medicare premiums and IRMAA past the last announced year #10103. A trial merge with this head (
pr-10103at01cc340a38) is clean. On the merged tree,medicare_cost.yamlgave 12 passed andtest_medicare_cost_part_d_premium.pygave 85 passed, which matches the body's claim. -
Charge the Medicare Part B premium only to people who pay it #10634 (open, not draft) changes
gross_medicare_part_b_premium_if_enrolled, whichmedicare_costsubtracts, and editstest_part_b_msp_offset.py. A trial merge is clean. On the merged tree,test_part_b_msp_offset.pygave 15 passed andmedicare_cost.yamlgave 12 passed. -
Body and docs. Each of these claims checks out:
- the
medicare_costdocumentation string matches the code; - "No other variable reads
medicare_cost"; - "about 82%" (share 0.8191 in 2025);
- the hand table, with "Before" values matching main's YAML;
- "85 cases";
- the seven invariants, each mapped to a test in the file.
The
impact:andaxiom:lines are present. The changelog fragment is a top-levelchangelog.d/medicare-cost-part-d-premiums.fixed.md. - the
-
References. The reference format follows CLAUDE.md: one entry per source, the href opens the first cited page, and multi-page titles are quoted with
#page …. -
Scope. Nothing under
.github/orpolicyengine_us/tests/policy/baseline/partners/. -
Overlap search. I searched open PRs for
medicare_cost,premiums_per_enrollee,enrollment_shareanduprating_extensions. Besides Project Medicare premiums and IRMAA past the last announced year #10103, Count Medicare net of premiums in CBO household income #10118 and Charge the Medicare Part B premium only to people who pay it #10634 above:- Scale Medicare per-enrollee spending to enrollees outside nursing homes #10126 (draft) is stacked on Count Medicare net of premiums in CBO household income #10118 and carries the old IRMAA-scaled
medicare_cost. Its trial merge with this head conflicts inmedicare_cost.py,medicare_cost.yaml,test_medicare_cost_part_d_premium.pyandcbo_social_insurance_benefits.yaml. - Apply immigration tests to Medicare and Medicare Savings Programs #10632 conflicts in
medicare_cost.yaml. - Model the Medicare Part D low-income subsidy as a CBO means-tested transfer #10127 (draft) conflicts only in
cbo_social_insurance_benefits.yaml.
Whichever of these lands second must take this PR's IRMAA-in-full form.
- Scale Medicare per-enrollee spending to enrollees outside nursing homes #10126 (draft) is stacked on Count Medicare net of premiums in CBO household income #10118 and carries the old IRMAA-scaled
What I couldn't check
-
The Trustees cells. cms.gov, govinfo.gov, uscode.house.gov, ecfr.gov, cbo.gov and ssa.gov all returned 403 at this VM's proxy. I couldn't read the 2026 Trustees Report, so I couldn't check any of these against it:
- premium income in Table III.D3, its footnotes 1 and 6, and the Dec. 31 credits for 2015, 2020 and 2026 (nor whether a Dec. 31, 2014 credit should be added to 2015);
- aggregate IRMAA in Table V.E4;
- enrollment in Table V.B3, and whether it includes RDS beneficiaries;
- Table V.D1, and the page-109 and page-154 explanations of the 2025 dip and 2030 jump.
The transcription and conservation tests compare the parameters to cells an earlier session recorded, not to the report. The hub needs to check these cells before landing.
-
Whether NHE Medicare spending is gross of Part D premiums paid directly to plans. The premium income here includes premiums "paid directly to Part D plans" (the PR quotes footnote 1). If the NHE Medicare row that
per_capita_costuses counts only federal payments for Part D, that part is subtracted twice. Main'sper_capita_cost.yamlcomment says premiums "finance part of that spending rather than reduce it" (NHE methodology, file pages 15, 23 and 30), but I couldn't open that paper to see whether it covers direct-to-plan Part D premiums. -
The CBO definition in publication 61910 (P2-2), and the 42 U.S.C. 1395w-113(c)(4) citation.
-
Whether microdata assign
takes_up_medicare_part_d_if_eligible(P2-1), and the population impact. Both need data I can't reach.
Generated by Claude Code
…re-net-of-premiums Brings in current main via #10111. Main renamed unemployment_compensation to total_unemployment_compensation in the CBO social insurance list, so the list now reads social_security, cbo_medicare_benefit, total_unemployment_compensation, workers_compensation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwQq6HHj8EHZr9hwABoKJk
PR Review: Subtract the expected Part D premium from medicare_costSummary
The PR also subtracts I checked the sources before the code:
The design is sound and every expectation I hand-checked is right. Findings: no critical findings and one should-address item. The CriticalNone. Should address1. The The body reads
A pe-parity issue would give the encoder nothing to encode. Suggested line:
This is a body-only edit. Suggestions1. Say in the variable documentation that Extra Help (LIS) enrollees and non-Part-D enrollees net the average premium.
2. Check the Part D IRMAA aggregate in the impact run (data, not code).
3. The Hypothesis consumer test re-implements the formula (
4. Reference and label nits.
5. Body wording.
Verified correct
Axiom line assessmentInvalid as written; CI status
No failures. Branch status
Verdict: REQUEST_CHANGES |
DTrim99
left a comment
There was a problem hiding this comment.
Requesting changes: 0 critical, 1 should. The design and all values check out (21 premium and 34 share values recompute from the 2026 Trustees Report, no double subtraction). Open: the axiom line isn't a valid form; since medicare_cost is a valuation built from Trustees aggregates, an n/a line applies (suggested wording in the report). Full report in the comment above.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Say in `medicare_cost`'s documentation that every enrollee nets the same average premium. - Name retiree drug subsidy beneficiaries in both labels, and add page 153 to the premium reference. - Drop the test's reference equality, keeping the bounds and the IRMAA slope. - Add a 2036 case that pins where the premium's extension starts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Answered at 1b77458c7a. The axiom line reads n/a (should 1). The medicare_cost documentation and the body say every enrollee nets the same average Part D premium (suggestion 1). The hub's comparison on the default dataset will set the weighted surcharge against Table V.E4's aggregate (suggestion 2). The test keeps the bounds and the IRMAA slope and drops the reference equality (suggestion 3). The premium reference lists page 153, and both labels name the retiree drug subsidy count (suggestion 4). The body drops "Draft.", its definition section names the gross Part B premium, and it points to #10118's CBO quotes (suggestion 5).
medicare_cost, the net value of Medicare, subtracts the Part A premium and the gross Part B premium fromcalibration.gov.hhs.medicare.per_capita_cost. That series holds CMS National Health Expenditure Medicare spending per enrollee (from #10107), which covers Parts A through D. Somedicare_costleft in the Part D spending that enrollees' Part D premiums finance, and it overstated Medicare's net value for every enrollee.What changes
medicare_costnow also subtracts the following Part D premiums:income_adjusted_part_d_premium_surchargereads zero for anyone not enrolled in Part D (medicare_part_d_enrolled), somedicare_costsubtracts it in full. Scaling it by the share would count non-enrollment twice and charge an enrollee only about 82% of the adjustment they pay.medicare_costalready subtracts the Part B IRMAA per person the same way.medicare_gross_costdoesn't change.New series under
calibration.gov.hhs.medicare.part_d, from the 2026 Medicare Trustees Report:premiums_per_enrolleeenrollment_shareTable V.B3's Part D enrollment includes retiree drug subsidy beneficiaries, and
takes_up_medicare_part_d_if_eligibleleaves them out.medicare_costreads only the product of the two series, so that denominator cancels. Both labels name the broader count.The Trustees' own figures dip in 2025 and jump in 2030. The report attributes those moves to the 2025 Part D premium demonstration (file page 109) and the 2030 change in the base beneficiary premium percent (file page 154).
Definition
medicare_costgives PolicyEngine's net-of-premiums Medicare value, and no published income measure defines it. #10118 quotes CBO's household-income measure, which counts Medicare at average cost per enrollee net of enrollee premiums, and implements that measure as its own variable.medicare_costsubtracts the person's own Part A premium, gross Part B premium (including the Part B IRMAA) and Part D IRMAA, and only the Part D base premium as an average over enrollees.Household changes (
medicare_cost.yaml, hand-computed)Invariants
policyengine_us/tests/core/test_medicare_cost_part_d_premium.pytests these in 85 cases, some of them Hypothesis properties:medicare_costlies in [0,medicare_gross_cost] for any year and premiums, and the gross cost doesn't depend on the Part D premium.medicare_costdollar for dollar.Tests
medicare_cost.yamlholds 13 cases, including a 2036 case that pins where the premium's extension starts.test_medicare_cost_part_d_premium.pyholds the 85 cases above.medicare_cost.yaml: scaling the IRMAA by the share, dropping the share, dropping the IRMAA, subtracting the average only for Part D enrollees, removing the floor, and extending the premium from 2034.Interactions
Project Medicare premiums and IRMAA past the last announced year #10103 (premium projections) and Charge the Medicare Part B premium only to people who pay it #10634 (which gates
gross_medicare_part_b_premium_if_enrolledon Part B enrollment) merge cleanly with this branch.Once Charge the Medicare Part B premium only to people who pay it #10634 lands too, a Part A-only enrollee nets no Part B premium and the average Part D premium, while
per_capita_costcredits everyone with both parts' average spending.Count Medicare net of premiums in CBO household income #10118 stacks on this branch.
cbo_medicare_benefitthere reuses the Part D premium and IRMAA aggregates through its own series.Scale Medicare per-enrollee spending to enrollees outside nursing homes #10126 and Model the Medicare Part D low-income subsidy as a CBO means-tested transfer #10127 stack on Count Medicare net of premiums in CBO household income #10118. Scale Medicare per-enrollee spending to enrollees outside nursing homes #10126's
medicare_costscales the IRMAA by the share, so its next merge takes this PR's formula.Apply immigration tests to Medicare and Medicare Savings Programs #10632 adds cases to
medicare_cost.yamland conflicts there, so whoever lands second keeps both sets.No other variable reads
medicare_cost.CI: the latest Pull request run at this head hasn't finished.
impact: pending a comparison on the default dataset for 2026, main against this branch.
axiom: n/a: this PR subtracts the average Part D premium and the person's own Part D surcharge from PolicyEngine's Medicare net-benefit value, and leaves eligibility and premium-liability calculations alone.
🤖 Generated with Claude Code