Repository navigation
Description
What is wrong
In the certified US frame (populace-us-2024-spm-20260909, H5 SHA-256 6496cc43…; Build P cae8640), each household's liquid assets (bank_account_assets, stock_assets, bond_assets) sit entirely on one person, the household's lowest-A_LINENO member. All 52,637 rows with a nonzero leaf are that person; every other member holds $0.
Two problems follow.
1. The selector contradicts the stage's own declaration. us/source_stages.json (:1135–1145) declares the carry as head_carry with condition: is_household_head. The runtime (scf_wealth.py _household_head_mask, :670–691 at cae8640, :673–694 on main; SIPP's equivalent in sipp_financial_assets.py:561–577) sorts by household, A_LINENO (missing → 9999) and row order and takes the first. is_household_head is P_SEQ == 1 (relationship_inputs.py). They disagree in 243 households. The docstring's "carried by the CPS reference person" holds only where line 1 is the reference person: 370 carriers have A_EXPRRP outside {1, 2} (for the 2022 and 2023 income years the loader synthesizes A_EXPRRP, assigning 1/2 only to line 1), and 123 carriers are under 18. In household 884, a 71-year-old wife on line 1 carries $17,500 while the line-2 reference person carries nothing.
2. One vector per household, on one person, for an engine that reads person ownership. The SCF draw describes a primary economic unit (the dominant person or couple and their financially dependent members). The SIPP leaves read TVAL_BANK, TVAL_STMF and TVAL_BOND, which are person-level values (THVAL_* are the household totals). Both are drawn once per household, conditioned on the carrier's characteristics, and placed on the carrier. PolicyEngine-US's ssi_countable_resources adds exactly these three leaves, and meets_ssi_resource_test uses the marital unit's sum for a joint claim and the person's own value otherwise (ssi_claim_is_joint requires two ABD heads or spouses in the marital unit). So everyone outside the carrier's marital unit has $0 countable resources by construction:
| Outside the carrier's marital unit | Weighted people | Share of group | With a positive vector elsewhere in the household |
|---|---|---|---|
| Adults 18+ | 63.66M | 24.2% | 56.03M |
| Aged 65+ | 5.70M | 9.3% | 4.92M |
SSI-relevant adults (65+, meets_ssi_disability_criteria or is_blind) |
13.06M | 17.3% | 11.45M |
They include 1.15M people 65+ coded as the reference person's parent, all living with an adult-child carrier, and 5.64M SSI-relevant adult children of the reference person. A rough income screen (annual Social Security plus earnings under $12,336 = 12 × ($943 + $20 + $65)) leaves 6.41M SSI-relevant adults outside the carrier's unit, 3.41M of them in households whose vector exceeds $2,000. These are exposure counts. Whether a given person's SSI eligibility changes depends on rules this screen omits, and on who in the household owns what.
Within a marital unit, a claimant whose spouse is not aged, blind or disabled files an individual claim. The PolicyEngine-US releases pinned with this frame (1.764.6 at build, 2.2.1 in policyengine.py's bundle) count only the claimant's own resources then, so a carrier spouse leaves the claimant at $0. PolicyEngine-US main now deems an ineligible spouse's resources, and parents' resources to children under 18 (policyengine-us#10040, merged 2026-10-11; 20 CFR 416.1202). No rule deems resources between adults outside a marital unit, so the adult children, parents and other relatives counted above test at $0 under every engine version.
Proposed fix
- Now, small: make the runtime honor its declaration. Select the carrier with
is_household_head, or change the declaration to the rule actually intended, and add a test that the declared condition and the runtime selector agree. This moves 243 households' vectors. It does not reach the ownership problem, and 104is_household_headpersons are still under 18. - The real fix needs consistent donor and recipient units: draw SIPP person-level leaves per adult (with spouse dependence), or draw per marital unit and allocate within it; map SCF's economic unit to CPS economic units and give independent adults their own draw. An SPM-unit draw alone would leave 12.15M of the 13.06M exposed SSI-relevant adults sharing the carrier's unit, so allocation within the unit is still needed.
INT_VALandDIV_VALcan guide ownership (4.22M exposed SSI-relevant adults report interest or dividends while holding $0 in the frame) but do not determine balances. Changing the draw unit changes aggregate assets, so it needs validation against SCF and SIPP totals, and a refit.
#1184 (imputed savings put SSI reporters over the limit) is a related symptom of the same stage; #1216 covers the stage's missing provenance record.
Downstream
Every SSI resource-test result on this frame: microcosm-dynamics Registration 19 and 20, the SSI asset-limit analyses (PolicyEngine/ssi-asset-limits), and policyengine.org SSI results through PolicyEngine-US's default dataset. The bias runs toward people passing the resource test, so asset-limit reforms add fewer new eligibles among adults living with relatives than they would with assets placed on owners.
Reproduce
The carrier fact: on the person table, compare A_LINENO == min(A_LINENO) per household with the nonzero-leaf mask and with is_household_head. Exposure: people with age >= 18 whose person_marital_unit_id differs from the carrier's, weighted by household_weight.
Found by read-only structural reads of the frame while registering microcosm-dynamics Registration 19.
Activity
MaxGhenis commentedon Oct 12, 2026
#1221 makes the SIPP/SCF asset stage carry on the declared
is_household_head(both blend halves, and the net-worth conditioning that reads the same mask). Review of it found one more stage with its own lowest-A_LINENOselector:sipp_vehicles.py(~line 728) conditions vehicle predictors on the lowest-line person, while assets and housing now use the declared head. The archived vehicle allocator (policyengine-us-data42ed5d45,utils/asset_imputation.py~548) preferredis_household_head. Vehicle outputs are household-grain, so this changes the donor profile only; worth aligning in a follow-up. The ownership problem in this issue (one household vector on one person) is unchanged by #1221.MaxGhenis commentedon Oct 23, 2026
The auto-loan leaves of this stage have the same unit question. PolicyEngine/policyengine-us#10406 (draft) changes how policyengine-us reads
qualified_passenger_vehicle_loan_interest. It used to be split across a household's tax units by member count: core's group-to-groupadd()divides by household size. Now the whole amount goes to the tax unit holdingis_household_head, which matches this stage's declaredhead_carryfor the liquid-asset leaves. No entity change is needed here, because the household column keeps working.Two facts for the unit fix, measured on the certified frame (
6496cc43…):is_household_head.scf_auto_loans._scf_reference_person_maskpicks the one adult, or the man of a married pair, or otherwise the oldest adult. There are 5,930 multi-tax-unit households with positive QPVLI (10.8M weighted, holding 31.3% of the $12.16bn weighted 2024 total). In those, that person is in the head's tax unit in 70.5% of records (72.3% weighted by QPVLI)._recipient_household_predictor_tablesums recipient income over the whole CPS household. The SCF donor'swageinc,intdivincandssretincdescribe the primary economic unit, which leaves out financially independent household members (2022 codebook, "Unit of analysis").If the stage later emits per-person loans, policyengine-us now has a person input,
qualified_passenger_vehicle_loan_interest_person. It counts for the tax unit head and spouse, following 26 U.S.C. 163(h)(4)(B)(i): "indebtedness incurred by the taxpayer".Scripts:
reference_vs_head.pyanddata_stats.py(read-only, pandas on the H5), kept with the policyengine-us PR's evidence.Restore the #1215 note above the US seed pins
MaxGhenis commentedon Nov 11, 2026
The education-debt draw on main (#1073) starts from the same lowest-line person. Under the stage's default recipient definition,
scf_education_debt._householder_couple_positions(:1390–1421at3f46d5958) takes each household's lowestA_LINENOas the householder, then picks the man of a mixed-sex couple or the older partner of a same-sex couple. The draw conditions its predictors on that person and sums income over that person's couple and children. When the line-1 person falls outside theis_household_headperson's couple, those predictors describe a different couple from the one that carries #1221's assets. I haven't counted those households. Align it in the same follow-up assipp_vehicles.pyabove, starting fromis_household_headthe way #1221'shousehold_head_carrier_maskdoes.