Materialize Medicaid take-up via anchored count-calibration to CMS state snapshots#334
Materialize Medicaid take-up via anchored count-calibration to CMS state snapshots#334daphnehanse11 wants to merge 7 commits into
Conversation
|
Ran an adversarial multi-angle review of this branch (8 finder angles, per-finding verification) before marking ready. Outcome, for reviewer context: Confirmed and fixed in 2cb7f97:
Refuted in verification (no change): NaN CMS targets can't reach the stage — ledger compilation enforces Known accepted interplay: if literally every state saturated (CMS count ≥ modeled eligibles everywhere), the flag would be constant-True — this stage's gate passes by saturation doctrine while the degenerate-input gate fails the build. That's treated as the intended loud backstop for a pathological feed, not a contradiction to engineer away; mechanics verified, trigger judged improbable (mixed saturation map expected). Deferred (chips filed): sharing the ACA batch sims instead of a second full-population eligibility pass (~15–45 min/build); generalizing the ACA-branded error labels in the shared binary ops; parity-gate staleness enforcement. |
The module helper shared a name with the builder's _state_fips_text but not its semantics (regex strip vs int coercion; silent 'nan' strings vs raising) — the same-name trap the #334 review flagged. Rename to _normalize_state_fips and refuse NaN input rather than letting a missing code surface later as a confusing states-without-targets gate failure. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Max's review of #294 (SNAP take-up) spotted that the static guard covered immigration outputs plus seeded_take_up_programs() only, so a program whose stage produces its flag under a different contract treatment sat outside it — SNAP (out_of_scope) was hand-fixed on #294, and count_calibrated programs (Medicaid in #334, CHIP next per the contract followup) had the same exposure: the staleness failure would only surface on the first release build, not in CI. The guard now derives the produced set from contract treatments ({seed, count_calibrated}) instead of the seeded helper, making it the CI enforcement of the rule that each producing PR removes its own register line. Keyed on the treatment string rather than #334's count_calibrated_take_up_programs() helper so the test imports nothing that does not exist on main yet; it picks up the Medicaid flip (and any future CHIP flip) automatically at merge, in either merge order. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Marking ready and requesting review. One decision here is genuinely doctrine-level and yours to bless: the |
…ate snapshots Add a count_calibrated take-up contract treatment (no administrative participation rate exists for Medicaid, but administrative enrollment counts do) and a medicaid_take_up source stage following the native ACA pattern: eligible reporters of has_medicaid_health_coverage_at_interview always take up, everyone else draws at an in-build state fill rate, and the assignment is greedily calibrated to CMS December 2024 state enrollment snapshots among eligible non-anchored persons. Point-in-time (average-month) semantics per #332, HIPSM precedent recorded in the contract doctrine. Builder wiring mirrors the ACA stage: batched engine simulation materializes person-level is_medicaid_eligible, the medicaid_enrollment target role compiles the state target table, and a release gate fails on the all-True landmine while classifying saturated states (CMS count at or above modeled eligible weight) as the eligibility-undercount symptom they are. Removes takes_up_medicaid_if_eligible from the reviewed degenerate-input exclusions; heals the #170 enrollment==eligibility degeneracy and stops weight calibration from absorbing take-up error into the eligible population. CHIP is deliberately deferred behind the #321 M-CHIP/separate-CHIP ledger concept split; its contract entry records that dependency. Closes #331; doctrine half of #332. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…review Findings from an 8-angle review with per-finding verification; the confirmed defects and their fixes: - Bump TARGET_FRAME_CHECKPOINT_MATERIALIZER_VERSION to 2: the checkpoint identity hashes the on-disk base dataset, not the staged frame, so a pre-stage checkpoint would silently calibrate weights against all-True medicaid_enrolled columns while the export ships the new flag. - Rework the gate landmine check: full enrollment explained by anchor mass or by a floor within granularity (2% or one person weight) of eligibility is a legitimate calibrated outcome, not a spurious build abort; the count-miss check gains the same granularity allowance. - Make anchor preservation a person-level invariant checked in every state, saturated or not (anchored_not_taking_up_count in diagnostics); aggregate comparisons cannot see a dropped anchor once calibration back-fills to the count. - Delete the now-false eCPS parity exemption for takes_up_medicaid_if_eligible (parity_gate has no staleness check, so the stale entry would exempt the layer forever); the anchored column clears the presence-based gate on its own. - Refuse duplicate per-state target rows in the builder table: the calibrate op is last-row-wins while rate/diagnostics/gate sum them. - Re-check count-calibrated columns on the export frame: a downstream transform dropping the column can no longer ship the engine-default landmine with only an observational JSON field recording it. - Keep the debt-ledger pointer (scope_owner/followup) on count_calibrated participation rows and label the share's universe (off-domain propensity included) so it cannot be read as enrollment. - Refuse empty state targets up front; record weights_basis and max_person_weight in diagnostics; reuse take_up's stable-draw keying and Frame.broadcast instead of private near-copies; drop dead knobs. Refuted in verification (no change): NaN targets cannot reach the stage (ledger compilation enforces math.isfinite at both entry points). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The module helper shared a name with the builder's _state_fips_text but not its semantics (regex strip vs int coercion; silent 'nan' strings vs raising) — the same-name trap the #334 review flagged. Rename to _normalize_state_fips and refuse NaN input rather than letting a missing code surface later as a confusing states-without-targets gate failure. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Max's #294 review pattern (he independently audited the FNS rate's vintage against newer publications) says the sharpest domain question on this stage is the one thing left implicit: the ASEC anchor is interview-point (~Feb-Apr) while the CMS target is the December snapshot. Record in the module docstring and the contract calibration block that both are point-in-time stocks with deliberately unaligned months, and that the gate's anchor-floor rule absorbs the declining-2024 direction (spring reporters exceeding a December count) rather than reading it as a calibration miss. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
eefdd92 to
6b0ea78
Compare
…ries parity_gate skipped known-gap names unconditionally, unlike its sibling gates (default_valued_columns_gate at gates.py:707 and source_stage_input_coverage_gate at gates.py:1499), which both fail on stale reviewed exclusions. That let register entries rot silently once the candidate started producing the exempted layer (the takes_up_medicaid_if_eligible case surfaced in PR #334). The gate now fails on any exemption whose candidate nonzero share is positive, reports exemptions the reference never populates as dormant, and records both in the gate details. Auditing the register against the current release pipeline found four entries stale on arrival — the pipeline deterministically materializes them before the gate runs: takes_up_tanf_if_eligible and takes_up_eitc (contract-seeded take-up, PR #315), ssn_card_type and immigration_status_str (immigration stage, #266). A new register test pins that statically-knowable subset so a stale entry is caught at test time, not first at release-build time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Max's review of #294 (SNAP take-up) spotted that the static guard covered immigration outputs plus seeded_take_up_programs() only, so a program whose stage produces its flag under a different contract treatment sat outside it — SNAP (out_of_scope) was hand-fixed on #294, and count_calibrated programs (Medicaid in #334, CHIP next per the contract followup) had the same exposure: the staleness failure would only surface on the first release build, not in CI. The guard now derives the produced set from contract treatments ({seed, count_calibrated}) instead of the seeded helper, making it the CI enforcement of the rule that each producing PR removes its own register line. Keyed on the treatment string rather than #334's count_calibrated_take_up_programs() helper so the test imports nothing that does not exist on main yet; it picks up the Medicaid flip (and any future CHIP flip) automatically at merge, in either merge order. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts: # tools/build_us_fiscal_refresh_release.py
…weight A state whose eligibility feed collapsed to zero classifies as saturated (any positive count exceeds zero eligible weight), which skipped the count checks and would ship anchored-only enrollment silently. Treat that saturation as a division artifact and fail the gate instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
Implements #331 (and the doctrine half of #332):
takes_up_medicaid_if_eligibleis now materialized by amedicaid_take_upsource stage instead of shipping at the engine's all-True default, healing the #170enrollment == eligibilitydegeneracy (72.30M == 72.30M in the published 2024 release) and stopping weight calibration from absorbing take-up error into the eligible population.No administrative participation rate clears the #315 provenance bar for Medicaid (us-data's state rates were MACPAC enrollment over modeled eligibility — a model-relative ratio), so this stage cites no rate. It follows the native ACA stage pattern instead, recorded as a new contract treatment:
count_calibratedcontract treatment: anchored assignment calibrated to administrative enrollment counts (Ledger facts). Loader validation requires acalibrationblock naming the anchor and target fact families, and refuses a sourced rate (that would beseedmisclassified).has_medicaid_health_coverage_at_interview(CPS ASEC, point-in-time) always take up. CPS undercounts Medicaid, so the anchor is a floor.calibrate_binary_assignmentgreedily hits the CMS December 2024 state snapshot among eligible non-anchored persons.Mechanics
is_medicaid_eligible, themedicaid_enrollmenttarget role compiles the state target table, and the stage output writes back to the person table.is_medicaid_eligible), but eligibility-expanding reforms enroll newly eligible people at a plausible propensity instead of a hard-coded zero. Not a behavioral model — that is future utility-layer work.takes_up_medicaid_if_eligiblefrom the reviewed degenerate-input exclusions (that registry fails when a listed column becomes non-degenerate).us_medicaid_take_up.jsonwritten beside the take-up participation diagnostics; the participation diagnostics now reportcount_calibratedprograms' live surface instead of "ships at engine default".Deliberately out of scope
total_chip_enrollmentmixes M-CHIP into a concept the model does not materialize (20 states zero-support). The contract entry records the dependency; CHIP follows this pattern once the ledger splits the concepts.Tests
test_us_medicaid_take_up.py: contract validation (calibration block required, sourced rate refused), anchor preservation (including model-ineligible reporters), unsaturated states hitting the CMS count to unit-weight granularity, saturation classification, bit-reproducible reruns, source-identity draw keying, off-domain propensity, gate failure modes (landmine, missing target, dropped anchor, anchor-above-count floor), and builder helpers (target-role filtering, person state mapping).main()end-to-end stubs,US_DONORS/stage-order plan alignment.uv run pytest),ruff checkclean.Closes #331. Refs #332, #170, #321, #323, #315.
🤖 Generated with Claude Code