Skip to content

Recalibrate SNAP take-up per state to FNS household caseloads#376

Merged
MaxGhenis merged 1 commit into
mainfrom
snap-state-take-up
Jul 11, 2026
Merged

Recalibrate SNAP take-up per state to FNS household caseloads#376
MaxGhenis merged 1 commit into
mainfrom
snap-state-take-up

Conversation

@daphnehanse11

Copy link
Copy Markdown
Collaborator

Implements #372. Stacked on #371 (base branch is snap-caseload-targets) — the take-up calibration consumes the same compiled snap_households target specs that PR adds, so the seed and the weight-calibration objective agree by construction.

Why

#294's national-rate fill bakes in CPS state underreporting: the buildi-sparse release undershoots ten state SNAP benefit targets by 7–43% while a per-record feasibility audit shows every state but California reachable under the 5× weight cap, and state taker-household counts err from −38% to +325% against FNS caseloads (full numbers in #372 and the #371 preview comment). The states that are short cannot be fixed by reweighting — they lack taker households; this stage supplies them.

What

A snap_state_take_up source stage following the medicaid_take_up pattern (#331) at SPM-unit grain, using the shared binary-assignment ops (#338):

  1. Anchor — units with reported ASEC receipt (SPM_SNAPSUB > 0) always take up, identical to the national stage's anchor.
  2. Fill — non-reporters draw at an in-build state rate (FNS average-monthly household count over weighted modeled-eligible units; an assignment prior, never cited as provenance per the contract's model-relative-ratio rule), then calibrate_binary_assignment greedily calibrates among eligible non-anchored units so each unsaturated state hits its FNS FY2024 count within unit-weight granularity.

The national snap_take_up stage remains as the prior; this stage recomputes the final surface after target compilation (it needs is_snap_eligible from the engine and the compiled targets, mirroring the Medicaid builder placement).

Deliberate carry-overs from the Medicaid stage: off-domain units keep a draw-based propensity (no hard-coded zero reform response), and saturation is recorded rather than failed (CA's ceiling is 1.05× its caseload — an eligibility-undercount symptom, not an assignment bug). Persons caseloads stay untargeted: the SNAP assistance unit is often a subset of the SPM unit, so member counts overcount FNS participants ~50% (see #371's second commit).

Gates and diagnostics

  • us_snap_state_take_up_gate (release-blocking): every state must carry an FNS target; anchors must survive calibration in every state (unit-level check); unsaturated states must land on their count within max(2%, one unit weight); positive count with zero eligible weight fails as an eligibility-feed collapse; caseload==eligibility with a reachable floor below it fails as the universal-take-up landmine.
  • us_snap_state_take_up.json release artifact + staging telemetry: per-state eligible/anchored/caseload weights, targets, saturation, plus national rollup.
  • Take-up contract: SNAP entry stays out_of_scope for Bernoulli seeding (it keeps a sourced national rate, which count_calibrated forbids) with scope_owner updated to name this stage as the final surface owner.

Tests

  • New test_us_snap_state_take_up.py (14 tests): manifest declaration, anchor derivation from raw subsidy, state fill-rate prior, exact count calibration in unsaturated states, saturation, anchor survival under downward pressure, empty-target refusal, and gate failure modes (missing state target, eligibility collapse, dropped anchor, count miss, universal-take-up landmine).
  • Builder main-path test extended with the new stage's monkeypatches.
  • Full populace-build suite passes (517 tests).

🤖 Generated with Claude Code

@MaxGhenis MaxGhenis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — design is sound and consistent with the merged #371, but two mechanical blockers stop a merge: the base is a merged branch and no CI has run.

Blockers (both mechanical)

  1. Base is snap-caseload-targets, which merged today. That was #371's branch; #371 landed on main as squash commit 027355b (main is now further ahead). GitHub did not retarget this PR because the branch wasn't deleted, so as it stands the PR would merge into snap-caseload-targets, not main. Retarget the base to main and rebase.
  2. No CI has run ("no checks reported on the snap-state-take-up branch"). The Tests workflow is pull_request: branches: [main], so a PR based on snap-caseload-targets never triggers it — the stale base is why there are no checks. Retargeting to main will run CI, which is the gate for merge.

What I verified locally (fresh clone, cherry-pick onto current main)

  • The incremental commit (e0e14c2) cherry-picks cleanly onto current main — no conflicts, exactly the +1113/−3 delta across the 7 files. The rebase is mechanical (#371's content is already in main, and this was built directly on it).
  • On that rebased-onto-main state, the delta is healthy: test_us_snap_state_take_up.py (14 tests), test_us_take_up_contract.py, and the register-consistency + take-up-contract preflight slices of test_us_fiscal_refresh_builder.py all pass — including the #384 preflights that this PR's own base predates.

Design — the seed/calibration double-pressure is resolved by construction

The stage calibrates the taker set to the FNS average_monthly_households facts — the same rows #371's snap_households weight-calibration targets compile from. So the seed (who takes up) and the weight-calibration objective (per-state household counts) pin the same administrative measure; they agree rather than fight. The take-up contract keeps takes_up_snap_if_eligible as out_of_scope (correctly not count_calibrated, since it retains a sourced national prior — that treatment forbids a sourced rate), so it stays out of the register-consistency signal-side registers and introduces no cross-register contradiction. Saturation-recorded-not-failed (CA at 1.05× its caseload) and persons-left-untargeted (SPM-unit overcount, per #371's second commit) are consistent with the merged surface.

One relationship note

This PR and #375 are two implementations of #372. This one is consistent with the merged #371 (FY2024 household caseloads); #375 uses a different basis (FY2022 eligible-person participation rates), which does not target the same quantity #371 now calibrates to. Worth settling which approach to carry before more investment — on the merged state, this is the consistent one.

Next step

Retarget base to main, rebase (trivial per the clean cherry-pick), and push so CI runs. With green checks it's a straightforward merge candidate; I did not merge because the base is non-main and no checks exist.

The snap_take_up stage (#294) anchors take-up on reported ASEC receipt and
fills non-reporters to the national FNS participation rate. CPS SNAP
receipt underreporting is strongly state-dependent, so the national fill
starves the worst-underreporting states of taker households no reweighting
can recover: the buildi-sparse release undershoots ten state benefit
targets by 7-43% while a feasibility audit shows every state but
California reachable under the weight cap, and state taker-household
counts err from -38% to +325% against FNS caseloads (#372).

Add a snap_state_take_up stage following the medicaid_take_up pattern
(#331) at SPM-unit grain: reported recipients always take up; the fill
draws at an in-build state rate (FNS average-monthly household count over
weighted modeled-eligible units, an assignment prior, never cited as
provenance) and is greedily calibrated to the FNS FY2024 state counts
among eligible non-anchored units. The targets are the same
snap_households rows the weight-calibration targets compile from, so the
seed and the calibration objective agree. Saturation is recorded, not
failed; a release gate enforces anchor preservation, per-state count
fidelity within unit-weight granularity, full state target coverage, and
the universal-take-up landmine.

Persons caseloads stay untargeted: the SNAP assistance unit is often a
subset of the SPM unit, so member counts overcount FNS participants by
roughly half.

Implements #372; stacked on the #371 caseload-target compilation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@MaxGhenis MaxGhenis changed the base branch from snap-caseload-targets to main July 10, 2026 23:56
@MaxGhenis MaxGhenis force-pushed the snap-state-take-up branch from e0e14c2 to a354ca2 Compare July 10, 2026 23:56
@MaxGhenis

Copy link
Copy Markdown
Contributor

Heads-up: since #371 merged to main today (squash 027355b), I rebased this branch onto current maingit rebase --onto origin/main b9c6e9e, which drops the already-merged #371 commits and keeps your incremental commit (e0e14c2a354ca2, content unchanged: +1113/−3 across the same 7 files) — and retargeted the PR base from snap-caseload-targets to main. This also un-sticks CI, which never ran here because the Tests workflow only triggers for PRs based on main. No content changes of any kind; if you have local work, git fetch && git reset --hard origin/snap-state-take-up before continuing. Will merge once checks come back green.

@MaxGhenis MaxGhenis merged commit 55d2c2c into main Jul 11, 2026
4 checks passed
MaxGhenis added a commit that referenced this pull request Jul 12, 2026
Integrates the SNAP state surface (#256 #371 #376), the PUMA ladder
(#394), the loader-test decouple (#392), and --no-latest (#397) with
the campaign's 93 restored input families. Six builder conflict hunks
+ the us_runtime export merges resolved keeping BOTH lineages: main's
snap_state_take_up stage/diagnostics/telemetry sequenced alongside the
campaign's other_health_insurance and ssi_take_up stages. Full
populace-build suite passes on the merged tree (exit 0, 1 cred skip).

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.

2 participants