Repository navigation
US Medicare IRMAA: enrollee-gated Part B premium target and held-out IRMAA validation rows (#1206) - #1211
Conversation
…1206) The CMS premiums-from-enrollees target (2025 Trustees Report Table III.C3, CY2024 $139.837B) was mapped to gross_medicare_part_b_premium, which PolicyEngine-US defines for is_medicare_eligible, so it also charged eligible people who never enrolled. Map it to gross_medicare_part_b_premium_if_enrolled, defined for medicare_enrolled and documented in PolicyEngine-US for this target. Tests: the compiled spec binds the new variable; a differential test compiles the same fact under the old and new mapping and finds only base_variable differs; an engine-contract test pins both variables' gates in the locked policyengine-us. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds us_medicare_irmaa_holdout.json beside the SSI holdout: weighted Part B and Part D income-related premium payers and annual amounts on the written release H5, compared with the 2026 Medicare Trustees Report, Tables V.E3 (p. 209) and V.E4 (p. 212), CY2024 historical rows: 4.8M payers and $12.8B (Part B), 4.2M and $2.2B (Part D). Validation only: it never enters the calibration registry and never refuses a release; an unavailable measurement is reported as such. Part B is gross_medicare_part_b_premium_if_enrolled minus base_part_b_premium for medicare_enrolled people, because PolicyEngine-US defines the base premium for every eligible person. Part D is income_adjusted_part_d_premium_surcharge. Invariants, tested (Hypothesis where stated): inputs are never written; totals scale with weights, are order invariant and additive over batches; payers are bounded by weighted enrollees; non-enrollees never reach Part B; the payload is batch-size invariant through the household-batched scorer; no ledger mapping or pinned Chronicle record set carries an IRMAA row. An engine-scenario test checks the measures on pe-us 2.2.1 and that its 2024 top-tier amounts equal the Trustees table. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The registry version hashes target metadata, so binding the Part B premium target to gross_medicare_part_b_premium_if_enrolled moves it from f96be9f41781 to e1f9906bf0e0. Target counts are unchanged, and the differential test pins that only base_variable differs. National targets' concept-budget keys exclude base_variable, so the target's loss weight is unchanged too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…2.2.1 caveat (#1206) Independent review (GPT-6.1 Sol, ~/reviews/irmaa-1206/review_42d899c52.md): - B1: on the locked policyengine-us 2.2.1 a 2024 release has no IRMAA (it rests on income two years earlier, absent from a single-year H5), so the holdout would have reported "available" with -100% comparisons. It now reports status no_payers (or no_medicare_enrollees) with a reason and withholds the Trustees comparisons. An engine-scenario test pins the locked engine's zero for a 2024-only household, so a bump that includes pe-us#10106 changes it visibly. - B2: the target comment and changelog say the bound premium carries no 2024 IRMAA until Microcosm's engine includes pe-us#10106. - N1: the payload and agent guide name both enrollment gates (Part A and/or B; Part D surcharge on every enrollee before pe-us#10115). - N2: Part D now counts medicare_enrolled people only, like Part B, and reports any surcharge outside enrollment; the Hypothesis property perturbs non-enrollees' Part B and Part D values. - N3: the feed test keeps only the IRMAA text check; a new test pins that only the release tool imports the holdout. - N4: the historical compiled-filter fixture's docstring notes it predates the remap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow-ups from the approving delta review (review_5570ee8b7.md): - A Part with no weighted payer now withholds its own Trustees comparison (comparison_withheld) even when the other Part has payers, so no Part is ever reported at -100% (e.g. pe-us#10115 before Part D take-up exists). The Hypothesis property checks comparisons per Part. - The no-payers reason says the outcome is expected on policyengine-us 2.2.1 rather than asserting that engine as the cause, and no longer says "rather than -100%" for years with no Trustees row. - The agent guide states the verified outcome: the release builder's batched scorer reports no_payers on the certified populace-us-2024-spm-20260915 file under pe-us 2.2.1 (57.52M weighted enrollees, no payer). - The import guard tests relative path parts, so a checkout under a directory named tests cannot exclude every file. - The changelog says the Part B row's calibration miss widens until the engine includes pe-us#10106. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged at the reviewed head 8714515 as a merge commit. The gates held:
Independent review (GPT-6.1 Sol, via Subfleet):
The review files are in Real-data runs of the holdout on the certified
No data was regenerated, released or published. |
The branch now sits on 51a2637. main brought the landlord work of microcosm#1145: the 13 SPI Table 3.7 property-income amounts bound unscaled and uprated by the band index, the property components stage, the PRIS resource, and policyengine-uk 2.123.0; and #1188 and #1211. Where the two sides touched one hunk the rebase kept the branch's side; this commit puts both sides in and regenerates what depends on them, on the 942a1fa feed. - The SPI property-income amount target takes main's note and band index and keeps this branch's taxpayer filter (income_tax > 0), as every other SPI row does here (decision "taxpayer basis everywhere"). - National references: 1,281 active, the branch's 1,268 plus the 13 property amounts. - The vendor selections keep both sides' resources; hmrc_property_rental_income_facts.json is vendored again under the 942a1fa feed identity, and the UK spine parity fixture follows it. - The two national parity receipts and the reference membership are regenerated; the local references, the local census and the local parity receipt come out byte-identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Implements the Microcosm changes Max approved in d1372 (2026-10-12) for #1206: changes 1 and 3. Change 2 waits on PolicyEngine/policyengine-us#10115, and change 4 is out of scope.
1. Part B premium target binds the enrollment-gated premium
The CMS premiums-from-enrollees target (2025 Trustees Report Table III.C3, CY2024 $139.837B, Chronicle package
cms-medicare-trustees-report-2025-part-b-premium-income) moves fromgross_medicare_part_b_premiumtogross_medicare_part_b_premium_if_enrolled.is_medicare_eligible, so it also charged eligible people who never enrolled.medicare_enrolled, and pe-us documents it "for CMS premiums-from-enrollees calibration targets".pe-us version. Microcosm locks policyengine-us 2.2.1 (
uv.lock; the extras allow>=2.2.1,<3), which does not include PolicyEngine/policyengine-us#10106.medicare_irmaa_magiand the same-year switch; 2.39.1 does not.Microcosm locks the engine its certified default was built with (#1086), so moving to ≥2.40.0 is a separate step, not done here. Until then the target reads without #10106.
What the target reads on the certified file (
populace-us-2024-spm-20260915, 2024, uncalibrated weighted sums). These come from real microsimulations in the #1206 diagnosis (crfb-m4a/research/irmaa-run/diagnosis/part_b_target_check_*.json):gross_medicare_part_b_premium..._if_enrolledUnder 2.2.1 the old mapping misses by less only because it charges eligible non-enrollees the base premium.
Caveat for #1206 (not changed here). The $139.837B total includes $14.410B from 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 this target is out of the model's reach. Whether to target the 65-and-over row instead is a methodology call, raised on #1206.
3. Held-out IRMAA validation rows
A new file,
us_medicare_irmaa_holdout.json, is written besideus_ssi_social_security_holdout.jsonand follows the same pattern as #1178.status: unavailable.It compares the model with the 2026 Medicare Trustees Report. These are the CY2024 historical rows, checked against the report PDF (sha256
ffa56b91…, pinned in the module):Model measures, per person, weighted by the calibrated household weight. Both Parts count
medicare_enrolledpeople only (Part A and/or Part B):gross_medicare_part_b_premium_if_enrolled − base_part_b_premium, becausebase_part_b_premiumis defined for every eligible person. This matches the table's footnote 2: amounts "in excess of the Part B standard premium".income_adjusted_part_d_premium_surcharge. pe-us charges it to every Medicare enrollee above the thresholds until pe-us#10115 gates it on Part D enrollment. Any surcharge outside Medicare enrollment is excluded and reported asoutside_enrollment_weighted_amount.availablewhen some weighted enrollee pays. Otherwise it isno_payersorno_medicare_enrollees: the model totals are still reported, the Trustees comparisons are withheld rather than shown as −100%, and the payload gives the reason.Source registration. The rows are cited constants in the module: title, URL, tables, printed and PDF pages, PDF hash, footnote definitions. That is how the SSI holdout registers SSA Table 9. I did not add a Chronicle package, because Chronicle facts feed the calibration compiler and these rows must stay out of it. The pinned feed carries no IRMAA record set, and a test pins that.
What the holdout will read. On locked 2.2.1 it reports
no_payersfor 2024, because 2024 IRMAA is effectively $0 on the certified file. Verified end to end: the builder's own_write_medicare_irmaa_holdout, run through the real household-batched scorer on the certified H5 (sha2566496cc43…), wrotestatus: no_payers. It found 57,519,410.6 weighted Medicare enrollees and no payer in either Part, in 46.7 s (crfb-m4a/research/irmaa-run/holdout_check/scorer_pinned.json). In the diagnosis run (irmaa_aggregates_pinned-pe-us-2.2.1.json), the weighted count of tax units with a nonzero two-years-prior MAGI rounds to 0.00M, and both IRMAA totals round to $0.000B. pe-us 2.2.1 takes that MAGI from 2022 AGI and income, and the H5 carries 2024 inputs only. With #10106 the diagnosis measured Part B IRMAA among enrollees at $17.067B and the Part D surcharge at $3.278B for 2024 (irmaa_aggregates_patched-2.2.1-pr10106.json).Invariants (tested)
base_variablediffers._if_enrolledis a yearly Person variable gated onmedicare_enrolled, and the old variable is gated onis_medicare_eligible.no_payersorno_medicare_enrollees, never a −100% comparison.base_part_b_premiumorincome_adjusted_part_d_premium_surchargeor carries an IRMAA role. No pinned Chronicle record set mentions IRMAA or income-related premiums. Onlytools/build_us_fiscal_refresh_release.pyimports the holdout module.no_payers. It fails, by design, once Microcosm's engine includes #10106.national_statesurface version moves from f96be9f41781 to e1f9906bf0e0, because it hashes target metadata. Target counts are unchanged. National concept-budget keys excludebase_variable, so this target's loss weight is unchanged.Review
Independent review by GPT-6.1 Sol at 42d899c (
~/reviews/irmaa-1206/review_42d899c52.md) asked for changes. All findings are addressed in 5570ee8, and the delta review there approved (review_5570ee8b7.md). Its five non-blocking observations are addressed in 8714515: per-Part withholding of comparisons, the verified scorer-path outcome in the guide, conditional reason wording, a robust import guard, and the changelog direction. The final delta review approved at 8714515 with no defects found (review_871451578.md). B1 is theno_payersstatus plus a locked-engine scenario test. B2 is the caveat in the code comment and changelog. N1 names the gates. N2 masks Part D by enrollment. N3 adds the import guard and drops the exact-set assertion. N4 is a fixture note.Not in this PR
axiom: n/a: Microcosm calibration-target mapping and a validation diagnostic, no policy rule.
🤖 Generated with Claude Code