Skip to content

US Medicare IRMAA: enrollee-gated Part B premium target, Part D take-up input, IRMAA validation rows, SOI age-by-AGI counts #1206

Open

Description

@MaxGhenis

Summary

PolicyEngine's Medicare IRMAA runs above the 2026 Medicare Trustees Report on the certified US dataset: Part B by 13% and Part D by 27.5% in 2026. Most of the gap is data. Three Microcosm changes, and one rule change already in a PE PR, address it. Waiting on Max's decision; do not build or ship until it is ruled.

The run used populace-us-2024-spm-20260915 on pe-us 2.2.1 with PolicyEngine/policyengine-us#10106 backported. Every figure is a weighted MicroSeries sum from a real microsimulation. Evidence, scripts and JSON are in ~/PolicyEngine/crfb-m4a/research/irmaa-run/diagnosis/ (REPORT.md).

Findings

2026 Model Trustees (V.E3 / V.E4)
Part B payers 6.07M 6.1M
Part B IRMAA $21.27B $18.8B
Part B per payer $292/month $257/month
Part D payers 6.07M 5.3M
Part D IRMAA $3.95B $3.1B
  1. Part D is charged to every Medicare enrollee. takes_up_medicare_if_eligible comes from ASEC MCARE == 1 and covers Part A or Part B. Nothing in the data marks Part D enrollment. Charge the Part D income-related premium only to Part D enrollees policyengine-us#10115 adds the input takes_up_medicare_part_d_if_eligible (default true) and gates the surcharge on it.

  2. The tier mix is top-heavy because of the AGI tail (US top-income tail is missing above $5M of AGI: Table 1.1 size-of-AGI facts never bind, and the pool's tail stratum carries capital gains only #958). The payer count is right, but too many payers sit in tiers 4–5. Comparing heads aged 65 or older in 2024 against SOI TY2023 Table 1.6 returns, the file holds:

    • 1.24x at $200K–500K;
    • 1.87x at $500K–1M;
    • 3.9x at $1M–1.5M.
  3. The Part B payer match is two errors offsetting. A prototype reweighting of the certified file to SOI Table 1.1 for $100K–$5M (Bind the SOI Table 1.1 size-of-AGI distribution as calibration targets #960's method) moves Part B 2026:

    • dollars to −8%;
    • payers to −20%;
    • per payer still +14%.

    Adding the Table 1.6 65+ counts gives −13.6%, −21.5% and +10%.

  4. The Part B premium target is mapped to the wrong variable. fiscal_targets.py maps CMS premiums-from-enrollees (CY2024, $139.837B, IRMAA included) to gross_medicare_part_b_premium. That variable is defined for is_medicare_eligible, so it also counts eligibles who aren't enrolled. For 2024:

    • $128.23B on pe-us 2.2.1, where 2024 IRMAA is zero;
    • $148.61B with #10106.

    gross_medicare_part_b_premium_if_enrolled, documented in pe-us for this target, gives $137.65B with #10106 (−1.6%).

Proposal (each item needs Max's go)

  1. Target mapping. Map ("cms_medicare", "actual_amount", "premiums_from_enrollees") to gross_medicare_part_b_premium_if_enrolled.

  2. Part D take-up.

    • After policyengine-us#10115 merges, produce takes_up_medicare_part_d_if_eligible for Medicare enrollees.
    • Weighted Part D enrollment should match Trustees Table V.B3 Part D less Table IV.B7 retiree drug subsidy enrollees: about 57.4M in 2026.
    • Condition the draw on a source with Part D status, such as MEPS or MCBS, rather than a flat rate.
    • Then validate against the Part D payer count.
  3. IRMAA validation rows (validation only). Trustees V.E3/V.E4 CY2024 actuals:

    • Part B: 4.8M payers and $12.8B;
    • Part D: 4.2M payers and $2.2B.

    Measure on the release with Part B as gross_medicare_part_b_premium_if_enrolled − base_part_b_premium over Medicare enrollees, and Part D as income_adjusted_part_d_premium_surcharge. Consider promoting them to calibration once Bind the SOI Table 1.1 size-of-AGI distribution as calibration targets #960/Carry PUF high-AGI donors into the own-tail stratum with their full income vector #964 land and the residual is known.

  4. SOI Table 1.6. Bind return counts by age × AGI size (at least the 65+ row, $100K and up) alongside Bind the SOI Table 1.1 size-of-AGI distribution as calibration targets #960's Table 1.1 rows. The source is 23in16ag.xls: counts only, with age taken from the primary taxpayer.

Not settled by these runs

Activity

  1. MaxGhenis commented on Oct 12, 2026

    @MaxGhenis
    ContributorAuthor

    Change 2 (takes_up_medicare_part_d_if_eligible): checked 2026-10-12 09:20 UTC, not started.

    The change waits on PolicyEngine/policyengine-us#10115, which is still open:

    • Review: none yet (REVIEW_REQUIRED).
    • CI at head 0470a2567f: one failure, in Full Suite - Rest (run 38168682127). The failing test is test_no_group_variables_in_person_adds.py::test_known_violations_only_shrink, which asks to delete ('il_aabd_expense_exemption_person', 'adds', 'state_withheld_income_tax') from KNOWN_VIOLATIONS. pe-us main has already dropped that entry, so the failure comes from main moving on, not from the Part D change. A new run (38228102580) was queued at 09:09 UTC.

    Two Microcosm-side blockers remain after #10115 merges:

    Next step: once #10115 merges and ships in a release, pick this up as specified in d1372.

  2. added a commit that references this issue on Oct 12, 2026
  3. MaxGhenis commented on Oct 12, 2026

    @MaxGhenis
    ContributorAuthor

    Changes 1 and 3 of d1372 (approved by Max 2026-10-12) landed in #1211, merged as 1239276. Change 2 is a tracked follow-up, below. Change 4 goes with #960. No data was released.

    1. Part B premium target remap (landed)

    ("cms_medicare", "actual_amount", "premiums_from_enrollees") now binds gross_medicare_part_b_premium_if_enrolled (defined for medicare_enrolled) instead of gross_medicare_part_b_premium (defined for is_medicare_eligible). Nothing else about the target changes. The national_state surface version moves from f96be9f41781 to e1f9906bf0e0; counts and this target's loss weight are unchanged.

    Engine version. Microcosm locks policyengine-us 2.2.1, which doesn't include PolicyEngine/policyengine-us#10106. The first PyPI release with #10106 is 2.40.0 (uploaded 2026-10-12T06:49Z; checked in the wheel). Until Microcosm's engine moves to 2.40.0 or later, a 2024 release has no IRMAA, so this row reads the standard premium alone.

    On the certified file in 2024, before calibration, from the diagnosis's real runs:

    old gross_medicare_part_b_premium new ..._if_enrolled
    pe-us 2.2.1 (locked) $128.23B (−8.3%) $120.58B (−13.8%)
    with #10106 $148.61B (+6.3%) $137.65B (−1.6%)

    So until the engine bump, a release built with this mapping leans harder on Medicare-enrollee weights. That's noted on the release decision (d1318).

    Out of reach for the model: the $139.837B target includes $14.410B paid by disabled enrollees under 65; enrollees aged 65 and over paid $125.427B. Source: 2025 Trustees Report Table III.C1, CY2024, PDF p. 86. The model has no Medicare enrollees under 65 (#876), so about 10.3% of the target is out of its reach. With #10106 the model's $137.65B is 9.7% above the 65-and-over row. Whether to target the 65-and-over row instead is queued for Max as d1410.

    3. IRMAA validation rows (landed, validation only)

    us_medicare_irmaa_holdout.json is written beside the SSI holdout. It is never a calibration row and never a release gate. It compares the model with the 2026 Medicare Trustees Report, CY2024 historical rows:

    • Part B: Table V.E3 (p. 209, PDF p. 215), 4.8M payers and $12.8B.
    • Part D: Table V.E4 (p. 212, PDF p. 218), 4.2M payers and $2.2B.

    How the model side is measured:

    • Part B: gross_medicare_part_b_premium_if_enrolled − base_part_b_premium, for medicare_enrolled people.
    • Part D: the surcharge, for enrollees. Any surcharge outside enrollment is reported separately.
    • No payers: with no weighted payer, the status is no_payers and the comparisons are withheld rather than shown as −100%. This also applies Part by Part.

    Real runs on the certified H5:

    • Release builder's batched scorer, pe-us 2.2.1: no_payers (57.52M weighted enrollees, no payer).
    • pe-us 2.2.1 with #10106:
      • Part B: 5.649M payers (+17.7%) and $17.067B (+33.3%).
      • Part D: 5.649M payers (+34.5%) and $3.278B (+49.0%).
      • Part D payers equal Part B payers until #10115.

    2. Part D take-up (tracked follow-up)

    PolicyEngine/policyengine-us#10115 is still open (review required), so change 2 hasn't started. A follow-up chip carries the brief. What it needs to handle:

    • Target: CY2024 Part D enrollment is 55.249M (V.B3, p. 190) less 0.8M retiree drug subsidy enrollees (IV.B7, p. 147), about 54.4M. Both figures include under-65 enrollees the model lacks.
    • Engine blocker: the stored-input register refuses takes_up_medicare_part_d_if_eligible on an engine that doesn't define it, so this also depends on Microcosm's engine bump.
    • Draw: condition it on MEPS or MCBS Part D status, as proposed above.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions