Skip to content

State child SPM rates: next steps from the November trace #1446

Open

Description

@MaxGhenis

State child SPM rates in Microcosm's US files differ from Census P60-290 Table 18 by 4.8 points on average on the default file, and by 3.19 on the ACS-local file that policyengine.py uses for state regions. We build these files to correct CPS errors, so we expect part of that difference and want to keep it. #1445 holds the trace, with the decomposition, scripts, state-level outputs and receipts. Nothing below adds a poverty target: state child rates stay out of the targets.

What the trace found

Measure, 51 jurisdictions Default, published Dense, initial Dense, published ACS-local (10/17)
Mean gap +3.24 0.00 +3.62 +0.89
Mean absolute gap 4.80 1.78 4.33 3.19
Correlation with Census .52 .78 .65 .54
Outside Census's margin 38 16 38 29
Median state standard error 4.04 1.07 1.86 2.98
State-specific standard deviation τ 2.73 1.50 3.07 2.04
τ, allowing for shared households 2.90 1.88 3.20 not run
  • On the dense pool, calibration creates most of the systematic shift, and most of the added state variation lands on ASEC rows. Dense initial weights carry no net shift. Published weights add +3.62, and state-specific variation rises about 1.7 times once we allow for households the pool shares with Table 18. A test with no shared households keeps the direction: dense 2022 records against Census's 2024 to 2025 average go from a mean absolute gap of 2.47 to 6.08. Shared households still pull the initial-weight columns toward Census.
  • This result stops at the dense pool. On the default file, record selection puts the shift in place before calibration starts, and the ACS-local file stores no initial weights. The dense zero nets +2.20 on ASEC rows against -2.20 on PUF rows, and initial weights miss the administrative targets, so this shows where the shift enters and does not recommend skipping calibration.
  • Noise accounts for most of the default file's state scatter. Three fits put it at 60%, 71% and 80%, and the first carries a 95% interval of about 10% to 89%.
  • Some state variation predates calibration, and we cannot yet pin it on particular states. State gaps exceed sampling noise at dense initial weights. Louisiana has the largest joint gap with ACS rows, but a gap that large on both turns up by chance about one time in five (p = .19).
  • The files we checked carry rent on the wrong households. Of records that should carry rent, 89% do not hold their own. Place each ACS rent draw on its own household's head #1444 fixed the code, and no published file carries the fix yet. At published weights, the fix nearly doubles modelled housing subsidies and barely moves the state gaps.
  • On the ACS-local file, 23,407 donor households hold 68% of child weight. ACS rows alone reach a mean absolute gap of 2.46 with a median standard error of 1.06, and still leave 22 jurisdictions outside the margin.

Next steps, in order

We want Microcosm's files to differ from Census where the CPS errs, so a smaller gap alone does not make a better file, and we judge each step by what it corrects. We order the steps by judgement: prerequisites first, then the change measured on the state path, then changes measured elsewhere or not at all. The measured effects come from different files, so they cannot rank the steps, and aging reaches as far on the default file as the ACS share does on ACS-local.

Aging and better benefit receipt should lower rates, and neither has a test on the ACS-local path. That path already reads +0.89, and -0.45 at share 0.9, or -0.78 with corrected rent, so measure steps 6 and 7 there before adopting a size from the default file.

1. Rebuild with the rent fix

#1444 fixed the assignment at 9434c50. Rebuild from frames that carry no rent: with_us_housing_inputs returns a frame unchanged when its signal gate passes, and the gate passes rent that moved from one renter household's head to another's, so an existing base can keep it. The rebuild itself needs a release decision. Corrected-rent runs in #1445 hold published weights fixed, so calibration can move results further.

The 10/17 ACS-local file leaves donors' parent links blank, and #1445 fills them with 0, which policyengine-us reads as unknown. The current build writes each ASEC person's co-resident parent's id, or 0 when the ASEC names none, and remaps those ids onto each PUF clone (#884). A -1 fill instead moves California's rate 0.56 points and South Dakota's reform effect 1.33, each through one ASEC unit, so real links can move state figures too.

Done when the rebuilt base has zero positive_rent_nonrenter, the rebuilt ACS-local file stores parent ids rather than blanks on donors, and scripts/ in the evidence folder reproduces its tables on the new file.

2. Fail the build on rent outside renter households

#1529 landed this at 3f7cfc1. us_housing_inputs_signal_gate now fails on positive pre_subsidy_rent outside renter households, and impute_us_pre_subsidy_rent sets the CPS draw to zero there, so the draw leaves no such rent at any seed.

3. Zero owners' rent after the ACS transfer

build_optional_acs_multispine passes the base to transfer_acs_inputs as its donor, and pre_subsidy_rent sits among the housing transfer targets with head and tenure as mandatory predictors. Published ACS rows learned from misplaced rent: 39.6% of ACS renters carry rent, and so do 30% of ACS owners. Step 1 should restore renters' link with no new code.

Owners need new code whether or not step 1 restores the link. transfer_acs_inputs fits its own QRF on the donor and draws with the same zero-or-positive classifier, so a rebuilt donor can still leave rent on ACS owners, and any such rent now stops a fiscal refresh built from --pool-manifest at the housing gate. Set it to zero in _apply_post_transfer_structure, and declare the step under post_transfer_structure in acs_transfer_execution_contract_identity, which moves the contract identity.

The same gate requires household tenure to carry the three ASEC categories and no others. acs_inputs.py maps ACS tenure code 2 to OWNED_OUTRIGHT, so ACS rows that keep that value already fail it on main, and this step needs to settle that mapping too.

acs_transfer.py keeps RNTP out of this path on purpose, with the comment "RNTP/GRNTP are not pre-subsidy rent", so any native-rent route has to handle subsidised tenants.

Done when ACS renters carry rent at the donor rate, ACS owners carry none, and a refresh from --pool-manifest passes the housing gate.

4. Raise the ACS share of state mass

acs_share in packages/microcosm-build/src/microcosm/build/us_runtime/acs_multispine.py defaults to 0.5. The solves from #1078, evaluated on all 51 states:

Solve Mean absolute gap Correlation Outside margin Median state SE Median reform SE
Share 0.5, release-like 2.79 .56 27 2.82 1.30
Share 0.7, λ = .03 2.40 .64 23 1.86 0.88
Share 0.9, λ = .03 2.16 .75 22 1.09 0.56
Share 0.964, λ = .03 2.14 .77 22 0.87 0.47

With corrected rent and these weights, shares 0.5 and 0.9 read 2.88 and 2.23, with 26 and 23 outside the margin.

Standard errors fall and gaps remain: at share 0.9, DC reads 24.47 against Census's 20.3. #1078 reports 36% and 43% more held-out target error on its two folds at share 0.9 with no penalty, mostly SOI, so the default needs a methodology decision. Run the sweep on the rebuilt base, including the λ = .03 rows' held-out error, and report SOI fit, child state Kish and reform standard error together.

Done when a table with those columns sits beside the recommendation, and the default changes only on that decision.

5. Test a calibration anchor on the dense pool

Dense initial weights carry a mean absolute gap of 1.78, against 4.21 on the default file, whose selected records also double the noise (median standard error 2.20 against 1.07). A chi-square anchor to default initial weights lowered noise and overshot on level: from the same-pipeline re-solve (mean gap 2.93, correlation .42), the mean gap reached 5.18 at λ = .1, above the 3.27 at initial weights, while correlation rose to .68. We do not know why it overshoots, so find that first. No one has tested an anchor on the dense pool, or measured ACS-local at its initial weights, which the published snapshot does not store.

Done when a dense solve with a chi-square anchor reports state gaps, τ and target fit against the published dense solve, the overshoot has an explanation, and the ACS-local build stores its initial weights.

6. Close benefit receipt before calibration

On the dense file, summed family cells start far short: SNAP 37.8%, Medicaid 26.2%, EITC 50.8%, ACA 43.9%, against AGI at 4.2%. Census's own flag rises from 13.03 to 16.31 as calibration closes them. The rent fix does not close SNAP's shortfall: it lifts SNAP at initial weights from $57.58bn to $58.86bn. Better take-up and receipt inputs should reduce the tilt, though no one has tested that, and each input needs its own issue with a before-and-after on this table. Run the ablations on the dense pool too, since those in #1445 ran on Build P.

Done when the Census-flag rate at final weights moves toward its value at initial weights while target fit holds.

7. Age pooled incomes

#944 tracks this. A single-factor run on 2022 and 2023 ASEC records lowers the default file's mean gap from 3.24 to 2.45 and its mean absolute gap from 4.80 to 4.05. A real fix ages each income source, and should test a 2023 to 2025 pool, which matches Table 18's window.

Done when aged records enter the build and dense_steps.csv shows the thresholds step falling.

8. Close the gap between ASEC and PUF rows

At dense initial weights, ASEC rows read 16.38 and PUF rows 11.34, and the pool's zero depends on that offset.

Start with cash income. At published weights, moving to PolicyEngine's cash income adds 0.75 points on ASEC rows and removes 2.06 on PUF rows, while housing adds 1.65 and 1.16 (dense_shapley9_asec.csv, dense_shapley9_puf_tax_detail.csv).

Two known fixes should widen the gap, so make them for their own reasons:

Done when the two channels' child rates differ by no more than their sampling error, or the difference has a documented source.

9. Reconcile 2025 resources near the line

PolicyEngine's child rate rises 1.48 points from 2024 to 2025 while Census's holds at 13.4. Between the 10th and 30th percentiles, child SPM resources grew 3.6% to 5.7% in Census's data, against 1.6% to 2.8% on the default file and 2.5% to 3.2% on dense. Compare growth by income source, starting with the uprating series for low earners.

Done when a table of child resource growth by source, for PolicyEngine and Census, names the sources behind the difference.

10. Check the state gaps that persist

Rank states by the smaller of their two z-scores at dense initial weights and on ACS rows, counting only gaps in the same direction: Louisiana 2.80, Delaware 1.91, Ohio 1.40, Illinois 1.39, Mississippi 1.38 and Washington 1.31.

These stay leads. Chance alone gives 0.7 states past 1.96 (we see one, p = .53), 1.4 past 1.645 (two, p = .41) and 3.0 past 1.28 (six, p = .05). Both gaps also subtract the same Census estimate, which accounts for about .16 of the observed .30 correlation.

Start with Louisiana and Delaware. Check state tax and benefit rules and Census's state estimates. Any rule bug belongs in the country model and the matching rulespec repository.

Done when each named state has a documented cause, or a finding that no cause beyond noise holds up.

11. Report state child rates as a standing holdout

Add the 51-state comparison, with standard errors and Census's margin, to each US release's receipts. Poverty stays a comparison and never a target.

Done when a release receipt carries the table, and CI checks that it exists rather than what it says.

Related

Activity

  1. MaxGhenis commented on Nov 2, 2026

    @MaxGhenis
    ContributorAuthor

    The ACS-local figures in this issue come from runs that filled blank parent links with -1. PolicyEngine-US reads 0 as unknown, so those figures carry a small error, largest in California (19.81 against 20.37). #1445 has the details and a pending rerun. Don't cite this issue's ACS-local figures until that rerun lands; I'll update them here then.

  2. MaxGhenis commented on Nov 6, 2026

    @MaxGhenis
    ContributorAuthor

    #1529 covers step 2: the housing gate now fails on positive rent outside renter households. It also sets the CPS rent draw to zero there, because the shared QRF's zero-or-positive classifier gives every owner and no-cash-rent head a small chance of positive rent. On the Route A pre-clone frame, seeds 1 to 7 each put rent on one to four such heads, and seed 0 on none.

    For step 3, zero owners' rent after the transfer whether or not the rebuild restores the link. transfer_acs_inputs fits its own QRF on the donor and draws pre_subsidy_rent for ACS rows with the same zero-or-positive classifier, so a rebuilt donor can still leave rent on ACS owners. Once #1529 lands, any such rent stops a fiscal refresh built from --pool-manifest at the housing gate. The same gate requires household tenure to carry the three ASEC categories and no others. acs_inputs.py maps ACS tenure code 2 to OWNED_OUTRIGHT, so ACS rows that keep that value already fail it on main. A refresh that reads its base through --base-h5 checks only that base, which holds no ACS rows.

  3. MaxGhenis commented on Nov 6, 2026

    @MaxGhenis
    ContributorAuthor

    #1531 adds Census state age targets to the ACS-local calibration, and we project that they leave the concentration on donor households about where it stands. We balanced each state slice to Census's 15 age groups while holding 16 of the slice's own aggregates, and simulated the results under policyengine-us 1.825.2. We ran the production file (Build P, 592ae5d6) and the October 17 candidate (Build O, 766b7c7). These runs change only household_weight in a copy of each slice, so the parent-link fill above doesn't affect them.

    51 states Build P Build P balanced Build O Build O balanced
    Kish effective sample, median state 331 314 280 278
    Top 1% of households' share of weight, median state 67.7% 67.7% 66.6% 66.3%
    Donor households' share of weight, median state 67.6% 67.0% 66.2% 66.0%
    Children lifted out of SPM poverty by $2,000 for each child under 4, 2026, net 638,471 615,401 796,573 770,047
    Sample households lifting a child, median state 19 19 18 18
    Top household's share of a state's lift, median state 43.2% 45.3% 50.2% 47.2%
    States where one household accounts for more than half of the lift 21 24 26 24
    States lifting at least 1,000 children 36 36 36 37
    Of those, states where one household accounts for more than half 15 15 16 14

    Donor households make up 3.6% of each file's households and account for 73.6% of Build P's lift and 78.4% of Build O's. For example, we estimate the $2,000 lifts 12,417 Indiana children on Build P, with one household accounting for 81.8% of them, and 2,356 on Build O, where the top household accounts for 21.1%. On Build P, the balance's rise from 21 states to 24 comes from states lifting fewer than 1,000 children. Among the 36 states where Build P lifts at least 1,000 children, Build O's lift falls below half of Build P's or exceeds double in 6. The balance moves Build P's by more than a fifth in 10 of them.

    For step 4, I'd add the top household's share of a small reform's state lift beside child state Kish and reform standard error, with a floor on the lift. The District of Columbia's share reads 100% on Build P with a net lift under 0.001 children. _lift_stats in census_age_reweight_modal.py computes it, and the by-state files for Build P and Build O hold these runs' values. PolicyEngine/child-poverty-impact-dashboard#129 has the analysis, and PolicyEngine/child-poverty-impact-dashboard#130 covers the dashboard's side.

    🤖 Generated with Claude Code

  4. changed the title [-]State child SPM rates miss Census: next steps from the November diagnosis[/-] [+]State child SPM rates: next steps from the November trace[/+] on Nov 6, 2026
  5. added a commit that references this issue on Nov 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions