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

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