Skip to content

Count each EITC child split once in the Table 2.5 uprating source total - #1227

Merged
MaxGhenis merged 4 commits into
mainfrom
fix-eitc-decomp-total-double-count
Oct 14, 2026
Merged

MaxGhenis merged 4 commits into
mainfrom
fix-eitc-decomp-total-double-count

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

What was wrong

_uprate_cross_period_eitc_decompositions scales the stale SOI Table 2.5 EITC-by-AGI-and-children rows (TY2023) to the TY2024 filing-season EITC by AGI. The factor is (active EITC in the row's AGI group) / (TY2023 EITC in that group, summed over child splits).

Each child split's record set carries 28 AGI bins and an all-AGI total row that sums them. _eitc_source_decomposition_total_for_bounds added every row inside the group. For the all-AGI total rows, that group is (-inf, inf), so the sum took each split's bins and its total row. The source EITC was counted twice and the four splits' total rows shipped at half their value.

Lane D of the CD concept sweep reported this (2026-09-27). It is reproduced here on the pinned feed consumer_facts_us_51aa40f.jsonl (sha256 965a29ac…) with a release-equivalent compile:

  • target_period=2024, the packaged CD crosswalk, age_targets=True and allow_unaged_dollar_targets=False;
  • then the Medicaid substitution and _select_target_surface.
origin/main 51a2637 this PR
Amount total rows' factor 0.520911812001927 1.04182361614341 (= $69,041,650,000 / $66,269,999,000)
Returns total rows' factor 0.487667991438275 0.975335982876551 (= 23,837,149 / 24,439,936)
Sum of the 4 amount total rows $34.52B $69.04B (= their 112 bins = the filing-season AGI bins)
Sum of the 4 returns total rows 11.92M 23.84M (same identity)

The fix

The source sum now groups contained rows by record set, one per child split. Per split it keeps only the outermost contained rows, those not inside another contained row of that split. A split whose kept rows overlap (crossing or duplicated AGI rows) raises ValueError rather than guessing.

Effect on national_state and full targets

All 232 Table 2.5 by-children rows are on national_state, so also on full. A whole-registry differential compiles the pinned feed with and without the fix in one process (differential.json).

  • Rows that move. Exactly 8 targets change: the all-AGI total rows of the 4 splits × {amount, returns}. Each doubles (×2.000). Only their uprating_factor metadata changes with them. No other target's value or metadata moves.
  • National target-loss weights. The weights scale with sqrt(|value|) within each basis, so the 8 rows' weights rise by sqrt(2): ×1.4136 for amount and ×1.4125 for returns. Renormalization moves every other national_state row's weight by at most 0.12%. The Table 2.5 by-children family's share of total loss weight goes from 1.472% to 1.555%.
  • Counts. Target counts are unchanged: 33,250 compiled and 6,102 national_state.
  • Registry versions.
    • national_state: e1f9906bf0e0 → 3fad14f85468, re-pinned in test_pinned_feed_national_state_surface_restores_the_fences.
    • full: f5bd921d6b21 → c3aaea9c1e8c.
  • CD file against the T2.5 total rows. Congressional-district US EITC-by-children rows now read 0.90-1.02 of the T2.5 total rows, against 1.80-2.03 before.
Split Claims, before → after Amount, before → after
Childless 1.994 → 0.997 2.035 → 1.017
One child 1.944 → 0.972 1.834 → 0.917
Two children 1.919 → 0.960 1.804 → 0.902
Three or more 1.943 → 0.971 1.834 → 0.917

Invariants

These hold for every decomposition whose bins tile AGI, whose total rows equal their bins, and whose active filing-season AGI bins coarsen the Table 2.5 bins.

  1. Conservation. The uprated total rows sum to the active EITC, and so do the uprated bins.
  2. The all-AGI group keeps the source child split. Every total row takes active / (source total rows).
  3. Each AGI group keeps its source child split. Every bin takes the group's active / source EITC.
  4. Uniform growth keeps totals equal to their bins. When every group grows by the same factor, each split's total row equals the sum of its uprated bins.

Intended non-identity. With non-uniform growth, a split's total row and the sum of its bins differ by design: the total keeps the all-AGI child mix, while each bin keeps its own group's. On the pinned feed:

Split Amount: total row / bins Returns: total row / bins
Childless 1.157 1.062
One child 1.018 1.001
Two children 0.975 0.961
Three or more 0.993 0.951

Reconciling the two levels (for example, raking the child × AGI matrix to both margins) is a methodology question, not part of this fix. It is proposed as a follow-up.

Tests

  • test_us_eitc_decomposition_uprating.py (new):
    • Example tests for amount and returns, where the total rows double from 400 and 300 to 800 and 600.
    • Refusal tests for crossing rows and for one interval repeated under two ids.
    • Two Hypothesis properties (150 examples each) through compile_us_fiscal_target_registry: invariants 1-3, and invariant 4.
    • All five fail on origin/main. Hypothesis minimizes the defect to one split with bins 1 and 1 under active bins 1 and 1, which compiles a total row of 1 instead of 2.
  • test_pinned_feed_eitc_child_total_rows_count_each_split_once pins the factors and checks both conservation identities on the pinned feed. Like the other pinned-feed tests it skips where the feed is absent, as in CI, and was run locally: 27 passed and none skipped across the new file, test_us_fiscal_targets.py -k 'eitc or pinned_feed' and test_us_fiscal_refresh_builder.py -k eitc.
  • The two EITC test helpers accept lower=None so that a synthetic under_1 row can carry no lower AGI bound, as the feed's does.

Review

Independent review: GPT-6.1 Sol (subfleet run --task review --tier hard, job 20261012-102546-eitc-decomp-1227-review) gave APPROVE at f44d26eea. The reviewer checked the arithmetic against the raw feed and the artifacts, and probed the extracted helpers; its own test run stalled on imports, so the 27-test run is author evidence.

Its two non-blocking notes:

  1. All-children record set (pre-existing, not fixed here). A decomposition that also carried an all-children record set beside the four splits would still be counted twice, in both this helper and the fallback _eitc_source_decomposition_totals. No current producer emits that shape: the feed scope has only the four split record sets per year. An existing synthetic test uses an all-children set alone, so a guard would need that test's fixture reshaped. It is left as a known limit of the producer contract.
  2. Identical-bounds coverage (addressed). The overlap refusal test now also covers one interval repeated under two ids.

origin/main (977ac98) is merged in. The pinned-feed tests were re-run on the merged tree: 28 passed, none skipped, including the pinned-feed version pin 3fad14f85468. Main moved no US target.

axiom: n/a: microsim target-compilation fix (no policy encoding)

🤖 Generated with Claude Code

MaxGhenis and others added 2 commits October 12, 2026 10:22
_eitc_source_decomposition_total_for_bounds summed every Table 2.5
by-children row inside the uprating group. For the all-AGI group that is
each split's 28 AGI bins and its total row, so the source EITC was counted
twice and the four all-AGI total rows compiled at half their value
(factors 0.5209 amount, 0.4877 returns on the pinned feed). Count only each
split's outermost contained rows, and refuse a split whose AGI rows cross.

Adds example tests, Hypothesis properties over synthetic decompositions
(conservation, child-split preservation, totals equal their bins under
uniform growth) and a pinned-feed check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Doubling the eight Table 2.5 EITC child total rows moves the pinned-feed
national_state registry from e1f9906bf0e0 to 3fad14f85468 (full:
f5bd921d6b21 to c3aaea9c1e8c). Target counts are unchanged: 33,250
compiled, 6,102 national_state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 12, 2026
…#1235 review)

Round 1 of the independent review found two majors. A group with no EITC
in either year made the per-row uprating fall back to the all-returns
total for its bins, and the plan's per-row index check then skipped the
whole rake. And plain IPF capped at 10,000 sweeps refused feasible,
ill-conditioned margins.

- _eitc_rake_plan decides from structure: one all-AGI total row per split,
  leaf bins tiling AGI, and an AGI group for the totals and every bin. An
  empty group takes a zero factor; a group with active EITC but no source
  EITC is refused.
- _rake_two_margins checks exactly whether a positive scaling exists
  (Menon/Brualdi) and refuses with a specific message if not. It then
  finds the IPF solution by damped Newton steps on the convex dual,
  pinning each block's largest column with a Jacobi-scaled solve, and
  finishes with exact IPF sweeps. Rows hold to rounding, columns to
  1e-12.
- The bin adjustment is relative to the recorded per-row group factor,
  so it stays consistent beside rounded published subtotals.
- The per-row AGI metadata moves into _eitc_agi_uprating_metadata,
  unchanged for #1227's rows.
- The plan uses an _EitcRow NamedTuple and the solver annotates its nested
  functions; the spine-blindness check had failed on lambda row: row[1:].
- New tests: the review's counterexamples, the empty and refused groups,
  the rounded-subtotal adjustment, a Hypothesis property against a
  scipy linprog feasibility oracle, and positive seeds spanning ten
  orders of magnitude.
- Experiments: option (b) records the factor its new total implies;
  results regenerated; README separates pairs from runs, qualifies the
  level gap (TY2018 amount) and states the bin-level tradeoff.

national_state 31bd18096d33 -> 1aafec44dbfa (values move in the last bits;
the adjustment metadata changed); full ac32014f3325 -> ec807d77a746.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 2 commits October 12, 2026 20:21
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The overlap test now also covers two rows of one split with identical
bounds, which the strict-containment rule must keep from nesting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis marked this pull request as ready for review October 13, 2026 00:25
@MaxGhenis
MaxGhenis merged commit de90c6c into main Oct 14, 2026
10 checks passed
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Merged at head 40bfb23ea (merge commit de90c6c7e).

Merge gates

  • gh pr checks green on the latest run (10/10).
  • MERGEABLE / CLEAN, not a draft, no CHANGES_REQUESTED.

Review

  • Independent review: GPT-6.1 Sol APPROVE at f44d26eea (subfleet job 20261012-102546-eitc-decomp-1227-review).
  • Delta since that review: a clean merge of origin/main 977ac983a, which touches none of this PR's files, and one added test case (identical-bounds overlap, the reviewer's non-blocking note 2).
  • After the merge, 28 local tests passed with none skipped, including the pinned-feed tests. The national_state pin 3fad14f85468 holds.

Effect

  • On both national_state and full: the 8 Table 2.5 child-split all-AGI total rows double. They now sum to the TY2024 filing-season EITC by AGI ($69.04B; 23.84M returns).
  • No other target's value moves. Counts are unchanged.

Open follow-ups

🤖 Generated with Claude Code

@MaxGhenis
MaxGhenis deleted the fix-eitc-decomp-total-double-count branch October 14, 2026 11:30
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Post-merge independent review at 40bfb23ea: APPROVE. The reviewer was an Opus 5.5 agent run in-session, because every Subfleet lane was closed. It began before the merge and finished after it. It covers the delta since the Sol review at f44d26eea.

  • Re-derived on the pinned feed, with a whole-registry differential of the old and new function:
    • Exactly 8 specs change, each ×2.0, and only their uprating_factor metadata moves.
    • The factors are 1.04182361614341 and 0.975335982876551.
    • Counts stay at 33,250 / 6,102.
    • national_state moves e1f9906bf0e0 → 3fad14f85468, and full moves f5bd921d6b21 → c3aaea9c1e8c.
  • Tests.
    • The EITC/decomposition selection at 40bfb23ea passed 19 of 19, and the two full files passed 176 of 176.
    • With the pre-fix function swapped in, 8 failed: the 6 new cases and both pinned cases.
    • On de90c6c7e, 29 passed with nothing skipped, including the national_state fence and pin.
  • Non-blocking notes.
    1. The "" default for ledger_layout_record_set_id (fiscal_targets.py:2487) is unreachable, because _is_eitc_decomposition_spec requires that key.
    2. Overlap detection is per group. A repeated row nested under a contained parent row passes unnoticed in the parent's group, but still raises in its own group. The flat feed layout is unaffected.
    3. As the body already records, an all-children record set beside the four splits would still be double counted, in this helper and in _eitc_source_decomposition_totals. No producer emits that shape.
    4. The body says "All five fail on origin/main". It is six: four test functions with six parametrized cases.

Follow-up: #1274 commits the real TY2023 Table 2.5 and TY2024 filing-season rows as a fixture. It checks this fix in CI against IRS-published EITC.

🤖 Generated with Claude Code

MaxGhenis added a commit that referenced this pull request Oct 21, 2026
After #1227 each Table 2.5 child split's all-AGI total row keeps the
TY2023 all-AGI child mix while each AGI bin keeps its group's, so on the
pinned feed a split's total row and the sum of its 28 bins differed by up
to 16% (childless amount 1.157), and calibration saw both.

_rake_eitc_child_splits now rakes each decomposition's child x AGI-group
matrix of source bins (iterative proportional fitting) to the splits'
uprated total rows and the active filing-season AGI groups. Total rows
keep their values; the 224 bins move by x0.857-1.226; every split's bins
sum to its total row and every AGI group's bins to its active EITC.
Decompositions with nothing to reconcile (no total rows, or uprated on
the all-returns total) keep their per-row factors.

Evidence (experiments/eitc-child-split-rake-20261012/): no TY2024 child
mix is published; on published Table 2.5 TY2013-2023 keeping the all-AGI
mix predicted next year's split totals with 40-70% of the bins' error
(the bins under-predicted childless EITC in every clean year pair); it
sits 3-4x closer to the HT2 state EITC-by-children rows on the same
surface; and SOI's full-population TY2023 state data confirm the TY2023
mix it keeps.

national_state stays at 6,102 targets, version 3fad14f85468 ->
31bd18096d33; full stays at 33,250, c3aaea9c1e8c -> ac32014f3325.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 21, 2026
…#1235 review)

Round 1 of the independent review found two majors. A group with no EITC
in either year made the per-row uprating fall back to the all-returns
total for its bins, and the plan's per-row index check then skipped the
whole rake. And plain IPF capped at 10,000 sweeps refused feasible,
ill-conditioned margins.

- _eitc_rake_plan decides from structure: one all-AGI total row per split,
  leaf bins tiling AGI, and an AGI group for the totals and every bin. An
  empty group takes a zero factor; a group with active EITC but no source
  EITC is refused.
- _rake_two_margins checks exactly whether a positive scaling exists
  (Menon/Brualdi) and refuses with a specific message if not. It then
  finds the IPF solution by damped Newton steps on the convex dual,
  pinning each block's largest column with a Jacobi-scaled solve, and
  finishes with exact IPF sweeps. Rows hold to rounding, columns to
  1e-12.
- The bin adjustment is relative to the recorded per-row group factor,
  so it stays consistent beside rounded published subtotals.
- The per-row AGI metadata moves into _eitc_agi_uprating_metadata,
  unchanged for #1227's rows.
- The plan uses an _EitcRow NamedTuple and the solver annotates its nested
  functions; the spine-blindness check had failed on lambda row: row[1:].
- New tests: the review's counterexamples, the empty and refused groups,
  the rounded-subtotal adjustment, a Hypothesis property against a
  scipy linprog feasibility oracle, and positive seeds spanning ten
  orders of magnitude.
- Experiments: option (b) records the factor its new total implies;
  results regenerated; README separates pairs from runs, qualifies the
  level gap (TY2018 amount) and states the bin-level tradeoff.

national_state 31bd18096d33 -> 1aafec44dbfa (values move in the last bits;
the adjustment metadata changed); full ac32014f3325 -> ec807d77a746.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 21, 2026
Round 3 found float subset sums rounding strict slack (2**-24) to an exact
fill, so the feasibility check still refused a feasible decomposition.
The subset sums are now exact (fractions.Fraction), and only an exact fill
that other splits also reach, or an excess beyond rounding, is refused.

Also from round 3:
- The Newton solver works in log shares, so no positive cell underflows
  in normalization (a 1e-308 cell beside 1e308), and rows are normalized
  directly against the seed in units of each row's largest cell, so they
  hold to rounding at any magnitude.
- Every published zero row over positive bins is refused, also one inside
  a larger AGI group.

Rebased onto main after #1227 merged; its final overlap test (crossing and
identical rows) is carried over. New tests: the round-3 counterexamples.
national_state f96aac657fa6 -> 45adaa24b243 and full cb0eaa0418f2 ->
60a52e2f204d (values move in the last bits); results regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant