Skip to content

Restore the SSI countable-resource asset columns via the SCF-wealth stage (#368 Deliverable 2)#373

Merged
MaxGhenis merged 7 commits into
mainfrom
buildj-asset-restore
Jul 9, 2026
Merged

Restore the SSI countable-resource asset columns via the SCF-wealth stage (#368 Deliverable 2)#373
MaxGhenis merged 7 commits into
mainfrom
buildj-asset-restore

Conversation

@MaxGhenis

Copy link
Copy Markdown
Contributor

populace #368 Deliverable 2 — restore the SSI countable-resource asset columns

Deliverable 1 (#369) shipped the hard input-coverage gate + SSI reform-coverage smoke, both RED by design on today's asset-less artifacts. This finishes Deliverable 2: it restores bank_account_assets / stock_assets / bond_assets so ssi_countable_resources derives again and the SSI resource-limit reform class scores nonzero — turning both #368 gates green.

What was missing

The scf_wealth stage (packages/populace-build/src/populace/build/us_runtime/scf_wealth.py) was implemented and unit-tested but never invoked by the release builder — the imports and the --scf-summary-extract CLI arg were wired, but with_us_scf_wealth_inputs was not called anywhere. This wires it into base_frame construction (after the SNAP discretionary-exemption stage, before the ACA source outputs), following the established with_us_*_inputsus_*_signal_gate → raise-on-fail pattern, so the three asset inputs reach export_frame → the input-coverage gate (line 6353) and the reform-coverage smoke (line 6484) both pass.

SCF fetch: fixed HTTP 403 + sha-pinned

fetch_scf_2022_summary_extract failed outright — Federal Reserve econres returns HTTP 403 to urllib's default user-agent. It now sends a browser UA and pins/verifies the SHA-256 of both the zip and the extracted rscfp2022.dta (the build_us_block_ladder_artifact._sha256 provenance convention): a cache hit is reused only when its digest matches, a drifted cache is re-fetched, a mismatched download raises. Digests verified against the Federal Reserve download 2026-07-09:

  • scfp2022s.zip3bb4d890ae2463ff6039ec7692e375f544dd98a55a37ca2cb2340354b9cc9d80
  • rscfp2022.dta6b8dd2d935a76ed225ddebc80fb2db22a467f0c80d9a1acaa67b4584aa4bafd1

Acceptance probe — the SSI reform binds again

Imputed the three columns onto the Build H dense frame (_buildh-runtime/.../dense/artifacts/populace_us_2024.h5, 337,704 households / 865,046 persons) via the real stage (with_us_scf_wealth_inputs, seed 0), wrote a release H5, and scored the reforms with policyengine-us 1.764.6 (the Build H engine; dense-native SSI is identical on 1.764.6 and the #356 reference's 1.752.2). Reference class = dense-native f0af251 verified from #356 (TY2026): baseline 8.05M / $63.6B, 42.5% nonzero on assets, $10k/$20k = +$1.60B / +499k, no-limit = +$16.05B / +3.23M.

measure (2026) before (no assets) after SCF restore #356 dense-native ref
ssi_countable_resources nonzero share 0.0% 40.3% 42.5%
SSI baseline $65.51B / 8.502M $38.30B / 4.742M $63.6B / 8.05M
$10k/$20k limit Δ +$0.00B / +0 +$9.66B / +1.365M +$1.60B / +499k
no-limit (remove test) Δ +$0.00B / +0 +$27.22B / +3.761M +$16.05B / +3.23M

Before restore, ssi_countable_resources is exactly $0 for every record and both reforms score +$0.00B — the #368 failure, probe-reproduced. After restore, countable resources carry signal and the reforms bind: +$9.66B at $10k/$20k clears the $1B reform-coverage floor ~9.7×, so the smoke gate passes. 2024 is consistent (+$9.23B / +$25.90B).

Plausible but above the dense-native magnitude — diagnosed (two documented factors, neither a stage bug)

The reform binds with the right sign and order of magnitude (no-limit +$27.22B is within ~1.7× of the +$16.05B reference), but the $10k/$20k delta runs above +$1.60B and the restored baseline (4.742M) sits below 8.05M. Two compounding causes, both already flagged on #356:

  1. Post-hoc overlay without an SSI-target refit. The probe imputes assets onto a frame whose weights were already calibrated with no assets (the SSI caseload was absorbed by the weight solve). Sparse-57k certified release drops SSI asset input columns; SSI resource-test reforms score $0 on bundle 4.18.8 #356 records that populace asset overlays land at baseline 3.0–4.0M / $10k-$20k +$9–26B — this probe's 4.742M / +$9.66B sits squarely in that band, corroborating the imputation is behaving correctly rather than buggily. The stage itself runs in-build, before calibration (the correct location), so a full certified rebuild re-solves the weights with assets present; Sparse-57k certified release drops SSI asset input columns; SSI resource-test reforms score $0 on bundle 4.18.8 #356 notes the restore "must ship with an SSI-target refit."
  2. SCF-only vs the retired SIPP+SCF blend. Confirmed from the retired source (policyengine_us_data/utils/asset_imputation.py): the retired pipeline draws these three leaves from a 50/50 SIPP+SCF blend (FINANCIAL_ASSET_SOURCE_SCF_PROBABILITY = 0.5); SIPP supplies the realistic low liquid-asset mass at the bottom. This stage ships the SCF draw only, assigning more liquid wealth to the SSI-marginal population.

The nonzero incidence already matches the reference (40.3% vs 42.5%); the gap is amount + calibration, not the grain. The module docstring records this precisely, and the prior draft's fabricated "the SSI baseline is preserved (verified on the Build H dense frame)" claim is replaced with the measured result. Aligning the magnitude — SIPP blend + SSI-target refit in a full rebuild — is the #356 follow-up.

Tests

  • test_us_scf_wealth.py (20 tests): manifest, donor load, sentinel/race mapping, head-carry, determinism, frame integration, signal gate, sha-pinned fetch.
  • test_us_fiscal_refresh_builder.py::test_main_writes_diagnostics_...: the full-pipeline main() test now monkeypatches the scf stage consistently with the other stages.
  • Full populace-build suite (~1,096 tests) green at PE-US 1.764.6; ruff check . clean (also fixed an unsorted scf export block the salvage left in us_runtime/__init__.py, which would have failed CI lint).

No changelog fragment: populace carries no changelog mechanism (no changelog.d/, no towncrier config), consistent with #369.

Refs #368. Reference and root cause verified against #356 and the retired asset_imputation.py.

🤖 Generated with Claude Code

MaxGhenis and others added 7 commits July 9, 2026 10:48
…#368)

- scf_wealth.py: person-level own-income predictors + head-carry output;
  add fetch_scf_2022_summary_extract provisioning helper.
- __init__.py: export the scf_wealth surface.
- l0_refit_export.py: bank/stock/bond join US_RELEASE_REQUIRED_PERSON_SOURCE_COLUMNS.
- ecps_parity_known_gaps.json: drop the three now-produced asset layers.
- test_us_l0_refit_export fixture: carry the three asset columns.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…CF fetch (#368)

Deliverable 2: the scf_wealth stage was implemented and unit-tested but never
invoked by the release builder — imported only. Wire with_us_scf_wealth_inputs +
us_scf_wealth_signal_gate into base_frame construction (after the SNAP
discretionary-exemption stage, before ACA source outputs), loading the SCF 2022
donor from --scf-summary-extract or the cached fetch. The three SSI
countable-resource asset inputs (bank_account_assets / stock_assets /
bond_assets) now reach the export, so the #368 input-coverage gate and the SSI
reform-coverage smoke both turn green instead of failing by design.

fetch_scf_2022_summary_extract:
- Send a browser User-Agent — Federal Reserve econres returns HTTP 403 to the
  default urllib agent, so the fetch failed outright.
- Pin and verify SHA-256 of both the zip and the extracted rscfp2022.dta
  (block-ladder _sha256 provenance convention); a cache hit is reused only when
  its digest matches, a drifted cache is re-fetched, and a mismatched download
  raises. Verified against the Federal Reserve download 2026-07-09.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…onkeypatch the stage in the main() pipeline test (#368)

Two fixes surfaced by the Deliverable 2 acceptance probe:

1. Data integrity: the module docstring claimed head-carry "keeps the SSI
   baseline intact ... the SSI baseline is preserved (verified on the Build H
   dense frame)". The acceptance probe (seed 0, policyengine-us 1.764.6)
   disproves it — the SCF-only draw drops the restored SSI baseline to 4.74M
   recipients vs the dense-native 8.05M and runs the $10k/$20k delta to +$9.7B
   vs the +$1.6B reference. Replace the fabricated claim with the measured
   numbers and the two documented root causes (both #356 follow-ups, neither a
   bug in this stage): the probe overlays assets on weights calibrated without
   them (no SSI-target refit; post-hoc overlays are documented to land at
   +$9-26B), and the stage ships the SCF draw without the retired SIPP blend
   (FINANCIAL_ASSET_SOURCE_SCF_PROBABILITY=0.5, confirmed in the retired
   asset_imputation.py). Also stop the docstring overclaiming that it
   "reproduces" the retired construction — it ships the SCF half of it.

2. test_main_writes_diagnostics_before_post_calibration_gate_failure runs the
   full main() pipeline with every stage monkeypatched; add the four scf-stage
   patches (fetch/load/with_us_scf_wealth_inputs/us_scf_wealth_signal_gate) so
   the newly wired stage does not fetch SCF or fit a QRF on the fake frame.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…368)

The salvage commit placed the scf_wealth import/export block out of alphabetical
order (before fiscal_targets instead of after release_input_coverage), which
fails `ruff check .` — the CI lint step. Move it to the sorted position; no
behavior change.

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.

1 participant