Skip to content

Compile USDA SNAP state and national caseload targets from FNS average-monthly facts#371

Merged
MaxGhenis merged 3 commits into
mainfrom
snap-caseload-targets
Jul 10, 2026
Merged

Compile USDA SNAP state and national caseload targets from FNS average-monthly facts#371
MaxGhenis merged 3 commits into
mainfrom
snap-caseload-targets

Conversation

@daphnehanse11

@daphnehanse11 daphnehanse11 commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Implements #370.

What

Maps the FNS household-caseload measure the ledger usda-snap-fy69-to-current package already ships (national + per-state, alongside the total_benefits facts populace compiles today) into calibration targets:

  • ("usda_snap", "average_monthly_households")indicator_sum over SPM units with positive annual snap (target_role snap_households)

It flows through the existing INDICATOR_LEDGER_TARGETS_direct_reference_from_fact machinery, so state facts compile per state with state_fips metadata automatically, exactly like the benefit-dollar targets.

average_monthly_persons is deliberately not mapped (second commit): previewed against the buildi-sparse release, a person indicator over taker-unit members overcounts FNS participants by +48% (61.8M vs 41.7M) because the real SNAP assistance unit is often a subset of the SPM unit (FNS persons per household 1.88 vs 2.82 members per simulated taker unit) and PolicyEngine-US does not model sub-unit participation. A test pins that the persons measure does not compile.

Aggregation contract change

FNS caseload facts carry aggregation: mean (a fiscal-year mean of 12 monthly stock counts), and target_spec_from_ledger_reference rejected every non-sum fact. This PR adds a scoped opt-in: a mapping may declare fact_aggregation: time_mean, asserting the mean is taken over time periods on a stock count — still a linear level target. Per-unit ratios (e.g. average_monthly_benefit_per_person) remain rejected by default, and a new test pins the rejection of mean facts without the contract.

Concept mapping and validation

The compiled counts are of the simulated taker set. The take-up assignment (#294) seeds takers to reproduce the FNS participation rate, so the taker set is the model counterpart of the average monthly caseload rather than an annual-ever count. Previewed on populace-us-2024-buildi-sparse-rmloss100 with shipped weights: 21.89M weighted taker units vs the 22.20M FY2024 FNS national average (−1.4%) with no calibration pressure on counts. Per-state preview in the PR comment below.

Why

The SNAP surface is currently dollars-only. In the default release, 10 states undershoot their benefit targets by 7–43% while a feasibility audit shows all but CA are reachable within the 5× weight cap — the dollar targets lose loss-weight trades (the usda_snap family holds ~0.9% of total loss weight under sqrt-value weighting). Caseload counts land in the count basis, which holds its own half of the 50/50 loss budget, raising SNAP's effective share within the standard scheme; and dollars + caseload jointly pin average benefit per household, anchoring recipient composition for downstream reform work. Full audit numbers in #370.

Dependency

Targets compile only for facts present in the consumer feed. If the current consumer_facts_buildh_v8.jsonl filters out the caseload measures, the consumer bundle needs regeneration before these targets appear in a build.

Tests

  • test_snap_household_caseload_fact_maps_to_snap_indicator (national, household grain)
  • test_state_snap_household_caseload_fact_compiles_with_state_fips (state grain, state_fips)
  • test_snap_person_caseload_fact_is_not_compiled (persons stays unmapped)
  • test__given_mean_fact_without_time_mean_contract__then_compilation_fails (doctrine guard)
  • Full test_us_fiscal_targets.py and test_ledger_targets.py pass.

🤖 Generated with Claude Code

daphnehanse11 and others added 2 commits July 9, 2026 13:55
…e-monthly facts

The ledger usda-snap-fy69-to-current package already ships
average_monthly_households and average_monthly_persons record sets at
national and per-state grain alongside the total_benefits facts populace
compiles today, but only the dollar measures were mapped. That leaves the
SNAP surface dollars-only: a build can hit a state's benefit total with an
unrepresentative recipient set, and nothing pins caseload or average
benefit per household.

Map both caseload measures as indicator_sum targets over the simulated
taker set: SPM units with positive annual snap (households) and their
members via indicator_map_to person (persons). The take-up assignment
(#294) seeds takers to reproduce the FNS participation rate, so the taker
set is the model counterpart of the average monthly caseload.

FNS caseload facts carry aggregation 'mean' (a fiscal-year mean of monthly
stock counts), which the ledger compile guard rejected wholesale. Add an
explicit per-mapping fact_aggregation=time_mean contract: a time-mean of a
stock count is still a linear level target, while per-unit ratios (e.g.
average_monthly_benefit_per_person) remain rejected by default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cipants ~50%

Previewing both indicator targets against the buildi-sparse release showed
the household mapping lands at -1.4% of the FY2024 FNS national average
(21.89M vs 22.20M) with no calibration pressure, while the person mapping
overcounts by +48% (61.8M vs 41.7M): the real SNAP assistance unit is often
a subset of the SPM unit (FNS persons per household 1.88 vs 2.82 members
per simulated taker unit) and PolicyEngine-US does not model sub-unit
participation. Calibrating to the person counts would fight the household
target, so the persons measure stays unmapped, with a test pinning that it
does not compile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@daphnehanse11

Copy link
Copy Markdown
Collaborator Author

Preview of the compiled household-caseload targets evaluated against populace-us-2024-buildi-sparse-rmloss100-6e8e929-20260709T034135Z with its shipped weights (no calibration pressure on counts), FNS FY24 values parsed from the ledger workbook using the source package's cell coordinates:

National: 21.89M weighted taker units vs 22.20M FNS (−1.4%). Feasibility ceiling under the 5× weight cap: 3.75× the target.

States (largest household-count errors, current weights):

state FNS hh (k) model hh (k) error ceiling/target
ID 65 278 +325% 15.1
MT 42 82 +97% 7.7
AK 31 51 +63% 7.2
NH 42 67 +57% 7.9
KS 92 129 +39% 7.7
DC 84 53 −38% 1.6
IL 1,061 732 −31% 3.2
CA 3,129 2,162 −31% 1.05
OK 334 234 −30% 2.7
TX 1,466 1,897 +29% 2.6

Two observations:

  1. The overshoots motivate the target directly: Idaho carries 4× too many taker households while its benefit-dollar target sits at +0.0% — dollars-only calibration leaves average benefit per household off by a factor of ~4 in the worst states. These rows are exactly what this target pins.
  2. Every state's household ceiling clears its target except the territories (GU/VI, which have no support and are already skipped by _state_fips), with CA marginal at 1.05× — consistent with the dollar-side feasibility audit in SNAP target surface is dollars-only: add FNS state/national caseload targets #370.

The persons measure was dropped from the mapping after the same preview showed a structural +48% overcount (see second commit); the per-state persons errors ranged +11% to +398% with the bias always positive, confirming it is a unit-definition mismatch rather than noise.

@MaxGhenis MaxGhenis merged commit 027355b into main Jul 10, 2026
4 checks passed
MaxGhenis added a commit that referenced this pull request Jul 10, 2026
…nt unit

Rebasing onto main (SNAP caseload targets, #371) surfaced two flaws in the
finding-4 unit gate:

- expected_unit was checked before the aggregation/value checks, so a fact
  that cannot compile at all (a mean fact with no time_mean contract) failed
  on the missing declaration rather than on its real cause. The gate now runs
  after the structural checks.
- the committed expected-unit map assigned usd to every indicator measure,
  but USDA SNAP average-monthly caseload is a count. Count-based indicators
  now map to their real denomination (SNAP -> count, CMS -> people), so the
  household caseload fact (unit 'count') passes and a genuine drift still
  fails the gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MaxGhenis pushed a commit that referenced this pull request Jul 10, 2026
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 pushed a commit that referenced this pull request Jul 11, 2026
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 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