Stop the US PUF imputation from predicting income from itself (#982) - #1033
Conversation
Replace the six self-predicting income predictors with head age, spouse age, head sex, dependent count and the unit's weighted income rank in its own population. Tests, spec regeneration and pins follow. Refs #982 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
) Evaluated on the 2026-09-12 genuine-build QRF inputs: ranking each unit at the midpoint of its weight span, on the same six income items on both sides, fits SOI Table 1.1 counts in the $1.5M-$10M bands within 1% before calibration and puts $10M+ capital gains at 39.7% of AGI (SOI 39.5%). The recipient rank now covers only the PUF-detail clone rows, so other spines or clone channels in the frame cannot shift it. A donor table that cannot supply a rank component leaves the predictor absent, so the missing-donor check names it. Refs #982 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The primary PUF predictor block now lists the five new predictors in place of the six survey income levels. Recomputed from the regenerated bundle: field-usage counts and the predictor-block claim, the primary predictor tuple, resource semantics, seed protocol and map, and full checkpoint digests, the pointer inventory digest, and the coverage evidence. Tests: new test_us_puf_income_rank_predictors.py pins the rank midpoint, demographics, donor construction, recipient-row scope, the strict role check, and an end-to-end case where topcoded survey units receive PUF wages above the topcode while the self-predicting design does not. Refs #982 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rebuilt from the session harness that a reboot wiped. Fits the production QRF on the saved 2026-09-12 donor frame and predicts onto the saved PUF-clone recipients per predictor variant; the scorer compares against SOI Tables 1.1 and 1.4, Colorado (#940), and audits wage self-prediction and participation. Refs #982 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The PUF covers filers, so the survey's zero-earnings group ranks level with low but positive PUF incomes; with rank alone, 48% of survey units with no wages or self-employment income drew PUF wages (8-tree offline run on the 2026-09-12 inputs). A has-earnings flag, taken from the donor's own wage and self-employment outputs and the recipient's survey amounts, cuts that to 7 of 67,908 units and leaves the SOI Table 1.1 fit intact. The flag fixes only participation, never a level. Adds property tests (Hypothesis) for the mid-rank share: bounds, weighted mean one half, monotonicity, tie and permutation invariance, weight-scale invariance, missing-row exclusion, and a differential test against the pairwise definition. Corrects a comment that said every clone landed within 10% of its survey wages; the measured share was 99.98%. Refs #982 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…c-rank-predictors # Conflicts: # docs/evidence/spec-engine/us-f0-coverage.json # packages/microcosm-build/src/microcosm/build/spec_engine/field_usage.py # packages/microcosm-build/src/microcosm/build/spec_engine/inventory_coverage.py # packages/microcosm-build/tests/test_us_multispine_pool.py # tools/spec_engine_coverage.py
Adds puf_predictor_has_earnings to the primary PUF predictor block. Against main the block swaps six survey income levels for six new predictors, so field counts and the predictor-block claim hold; recomputed from observed values: primary predictor tuples, resource semantics, full checkpoint identity, seed protocol and seed map digests, and the coverage evidence. Refs #982 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four variants at 8 trees on the 2026-09-12 inputs: the old self-predicting design, pooled rank, pooled rank plus the earnings flag (chosen), and rank within earnings groups plus the flag. README states the SOI band fit, the wage self-prediction and participation audit, and the Colorado limit (#940). Refs #982 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #982 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review findings (independent Astra review of #1033): - Survey side: a tax-unit role outside HEAD/SPOUSE/DEPENDENT now raises; a missing role leaves that unit's demographics missing under the strict policy and raises under the legacy policy, instead of being ignored. - PUF side: person arrays keep missing values instead of zero-filling them, and a missing age, sex or role flag, a person with no role, is_female and is_male that disagree, or a unit without a head now raises by name. The processed PUF has none of these (checked: 499,045 persons, no NaN, one role each, one head per unit), nor does the 9/12 survey frame. - Evidence: the scorer now reports survey non-earners given any earnings (the old design gave earnings to 127 of 67,908, not 0), the Colorado top record, and inputs and run metadata; the within-10% figures state their denominator (units with positive survey wages); the unreceipted 32-tree claim is removed; the changelog no longer promises zero earnings. Re-pins the seed protocol and seed map digests, the US bundle digest and the loader golden, which hash puf_support.py source bytes. Refs #982 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drop the duplicate _TAX_UNIT_ROLES declaration; say the earnings flag reduces earnings assignments to survey non-earners (7 of 67,908 in the offline evaluation) rather than preventing them. Re-pins the seed protocol and seed map digests, the US bundle digest and the loader golden. Refs #982 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Independent review by GPT-6 Astra through Subfleet, read-only. Round 1 (head
It also raised three minor findings about evidence accuracy. Round 2 (head
|
CI on 8ea1d24 failed four test_us_multispine_pool tests in the us-am, fast-rest and wheels lanes: the hand-built ACS fixture had no tax_unit_role_input column, so the fail-closed demographics refused its two people. Production ACS spines carry the role from assign_us_unit_structure, so the fixture now does too (each person heads a one-person unit). The missing-role error now names the affected tax units. Re-pins the seed protocol and seed map digests, the US bundle digest and the loader golden (puf_support.py source bytes). Refs #982 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
CI on |
|
Correction to my earlier numbers. The top-tail figures I posted weighted the PUF-clone half ×2, "as if this half were the whole population", and compared that with SOI. The real pool gives the survey half the other 50% of the weight, and the survey half has no units at $5M or more. Both halves at frame weight, before calibration and before main's capital-gains tail stage (8 trees, 15-item proxy AGI):
#1033 roughly quadruples the pool's mass at $5M+, but on its own it reaches about half of SOI there, not SOI. Colorado is about 2× SOI above $1M, not 4×. One record still carries 81% of that. The script and output are at |
Brings in the 19 main merges since 6442030: the Route A remediation stack (#1016, #1018, #1017, #1025, #1024, #1028), #1029, #1005, #1008, #1004, #992, #1031, #1015, #1033, #994, #954, #966, #1006 and #1010. Eight conflicted paths, each recorded in docs/native-integration-20260923.md: - test.yml keeps the native sharded matrix (main's --durations=25 is already in every native pytest call); - the us_runtime facade stays lazy and gains main's three fiscal-target exclusion exports (the parent union, 920 names; the facade-union test is re-pinned to that union's digest); - reform_validation.py keeps main's _released_engine_state and the native explicit-constructor default_simulate_factory; - build_us_fiscal_refresh_release.py carries the native explicit consumer seams (formula metadata, dataset and microsimulation constructors, SPM selection) onto main's household-batched post-export scorer and batched base materialization; omitted seams keep main's exact calls, and _main supplies none. Three native test files that addressed the removed frame-based factory now address the scorer with the same assertions; - identity pins observed on the merged tree: five EXPECTED_HASHES entries, the regenerated F0 coverage report (42,239/42,239 fields, 41/41 checks), the US spec identity 2dfa51b8... and the loader golden f2047cb9... tools/generate_us_bundle_from_constants.py --check, tools/ci_test_groups.py --verify, the CI matrix contract and ruff pass. The fiscal consumer, formula-metadata, shared target/solve and calibration-attachment files pass; the rest of the fiscal battery and the known pre-existing failures are follow-up work on this branch. No actual-data native run, release certification or publication. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes #982.
Summary
The US PUF tax-detail QRF predicted each income item from the recipient's own survey value of that item. On the donor side, each of those predictor columns was an alias of the donor's own imputed output, so the forest learned output = input. On the 2026-09-12 genuine-build inputs, PUF-clone wages had rank correlation 1.000 with survey wages. Among units with positive survey wages, 99.98% landed within 10% of the survey value. No clone exceeded the survey maximum, so ASEC topcodes and underreporting carried straight into the PUF half. This is the root of the missing $5M+ tail in #958.
This PR replaces the six survey income levels with these predictors, derived identically on both sides:
Filing status and unit size stay. No income level is a predictor any more.
Evidence (pre-calibration)
The fit uses the saved 9/12 donor frame (211,677 tax units), predicted onto 231,007 PUF-clone recipients at 8 trees per variant. AGI is a proxy: the sum of 15 imputed items. Full receipts are in
experiments/982-puf-self-prediction/.Capital gains are 42.6% of $10M+ AGI under the new design, against 39.5% in SOI Table 1.4 and 0.1% under the old one.
Invariants
These hold for every input, and Hypothesis property tests in
test_us_puf_income_rank_properties.pycheck them:Structurally, no default predictor resolves to an imputed output column.
test_default_predictors_never_resolve_to_an_imputed_outputpins this so the self-prediction cannot come back.Fail-closed demographics
The demographics refuse malformed input rather than filling it:
is_femaleandis_malethat disagree, or a unit without a head. The processed PUF has none of these: 499,045 persons, no missing values, one role each, one head per unit. Neither does the 9/12 survey frame.Tests
test_us_puf_income_rank_predictors.pycovers the mid-rank, demographics, donor construction from PUF arrays, recipient-row scope, the strict role check, and two end-to-end discriminating cases:Not in this PR
scores_8_trees.json). State top-tail totals stay noisy until the state × AGI-band amount targets from State top-tail income mass is uncalibrated: CO M+ AGI at 24% of SOI in buildp-acs-local (fails progressive-bracket scoring) #940 enter calibration.full_puf_enrichmentpath imports this predictor list and will pick up the new predictors. Its owning session was told in the coordination file.axiom: n/a: microsimulation data imputation, no policy rule changes.
🤖 Generated with Claude Code