Repository navigation
Key IRS SOI AGI-by-size calibration amounts to tax year 2020 and fix SOI citations - #9604
Merged
Merged
Conversation
…esholds The 2024 Arkansas low-income tax table for head of household and surviving spouse filers with two or more dependents keyed its $92 row at $24,200, the threshold of the $104 row. ParameterScale._get_at_instant passes each bracket to add_bracket, which sums the amounts of equal thresholds, so the 2024 scale became [(0, 0), (24200, 196), (24300, 116), ...]: AGI of $24,177-$24,200 got $0 instead of $92 and $24,201-$24,300 got $196 instead of $104. The official table (2024 AR1000F/AR1000NR instructions, PDF page 28) reads $0-$24,176 -> $0 and $24,177-$24,200 -> $92. The tables are evaluated with right=True, so the threshold is the upper bound of the $0 row: 24_176. The 2024 reference anchor now points at PDF page 28. calibration/gov/irs/soi/agi/total_agi repeated its $10,000,000-or-more bracket, which doubled the top bucket when loaded. Its 18 values match IRS SOI Table 1.1 for tax year 2020 (20in11si.xls, column 3), where the top bucket appears once. No code reads this parameter. Add YAML tests for AGI of $24,100-$24,401 for both filing statuses and a repo-level test that fails when any parameter scale has two brackets with the same finite threshold at any instant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
force-pushed
the
fix-soi-agi-year-keying
branch
from
September 25, 2026 01:18
d3a8148 to
7f53c87
Compare
This was referenced Sep 25, 2026
calibration.gov.irs.soi.agi.total_agi holds IRS SOI Table 1.1 values for tax year 2020 (20in11si.xls, column 3), matching all 18 brackets, but keyed them at 2015-01-01. uprate_parameter grows values forward from the latest dated value, so uprating by calibration.gov.cbo.income_by_source.adjusted_gross_income put the 2020 amounts 40.03% above the published table (the index's 2020/2015 ratio) and every later year 40.03% above its value uprated from the published 2020 base. Rekey the amounts to 2020-01-01; thresholds stay at 2015-01-01, and backdate_parameters extends the 2020 values back to 2015 as it already does for number_of_returns. Correct both citations: total_agi cited "IRS SOI PUF 2015 (20in11.xls)" with no href; number_of_returns matches tax year 2021 (21in11si.xls) on all 18 brackets and its href was right, but its title named 20in11si.xls. Add tests pinning each scale's amounts at its table's tax year, checking that uprating runs from the latest dated value, and that 2015 up to the first dated value still evaluates. They select values by date, so a later SOI year can be added without rewriting them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
force-pushed
the
fix-soi-agi-year-keying
branch
from
September 26, 2026 13:07
7f53c87 to
52e3c67
Compare
MaxGhenis
marked this pull request as ready for review
September 27, 2026 09:10
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
Merged by the US + core hub on its merge gates. Audit:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
calibration/gov/irs/soi/agi/total_agi.yamlfrom2015-01-01to2020-01-01. They are IRS SOI Table 1.1 values for tax year 2020, but keyed at 2015 they were uprated bycalibration.gov.cbo.income_by_source.adjusted_gross_income, which put the 2020 amounts 40.03% above the published table, and every later year 40.03% above the value uprated from the published 2020 base. 40.03% is the index's 2020/2015 ratio, 12.650T / 9.034T. Its 2015 value is sourced to the Tax Foundation, its 2019–2022 values to the historical years of CBO's revenue-projection workbooks, and its 2023+ values to CBO's February 2026 projections.total_agicited "IRS SOI PUF 2015 (20in11.xls)" with no href, although the values aren't from the PUF, aren't for 2015, and the file is20in11si.xls.number_of_returnshad the right href and year key (TY2021,21in11si.xls), but its title named20in11si.xls.Builds on #9599 (merged as squash 38d6775), which removed the duplicated
$10,000,000 or morebracket fromtotal_agi.yaml.mainis merged into this branch, so the diff is this PR's own change. Its diff againstmainis byte-identical to the reviewed commit's (a8b1884..52e3c67). #9599's finaltest_parameter_scale_thresholds.pypasses with this PR's split keying (thresholds at 2015-01-01, amounts at 2020-01-01).Which SOI year each file holds
I downloaded
20in11si.xls("Table 1.1 ... Tax Year 2020 (Filing Year 2021)") and21in11si.xls("... Tax Year 2021 (Filing Year 2022)"). I compared every bracket against the "Size of adjusted gross income" rows from "$1 under $5,000" through "$10,000,000 or more", using exact integer equality (SOI money amounts are in thousands):total_agi.yamlnumber_of_returns.yamlBoth files are linked from the IRS page SOI Tax Stats - Individual Statistical Tables by Size of Adjusted Gross Income, which the new comment in
total_agi.yamlcites, asnumber_of_returns.yamlalready did.Instants before 2020
The thresholds stay keyed at
2015-01-01, as innumber_of_returns.yaml, where the amounts start in 2021. I read the two steps that decide what the scale returns before 2020:uprate_parameter(policyengine-core 3.30.2, the version inuv.lock) grows values only forward, from the parameter's latest dated value. It raises if the uprating parameter is undefined at that instant; the AGI index has a2020-01-01value.CountryTaxBenefitSystem.__init__then callsbackdate_parameters(self.parameters, first_instant="2015-01-01")(policyengine_us/system.py), which extends each parameter's earliest value back to 2015-01-01 at that same value.So 2015–2019 return the published TY2020 values, the same treatment
number_of_returnsalready gets for 2015–2020. Before 2015 there are no thresholds, so the scale is empty, as before this PR. Loaded values of the "$1 under $5,000" bracket:Every bracket moves by the same ratio. SOI publishes Table 1.1 for 2015–2019 too, so per-year values could replace the flat backdate later; that is outside this fix.
Reforms to these scales (pre-existing, not changed here).
CountryTaxBenefitSystem.__init__applies reforms beforebackdate_parameters. For any parameter whose first value falls after 2015, that ordering lets a reform starting at the first value's date carry back to 2015. A reform bounded to an earlier year can also leave a gap before the first value.number_of_returnsalready behaves this way onmain. A reform setting[0].amountforyear:2021:1also sets 2015–2020, and one setting[2].amountforyear:2019:1leaves 2020 with 17 brackets. This PR addstotal_agito the parameters affected. Nothing reforms these calibration targets, and fixing the ordering would change reform semantics across the repo (see the #9075 comment insystem.py), so it is fixed separately in #9628 (draft, "Backdate parameters before applying reforms").Consumers
No code reads
calibration.gov.irs.soi.agi.*, so this changes no model output. I re-checked on 2026-09-24:upstream/main60a4805): no variable, formula,upratingpath orMICRODATA_UPRATING_OVERRIDESentry referencessoi.agi. The tests that take the wholecalibration.gov.irs.soinode (test_uprating_extensions.py,test_per_capita_uprating.py,contrib/ssa/test_trustees_core_thresholds.py) name other SOI parameters explicitly.mainf338ae066): the pe-us adapter treatscalibration.gov.irs.soi.*as national totals, but only for variables'upratingpaths and the pe-us overrides, and neither namessoi.agi. Itsirs_soi/agicalibration target is its own registry entry.main42ed5d45c5):loss.py's SOI aggregate targets read onlysoi.long_term_capital_gains.total_agifields come from its own SOI ETL."soi.agi","agi.total_agi","agi.number_of_returns","soi/agi"and"irs.soi" agi: the only hit that names these parameters is an inventory CSV,ai-inequality/public/data/uprating_analysis.csv. The other hits are unrelated identifiers (us.soi.agi.v1in calibration-diagnostics,irs_soi.agiin chronicle) or other SOI parameters.axiom: n/a: calibration target data (IRS SOI statistics), not a policy rule.
Tests
policyengine_us/tests/test_parameter_files.py::test_soi_agi_bands_are_keyed_to_their_table_1_1_tax_yearreads each YAML and checks that every one of the 18 brackets has an amount keyed to its table's tax year. It also checks the lowest and highest bracket against the published cells, and that the reference href is that year'sin11si.xlswith the tax year named in the title.policyengine_us/tests/core/test_soi_agi_band_keying.pyloads the system and checks three things for each scale. The amounts at the table's tax year must equal the YAML values for that date, un-uprated. The amounts five years after the latest dated value must equal that value times the uprating index's growth. And every year from 2015 up to the first dated value must return the scale at that first date. The tests select values by date, so adding another SOI year later won't break them.Locally:
uv run pytest policyengine_us/tests/test_parameter_scale_thresholds.py policyengine_us/tests/test_parameter_files.py policyengine_us/tests/test_system_import.py policyengine_us/tests/core/test_soi_agi_band_keying.py: 22 passed.total_agi's tax-year check, because the old file has no 2020 amounts. The other 5 pass on both trees. Those are the threenumber_of_returnscases, whose keying was already right, and twototal_agicases. The uprating check compares growth ratios, so it holds under either key. The pre-first-value check has no years to test when the first value is in 2015.make format: no changes;ruff check: clean.🤖 Generated with Claude Code