Skip to content

Materialize Medicaid take-up via anchored count-calibration to CMS state snapshots#334

Open
daphnehanse11 wants to merge 7 commits into
mainfrom
medicaid-chip-take-up
Open

Materialize Medicaid take-up via anchored count-calibration to CMS state snapshots#334
daphnehanse11 wants to merge 7 commits into
mainfrom
medicaid-chip-take-up

Conversation

@daphnehanse11

Copy link
Copy Markdown
Collaborator

Summary

Implements #331 (and the doctrine half of #332): takes_up_medicaid_if_eligible is now materialized by a medicaid_take_up source stage instead of shipping at the engine's all-True default, healing the #170 enrollment == eligibility degeneracy (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_calibrated contract treatment: anchored assignment calibrated to administrative enrollment counts (Ledger facts). Loader validation requires a calibration block naming the anchor and target fact families, and refuses a sourced rate (that would be seed misclassified).
  • Anchor: eligible reporters of has_medicaid_health_coverage_at_interview (CPS ASEC, point-in-time) always take up. CPS undercounts Medicaid, so the anchor is a floor.
  • Fill: everyone else draws at an in-build state fill rate (CMS count over weighted modeled eligibles — computed transparently, never cited as provenance), then calibrate_binary_assignment greedily hits the CMS December 2024 state snapshot among eligible non-anchored persons.
  • Point-in-time semantics (Declare point-in-time (average-month) semantics for enrollment take-up flags and snapshot-month provenance #332): anchor, targets, and concept all sit at a representative month — the Urban HIPSM convention ("a point-in-time snapshot … rather than an annual average"), now contract doctrine. Ever-enrolled counts would overstate spending against per-FYE cost denominators.

Mechanics

  • Builder mirrors the ACA stage: batched engine simulation materializes person-level is_medicaid_eligible, the medicaid_enrollment target role compiles the state target table, and the stage output writes back to the person table.
  • Gate: fails on the all-True landmine (unsaturated state with enrollment == eligibility), a missed CMS count beyond 2% relative, a dropped anchor, or a state missing its target row. Saturated states (CMS count ≥ modeled eligible weight — Add Medicaid and CHIP eligibility-to-enrollment diagnostics #170's −2.4% combined shortfall) are classified and reported, not failed: full enrollment is the calibrated answer there, and the shortfall is an eligibility-undercount question out of scope here.
  • Off-domain propensity: the assign op deliberately carries no eligibility mask, so persons outside today's modeled eligibility keep a draw-based flag at the state fill rate. Invisible at baseline (the engine gates on 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.
  • Removes takes_up_medicaid_if_eligible from the reviewed degenerate-input exclusions (that registry fails when a listed column becomes non-degenerate).
  • Diagnostics artifact us_medicaid_take_up.json written beside the take-up participation diagnostics; the participation diagnostics now report count_calibrated programs' live surface instead of "ships at engine default".

Deliberately out of scope

Tests

  • 23 new tests in 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).
  • Updated: contract treatment enum test, builder main() end-to-end stubs, US_DONORS/stage-order plan alignment.
  • Full workspace suite green (uv run pytest), ruff check clean.

Closes #331. Refs #332, #170, #321, #323, #315.

🤖 Generated with Claude Code

@daphnehanse11

Copy link
Copy Markdown
Collaborator Author

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:

  • Checkpoint staleness: the target-frame checkpoint identity hashes the on-disk base dataset, not the staged frame, so a pre-branch checkpoint would have silently calibrated weights against all-True medicaid_enrolled. TARGET_FRAME_CHECKPOINT_MATERIALIZER_VERSION bumped to 2.
  • Gate landmine false-positives: full enrollment explained by anchor mass (all eligibles reported coverage) or by greedy overshoot within one person weight of the count is a legitimate outcome; the check now exempts both instead of aborting the build.
  • Anchor preservation is now a person-level invariant (anchored_not_taking_up_count) checked in every state including saturated ones — aggregates can't see a dropped anchor once calibration back-fills to the count.
  • Stale eCPS parity exemption for takes_up_medicaid_if_eligible deleted (its "Populace does not yet produce it" reason is false as of this PR; parity_gate has no staleness check so it would have exempted the layer forever).
  • Duplicate per-state target rows now refuse to compile (calibrate op is last-row-wins; rate/diagnostics/gate sum them).
  • Export-frame re-check: a count-calibrated column shipping at the engine default on the export frame now fails the release rather than being recorded observationally.
  • Participation diagnostics keep the scope_owner debt pointer on count_calibrated rows and label the share's universe (the flag deliberately carries off-domain propensity Trues).
  • Reuse: draws now delegate to take_up._stable_unit_draws, person state codes use Frame.broadcast; empty targets refuse early; dead knobs removed.

Refuted in verification (no change): NaN CMS targets can't reach the stage — ledger compilation enforces math.isfinite at both entry points.

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.

daphnehanse11 added a commit that referenced this pull request Jul 6, 2026
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>
daphnehanse11 added a commit that referenced this pull request Jul 7, 2026
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>
@daphnehanse11 daphnehanse11 marked this pull request as ready for review July 7, 2026 14:42
@daphnehanse11 daphnehanse11 requested a review from MaxGhenis July 7, 2026 14:42
@daphnehanse11

Copy link
Copy Markdown
Collaborator Author

Marking ready and requesting review. One decision here is genuinely doctrine-level and yours to bless: the count_calibrated contract treatment (amends the #315 enum) — used when no administrative participation rate exists but administrative counts do, per the native ACA stage pattern your contract note describes. Your #294 approval covered the same mechanical family (reported anchor, identity-keyed draws, constant-True healing, gate); the deltas here are count-calibration in place of a cited rate, the unmasked anchor, and the off-domain propensity, all flagged in the PR body with rationale. #331/#332 carry the full design record, including the HIPSM point-in-time precedent. The register line for this flag is removed in-PR per your #294 guidance, and #339 (stacked on this branch) holds the batch-sim sharing follow-up.

daphnehanse11 and others added 4 commits July 7, 2026 15:07
…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>
daphnehanse11 added a commit that referenced this pull request Jul 8, 2026
…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>
daphnehanse11 added a commit that referenced this pull request Jul 8, 2026
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>
daphnehanse11 and others added 2 commits July 8, 2026 17:18
# 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Materialize Medicaid and CHIP take-up via anchored count-calibration (ACA stage pattern)

1 participant