Repository navigation
Count all child care subsidies in household benefits - #9503
RajveerKapoor wants to merge 14 commits into
Conversation
Added test cases for Massachusetts TAFDC and EAEDC benefits, as well as Alaska and North Carolina child care subsidies. Updated household state benefits calculations to ensure accurate representation of state programs.
|
Independent confirmation of the problem this fixes, and one observation about its size. We (HotGap, a benefits-cliff calculator on PolicyEngine) hit #9405 from the outside on 2026-09-15 against the hosted API (policyengine-us 1.764.6). The omission is worse than a missing line item: because Connecticut, single parent, one 3-year-old, $9,600/yr of care, $25,000 of pay,
Colorado (in the list) is the control: the same household at $25,000 shows On the diff itself: I pulled the branch and checked the parameter — the aggregate is in all four periods (2023–2026) and none of the 84 per-state entries survive, so the double-count the PR description guards against can't happen. Swapping to Downstream note for anyone else adding the subsidy back in the omitted states: this PR moves it inside |
MaxGhenis
left a comment
There was a problem hiding this comment.
Thanks for picking up #9405, and sorry this sat for four days without a review.
Replacing the hand-listed state entries with child_care_subsidies is the right fix. There are 51 *_child_care_subsidies variables and gov.hhs.ccdf.child_care_subsidy_programs lists all 51 for 2025, so the aggregate cannot drift the way the hand list did.
One thing blocks merge. Full Suite - Rest fails in test_aggregate_list_integrity.py::test_members_are_state_gated:
gov/household/household_state_benefits.yaml members without a StateCode gate in their defined_for chain (dc_ctc-style leak risk): ['child_care_subsidies']
That guard is recent (#9497) and only walks defined_for, so it cannot see that every member of the aggregate's adds list is state-gated. The problem is the guard, not your change. Either teach it to accept a member with no defined_for when everything in its adds list is itself state-gated, or add an explicit exemption for child_care_subsidies with a comment saying why. The first is better because the next national aggregate will hit the same wall.
Three things to know about, none blocking:
- Vermont.
vt_ccfappays the state rate whatever the provider charges, which is Vermont's rule since December 2023, so it can exceed the family's child care cost. With this PR that amount reacheshousehold_net_incomefor the first time. A Vermont case that pins it would make the number a decision instead of a surprise. - SPM side.
spm_unit_benefitsstill lists onlyco_ccap_subsidy,wa_child_care_subsidiesandwv_child_care_subsidies, so household and SPM net income will disagree on child care in most states. Fine as a follow-up, but worth a line in the PR body. - Tests. The two new cases set the state variable as an input, so they check list membership only. One case that computes a real subsidy in a newly connected state would be stronger.
After the guard fix, please merge main so CI runs on a current base.
test_members_are_state_gated failed on household_state_benefits because child_care_subsidies is itself an aggregate with no defined_for gate. An aggregate whose every summed member is state-gated cannot leak across states, so the guard now recurses through a member's adds list (parameter path or literal list, depth-capped at 3) and accepts it only when every addend is gated. Requested in review of PolicyEngine#9503.
Merge PolicyEngine main into fix/household-child-care-subsidies
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vermont pays the state rate whatever the provider charges, so a computed case now records that the payment above the bill reaches household benefits. The property tests state that no subsidy counts twice and that a dollar of subsidy adds a dollar to household benefits and net income. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
I merged main into this branch, because main had added state supplement entries to the same list and the PR conflicted.
The SPM side stays with #10569. I'll add the state impact here once the runs finish. Thanks for the fix, @RajveerKapoor. |
Vermont's family still owes its share, so the zero that childcare_expenses returns isn't a result worth pinning. State why the household test is Python and halve its examples. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Handing this PR to the US hub to land once CI passes.
|
|
Ready for hub review The head hasn't moved since The list swap is correct. Every block now carries the aggregate once, nothing counts twice, CI is green, and FindingsP1. Vermont's payment above the bill now counts in full in
|
| Mutant | test_child_care_subsidies_household_benefits.py |
test_aggregate_list_integrity.py |
household_state_benefits.yaml |
|---|---|---|---|
Delete ct_child_care_subsidies from every CCDF block |
5 passed | 29 passed | 10 passed |
| Delete it from the 2025 block only | 5 passed | killed by test_no_silent_removals_between_year_blocks |
10 passed |
Re-add vt_child_care_subsidies to the 2026 household block (mine) |
killed by test_each_block_counts_only_the_aggregate[2026-01-01]; the property passed |
29 passed | 10 passed |
| Delete the aggregate from the 2023 household block (mine) | killed by test_each_block_counts_only_the_aggregate[2023-01-01] and the property |
29 passed | 10 passed |
No Connecticut test asserts child_care_subsidies or a household output either: grep -rln ct_child_care_subsidies policyengine_us/tests finds nothing. Add this to test_child_care_subsidies_household_benefits.py:
def test_latest_blocks_list_every_state_program():
defined = {
path.stem
for path in (PARAMETERS.parent / "variables/gov/states").rglob(
"*_child_care_subsidies.py"
)
}
assert len(defined) == 51
for start, programs in blocks(PROGRAMS).items():
if start >= "2025":
assert set(programs) == defined, startRun as a script, the same check passes at head with 51 programs. With Connecticut deleted it fails with {'2025-01-01': ['ct_child_care_subsidies']}. (#10569 carries a stricter version from 2024, which only holds once #10569 adds DC, Tennessee and Texas to the 2024 block.)
P2. The property test is weaker than the body says
Where: test_child_care_subsidies_household_benefits.py:63-69; the body's "Invariants" section.
- The test can't pass trivially. It sets the program's value directly and checks a dollar-for-dollar change, and it killed the aggregate-deleted mutant.
- It samples programs only from the CCDF list, so it can never notice a program missing from that list (the first mutant above).
- It draws 15 (year, program) pairs from about 190, with no fixed seed, so any one pair is checked with low probability. In my run it missed the re-added Vermont entry in 2026, and only the block test killed that mutant.
- It samples 2023–2026. The 2023 household block also governs 2021 and 2022, and the 2026 block governs 2027.
The body says "A dollar of subsidy adds a dollar … in every state with a program that year. Hypothesis samples the year, program and amount." Replace that with: "On each run, Hypothesis checks 15 sampled (year, program, amount) cases from 2023 to 2026 that a dollar of subsidy adds a dollar to household_benefits and household_net_income. The block test checks every year structurally." Or make the test check every program for one fixed year, which costs 51 × 2 simulations.
P3. The body has no impact: line
The body has an "## Impact" section and an axiom: line (axiom: n/a: household benefit aggregate list, no rule changes), but no impact: line. Add one, for example impact: +$0.85B household_benefits and household_net_income, 2026, populace-us-2024-spm-20260909, 469ad12 vs this branch with #10584. It should be restated once the hub runs the landing pair.
P3 (follow-up, the same on main). Programs that compute in years their CCDF block leaves out
The family below (one parent, a 3-year-old in full-time care, $12,000 bill) gets a subsidy from the state variable, but child_care_subsidies and household_state_benefits are 0:
NC 2021-2023 nc_child_care_subsidies 7632.0 child_care_subsidies 0.0 household_state_benefits 0.0
OR 2021-2023 or_child_care_subsidies 11940.0 child_care_subsidies 0.0 household_state_benefits 0.0
DC 2021-2024 dc_child_care_subsidies 12000.0 child_care_subsidies 0.0 household_state_benefits 0.0
TX 2021-2024 tx_child_care_subsidies 5298.66-5396.45 child_care_subsidies 0.0 household_state_benefits 0.0
TN 2024 tn_child_care_subsidies 2704.0 child_care_subsidies 0.0 household_state_benefits 0.0
On main, none of these was directly listed in those years, so this PR doesn't make it worse. But the changelog line "Count every implemented state child care subsidy in household benefits" is too broad. Either change it to "Count every state child care subsidy in the CCDF program list in household benefits", or open a follow-up deciding whether these programs belong in their earlier blocks. Several of them run on rates copied back from later years, so adding them isn't automatically right. #10569 adds DC, Tennessee and Texas to 2024.
P3 (follow-up, the same on main). childcare_expenses loses the Vermont family's share
As the new YAML comment says, childcare_expenses = max(bill − subsidy, 0) is 0 for the Vermont case, although the family pays a $100 weekly share ($5,200 a year). childcare_expenses is unchanged by this PR, so this belongs in a follow-up, not here.
Checked and sound
-
The list, at head and main. I parsed every dated block of
household_state_benefits.yamlat head and at main, and ofchild_care_subsidy_programs.yaml(identical at both).- Each head block (2023, 2024, 2025, 2026) contains
child_care_subsidiesexactly once and no*_child_care_subsidiesentry. - Each head block equals main's block with its direct child care entries removed and the aggregate added.
- Every direct entry on main sits in the CCDF block for that year (none dropped), so the swap only adds states. Newly counted: 30 states for 2023 (and earlier), 26 for 2024, and 28 for 2025 and 2026: AK CT FL HI IA IL KS KY LA MA ME MI MN MS ND NH NJ NM OR PA RI SD TN TX UT VT WI WY.
- The code defines 51
*_child_care_subsidiesvariables. The 2025 CCDF block lists all 51, the 2024 block lacks DC, TN and TX, and the 2021 block also lacks NC and OR (see the follow-up above).
- Each head block (2023, 2024, 2025, 2026) contains
-
Double counting. The paths from a state subsidy to the outputs:
household_net_incomeaddshousehold_benefits(which addshousehold_state_benefits→child_care_subsidies) and doesn't readspm_unit_benefits.spm_unit_benefitslistsco_ccap_subsidy(=co_child_care_subsidies),wa_child_care_subsidiesandwv_child_care_subsidies. It feeds onlyspm_unit_net_income.household_local_benefitsholds onlyco_denver_property_tax_relief.- No other household list names a child care program.
- Family runs (2025; one parent at $25,000 working 40 hours, a 3-year-old in 40h/week care, 20 attending days a month, $12,000 bill), identical at head and main:
Each subsidy counts once in each output. Connecticut shows the fix:
CO: co_child_care_subsidies 11249.52 household_state_benefits 11249.52 household_benefits 13386.58 spm_unit_benefits 13386.58 WA: wa_child_care_subsidies 11220.00 household_state_benefits 11220.00 household_benefits 13360.06 spm_unit_benefits 13360.06 WV: wv_child_care_subsidies 7500.00 household_state_benefits 7500.00 household_benefits 10756.06 spm_unit_benefits 10756.06household_net_incomegoes from 33,222.62 at main to 44,472.62 at head (+11,250 = the subsidy), andspm_unit_net_incomestays at 21,222.62.
-
The hand values. Vermont case: age 3 = 36 months, so PRESCHOOL. 40 hours is FULL_TIME (more than 25, up to 50). The 2025 licensed-center rate is $439 a week. $52,875 is 250% of the 2025 guideline for two ($21,150), which falls in the 2.2501–2.5001 band, so the share is $100 a week. ($439 − $100) × 52 = $17,628, matching the test. The Alaska and North Carolina cases set the program value directly.
-
Calculation cost. I ran
household_net_income(withchild_care_subsidies,household_state_benefits,household_benefitsandspm_unit_benefits) at head for every StateCode except the territories and military codes: 50 states, DC and PW. That was each year 2021–2027, for the family above with care and for one without care or a bill: 364 runs each, 0 errors. Mean 1.9s per run after the first, maximum 15.0s (the first run loads the system). No state needed an input without a default. With no care, no state paid a subsidy in any year. -
Tests. I ran each changed file once, one per process, on a 4-vCPU Linux VM shared with my sweep, so these are contended local numbers, not CI:
test_child_care_subsidies_household_benefits.py: 5 passed in 65.7s, peak RSS 2.08 GB.test_aggregate_list_integrity.py: 29 passed.household_state_benefits.yaml: 10 passed.- The hand-off's "92 pytest cases and 318 YAML cases" rest on a local run I didn't repeat.
- The
test_aggregate_list_integrity.pychange (is_state_gatedlooks through an aggregate whose members are all state-gated) is sound.child_care_subsidiesitself has no gate, and every CCDF member has aStateCodegate.
-
CI at
100a1aed40: run 41460582333, all 34 jobssuccess, including Household API Partners and every baseline, contrib and Python shard. -
Readers. Readers of
household_state_benefits:household_benefits(and the BOOST, ECPA and EDAA overrides of it);- the
tx_rebatereform, which copies the 2027 list and appendstx_rebate, so it now carries the aggregate.
Readers of
household_benefits:household_net_income.Readers of
household_net_income:household_net_income_including_health_benefits,equiv_household_net_income,household_income_decile,marginal_tax_rate(and its helpers),behavioral_response_measurementsandcapital_gains_responses.These move at baseline for families with a subsidy in a newly counted state:
household_state_benefits,household_benefits,household_net_income,household_net_income_including_health_benefits,equiv_household_net_income,household_income_decileandmarginal_tax_rate. For example, Connecticut'sequiv_household_net_incomegoes from 23,491.94 to 31,446.89, and the summedmarginal_tax_ratefrom −6.28 to −6.25. Behavioral responses move only under a reform with nonzero elasticities.spm_unit_net_income, SPM poverty andchildcare_expensesdon't move. Nothing in a state tax or benefit formula reads these aggregates. -
Impact provenance. 469ad12 is on main. Of main's commits since then:
- Pay Vermont and New Hampshire child care subsidies only for care #10584 is in the body's run.
- Stop requiring an activity input from Nevada's working parents #10575 changes Nevada's subsidy, but Nevada was already listed directly on main, so it doesn't change this PR's delta.
- Count the same cash payments in household and SPM income #10573 adds non-child-care cash programs to both lists.
- Prorate Wisconsin Works Community Service Jobs by unsubsidized hours #10576 changes Wisconsin Works, which reaches Wisconsin Shares only through
receives_tanf, an input. - The Medicaid, SSI and Trustees commits don't touch child care.
- None can change the household-side delta. The PR's own commits after its parameter change touch only tests, so the body's "commits after that run change only tests" holds for the branch. The final merge commit brings in main.
The landing pair needs these variables, by state and nationally:
child_care_subsidies, each newly counted*_child_care_subsidies,household_state_benefits,household_benefits,household_net_income,equiv_household_net_income/household_income_decile(deciles and Gini),spm_unit_net_incomeand SPM poverty (expected unchanged). Also, for P1: Vermont'smax(child_care_subsidies − spm_unit_pre_subsidy_childcare_expenses, 0). -
Overlap.
- Count state child care subsidies in the SPM once #10569 (open, held) changes the CCDF list by folding DC, Tennessee and Texas into the 2024 block, and changes
spm_unit_benefits. It merges cleanly with this PR. Itssubsidy_counted_by_household_measurehandles either the direct entry or the aggregate, so landing this PR breaks nothing it assumes. Once both land, DC, Tennessee and Texas reach household benefits in 2024 as well. - Count Illinois AABD cash in household and SPM benefits #10086 (Illinois AABD) also edits
household_state_benefits.yaml. It merges cleanly with main but conflicts textually with this PR.
- Count state child care subsidies in the SPM once #10569 (open, held) changes the CCDF list by folding DC, Tennessee and Texas into the 2024 block, and changes
-
Scope and partners. The PR changes no file under
.github/ortests/policy/baseline/partners/. No partner test asserts a household aggregate,child_care_subsidiesormarginal_tax_rate, and the Household API Partners job passed. No partner value moves. -
Changelog:
changelog.d/9405.fixed.mdis top-level with a valid type.
What I couldn't check
- Vermont's sources. This session's network blocks
outside.vermont.govandlegislature.vermont.gov, so I couldn't read the CCFAP regulations, "Understanding Payments" or the capped-rates guidance. The statement that Vermont pays above the provider's charge rests on the parameter, the code comment and a search-result excerpt of a DCF memo. That includes whether the July 13, 2025 "Capped State Rates" guidance limits payment to the charge. - The body's population figures. The +$0.85B and the state rows come from a dataset I can't reach. I ran no microsimulation.
- The hand-off's local logs and counts.
- A main-side copy of the full sweep. The main-worktree sweep stopped partway, so I compared main and head only for the CO, WA, WV, CT and VT families shown.
Generated by Claude Code
|
Picking this up for the US hub. I'll post a hand-off comment here when it's ready. Generated by Claude Code |
|
Ready for hub review Head: Ownership check: PR open, not draft, head matched the item, last push 2026-11-02T14:11:56Z (over 8 hours before pick-up). What the item asked for
Sources
Tests (one file per process, Linux container)
Cost of the Python file (local, not CI):
Hand computationThe subsidy is ($439 − $100) × 52 = $17,628. Each capped value is min(17,628, bill): 10,000, 1,000 and 17,628. With no bill it is 17,628. Mutants (each in its own process, restored with
|
Vermont pays its state rate to the provider whatever the provider charges, so its subsidy can exceed the family's bill, and the amount above the bill never reaches the family. household_child_care_subsidies caps child_care_subsidies at the pre-subsidy bill (whole subsidy with no bill recorded), and every household_state_benefits block counts it in place of the uncapped aggregate. child_care_subsidies stays the full payment. Tests: Vermont cases at bills below, at and above the payment, with no bill and with the bill set on the child; a Connecticut case; a Hypothesis property of the cap; and a check that the 2025+ CCDF blocks list every state program. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Hub review in progress Reviewing head
Generated by Claude Code |
|
Ready for hub review This is round 2, a delta review. The head hasn't moved past Round 1's findings are resolved as the hub ruled, and the Vermont values are right. One new finding blocks: the cap's bill leaves out FindingsP1. The cap leaves out after-school expenses, so Colorado's counted subsidy falls below mainWhere:
The witness family is 2025 Colorado: one parent with $25,000 of earnings working 40 hours, and a 3-year-old in 40 hours a week of care, 20 days a month. This reaches the household calculator, because Fix. Count what the payment covers in the bill: bill = add(
spm_unit,
period,
["pre_subsidy_childcare_expenses", "after_school_expenses"],
)I applied exactly that on a scratch copy and reran everything, then restored it (
Add a YAML case beside Connecticut's, and update the docstring and documentation to say "pre-subsidy child care and after-school bill": # Colorado's subsidy pays for after-school care too, so the cap counts that bill.
- name: Colorado's subsidy for child care and after-school care counts in full
period: 2025
input:
people:
parent:
age: 30
child:
age: 3
after_school_expenses: 6_000
spm_units:
spm_unit:
members: [parent, child]
co_child_care_subsidies: 12_000
spm_unit_pre_subsidy_childcare_expenses: 6_000
households:
household:
members: [parent, child]
state_code: CO
output:
household_child_care_subsidies: 12_000(I didn't run this exact case. My witness above computes the subsidy rather than setting it.) P2. On population data, the "pre-subsidy bill" is what parents reported paying after the subsidyWhere: the body's "## Fix" ("counts each family's subsidy up to its pre-subsidy bill"), and the variable's #10569 adds this documentation to
P3. The cap property doesn't reliably reach its
|
| Mutant | Household YAML | Python file | List integrity |
|---|---|---|---|
(b) min_ with no where (hand-off) |
5 failed | test_a_dollar_of_subsidy_adds_a_dollar failed; the cap property passed (P3) |
29 passed |
| (f) bill from the SPM input (hand-off) | 1 failed, the child-level case | 7 passed | 29 passed |
| (g) no bill counts 0 (mine) | 5 failed | both property tests failed | 29 passed |
(h) CAPPED_AGGREGATES → {"de_ssp"} (mine) |
15 passed | 5 passed (-k "each_block or latest") |
29 passed: survives |
(i) formula reads snap (mine) |
10 failed | 5 passed (-k) |
29 passed |
The reference is one href (...Understanding-Payments.pdf#page=1) with a # PDF pages 1-2 comment above it and no title. That follows CLAUDE.md's rule for variable tuples. I couldn't open the PDF to confirm the two pages.
Changelog: changelog.d/9405.fixed.md is top-level, has a valid type, and carries round 1's wording.
Body: it has both the impact: and axiom: lines.
- The test-cost count (7 tests: 4 block cases, the list test and two properties) matches the file.
- "The file isn't named in the Makefile" holds:
grep test_child_care Makefilefinds nothing.
Baseline movement. The diff moves outputs at baseline:
household_state_benefits,household_benefits,household_net_incomeand their readers (equiv_household_net_income,household_income_decile,household_net_income_including_health_benefits,marginal_tax_rate) rise in the 28 newly counted states.- Vermont's change is now capped.
- With P1 unfixed, Colorado's change falls wherever
after_school_expenses > 0.
spm_unit_net_income and SPM poverty don't move. The landing pair should report:
- these by state;
- Vermont's
child_care_subsidies − household_child_care_subsidies; - Colorado's
household_state_benefitsagainst main (zero change once P1 is fixed); - the count of records with
after_school_expenses > 0.
Overlap. Open PRs touching household_state_benefits: Count Illinois AABD cash in household and SPM benefits #10086, Fix four statute-first benefit bugs: MO TANF rounding, SNAP GU/VI limits, HI CCAP TANF income, SSI food ISM date #10332, Guard county-keyed lookups in VT renter credit, MN CCAP, WI Shares and MD CCS #10116 and Count state child care subsidies in the SPM once #10569. Of these, only Count Illinois AABD cash in household and SPM benefits #10086 changes household_state_benefits.yaml; Fix four statute-first benefit bugs: MO TANF rounding, SNAP GU/VI limits, HI CCAP TANF income, SSI food ISM date #10332 and Guard county-keyed lookups in VT renter credit, MN CCAP, WI Shares and MD CCS #10116 don't change any of this PR's files.
- Count state child care subsidies in the SPM once #10569: its
subsidy_counted_by_household_measure(policyengine_us/tests/test_spm_child_care_subsidy_invariants.py:331-332at652e968a94) checks for"child_care_subsidies"or the state's own variable. After this PR, neither is listed, so it returns 0. That's the known issue. One thing beyond the textual fix: that helper should returnhousehold_child_care_subsidies, notchild_care_subsidies. Its strategy draws Vermont with expenses from 0 to 25,000, where the two differ. - Count Illinois AABD cash in household and SPM benefits #10086: the known textual conflict in
household_state_benefits.yaml. The new variable adds nothing to it.
Scope: no file under .github/ or policyengine_us/tests/policy/baseline/partners/.
CI at 9453494314: run 41550947303 is still in progress. Household API Partners, PR validation, Quick Feedback, Package and compatibility, every Baseline shard and every Python shard (including Microsimulation) succeeded. Contrib other-shard-2a succeeded; the other eight Contrib shards were still running when I posted, so this is not a green run yet.
What I couldn't check
- Sources. This session's network refuses
outside.vermont.gov(both the Understanding Payments PDF and the Capped Rates guidance),web.archive.organdecfr.gov. I couldn't confirm the body's two Vermont quotes, the "Jane Smith" example, or that the PDF has two pages. The hub's ruling and round 1's search excerpt are the only support. - Population data. I couldn't check whether the data fills
after_school_expenses, so the population size of P1 is unknown. The same goes for whetherspm_unit_pre_subsidy_childcare_expensesis post-subsidy spending in the current build (P2). I ran no microsimulation. - The remaining hand-off mutants (a), (c), (d) and (e). I reran (b) and (f) and added three of my own.
- The final CI result for the Contrib shards.
Generated by Claude Code
Colorado's CCAP subsidy pays for pre-subsidy child care and after-school expenses (co_ccap_subsidy), so a cap at the child care bill alone cut part of it and lowered household_net_income below main for families with after-school costs. Add a Colorado YAML case and pin the cap property's zero, equal and one-dollar-over boundaries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Hub impact pair, progress The base side of the impact pair finished. The head side runs next.
Base weighted sums as JSON{
"ca_child_care_subsidies": 0.0,
"child_care_subsidies": 1786291922.607019,
"ct_child_care_subsidies": 405603760.7995658,
"equiv_household_net_income": 10834823468521.645,
"household_benefits": 2059793926050.1262,
"household_net_income": 17161428039502.375,
"household_state_benefits": 8201643950.43103,
"income_tax": 2595105975379.617,
"ma_child_care_subsidies": 0.0,
"ny_child_care_subsidies": 0.0,
"spm_unit_net_income": 14653599190798.559,
"spm_unit_pre_subsidy_childcare_expenses": 83421131008.018,
"state_income_tax": 578044599361.5671,
"vt_child_care_subsidies": 45948444.54842574
}Generated by Claude Code |
|
Hub impact pair, progress The base side of the impact pair finished. The head side runs next.
Base weighted sums as JSON{
"ca_child_care_subsidies": 0.0,
"child_care_subsidies": 1786291922.607019,
"ct_child_care_subsidies": 405603760.7995658,
"equiv_household_net_income": 10834823468521.645,
"household_benefits": 2059793926050.1262,
"household_net_income": 17161428039502.375,
"household_state_benefits": 8201643950.43103,
"income_tax": 2595105975379.617,
"ma_child_care_subsidies": 0.0,
"ny_child_care_subsidies": 0.0,
"spm_unit_net_income": 14653599190798.559,
"spm_unit_pre_subsidy_childcare_expenses": 83421131008.018,
"state_income_tax": 578044599361.5671,
"vt_child_care_subsidies": 45948444.54842574
}Generated by Claude Code |
|
Ready for hub review
Variables that move (records that differ by more than 1e-6):
Each sum multiplies values by the simulation weights that What moves. In the 2026 block of State rows. The 144 households are in the 10 states below, the only states outside the base's list with any
Rows may not add to the totals because of rounding. "Above the bill" is each state's The base already listed CO, DE, GA, IN, MD, MT, NC, NE, NV and WA, the other states with Vermont's amount above the bill. The other nine states' additions match the PR body's pre-cap state table to the $0.01M it shows. Vermont's falls from the $45.95M there, its whole Record check. Sub-cent rows. In CO, DE, IN, NC and WA, states the base already listed, 19 households lose less than a cent of Resources (4 vCPU, 16 GB VM, one side at a time; beside each side ran only a log-watching loop and a few short file reads and API calls):
Dropped variables: Notes:
Commands: # in an empty directory (it ends up holding pe, base, head, out and the scripts)
curl -sI https://huggingface.co/api/datasets/policyengine/populace-us | head -1 # HTTP/1.1 200 Connection Established (the proxy's line)
curl -sS -o /dev/null -w 'HF api status: %{http_code}\n' https://huggingface.co/api/datasets/policyengine/populace-us # 200
apt-get install -y time || (apt-get update && apt-get install -y time) # the VM had no GNU time
git clone --filter=blob:none -q https://github.com/PolicyEngine/policyengine-us.git pe && cd pe
git fetch -q origin pull/9503/head:pr-9503 pull/10561/head:pr-10561
HEAD_SHA=$(git rev-parse pr-9503); BASE_SHA=$(git merge-base origin/main $HEAD_SHA); echo $HEAD_SHA $BASE_SHA
# 8ce831b6dcee3deda178a9a8aeb7fdf118c8e6ac 0d933918a82827b92243c0a43d0d8d03571604fd (a rerun should pin these)
git worktree add -q --detach ../head $HEAD_SHA
git worktree add -q --detach ../base $BASE_SHA
git show pr-10561:policyengine_us/tools/calculate_chunked.py > ../calculate_chunked.py # pr-10561 was c545c47877c70b367a6fab2322037e453364997e
git diff --stat $BASE_SHA $HEAD_SHA -- pyproject.toml uv.lock # empty
git diff --stat $BASE_SHA $HEAD_SHA # 6 files, not system.py
grep -n DEFAULT_DATASET ../head/policyengine_us/system.py ../base/policyengine_us/system.py # the same in both trees
cd ../head && (pip install uv >/dev/null 2>&1; uv sync > ../uv-sync.log 2>&1; echo "uv sync exit: $?" >> ../uv-sync.log) # uv was already installed; exit 0
# the pair, one side at a time (pair_side.py and compare.py below, saved next to calculate_chunked.py)
cd ../pe; mkdir -p ../out
H=$PWD/../calculate_chunked.py; PY=$PWD/../head/.venv/bin/python
export OMP_NUM_THREADS=4 OPENBLAS_NUM_THREADS=4 MKL_NUM_THREADS=4
# first base attempt: pair_side.py's VARIABLES held the hub's 15 names, the 14 below plus household_income_decile
# after equiv_household_net_income; the helper rejects household_income_decile (exit status 1)
date -u +"start %FT%TZ" > ../out/base.when
PYTHONPATH=$PWD/../base /usr/bin/time -v $PY ../pair_side.py $H 8 2026 ../out/base > ../out/base.log 2> ../out/base.err
echo "exit $?" >> ../out/base.when; date -u +"end %FT%TZ" >> ../out/base.when
(cd ../out && for f in base.log base.err base.when; do mv $f try1-15vars-$f; done)
# then household_income_decile comes out of VARIABLES in pair_side.py, for both sides
F=$($PY -I -c "from huggingface_hub import hf_hub_download; print(hf_hub_download(repo_id='policyengine/populace-us', filename='populace_us_2024.h5', repo_type='dataset', revision='populace-us-2024-spm-20260909', local_files_only=True))")
sha256sum "$F" # before the base side
echo "$F" > ../out/dataset_path.txt
date -u +"start %FT%TZ" > ../out/base.when
PYTHONPATH=$PWD/../base /usr/bin/time -v $PY ../pair_side.py $H 8 2026 ../out/base > ../out/base.log 2> ../out/base.err
echo "exit $?" >> ../out/base.when; date -u +"end %FT%TZ" >> ../out/base.when
# the earlier progress comment's base sums (fetched while the base side ran), and the check against them
gh api repos/PolicyEngine/policyengine-us/issues/comments/6596150484 --jq '.body' > ../out/prev_progress_6596150484.md
$PY -I -c "import json, re; body = open('../out/prev_progress_6596150484.md').read(); m = re.search(r'\`\`\`json\n(.*?)\n\`\`\`', body, re.S); json.dump(json.loads(m.group(1)), open('../out/prev_base_sums.json', 'w'), indent=1, sort_keys=True)"
$PY -I -c "import json; s = json.load(open('../out/base/summary.json'))['weighted_sums']; p = json.load(open('../out/prev_base_sums.json')); print(set(s) == set(p), all(s[k] == p[k] for k in p))" # True True
sha256sum "$F" | awk '{print $1}' | tee ../out/dataset.sha256 # between the sides
date -u +"start %FT%TZ" > ../out/head.when
PYTHONPATH=$PWD/../head /usr/bin/time -v $PY ../pair_side.py $H 8 2026 ../out/head > ../out/head.log 2> ../out/head.err
echo "exit $?" >> ../out/head.when; date -u +"end %FT%TZ" >> ../out/head.when
sha256sum "$F" | awk '{print $1}' | tee ../out/dataset.sha256.after # after the head side
$PY ../compare.py ../out/base ../out/head ../out/compare.json
# state rows, amounts above the bill and the record check (analysis.py below: it reads the pickles,
# the dataset's stored state_fips and entity links, and both trees' household_state_benefits parameter files)
$PY -I ../analysis.py ../out/base ../out/head "$F" ../base ../head ../out/analysis.json > ../out/analysis.stdoutScripts: pair_side.py, compare.py, analysis.py
"""One side of the pair: python pair_side.py <helper.py> <chunks> <year> <out_dir>."""
import importlib.util
import json
import sys
import time
from pathlib import Path
helper, chunks, year, out = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), Path(sys.argv[4])
VARIABLES = (
"child_care_subsidies",
"household_state_benefits",
"equiv_household_net_income",
"spm_unit_net_income",
"spm_unit_pre_subsidy_childcare_expenses",
"vt_child_care_subsidies",
"ct_child_care_subsidies",
"ma_child_care_subsidies",
"ca_child_care_subsidies",
"ny_child_care_subsidies",
"income_tax",
"state_income_tax",
"household_net_income",
"household_benefits",
)
spec = importlib.util.spec_from_file_location("hub_calculate_chunked", helper)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
import policyengine_us # noqa: E402
from policyengine_us import Microsimulation # noqa: E402
start = time.monotonic()
result = module.calculate_chunked(
lambda dataset: Microsimulation(dataset=dataset, dataset_end_year=year),
VARIABLES,
year,
chunks,
dataset=Microsimulation.default_dataset,
)
out.mkdir(parents=True, exist_ok=True)
for entity, table in result.entities.items():
table.to_pickle(out / f"{entity}.pkl")
summary = {
"package": str(Path(policyengine_us.__file__).parent),
"chunks": chunks,
"year": year,
"weighted_sums": result.weighted_sums,
"wall_seconds": time.monotonic() - start,
}
(out / "summary.json").write_text(json.dumps(summary, indent=1, sort_keys=True))
print(json.dumps(summary, sort_keys=True), flush=True)
"""Compare the two sides: python compare.py <base_dir> <head_dir> <out.json>."""
import json
import sys
from pathlib import Path
import numpy as np
import pandas as pd
base, head, out = Path(sys.argv[1]), Path(sys.argv[2]), Path(sys.argv[3])
compare = {}
for base_path in sorted(base.glob("*.pkl")):
entity = base_path.stem
left = pd.read_pickle(base_path)
right = pd.read_pickle(head / base_path.name)
key = f"{entity}_id"
if not np.array_equal(left[key].to_numpy(), right[key].to_numpy()):
sys.exit(f"{entity}: the two sides hold different IDs or a different order")
if not np.array_equal(left["weight"].to_numpy(), right["weight"].to_numpy()):
sys.exit(f"{entity}: the two sides hold different weights")
if set(left.columns) != set(right.columns):
sys.exit(f"{entity}: the two sides hold different columns")
ids = left[key].to_numpy()
weight = left["weight"].to_numpy(dtype=float)
for name in left.columns.drop([key, "weight"]):
a = left[name].to_numpy(dtype=float)
b = right[name].to_numpy(dtype=float)
if np.isnan(a).any() or np.isnan(b).any():
sys.exit(f"{name}: a side holds NaN")
difference = b - a
moved = np.abs(difference) > 1e-6
largest = np.argsort(-np.abs(difference))[: min(5, int(moved.sum()))]
compare[name] = {
"entity": entity,
"base_sum": float((a * weight).sum()),
"head_sum": float((b * weight).sum()),
"change": float((b * weight).sum() - (a * weight).sum()),
"records_changed": int(moved.sum()),
"weighted_changed": float(weight[moved].sum()),
"weighted_up": float(weight[difference > 1e-6].sum()),
"weighted_down": float(weight[difference < -1e-6].sum()),
"largest_abs": float(np.abs(difference).max()),
"rows": [
{"id": int(ids[i]), "base": float(a[i]), "head": float(b[i])}
for i in largest
],
}
out.write_text(json.dumps(compare, indent=1))
print(json.dumps({k: v["records_changed"] for k, v in compare.items()}, indent=1))
"""State rows and amounts above the bill from the pair's per-record outputs.
python analysis.py <base_dir> <head_dir> <dataset.h5> <base_tree> <head_tree> <out.json>
Reads only the two sides' pickles, the dataset's stored household state and
entity links, and the household_state_benefits parameter files of both trees.
"""
import json
import sys
from pathlib import Path
import numpy as np
import pandas as pd
import yaml
base, head, dataset_path, base_tree, head_tree, out = (
Path(sys.argv[1]),
Path(sys.argv[2]),
sys.argv[3],
Path(sys.argv[4]),
Path(sys.argv[5]),
Path(sys.argv[6]),
)
YEAR_BLOCK = "2026-01-01"
TOL = 1e-6 # compare.py's threshold
CENT = 0.01
FIPS = {
1: "AL", 2: "AK", 4: "AZ", 5: "AR", 6: "CA", 8: "CO", 9: "CT", 10: "DE",
11: "DC", 12: "FL", 13: "GA", 15: "HI", 16: "ID", 17: "IL", 18: "IN",
19: "IA", 20: "KS", 21: "KY", 22: "LA", 23: "ME", 24: "MD", 25: "MA",
26: "MI", 27: "MN", 28: "MS", 29: "MO", 30: "MT", 31: "NE", 32: "NV",
33: "NH", 34: "NJ", 35: "NM", 36: "NY", 37: "NC", 38: "ND", 39: "OH",
40: "OK", 41: "OR", 42: "PA", 44: "RI", 45: "SC", 46: "SD", 47: "TN",
48: "TX", 49: "UT", 50: "VT", 51: "VA", 53: "WA", 54: "WV", 55: "WI",
56: "WY",
}
def load(side, entity):
return pd.read_pickle(side / f"{entity}.pkl")
def listed_block(tree):
path = tree / "policyengine_us/parameters/gov/household/household_state_benefits.yaml"
values = yaml.safe_load(path.read_text())["values"]
return list({str(k): v for k, v in values.items()}[YEAR_BLOCK])
base_list = listed_block(base_tree)
head_list = listed_block(head_tree)
base_ccs_states = sorted(
name[:2].upper() for name in base_list if name.endswith("_child_care_subsidies")
)
head_ccs_items = [
name
for name in head_list
if name.endswith("_child_care_subsidies") or name == "household_child_care_subsidies"
]
other_base = [n for n in base_list if not n.endswith("_child_care_subsidies")]
other_head = [n for n in head_list if n != "household_child_care_subsidies"]
list_check = {
"base_child_care_entries": len(base_ccs_states),
"head_child_care_entries": head_ccs_items,
"other_entries_identical": other_base == other_head,
}
# The dataset's stored state and entity links (inputs, the same on both sides).
with pd.HDFStore(dataset_path, mode="r") as store:
keys = store.keys()
def table(entity):
if f"/{entity}" in keys:
return store[f"/{entity}"]
return store[sorted(k for k in keys if k.startswith(f"/{entity}/"))[0]]
household_table = table("household")
person_table = table("person")
household_state = pd.Series(
household_table["state_fips"].to_numpy().astype(int),
index=household_table["household_id"].to_numpy(),
)
spm_household = (
pd.DataFrame(
{
"spm": person_table["person_spm_unit_id"].to_numpy(),
"household": person_table["person_household_id"].to_numpy(),
}
)
.groupby("spm", sort=False)
.household.first()
)
hb, hh = load(base, "household"), load(head, "household")
sb, sh = load(base, "spm_unit"), load(head, "spm_unit")
for left, right, key in ((hb, hh, "household_id"), (sb, sh, "spm_unit_id")):
assert np.array_equal(left[key].to_numpy(), right[key].to_numpy())
assert np.array_equal(left["weight"].to_numpy(), right["weight"].to_numpy())
household_ids = hb["household_id"].to_numpy()
hw = hb["weight"].to_numpy(dtype=float)
sw = sb["weight"].to_numpy(dtype=float)
h_state = household_state.reindex(household_ids).to_numpy()
s_household = spm_household.reindex(sb["spm_unit_id"].to_numpy()).to_numpy()
s_state = household_state.reindex(s_household).to_numpy()
assert not pd.isna(h_state).any() and not pd.isna(s_state).any()
h_code = np.array([FIPS[int(f)] for f in h_state])
s_code = np.array([FIPS[int(f)] for f in s_state])
household_position = pd.Series(np.arange(len(household_ids)), index=household_ids)
s_position = household_position.reindex(s_household).to_numpy()
# Each SPM unit's weight equals its household's weight?
weights_match = bool(np.array_equal(sw, hw[s_position]))
delta = {
name: hh[name].to_numpy(dtype=float) - hb[name].to_numpy(dtype=float)
for name in hb.columns.drop(["household_id", "weight"])
}
d_hsb = delta["household_state_benefits"]
consistency = {
"max_abs(d_household_benefits - d_household_state_benefits)": float(
np.abs(delta["household_benefits"] - d_hsb).max()
),
"max_abs(d_household_net_income - d_household_benefits)": float(
np.abs(delta["household_net_income"] - delta["household_benefits"]).max()
),
"households_where_net_income_moves_without_state_benefits": int(
((np.abs(delta["household_net_income"]) > TOL) & (np.abs(d_hsb) <= TOL)).sum()
),
"households_where_state_benefits_move_without_net_income": int(
((np.abs(d_hsb) > TOL) & (np.abs(delta["household_net_income"]) <= TOL)).sum()
),
}
spm_records_changed = {
name: int((np.abs(sh[name].to_numpy(dtype=float) - sb[name].to_numpy(dtype=float)) > TOL).sum())
for name in sb.columns.drop(["spm_unit_id", "weight"])
}
ccs = sb["child_care_subsidies"].to_numpy(dtype=float)
bill = sb["spm_unit_pre_subsidy_childcare_expenses"].to_numpy(dtype=float)
listed = np.isin(s_code, base_ccs_states)
# The head counts min(subsidy, bill) when the bill is positive, else the whole
# subsidy; the base counted the whole subsidy in listed states, none elsewhere.
# The head's bill is the person-level sum (the unit's bill split across its
# children, plus after-school expenses), which equals this SPM-unit bill in a
# unit with children and no after-school expenses.
counted_head = np.where(bill > 0, np.minimum(ccs, bill), ccs)
counted_base = np.where(listed, ccs, 0.0)
expected_spm = counted_head - counted_base
expected_household = np.bincount(s_position, weights=expected_spm, minlength=len(household_ids))
deviation = d_hsb - expected_household
mechanism = {
"max_abs(actual - expected) per household": float(np.abs(deviation).max()),
"households_off_by_a_cent_or_more": int((np.abs(deviation) >= CENT).sum()),
"weighted_sum(actual - expected)": float((deviation * hw).sum()),
}
rows = []
for code in sorted(set(h_code.tolist())):
hm = h_code == code
sm = s_code == code
d = d_hsb[hm]
full = float((ccs[sm] * sw[sm]).sum())
change = float((d * hw[hm]).sum())
was_listed = code in base_ccs_states
above_spm = np.where(bill[sm] > 0, np.maximum(ccs[sm] - bill[sm], 0), 0)
rows.append(
{
"state": code,
"base_listed_2026": was_listed,
"child_care_subsidies": full,
"change_household_state_benefits": change,
"change_household_benefits": float((delta["household_benefits"][hm] * hw[hm]).sum()),
"change_household_net_income": float((delta["household_net_income"][hm] * hw[hm]).sum()),
"households_changed": int((np.abs(d) > TOL).sum()),
"households_changed_cent_or_more": int((np.abs(d) >= CENT).sum()),
"households_changed_sub_cent": int(((np.abs(d) > TOL) & (np.abs(d) < CENT)).sum()),
"largest_sub_cent": float(np.abs(d[(np.abs(d) > TOL) & (np.abs(d) < CENT)]).max()) if ((np.abs(d) > TOL) & (np.abs(d) < CENT)).any() else 0.0,
"weighted_households_up": float(hw[hm][d > TOL].sum()),
"weighted_households_down": float(hw[hm][d < -TOL].sum()),
# Previously listed: the change is minus the amount above the bill.
# Previously omitted: the change is the subsidy up to the bill.
"amount_above_bill": -change if was_listed else full - change,
"amount_above_bill_from_spm_bill": float((above_spm * sw[sm]).sum()),
"spm_units_with_subsidy": int((ccs[sm] > 0).sum()),
"weighted_spm_units_with_subsidy": float(sw[sm][ccs[sm] > 0].sum()),
"spm_units_subsidy_above_positive_bill": int(((bill[sm] > 0) & (ccs[sm] - bill[sm] >= CENT)).sum()),
"weighted_spm_units_subsidy_above_positive_bill": float(sw[sm][(bill[sm] > 0) & (ccs[sm] - bill[sm] >= CENT)].sum()),
"spm_units_subsidy_no_bill": int(((ccs[sm] > 0) & (bill[sm] <= 0)).sum()),
"weighted_spm_units_subsidy_no_bill": float(sw[sm][(ccs[sm] > 0) & (bill[sm] <= 0)].sum()),
}
)
for row in rows:
sm = s_code == row["state"]
excess = np.where(bill[sm] > 0, np.maximum(ccs[sm] - bill[sm], 0), 0)
row["spm_units_any_excess"] = int((excess > 0).sum())
row["largest_spm_unit_excess"] = float(excess.max()) if sm.any() else 0.0
row["spm_units_subsidy_equals_bill_within_cent"] = int(((ccs[sm] > 0) & (np.abs(ccs[sm] - bill[sm]) < CENT)).sum())
# Size of each household variable's changes, against float32 spacing.
by_size = {}
for name, d in delta.items():
stored_base = hb[name].to_numpy()
stored_head = hh[name].to_numpy()
cent = np.abs(d) >= CENT
sub = (np.abs(d) > TOL) & ~cent
spacing = np.spacing(np.maximum(np.abs(stored_base), np.abs(stored_head)).astype(np.float32)).astype(float)
steps = np.abs(d[sub]) / spacing[sub]
by_size[name] = {
"stored_dtype": str(stored_base.dtype),
"households_cent_or_more": int(cent.sum()),
"households_cent_or_more_down": int((d[cent] < 0).sum()),
"weighted_change_cent_or_more": float((d[cent] * hw[cent]).sum()),
"households_sub_cent": int(sub.sum()),
"sub_cent_states": sorted(set(h_code[sub].tolist())),
"largest_sub_cent": float(np.abs(d[sub]).max()) if sub.any() else 0.0,
"sub_cent_float32_steps": sorted(set(np.round(steps, 6).tolist())),
"weighted_change_sub_cent": float((d[sub] * hw[sub]).sum()),
"weighted_households_sub_cent": float(hw[sub].sum()),
}
real = np.abs(d_hsb) >= CENT
gap = np.abs(delta["household_net_income"][real] - d_hsb[real])
at = np.argmax(gap)
net_income_at = float(hb["household_net_income"].to_numpy()[real][at])
net_income_head_at = float(hh["household_net_income"].to_numpy()[real][at])
by_size["net_income_vs_state_benefits_where_cent_or_more"] = {
"households": int(real.sum()),
"max_abs_gap": float(gap.max()),
"household_id_at_max_gap": int(household_ids[real][at]),
"net_income_at_max_gap": net_income_at,
"float32_spacing_there": float(np.spacing(np.float32(net_income_at))),
"head_net_income_at_max_gap": net_income_head_at,
"head_float32_spacing_there": float(np.spacing(np.float32(net_income_head_at))),
}
vt = next(r for r in rows if r["state"] == "VT")
vt_mask = s_code == "VT"
vt_records = pd.DataFrame(
{
"spm_unit_id": sb["spm_unit_id"].to_numpy()[vt_mask & (ccs > 0)],
"subsidy": ccs[vt_mask & (ccs > 0)],
"bill": bill[vt_mask & (ccs > 0)],
"weight": sw[vt_mask & (ccs > 0)],
}
)
vt_records["above_bill"] = np.where(
vt_records.bill > 0, np.maximum(vt_records.subsidy - vt_records.bill, 0), 0
)
vt_records["weighted_above_bill"] = vt_records.above_bill * vt_records.weight
vt_records = vt_records.sort_values("weighted_above_bill", ascending=False)
vt_full = float((sb["vt_child_care_subsidies"].to_numpy(dtype=float)[vt_mask] * sw[vt_mask]).sum())
vt_elsewhere = float(np.abs(sb["vt_child_care_subsidies"].to_numpy(dtype=float)[~vt_mask]).sum())
result = {
"year_block": YEAR_BLOCK,
"base_listed_child_care_states_2026": base_ccs_states,
"list_check": list_check,
"spm_weight_equals_household_weight": weights_match,
"consistency": consistency,
"mechanism_check": mechanism,
"spm_unit_records_changed": spm_records_changed,
"national": {
"child_care_subsidies": float((ccs * sw).sum()),
"change_household_state_benefits": float((d_hsb * hw).sum()),
"amount_above_bill": float(sum(r["amount_above_bill"] for r in rows)),
"amount_above_bill_from_spm_bill": float(sum(r["amount_above_bill_from_spm_bill"] for r in rows)),
},
"vermont": {
"vt_child_care_subsidies": vt_full,
"vt_child_care_subsidies_outside_vermont_abs": vt_elsewhere,
"child_care_subsidies_in_vermont": vt["child_care_subsidies"],
"added_to_household_benefits": vt["change_household_state_benefits"],
"amount_above_bill": vt_full - vt["change_household_state_benefits"],
"amount_above_bill_from_spm_bill": vt["amount_above_bill_from_spm_bill"],
"spm_units_with_subsidy": vt["spm_units_with_subsidy"],
"weighted_spm_units_with_subsidy": vt["weighted_spm_units_with_subsidy"],
"spm_units_subsidy_above_positive_bill": vt["spm_units_subsidy_above_positive_bill"],
"weighted_spm_units_subsidy_above_positive_bill": vt["weighted_spm_units_subsidy_above_positive_bill"],
"spm_units_subsidy_no_bill": vt["spm_units_subsidy_no_bill"],
"weighted_spm_units_subsidy_no_bill": vt["weighted_spm_units_subsidy_no_bill"],
"records": vt_records.to_dict(orient="records"),
},
"by_size": by_size,
"states": rows,
}
out.write_text(json.dumps(result, indent=1))
moving = [r for r in rows if r["households_changed"]]
print(
json.dumps(
{
"list_check": list_check,
"weights_match": weights_match,
"consistency": consistency,
"mechanism_check": mechanism,
"vermont": result["vermont"],
"moving_states": [
(r["state"], round(r["change_household_state_benefits"], 2), r["households_changed_cent_or_more"], r["households_changed_sub_cent"])
for r in moving
],
},
indent=1,
)
)compare.json{
"household_state_benefits": {
"entity": "household",
"base_sum": 8201643950.43103,
"head_sum": 9036055227.81625,
"change": 834411277.3852196,
"records_changed": 163,
"weighted_changed": 215540.7354988549,
"weighted_up": 207250.77999245364,
"weighted_down": 8289.955506401253,
"largest_abs": 20176.0,
"rows": [
{
"id": 1001496,
"base": 0.0,
"head": 20176.0
},
{
"id": 56848,
"base": 0.0,
"head": 18975.783203125
},
{
"id": 1115989,
"base": 0.0,
"head": 16737.326171875
},
{
"id": 1664,
"base": 0.0,
"head": 15288.0
},
{
"id": 3590,
"base": 2368.199951171875,
"head": 17437.783203125
}
]
},
"equiv_household_net_income": {
"entity": "household",
"base_sum": 10834823468521.645,
"head_sum": 10835263037428.23,
"change": 439568906.5859375,
"records_changed": 147,
"weighted_changed": 209135.37153056182,
"weighted_up": 207250.77999245364,
"weighted_down": 1884.5915381081868,
"largest_abs": 9022.98046875,
"rows": [
{
"id": 1001496,
"base": 65052.94140625,
"head": 74075.921875
},
{
"id": 723,
"base": 22973.876953125,
"head": 31223.115234375
},
{
"id": 1664,
"base": 42014.109375,
"head": 49658.109375
},
{
"id": 1110560,
"base": 36983.69140625,
"head": 44401.98828125
},
{
"id": 56848,
"base": 59436.4765625,
"head": 66608.6484375
}
]
},
"household_net_income": {
"entity": "household",
"base_sum": 17161428039502.375,
"head_sum": 17162262450796.174,
"change": 834411293.7988281,
"records_changed": 147,
"weighted_changed": 209135.37153056182,
"weighted_up": 207250.77999245364,
"weighted_down": 1884.5915381081868,
"largest_abs": 20176.0,
"rows": [
{
"id": 1001496,
"base": 145462.796875,
"head": 165638.796875
},
{
"id": 56848,
"base": 157254.140625,
"head": 176229.921875
},
{
"id": 1115989,
"base": 102615.234375,
"head": 119352.5625
},
{
"id": 1664,
"base": 84028.21875,
"head": 99316.21875
},
{
"id": 3590,
"base": 64153.34765625,
"head": 79222.9296875
}
]
},
"household_benefits": {
"entity": "household",
"base_sum": 2059793926050.1262,
"head_sum": 2060628337292.8345,
"change": 834411242.708252,
"records_changed": 151,
"weighted_changed": 209135.64504279534,
"weighted_up": 207250.77999245364,
"weighted_down": 1884.8650503417011,
"largest_abs": 20175.999267578125,
"rows": [
{
"id": 1001496,
"base": 2239.199951171875,
"head": 22415.19921875
},
{
"id": 56848,
"base": 5743.4521484375,
"head": 24719.234375
},
{
"id": 1115989,
"base": 7712.703125,
"head": 24450.029296875
},
{
"id": 1664,
"base": 1843.3277587890625,
"head": 17131.328125
},
{
"id": 3590,
"base": 14287.232421875,
"head": 29356.81640625
}
]
},
"child_care_subsidies": {
"entity": "spm_unit",
"base_sum": 1786291922.607019,
"head_sum": 1786291922.607019,
"change": 0.0,
"records_changed": 0,
"weighted_changed": 0.0,
"weighted_up": 0.0,
"weighted_down": 0.0,
"largest_abs": 0.0,
"rows": []
},
"spm_unit_net_income": {
"entity": "spm_unit",
"base_sum": 14653599190798.559,
"head_sum": 14653599190798.559,
"change": 0.0,
"records_changed": 0,
"weighted_changed": 0.0,
"weighted_up": 0.0,
"weighted_down": 0.0,
"largest_abs": 0.0,
"rows": []
},
"spm_unit_pre_subsidy_childcare_expenses": {
"entity": "spm_unit",
"base_sum": 83421131008.018,
"head_sum": 83421131008.018,
"change": 0.0,
"records_changed": 0,
"weighted_changed": 0.0,
"weighted_up": 0.0,
"weighted_down": 0.0,
"largest_abs": 0.0,
"rows": []
},
"vt_child_care_subsidies": {
"entity": "spm_unit",
"base_sum": 45948444.54842574,
"head_sum": 45948444.54842574,
"change": 0.0,
"records_changed": 0,
"weighted_changed": 0.0,
"weighted_up": 0.0,
"weighted_down": 0.0,
"largest_abs": 0.0,
"rows": []
},
"ct_child_care_subsidies": {
"entity": "spm_unit",
"base_sum": 405603760.7995658,
"head_sum": 405603760.7995658,
"change": 0.0,
"records_changed": 0,
"weighted_changed": 0.0,
"weighted_up": 0.0,
"weighted_down": 0.0,
"largest_abs": 0.0,
"rows": []
},
"ma_child_care_subsidies": {
"entity": "spm_unit",
"base_sum": 0.0,
"head_sum": 0.0,
"change": 0.0,
"records_changed": 0,
"weighted_changed": 0.0,
"weighted_up": 0.0,
"weighted_down": 0.0,
"largest_abs": 0.0,
"rows": []
},
"ca_child_care_subsidies": {
"entity": "spm_unit",
"base_sum": 0.0,
"head_sum": 0.0,
"change": 0.0,
"records_changed": 0,
"weighted_changed": 0.0,
"weighted_up": 0.0,
"weighted_down": 0.0,
"largest_abs": 0.0,
"rows": []
},
"ny_child_care_subsidies": {
"entity": "spm_unit",
"base_sum": 0.0,
"head_sum": 0.0,
"change": 0.0,
"records_changed": 0,
"weighted_changed": 0.0,
"weighted_up": 0.0,
"weighted_down": 0.0,
"largest_abs": 0.0,
"rows": []
},
"income_tax": {
"entity": "tax_unit",
"base_sum": 2595105975379.617,
"head_sum": 2595105975379.617,
"change": 0.0,
"records_changed": 0,
"weighted_changed": 0.0,
"weighted_up": 0.0,
"weighted_down": 0.0,
"largest_abs": 0.0,
"rows": []
},
"state_income_tax": {
"entity": "tax_unit",
"base_sum": 578044599361.5671,
"head_sum": 578044599361.5671,
"change": 0.0,
"records_changed": 0,
"weighted_changed": 0.0,
"weighted_up": 0.0,
"weighted_down": 0.0,
"largest_abs": 0.0,
"rows": []
}
}Generated by Claude Code |
|
Hub review in progress Reviewing head
Generated by Claude Code |
|
Ready for hub review This is round 3, a delta review of
Since round 2 the body has also gained the hub's landing pair, round 2's P2 sentence and round 2's axiom wording. Round 2's P1 is fixed: every Colorado value I ran equals main's, and the new YAML case fails at FindingsP3. The cap property never sets after-school costs, so one YAML case is the only guard on this round's fixWhere: The module docstring's invariant 3 now says the cap uses "the family's pre-subsidy bill for child care and after-school care". But
Fix. Draw how much of the bill is after-school care. This adds no simulations: -def counted_subsidy(state, year, program, amount, bill):
+def counted_subsidy(state, year, program, amount, bill, after_school=0):
members = ["parent", "child"]
spm_unit = {
"members": members,
program: {year: amount},
- "spm_unit_pre_subsidy_childcare_expenses": {year: bill},
+ "spm_unit_pre_subsidy_childcare_expenses": {year: bill - after_school},
}
+ child = {"age": {year: 3}, "after_school_expenses": {year: after_school}}
situation = {
- "people": {"parent": {"age": {year: 30}}, "child": {"age": {year: 3}}},
+ "people": {"parent": {"age": {year: 30}}, "child": child},
@@
-@example(year=2025, index=0, amount=5_000, bill=0, extra=1)
-@example(year=2025, index=0, amount=5_000, bill=5_000, extra=1)
-@example(year=2025, index=0, amount=5_001, bill=5_000, extra=1)
+@example(year=2025, index=0, amount=5_000, bill=0, extra=1, after_school_share=0)
+@example(year=2025, index=0, amount=5_000, bill=5_000, extra=1, after_school_share=0)
+@example(year=2025, index=0, amount=5_001, bill=5_000, extra=1, after_school_share=0)
+# Half the bill is after-school care, and then all of it.
+@example(year=2025, index=0, amount=5_000, bill=5_000, extra=1, after_school_share=50)
+@example(year=2025, index=0, amount=5_001, bill=5_000, extra=1, after_school_share=100)
@given(
@@
extra=st.integers(min_value=1, max_value=30_000),
+ after_school_share=st.integers(min_value=0, max_value=100),
)
def test_household_benefits_count_the_subsidy_up_to_the_bill(
- year, index, amount, bill, extra
+ year, index, amount, bill, extra, after_school_share
):
programs = programs_in(year)
program = programs[index % len(programs)]
state = program[:2].upper()
- counted = counted_subsidy(state, year, program, amount, bill)
+ after_school = bill * after_school_share // 100
+ counted = counted_subsidy(state, year, program, amount, bill, after_school)
@@
if bill > 0:
- bigger = counted_subsidy(state, year, program, amount, bill + extra)
+ bigger = counted_subsidy(
+ state, year, program, amount, bill + extra, after_school
+ )
assert bigger >= counted - TOLERANCEI applied this on a scratch copy and ran
P3. After-school costs now open or raise the cap for a subsidy that doesn't pay for them, and no test pins which reading holdsWhere: The bill adds
Values are
Neither row falls below main, which leaves Vermont out. The documentation states the head's reading ("up to the family's pre-subsidy bill for child care and after-school care"), so the code matches its docs. My sweeps found Vermont the only state where this binds. The pair says the population file doesn't store This is the hub's call:
I'd take option 1. It is the documented rule, it never counts less than main, and option 2 puts state-specific logic in a federal variable for a case the population data doesn't carry. Mutant (k) switches on the child care bill alone. That is a halfway reading I wouldn't pick: it brings back round 2's 17,628 in row 2 but leaves row 1 at 7,000. P3. The body's test-cost paragraph predates the three
|
| State | Subsidy | household_state_benefits |
household_net_income |
|---|---|---|---|
| CT | 11,250 | 0 | 33,222.62 |
| UT | 10,584 | 0 | 31,183.89 |
| CO (already listed) | 11,249.52 | 11,249.52 | 48,208.29 |
AK, ak_child_care_subsidies set to 4,800 |
4,800 | 0 | 5,548.00 |
At head, CT gives 11,250 and 44,472.62, UT 10,584 and 41,767.89, AK 4,800 and 10,348.00, and CO doesn't change. spm_unit_net_income doesn't move (CT 21,222.62, UT 19,183.89 at both).
Round 2's P1: the new YAML case. I ran the head's household_state_benefits.yaml against 9453494314's code. It gives 1 failed, 15 passed, the Colorado case: household_child_care_subsidies@2025: [6000.] differs from 12000.0. At head the file gives 16 passed.
Check 1: round 2's Colorado witness. The family is 2025 Colorado: a parent aged 30 with $25,000 of earnings and 40 work hours, and a child aged 3, a dependent, with 20 attending days a month. Round 2's table reproduces to the cent when the child's childcare_hours_per_week is set to 40 directly, which gives a parent fee of 0.
| Case | co_child_care_subsidies |
household_state_benefits |
spm_unit_benefits |
household_net_income |
|---|---|---|---|---|
| bill 6,000, after-school 6,000 | 12,000.00 | 12,000.00 (r2: 6,000.00) | 13,909.06 | 48,662.02 (r2: 42,662.02) |
| bill 1, after-school 6,000 | 6,001.00 | 6,001.00 (r2: 1.00) | 7,910.06 | 42,663.02 (r2: 36,663.02) |
| no bill, after-school 6,000 | 6,000.00 | 6,000.00 | 7,909.06 | 42,662.02 |
| bill 12,000, no after-school | 12,000.00 | 12,000.00 | 13,909.06 | 48,662.02 |
| bill 6,000, no after-school | 6,000.00 | 6,000.00 | 7,909.06 | 42,662.02 |
| no bill, no after-school | 0.00 | 0.00 | 1,909.06 | 36,662.02 |
- These are head's values. The merge base
0d933918a8and current maindb83eb5ab6give the same in every cell.9453494314differs only where the table shows "r2". - I also set the care as 5 days × 8 hours a day, which gives a parent fee of $750.48 a year. In all six cases,
household_state_benefitsandhousehold_net_incomeagain equal main's at head and at main merged with head (b8515cd2b4, a local merge only). For example, a 6,000 bill plus 6,000 after-school gives 11,249.52 and 47,911.54 there, against 6,000.00 and 42,662.02 at9453494314.
Hand values, computed from the parameters before I read the cases.
- Colorado's parent fee. The 2025 FPG for two is $15,650 + $5,500 = $21,150 (
gov/hhs/fpg.yaml). Monthly income of $2,083.33 is 1.1820 times the monthly FPG of $1,762.50. The base fee rate is 1% up to 1.0 and 14% above (parent_fee/base.yaml), so the fee is (0.01 + 0.14 × 0.1820) × $1,762.50 = $62.54 a month. At 8 hours a day the hours factor is 1 (parent_fee_rate_by_child_care_hours.yaml: 5 < 8 ≤ 12). That makes $750.48 a year, and the subsidy is $11,249.52 for any $12,000 split. The model agrees. - The cap. min(11,249.52, 6,000 + 6,000) = 11,249.52; min(5,250.52, 1 + 6,000) = 5,250.52; min(5,249.52, 6,000) = 5,249.52. At
9453494314: min(11,249.52, 6,000) = 6,000 and min(5,250.52, 1) = 1. - The new YAML case. min(12,000, 6,000 + 6,000) = 12,000. At
9453494314it is min(12,000, 6,000) = 6,000, the failure above. - Vermont. Age 3 is 36 months, so PRESCHOOL. 40 hours is FULL_TIME (25 < 40 ≤ 50). The licensed-center rate has been $439 a week since 2024 (
rates/licensed_center.yaml). $52,875 / $21,150 = 2.50 falls in the 2.2501 band, a $100 weekly share (family_share/scale.yaml). ($439 − $100) × 52 = $17,628, and each Vermont expectation is min(17,628, bill), or 17,628 with no bill.
The rule in every year and entity. The formula has no date logic, so it applies in every year the four list blocks govern (2021 to 2027). The bill sums two things over the SPM unit's own members: pre_subsidy_childcare_expenses (which spreads the SPM input over the unit's children) and after_school_expenses (a person input with no formula). One unit's bill therefore never caps another unit's subsidy.
Check 2: the other readers of the bill. The PR changes no bill variable.
childcare_expenses.py:12-16still readsadd(spm_unit, period, ["pre_subsidy_childcare_expenses"])alone.git diff 0d933918a8 8ce831b6dc -- policyengine_us/variables/householdis empty.after_school_expensesgains one reader, this variable. Its only other reader isco_ccap_subsidy.spm_unit_capped_work_childcare_expensesreadsspm_unit_pre_subsidy_childcare_expensesand is unchanged.
Can adding after-school costs lower a counted subsidy below main? Not for any state in anything I ran. The bill only grows, but the zero-bill boundary can still lower Vermont's count below round 2's head (not below main):
- With a positive child care bill, adding after-school costs only raises the cap, so the count can't fall below round 2's either.
- With no child care bill, a positive after-school cost moves the unit from the uncapped branch to the cap (the second P3).
- I swept 51 states × 2023–2026 × a child aged 3 or 7 (408 households a run). Each family has a parent with $25,000, 5 × 8 hours of care and the activity test met.
- No child care bill, $1,000 of after-school costs. Only Colorado and Vermont pay. Colorado pays the after-school cost less the fee (63.64 to 313.24), which is under the bill. No household's
household_state_benefitsfalls below the merge base's. Head differs from9453494314only in Vermont: 1,000 against 14,300 to 22,828. - A $1,000 child care bill plus $1,000 of after-school costs. 34 states pay. Only Vermont, in 2024 to 2026, pays above the $2,000 bill (22,828 at age 3, 19,292 at age 7), and the cap counts 2,000. No household falls below the merge base.
- No child care bill, $1,000 of after-school costs. Only Colorado and Vermont pay. Colorado pays the after-school cost less the fee (63.64 to 313.24), which is under the bill. No household's
Check 3: round 2's two follow-ups. Neither blocks landing.
CAPPED_AGGREGATESstill checks a mapping, not the formula. Mutant (h) survives every file but changes no behavior. Mutant (i), whose formula readssnap, still gets past the gate test, though 11 YAML cases fail. It's harmless today.childcare_expensesstill ignores after-school costs, the same as on main. For the fee-paying Colorado family, a $12,000 child care bill giveschildcare_expenses750.48. A $6,000 bill plus $6,000 of after-school costs gives 0, at head and at main. This PR doesn't change it.
Check 4: tests at head, one file per process, local:
household_state_benefits.yaml: 16 passed.vt/dcf/ccfap/integration.yaml: 13 passed.vt/dcf/ccfap/vt_ccfap.yaml: 17 passed.test_child_care_subsidies_household_benefits.py: 7 passed (57.9s in pytest, 122.1s wall, 2.06 GiB peak RSS).test_aggregate_list_integrity.py: 29 passed.- Colorado CCAP, every file under
gov/states/co/ccap/(14 files, of whichco_ccap_subsidy.yamlsetsafter_school_expenses): 65 passed across the 14 files, none failed (co_ccap_subsidy.yaml: 2 passed). household/expense/childcare/childcare_expenses.yaml: 2 passed.- Other readers of
household_state_benefitsthat use child care cases:mt/dphhs/ccap/integration.yaml10 passed,nv/dwss/ccdp/integration.yaml9 passed,household_benefits.yaml7 passed andhousehold_net_income.yaml6 passed. - Main has moved to
db83eb5ab6, which includes a refactor of Colorado CCAP's parameter instants. I merged the head into it locally (git merge-treeexits 0). On that tree,household_state_benefits.yamlgives 16 passed,test_child_care_subsidies_household_benefits.py7 passed andtest_aggregate_list_integrity.py29 passed.
My venv first lacked hypothesis, because uv sync skips the dev extra. I installed it with uv sync --extra dev and reran the Python files.
Check 5: CI at 8ce831b6dc. The Pull request run 41577574038 concluded success on attempt 1, with all 34 jobs success, none cancelled or skipped. They include Household API Partners, PR validation and every Baseline, Contrib and Python shard. The shard plan put both changed Python test files in shard 5's remaining-1 process, which reported 1389 passed, 1 skipped.
Mutants. Each ran on a scratch worktree at 8ce831b6dc, one process per test file. I restored the worktree with git checkout -- . after each one, and git status --short was empty every time. The main checkout stayed clean.
| Mutant | household_state_benefits.yaml |
test_child_care_subsidies_household_benefits.py |
test_aggregate_list_integrity.py |
|---|---|---|---|
(a) return subsidies |
3 failed: Vermont at 10,000, at 1,000, bill on the child | 1 failed: cap property, explicit example (5,001 against 5,000) | 29 passed |
(b) min_ with no where |
5 failed: NC 2024, DC 2025, Alaska, NC once, Vermont with no bill | 2 failed: dollar property; cap property, explicit example bill=0 |
29 passed |
(c) max_ for min_ |
5 failed: Vermont at 10,000, 1,000 and 20,000, bill on the child; Connecticut | 1 failed: cap property, explicit example (5,001 against 5,000) | 29 passed |
(d) child_care_subsidies back in the 2026 block |
16 passed | 2 failed: test_each_block_counts_only_the_aggregate[2026-01-01]; the dollar property (it drew 2026) |
29 passed |
(e) ct_child_care_subsidies out of all 3 CCDF blocks |
1 failed: Connecticut (child_care_subsidies 0 against 5,000) |
1 failed: test_latest_blocks_list_every_state_program |
29 passed |
| (f) SPM input for the child care bill | 1 failed: Vermont, bill on the child (17,628 against 10,000) | 7 passed | 29 passed |
| (g) no bill counts 0 (round 2's) | 5 failed, as (b) | 2 failed: dollar property; cap property, bill=0 |
29 passed |
(h) CAPPED_AGGREGATES maps to {"de_ssp"} (round 2's) |
16 passed | 7 passed | 29 passed: survives |
(i) formula reads snap (round 2's) |
11 failed | 2 failed | 29 passed |
(j) bill without after_school_expenses (mine) |
1 failed: Colorado | 7 passed | 29 passed |
| (k) switch on the child care bill alone (mine) | 16 passed | 7 passed | 29 passed: survives |
| (l) after-school costs alone as the bill (mine) | 4 failed: Vermont at 10,000 and 1,000, bill on the child; Colorado | 1 failed: cap property (5,001 against 5,000) | 29 passed |
Round 2's boundary fix holds: (a), (b), (c), (g) and (l) now fail the cap property through its explicit examples, not by chance draws. (b), the brief's check, fails on bill=0 with assert 0.0 == 5000 ± 0.02.
Readers, unchanged from round 2's list and checked with grep at head:
household_child_care_subsidiesfeeds the fourhousehold_state_benefitsblocks.- Those feed
household_benefits, the Tlaib ECPA, BOOST and EDAA copies of it, andtx_rebate's 2027 copy of the list. household_benefitsfeedshousehold_net_income.household_net_incomefeedsequiv_household_net_income,household_income_decile,household_net_income_including_health_benefits,marginal_tax_rate,behavioral_response_measurementsandcapital_gains_responses.
No tax or benefit formula reads them, and spm_unit_net_income doesn't either.
Body and docs. I checked each sentence against the code and the pair comment.
-
Problem. "23 of the 51" holds for main's 2025 and 2026 blocks. Main's 2023 block lists 16 and its 2024 block 22.
-
Fix. Every statement about the code holds. I couldn't fetch the source of the Vermont quotes (see below). "Both data builds fill the bill from parent-paid ASEC child care" rests on Count state child care subsidies in the SPM once #10569's body, which names the two build files; I can't read those repositories. "Only Vermont paid above the bill" agrees with my two sweeps for 2023–2026.
-
Limit. $100 × 52 = $5,200, and 10,000 − 5,200 = 4,800.
-
Tests. Every case listed exists and passes, and each expected value matches my hand values.
- "Each of the 51 programs in the 2025 CCDF list reaches
child_care_subsidiesandhousehold_state_benefitsonce": at head, I set each program in each CCDF block (46 for 2023, 48 for 2024, 51 for 2025 and 2026) to a distinct amount on its own household with no bill. Each gives exactly that amount inchild_care_subsidies,household_child_care_subsidiesandhousehold_state_benefits.
- "Each of the 51 programs in the 2025 CCDF list reaches
-
SPM side.
spm_unit_benefits.py:61,65,73listsco_ccap_subsidy,wa_child_care_subsidiesandwv_child_care_subsidiesand no other subsidy.spm_unit_capped_work_childcare_expensesstarts fromspm_unit_pre_subsidy_childcare_expenses. -
Invariants. Each bullet matches a test. The cap bullet's "15 sampled cases" plus three explicit examples is right.
-
Impact. Every figure matches the pair comment:
- $834M (
household_benefits+834,411,242.71;household_net_income+834,411,293.80); - the four national rows;
- all ten state rows, ordered by the amount added;
- Vermont's 12 of 17 records and 1,494 of 5,124 weighted units;
- $16.16M, which is 35.2% of $45.95M;
- the 1,306-weight record, $10,608 against $1,054 and $12.48M;
- "no household's benefits fall by a cent or more".
Two figures come from Pay Vermont and New Hampshire child care subsidies only for care #10584's body, not the pair: "an effective sample of 156 households" and "$141.6M … $172.6M", which the body rounds to $142M to $173M.
- $834M (
-
"
household_net_incomecalculates in all 51 jurisdictions for each year from 2021 to 2027." At head I ran one simulation a year holding 102 households: 50 states and DC, each with one family in care with a $12,000 bill and one family without care. Every year gave finitehousehold_net_incomeand no error, and no state paid a subsidy without care. -
Lines. The
impact:andaxiom:lines exist. The axiom line uses round 2's wording and still fits. -
Changelog.
changelog.d/9405.fixed.mdis top-level, has a valid type and describes the change. -
Docs. The variable's documentation, the module docstring and the formula comment all match the code.
Baseline movement and the population check. This round's delta moves household_child_care_subsidies, and through it household_state_benefits, household_benefits, household_net_income and their readers, only for units with after_school_expenses above 0.
- Colorado. It restores main's values.
- Vermont. See the second P3.
The pair at 8ce831b6dc already measures the whole PR. The population check this delta needs is that the file stores no after_school_expenses. The pair says it doesn't, so the delta moves nothing there.
The author's brief. The PR does everything it asked for. It departs from the brief in two places, and both are justified:
- the reference is one href with a
# PDF pages 1-2comment, CLAUDE.md's rule for variable tuples; - the bill now adds after-school costs, round 2's P1.
The hub later replaced the body text the brief prescribed.
Scope. The PR changes six files, none under .github/ or tests/policy/baseline/partners/. ruff format --check and ruff check pass on its three Python files.
Overlap. I searched open PRs for the changed names.
- Count Illinois AABD cash in household and SPM benefits #10086 changes
household_state_benefits.yaml. Main merges it cleanly, but main merged with this head conflicts with it in that file (git merge-treeexits 1), as round 1 found. - Count state child care subsidies in the SPM once #10569 (head
a8b9e1b60a) merges cleanly with this PR. Itssubsidy_counted_by_household_measure(tests/test_spm_child_care_subsidy_invariants.py:326-335), though, still looks forchild_care_subsidiesor the state's own variable. After this PR the list holds neither, so whichever PR lands second has to make that helper readhousehold_child_care_subsidies. - Fix four statute-first benefit bugs: MO TANF rounding, SNAP GU/VI limits, HI CCAP TANF income, SSI food ISM date #10332, Guard county-keyed lookups in VT renter credit, MN CCAP, WI Shares and MD CCS #10116, Apply 416.1163(e)(2) to deemed SSI and identify SSI couples by marriage, not tax roles #10141 and Deem SSI parental income from the child's co-resident parents, not the tax unit #10159 matched only in their bodies and change none of this PR's files.
What I couldn't check
-
Sources. The session's egress proxy refused every source host I tried, each with HTTP 403 on CONNECT:
outside.vermont.gov(both Vermont PDFs) anddcf.vermont.gov;legislature.vermont.gov;www.sos.state.co.us(Colorado's 8 CCR 1403-1);www.govinfo.gov,www.ecfr.govanduscode.house.gov;web.archive.organdwww.law.cornell.edu.
I confirmed no source, so I can't say whether the body misreads one. The two Vermont quotes and "which providers may waive" still rest on the hub's ruling. The Vermont and Colorado values above come from the parameter files, not from the agencies' documents.
-
Population data. I ran no microsimulation. Two claims rest on others' runs: that the file stores no
after_school_expenses(the pair), and that both data builds fill the bill from ASEC spending (Count state child care subsidies in the SPM once #10569's body). -
"Waits on a data decision" (SPM side). Count state child care subsidies in the SPM once #10569's body doesn't mention a hold, and I can't see the hub's hold list.
-
Round 1's $12,000 sweep for 2021 to 2027. I didn't rerun it. My sweeps cover 2023 to 2026, with no child care bill or a $1,000 one, plus after-school costs.
-
CI timing for the changed Python file. My timings are local.
Generated by Claude Code
Fixes #9405.
Problem
household_state_benefitslisted only 23 of the 51 state child care subsidy variables in the CCDF aggregate. Implemented subsidies in the other states therefore never reached household benefits or household net income.Fix
Replace the per-state child care entries in
household_state_benefitswith a new variable,household_child_care_subsidies, in every period. It reads the existingchild_care_subsidiesaggregate, so the CCDF list stays the single source of truth and no subsidy counts twice. It counts each family's subsidy up to its pre-subsidy bill for child care and after-school care, since Colorado pays its subsidy for both. Vermont pays the state rate "regardless of the provider's established rates" and pays it "directly to the family's child care provider(s)" (CCFAP Understanding Payments), so the amount above the bill doesn't reach the family. With no bill recorded, the whole subsidy counts.child_care_subsidiesitself stays the full payment. When we ran one family in every state (a $1,000 bill for 2024 to 2026, a $12,000 bill for 2021 to 2027), only Vermont paid above the bill. Both data builds fill the bill from parent-paid ASEC child care (see the SPM section below). So in a microsimulation, Vermont's payment counts up to what parents reported paying, which can fall short of the provider's charge.We don't subtract Vermont's family share, which providers may waive, so a family that pays the share gains up to that much less than we count. In the $10,000 case below, we count $10,000, and that family's $5,200 share would leave it $4,800 better off.
Tests
Added an Alaska regression showing a previously omitted subsidy reaches household state benefits.
Added a North Carolina regression showing that household state benefits count a previously direct-listed subsidy once.
Verified that each of the 51 programs in the 2025 CCDF list reaches
child_care_subsidiesandhousehold_state_benefitsonce.Vermont cases for a 2025 family (parent with $52,875 of earnings, child aged 3 at a licensed center 5 days a week, 8 hours a day).
child_care_subsidiescomes to $17,628 in each, from ($439 − $100) × 52:spm_unit_pre_subsidy_childcare_expenseshousehold_child_care_subsidieshousehold_state_benefitspre_subsidy_childcare_expenses10,000)A Connecticut case with
ct_child_care_subsidiesof $5,000 and a $12,000 bill:household_child_care_subsidiesreturns $5,000.A Colorado case with
co_child_care_subsidiesof $12,000, a $6,000 child care bill and $6,000 of after-school expenses:household_child_care_subsidiesreturns $12,000.SPM side
This PR changes household totals only.
spm_unit_benefitsstill lists the subsidy for Colorado, Washington and West Virginia alone, and the SPM's child care subtraction still starts from the pre-subsidy bill. #10569 covers that side and waits on a data decision, because both data builds copy parent-paid ASEC child care into the pre-subsidy input.Invariants
policyengine_us/tests/test_child_care_subsidies_household_benefits.pychecks these for every dated block:household_state_benefitslistshousehold_child_care_subsidiesonce, doesn't listchild_care_subsidies, and no state's own subsidy sits beside it, so no subsidy counts twice.household_benefitsandhousehold_net_income. The block test checks every year structurally.household_child_care_subsidiesnever exceeds the subsidy or a positive bill, equals the subsidy when the inputs hold no bill or the subsidy fits within the bill, and never falls when a positive bill rises. Three explicit examples pin a zero bill, a subsidy equal to the bill, and a subsidy a dollar above it.test_latest_blocks_list_every_state_programchecks that the CCDF list's 2025 and later blocks name every one of the 51*_child_care_subsidiesvariables undervariables/gov/states, so dropping a program from the list fails a test.Impact
We estimate that this PR adds $834 million to household benefits and household net income in 2026 (pair). We simulated the whole Populace US file (
populace-us-2024-spm-20260909) at8ce831b6dcand at its merge base with main,0d933918a8.household_benefitshousehold_net_incomespm_unit_net_incomechild_care_subsidiesHouseholds in the 10 states below gain. Main computes these states' subsidies in
child_care_subsidiesbut leaves them out of household benefits.child_care_subsidiesIn Vermont, 12 of the 17 records with a subsidy (1,494 of 5,124 weighted SPM units) get more than their bill. We leave the $16.16 million above those bills, 35% of the state's $45.95 million, out of household benefits. One of them, with a weight of 1,306, gets $10,608 against a $1,054 bill and accounts for $12.48 million of that. No household's benefits fall by a cent or more.
child_care_subsidiesalready held each dollar, so apart from Vermont's amounts above the bill, this PR only stops dropping it from household totals. State rows rest on few records (Vermont's effective sample comes to 156 households), so they show exposure, not state spending. Vermont's $46M in subsidies falls short of the state's own $142M to $173M, and #10584 explains why.This PR makes
household_net_incomerun every state's subsidy formula. With this PR and #10584 merged,household_net_incomecalculates in all 51 jurisdictions for each year from 2021 to 2027, for a family with care and a family without.test_child_care_subsidies_household_benefits.pyhas 7 tests: the block test (4 cases), the list-completeness test, and two property tests of 15 examples each. The cap property runs 1 simulation per example, or 2 when the bill is positive. On a 4 vCPU Linux cloud container, one process each, the old tests alone (-k "not up_to_the_bill and not latest") took 61.5s in pytest, 106.8s wall with imports, at 2.06 GB peak RSS; the whole file took 67.8s in pytest, 113.6s wall, at 2.19 GB peak. An earlier run of the whole file took 84.0s in pytest, so timings vary by about 20s between runs. We timed these outside CI. The Makefile doesn't name the file, so it runs in the rest Python group. The YAML cases pin single families, while this file reads every dated block of the list and generates programs, years, amounts and bills.impact: we estimate that household benefits and household net income rise $834 million in 2026. The gain goes to households in 10 states whose child care subsidies main leaves out of household benefits. In Vermont, we count $29.8 million of $45.9 million in subsidies, up to each family's bill (pair).
axiom: n/a: household benefit measurement (child care subsidies counted up to the family's bill), no legal provision changes
🤖 Generated with Claude Code