Assign SNAP take-up from reported receipt and the FNS participation rate#294
Conversation
The two data-seeded take-up programs whose participation rate clears the provenance bar, assigned by calibrated Bernoulli at the administrative rate: - TANF (takes_up_tanf_if_eligible, spm_unit): 21.9% (HHS ASPE 24th Welfare Indicators Report, 2022, Table 10 Indicator 4). - EITC (takes_up_eitc, tax_unit): per-child rates (IRS National Taxpayer Advocate 2020, TY2016: 0=65%, 1=86%, 2=85%, 3+=82%). Child count is approximated from tax-unit member ages under 19; the rate is nearly flat above zero children so the coarse count picks the right bin. with_us_take_up_inputs seeds every program the take-up contract marks `seed` (and only those), keying draws on stable source identity so support-channel clones agree and reruns are bit-reproducible (the SNAP #294 keying). A frame already carrying a non-constant column passes through untouched; a missing or constant column is recomputed, so the published all-True landmine is healed. No reported-receipt column is threaded through the base spine, so assignment is calibrated Bernoulli rather than the SNAP reported-receipt anchor. us_take_up_summary and us_take_up_signal_gate expose the per-program participation-vs-administrative surface: weighted share, target rate, source, and a plausibility band, failing on a missing/constant column or an out-of-band share. Validated at n=8000: TANF share 0.210 (target 0.219); EITC per-bin 0.648/0.866/0.842/0.832 (targets 0.65/0.86/0.85/0.82); EITC overall 0.797 (IRS ~0.78). 17 seeding tests plus the gate failure modes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… take-up contract (#312) (#315) * Add engine-asserted take-up contract inventory (#312 step 1) The blocking first deliverable: a checked-in table classifying every policyengine-us take-up flag, asserted against the installed engine. - PolicyEngineUSEngine.take_up_variables() / take_up_contract() derive, from engine metadata, each takes_up_* flag's entity, default, whether the engine computes it (formula / adds / start-date formula), its consumers, and thus an engine_class (model_simulated / data_seeded / dead). Same metadata-derivation doctrine as the #301 formula-owned guard. - us/take_up_contract.json records the engine facts plus the curated populace treatment (seed / rate_unsourced / model_simulated / out_of_scope / near_universal) with each rate's administrative provenance. - assert_take_up_contract_current() fails the build when the table drifts from the pinned engine (new/renamed flag, a flag gaining a formula, a changed default); assert_take_up_treatments_consistent() catches treatments that contradict the engine class. The loader enforces the provenance rule: a program marked seed must carry a sourced administrative rate. Finding against pinned policyengine-us 1.752.2: all 13 take-up flags are data_seeded (default True, no formula, all consumed); there are no *_seed variables and no model-simulated take-up. The chip_take_up_seed / aca_take_up_seed model-side migration the issue references has not landed in the pinned engine. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Seed TANF and EITC take-up flags from administrative rates (#312 step 2) The two data-seeded take-up programs whose participation rate clears the provenance bar, assigned by calibrated Bernoulli at the administrative rate: - TANF (takes_up_tanf_if_eligible, spm_unit): 21.9% (HHS ASPE 24th Welfare Indicators Report, 2022, Table 10 Indicator 4). - EITC (takes_up_eitc, tax_unit): per-child rates (IRS National Taxpayer Advocate 2020, TY2016: 0=65%, 1=86%, 2=85%, 3+=82%). Child count is approximated from tax-unit member ages under 19; the rate is nearly flat above zero children so the coarse count picks the right bin. with_us_take_up_inputs seeds every program the take-up contract marks `seed` (and only those), keying draws on stable source identity so support-channel clones agree and reruns are bit-reproducible (the SNAP #294 keying). A frame already carrying a non-constant column passes through untouched; a missing or constant column is recomputed, so the published all-True landmine is healed. No reported-receipt column is threaded through the base spine, so assignment is calibrated Bernoulli rather than the SNAP reported-receipt anchor. us_take_up_summary and us_take_up_signal_gate expose the per-program participation-vs-administrative surface: weighted share, target rate, source, and a plausibility band, failing on a missing/constant column or an out-of-band share. Validated at n=8000: TANF share 0.210 (target 0.219); EITC per-bin 0.648/0.866/0.842/0.832 (targets 0.65/0.86/0.85/0.82); EITC overall 0.797 (IRS ~0.78). 17 seeding tests plus the gate failure modes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * WIP checkpoint: agent stopped mid-task; lead salvaged state Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * WIP checkpoint: agent killed (session limit / stop); lead salvaged Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Register take-up contract resource; fake take-up stages in the builder test; diagnostics test class Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Sort imports (ruff --fix) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
751491d to
5c9f905
Compare
|
Rebased onto current main ( Conflict resolutions were the same 11 additive anchor points as #293 — main's TANF/EITC take-up (#315) and eCPS-parity (#316) wiring interleaved with this PR's SNAP wiring. In Intended changes beyond conflict resolution:
Verified the rebased net diff against the original with interdiff — no other semantic drift. Full Note for merge order with #293: both PRs insert at the same manifest/builder anchor points; whichever merges second gets the same-shape trivial conflict both descriptions anticipate. 🤖 Generated with Claude Code |
|
Heads-up from the medicaid take-up workstream (#334/#337): once #337 lands, parity_gate fails on stale known-gap exemptions — any register entry whose layer the candidate now populates. This branch's SNAP stage will make |
MaxGhenis
left a comment
There was a problem hiding this comment.
Approving (agent-assisted review; I verified the load-bearing pieces). The logic is right and the tests are thorough — reported-receipt anchor, weighted fill to the FNS rate, blake2b seeding keyed on source identity for clone-consistency, constant-True healing, and a gate that mirrors the immigration one. 0.82 checks out as the FY2018 FNS national participation rate.
One thing I fixed on the branch rather than bouncing back: this stage makes takes_up_snap_if_eligible a produced layer, but the parity register still exempted it with reason "Populace does not yet produce it." #337 removes the four entries rotted by already-merged work but not this one (it only rots once this merges), and because SNAP stays out_of_scope, it sits outside #337's seeded_take_up_programs() static guard — so the failure would only surface on the first release build after both land. I pushed a commit deleting the entry here; it's safe under current CI (the register only shrinks, nothing asserts presence) and under the pre-#337 gate (the candidate populates the column). Same pattern applies to #293's hours entries — cleanest if each producing PR removes its own register line.
Two nits, neither blocking:
- The manifest labels the FNS artifact vintage "latest_available" but 0.82 is FY2018 (latest published is FY2022 at 88%) — either pin the vintage to FY2018 in the notes or refresh the rate.
- The fill target is computed on person "first" weights per SPM unit while the gate measures on
resolve_weights("spm_unit"); they coincide today (0.821 achieved vs 0.820 target) but computing the target on the spm_unit basis would keep them locked together if weight resolution ever changes.
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>
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>
Add a snap_take_up source stage (manifest entry, runtime handler, frame transform, release gate, builder wiring) following the immigration and hours templates. Reported SPM_SNAPSUB recipients always take up; non-reporters fill via seeded stable-identity draws to the cited FNS 0.82 participation rate. Heals the published all-True landmine. Closes #243; second half of #248 alongside #293.
…duces it
The register reason ('Populace does not yet produce it') becomes false once
this PR's SNAP stage populates the layer, and the entry would fail the
stale-exemption check when #337 lands. #337 removes the four entries rotted
by already-merged work; each producing PR removes its own.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
85f331d to
d2f2fa2
Compare
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>
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>
Summary
Closes #243. Second half of the SNAP work-requirements input surface (#248), alongside #293 (hours).
The published dataset ships
takes_up_snap_if_eligibleconstantTrue(or omits it — same engine default), so PolicyEngine-US pays SNAP to 100% of eligible units. FNS measures participation among the eligible at ~82%. Universal take-up misstates who receives SNAP and overstates the reach of any eligibility-side reform.This adds a
snap_take_upsource stage following the #266/#293 template: manifest entry + shared runtime handler + frame transform + release gate + builder wiring.Semantics (identical to the retired enhanced-CPS pipeline)
SPM_SNAPSUBreported receiving SNAP; survey measurement wins unconditionally.Draws are blake2b hashes keyed by stable source identity (
source_year/source_household_id/minsource_person_id), so support-channel clones of one source unit always agree and reruns are bit-reproducible. The rate lives in the manifest with its FNS citation — a rate without a citation refuses to run.Healing + gate
snap_take_up_signal): column nonconstant, every reporting unit takes up (anchor preserved), weighted share within [0.70, 0.95]. Threaded through release gate failures, calibration diagnostics, and both manifests, mirroring the immigration and hours gates.Verification
Follow-up (not in this PR)
State-calibrated take-up (greedy fill against FNS state household counts, the
calibrate_binary_assignmentmachinery) is deliberately deferred: FNS state counts are average-monthly while the flag is annual-ever, and that bridge deserves its own reviewed decision — see the discussion on #292. National-rate + reported-anchor matches the retired pipeline's published behavior.Merge-order notes
takes_up_snap_if_eligiblereviewed exclusion in Gate releases on input columns stuck at the engine default #286's degenerate-input gate becomes stale and should be removed.🤖 Generated with Claude Code