Add Census state SPM poverty-rate backtests (104 out-of-sample validation rows) - #348
Conversation
…evels)
BaselineLevelSpec gains statistic="rate" (weighted person-level share of a
boolean variable, optionally restricted by mask_variable and sliced by
state via the numeric state_fips broadcast) and level rows now carry a
unit field ("percent" for rates, "currency-USD" otherwise) so the
dashboard can format them.
The shipped config compares the baseline's SPM poverty and child-poverty
rates for all 51 jurisdictions (plus two national single-year-2024 rows)
against Census P60-287 2022-2024 three-year averages. Poverty appears
nowhere in the calibration target surface, so every row is genuinely
out-of-sample; per-state person/child counts ARE calibrated, pinning the
denominators but not the rates.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MaxGhenis
left a comment
There was a problem hiding this comment.
Independent verification — approve
Verified the shipped data, the rate math, and the anchoring measure independently of the earlier conductor pass. Everything checks out.
Data: all 104 rows verified against the cited sources
Parsed the two cited Census P60-287 XLSX tables directly (spm_opm_state.xlsx, spm_opm_state_by_age.xlsx) and compared every shipped benchmark:
- 102 state rows match the tables exactly, to within 1-decimal rounding — 0 mismatches across all 51 jurisdictions × (overall, child). Spot examples: AK overall 10.4% (
0.104), AL child 14.6% (0.146), CA child 18.2% (0.182), DC overall 15.3% (0.153). State rows correctly use the SPMPercentcolumn (the 3-year 2022–2024 average), not the official-measure column. - The 2 national rows use single-year 2024, matching the P60-287 publication text: "The estimated SPM rate in 2024 was 12.9 percent" (
0.129) and 13.4 percent for children (0.134). The vintage split — 3-year averages for states (single-year CPS state samples are too noisy), single-year for the nation — is the right call and is documented in the config_comment.
Rate math is correct and anchored to the right measure
in_povertyis genuinely the SPM indicator: it is anSPMUnitvariable equal topoverty_gap > 0, wherepoverty_gap = max(spm_unit_spm_threshold − spm_unit_net_income, 0). So the backtest anchors SPM to SPM, not to the official measure._person_ratecomputes the weighted person share correctly:in_povertybroadcast to persons (map_to="person") gives the Census SPM-rate definition (share of persons in poverty), theis_childmask is aPersonvariable, and the state slice uses numericstate_fips(aHouseholdint, default 6) broadcast to persons — matchingint(fips)fromSTATE_FIPS_TO_POSTAL. The stringstate_code_strgenuinely can't cross entities, so the numeric route is the correct one.- The hand-computed
test_rate_level_computes_state_sliced_person_sharecase (4 persons, two states, child mask) pins the arithmetic: AL overall 10/40 = 0.25, CA overall 1.0, US child 30/60 = 0.5, AL child 10/40 = 0.25. All correct.
Tests and packaging
- Coverage is solid: rate arithmetic, spec-validation errors (unknown statistic, multi-variable rate,
cap_variableon a rate), and a shipped-config contract asserting 104 rows, 51 states, unique ids, plausible rate bounds, andcensus.govsources. state_spm_poverty_levels.jsonis registered incountry_package.jsonresources (and the resource list is re-sorted + gets its trailing newline), so it packages and the declaration-contract test holds.- CI is fully green:
test (3.13),test (3.14),wheels (3.13),wheels (3.14)all pass.
On the out-of-sample framing
The framing is exactly right and worth ratifying: poverty appears nowhere in the calibration target surface, so these are true out-of-sample diagnostics. Per-state person and child counts are calibrated (pinning the rate denominators, not the numerators), which is what makes the per-state error surface — AL child −2.8pp, CA child +4.6pp, TX child on the nose — informative rather than circular. Poverty must never become a calibration target; encoding it as a diagnostic anchor rather than a fit target is the correct design.
One transitional note (non-blocking)
Rate rows reuse the budget_effect field to carry a share (0.13), which the current dashboard would render as $0.13 until the companion calibration-diagnostics PR consumes the new unit: "percent" field. This is purely additive and self-flagged in the PR; the unit field is already emitted, so the fix is a downstream consumption change, not a data change.
Approving and merging.
Follow-up to #295, motivated by the populace-vs-ECPS parity finding: the two datasets' state poverty levels diverge by up to 11.7pp, and nothing in the validation surface would have caught it — poverty appears nowhere in the calibration targets or the reform-validation payload. This adds the missing anchor.
What's new
statistic: "rate"onBaselineLevelSpec: weighted person-level share of a boolean variable (in_poverty), optionally restricted to a subpopulation (mask_variable: is_child) and sliced to a state. State slicing uses the numericstate_fipsbroadcast to persons (the stringstate_code_strcan't cross entities), reusingSTATE_FIPS_TO_POSTALfromfiscal_targets.unitfield on level rows ("percent"for rates,"currency-USD"otherwise) so the dashboard can format rates; a small calibration-diagnostics PR will consume it (currently rates would render as$0.13— transitional).us/state_spm_poverty_levels.json: 104 rows — overall + child SPM poverty for all 51 jurisdictions vs Census P60-287 2022–2024 three-year averages (overall table, by-age table), plus two national single-year-2024 rows (12.9% overall / 13.4% child, P60-287).Out-of-sample status
Poverty is not a calibration target. Per-state person/child counts are calibrated, which pins the rate denominators but not the numerators — noted in the config
_commentalong with the vintage mismatch (2024 sim vs 2022–2024 average) and small-state CPS MOE caveats.Expected results (from the parity run, national file)
Populace's national rates already sit near the anchor (13.3% overall / 14.6% child vs Census 12.9% / 13.4%), but state rows will show real spread — e.g. AL child 11.8% vs 14.6% benchmark (−2.8pp), CA child 22.8% vs 18.2% (+4.6pp), TX 15.8% vs 15.8%. That per-state error surface is exactly what the dashboard should be showing.
Tests
42 pass in
test_reform_validation.py(rate math on a fake person sim incl. state slicing and child mask, spec validation, shipped-config well-formedness: 104 unique ids, 51 states, plausible rates, census.gov sources) + the country-package declaration contract.🤖 Generated with Claude Code