From 534a28b2e86dbba5668fece20b7ed56cb99549c9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 23:16:26 +0000 Subject: [PATCH 1/3] Add the 2015-2017 federal income tax brackets gov.irs.income.bracket started in 2018, so backdating to the first modeled year (2015) gave 2015-2017 the TCJA schedule: 12/22/24/32/37% rates and 2018 thresholds. Add the pre-TCJA rates (10/15/25/28/33/35/ 39.6%) from 2015 and each year's thresholds from Rev. Procs. 2014-61, 2015-53 and 2016-55, with hand-computed YAML cases for each filing status and a grid property that the main-rates tax never falls as taxable income rises. Co-Authored-By: Claude Opus 5.5 --- .../federal-brackets-before-2018.fixed.md | 1 + .../parameters/gov/irs/income/bracket.yaml | 113 +++++- .../before_credits/income_tax_main_rates.yaml | 340 ++++++++++++++++++ .../test_main_rate_schedule_monotone.py | 79 ++++ 4 files changed, 532 insertions(+), 1 deletion(-) create mode 100644 changelog.d/federal-brackets-before-2018.fixed.md create mode 100644 policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/before_credits/test_main_rate_schedule_monotone.py diff --git a/changelog.d/federal-brackets-before-2018.fixed.md b/changelog.d/federal-brackets-before-2018.fixed.md new file mode 100644 index 00000000000..7fc6741a95a --- /dev/null +++ b/changelog.d/federal-brackets-before-2018.fixed.md @@ -0,0 +1 @@ +Add the 2015-2017 federal income tax rates and bracket thresholds (10% to 39.6%), which had fallen back to the 2018 schedule. diff --git a/policyengine_us/parameters/gov/irs/income/bracket.yaml b/policyengine_us/parameters/gov/irs/income/bracket.yaml index 9a8f0d234dc..108fb4be4bc 100644 --- a/policyengine_us/parameters/gov/irs/income/bracket.yaml +++ b/policyengine_us/parameters/gov/irs/income/bracket.yaml @@ -12,21 +12,31 @@ rates: reference: # TCJA adjustments applied in (j) # Rate reductions applied in (i) - - title: 26 U.S. Code § 1 - Tax imposed (a-d) + # Before 2018: the (a)-(d) tables as modified by (i)(1)-(3). + - title: 26 U.S. Code § 1 - Tax imposed (a-d) href: https://www.law.cornell.edu/uscode/text/26/1#i + - title: 26 U.S. Code § 1(j)(2) - Rate tables for 2018 and later + href: https://www.law.cornell.edu/uscode/text/26/1#j_2 1: + 2015-01-01: 0.1 2018-01-01: 0.1 2: + 2015-01-01: 0.15 2018-01-01: 0.12 3: + 2015-01-01: 0.25 2018-01-01: 0.22 4: + 2015-01-01: 0.28 2018-01-01: 0.24 5: + 2015-01-01: 0.33 2018-01-01: 0.32 6: + 2015-01-01: 0.35 2018-01-01: 0.35 7: + 2015-01-01: 0.396 2018-01-01: 0.37 thresholds: description: The upper threshold for each income tax bracket, for each tax filer type. @@ -60,6 +70,12 @@ thresholds: href: https://www.irs.gov/pub/irs-drop/rp-19-44.pdf - title: 2019 IRS data release href: https://www.irs.gov/pub/irs-drop/rp-18-57.pdf + - title: 2017 IRS data release, Rev. Proc. 2016-55 section 3.01 + href: https://www.irs.gov/pub/irs-drop/rp-16-55.pdf + - title: 2016 IRS data release, Rev. Proc. 2015-53 section 3.01 + href: https://www.irs.gov/pub/irs-drop/rp-15-53.pdf + - title: 2015 IRS data release, Rev. Proc. 2014-61 section 3.01 + href: https://www.irs.gov/pub/irs-drop/rp-14-61.pdf - title: CBO Tax Parameters and Effective Marginal Tax Rates | Feb 2024 # 1. Tax Parameters. href: https://www.cbo.gov/system/files/2024-02/53724-2024-02-Tax-Parameters.xlsx @@ -69,6 +85,9 @@ thresholds: 1: SINGLE: values: + 2015-01-01: 9_225 + 2016-01-01: 9_275 + 2017-01-01: 9_325 2018-01-01: 9_525 2019-01-01: 9_700 2020-01-01: 9_875 @@ -86,6 +105,9 @@ thresholds: interval: 25 JOINT: values: + 2015-01-01: 18_450 + 2016-01-01: 18_550 + 2017-01-01: 18_650 2018-01-01: 19_050 2019-01-01: 19_400 2020-01-01: 19_750 @@ -103,6 +125,9 @@ thresholds: interval: 50 SEPARATE: values: + 2015-01-01: 9_225 + 2016-01-01: 9_275 + 2017-01-01: 9_325 2018-01-01: 9_525 2019-01-01: 9_700 2020-01-01: 9_875 @@ -120,6 +145,9 @@ thresholds: interval: 25 HEAD_OF_HOUSEHOLD: values: + 2015-01-01: 13_150 + 2016-01-01: 13_250 + 2017-01-01: 13_350 2018-01-01: 13_600 2019-01-01: 13_850 2020-01-01: 14_100 @@ -137,6 +165,9 @@ thresholds: interval: 50 SURVIVING_SPOUSE: values: + 2015-01-01: 18_450 + 2016-01-01: 18_550 + 2017-01-01: 18_650 2018-01-01: 19_050 2019-01-01: 19_400 2020-01-01: 19_750 @@ -155,6 +186,9 @@ thresholds: 2: SINGLE: values: + 2015-01-01: 37_450 + 2016-01-01: 37_650 + 2017-01-01: 37_950 2018-01-01: 38_700 2019-01-01: 39_475 2020-01-01: 40_125 @@ -172,6 +206,9 @@ thresholds: interval: 25 JOINT: values: + 2015-01-01: 74_900 + 2016-01-01: 75_300 + 2017-01-01: 75_900 2018-01-01: 77_400 2019-01-01: 78_950 2020-01-01: 80_250 @@ -189,6 +226,9 @@ thresholds: interval: 50 SEPARATE: values: + 2015-01-01: 37_450 + 2016-01-01: 37_650 + 2017-01-01: 37_950 2018-01-01: 38_700 2019-01-01: 39_475 2020-01-01: 40_125 @@ -206,6 +246,9 @@ thresholds: interval: 25 HEAD_OF_HOUSEHOLD: values: + 2015-01-01: 50_200 + 2016-01-01: 50_400 + 2017-01-01: 50_800 2018-01-01: 51_800 2019-01-01: 52_850 2020-01-01: 53_700 @@ -223,6 +266,9 @@ thresholds: interval: 50 SURVIVING_SPOUSE: values: + 2015-01-01: 74_900 + 2016-01-01: 75_300 + 2017-01-01: 75_900 2018-01-01: 77_400 2019-01-01: 78_950 2020-01-01: 80_250 @@ -241,6 +287,9 @@ thresholds: 3: SINGLE: values: + 2015-01-01: 90_750 + 2016-01-01: 91_150 + 2017-01-01: 91_900 2018-01-01: 82_500 2019-01-01: 84_200 2020-01-01: 85_525 @@ -258,6 +307,9 @@ thresholds: interval: 25 JOINT: values: + 2015-01-01: 151_200 + 2016-01-01: 151_900 + 2017-01-01: 153_100 2018-01-01: 165_000 2019-01-01: 168_400 2020-01-01: 171_050 @@ -275,6 +327,9 @@ thresholds: interval: 50 SEPARATE: values: + 2015-01-01: 75_600 + 2016-01-01: 75_950 + 2017-01-01: 76_550 2018-01-01: 82_500 2019-01-01: 84_200 2020-01-01: 85_525 @@ -292,6 +347,9 @@ thresholds: interval: 25 HEAD_OF_HOUSEHOLD: values: + 2015-01-01: 129_600 + 2016-01-01: 130_150 + 2017-01-01: 131_200 2018-01-01: 82_500 2019-01-01: 84_200 2020-01-01: 85_500 @@ -309,6 +367,9 @@ thresholds: interval: 50 SURVIVING_SPOUSE: values: + 2015-01-01: 151_200 + 2016-01-01: 151_900 + 2017-01-01: 153_100 2018-01-01: 165_000 2019-01-01: 168_400 2020-01-01: 171_050 @@ -327,6 +388,9 @@ thresholds: 4: SINGLE: values: + 2015-01-01: 189_300 + 2016-01-01: 190_150 + 2017-01-01: 191_650 2018-01-01: 157_500 2019-01-01: 160_725 2020-01-01: 163_300 @@ -344,6 +408,9 @@ thresholds: interval: 25 JOINT: values: + 2015-01-01: 230_450 + 2016-01-01: 231_450 + 2017-01-01: 233_350 2018-01-01: 315_000 2019-01-01: 321_450 2020-01-01: 326_600 @@ -361,6 +428,9 @@ thresholds: interval: 50 SEPARATE: values: + 2015-01-01: 115_225 + 2016-01-01: 115_725 + 2017-01-01: 116_675 2018-01-01: 157_500 2019-01-01: 160_725 2020-01-01: 163_300 @@ -388,6 +458,9 @@ thresholds: interval: 25 HEAD_OF_HOUSEHOLD: values: + 2015-01-01: 209_850 + 2016-01-01: 210_800 + 2017-01-01: 212_500 2018-01-01: 157_500 2019-01-01: 160_700 2020-01-01: 163_300 @@ -405,6 +478,9 @@ thresholds: interval: 50 SURVIVING_SPOUSE: values: + 2015-01-01: 230_450 + 2016-01-01: 231_450 + 2017-01-01: 233_350 2018-01-01: 315_000 2019-01-01: 321_450 2020-01-01: 326_600 @@ -423,6 +499,9 @@ thresholds: 5: SINGLE: values: + 2015-01-01: 411_500 + 2016-01-01: 413_350 + 2017-01-01: 416_700 2018-01-01: 200_000 2019-01-01: 204_100 2020-01-01: 207_350 @@ -440,6 +519,9 @@ thresholds: interval: 25 JOINT: values: + 2015-01-01: 411_500 + 2016-01-01: 413_350 + 2017-01-01: 416_700 2018-01-01: 400_000 2019-01-01: 408_200 2020-01-01: 414_700 @@ -457,6 +539,9 @@ thresholds: interval: 50 SEPARATE: values: + 2015-01-01: 205_750 + 2016-01-01: 206_675 + 2017-01-01: 208_350 2018-01-01: 200_000 2019-01-01: 204_100 2020-01-01: 207_350 @@ -484,6 +569,9 @@ thresholds: interval: 25 HEAD_OF_HOUSEHOLD: values: + 2015-01-01: 411_500 + 2016-01-01: 413_350 + 2017-01-01: 416_700 2018-01-01: 200_000 2019-01-01: 204_100 2020-01-01: 207_350 @@ -511,6 +599,9 @@ thresholds: interval: 50 SURVIVING_SPOUSE: values: + 2015-01-01: 411_500 + 2016-01-01: 413_350 + 2017-01-01: 416_700 2018-01-01: 400_000 2019-01-01: 408_200 2020-01-01: 414_700 @@ -529,6 +620,9 @@ thresholds: 6: SINGLE: values: + 2015-01-01: 413_200 + 2016-01-01: 415_050 + 2017-01-01: 418_400 2018-01-01: 500_000 2019-01-01: 510_300 2020-01-01: 518_400 @@ -546,6 +640,9 @@ thresholds: interval: 25 JOINT: values: + 2015-01-01: 464_850 + 2016-01-01: 466_950 + 2017-01-01: 470_700 2018-01-01: 600_000 2019-01-01: 612_350 2020-01-01: 622_050 @@ -563,6 +660,9 @@ thresholds: interval: 50 SEPARATE: values: + 2015-01-01: 232_425 + 2016-01-01: 233_475 + 2017-01-01: 235_350 2018-01-01: 300_000 2019-01-01: 306_175 2020-01-01: 311_025 @@ -580,6 +680,9 @@ thresholds: interval: 25 HEAD_OF_HOUSEHOLD: values: + 2015-01-01: 439_000 + 2016-01-01: 441_000 + 2017-01-01: 444_550 2018-01-01: 500_000 2019-01-01: 510_300 2020-01-01: 518_400 @@ -597,6 +700,9 @@ thresholds: interval: 50 SURVIVING_SPOUSE: values: + 2015-01-01: 464_850 + 2016-01-01: 466_950 + 2017-01-01: 470_700 2018-01-01: 600_000 2019-01-01: 612_350 2020-01-01: 622_050 @@ -615,16 +721,21 @@ thresholds: 7: SINGLE: values: + 2015-01-01: .inf 2018-01-01: .inf JOINT: values: + 2015-01-01: .inf 2018-01-01: .inf SEPARATE: values: + 2015-01-01: .inf 2018-01-01: .inf HEAD_OF_HOUSEHOLD: values: + 2015-01-01: .inf 2018-01-01: .inf SURVIVING_SPOUSE: values: + 2015-01-01: .inf 2018-01-01: .inf diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/before_credits/income_tax_main_rates.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/before_credits/income_tax_main_rates.yaml index 3c591066f52..3b225ad5a12 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/before_credits/income_tax_main_rates.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/before_credits/income_tax_main_rates.yaml @@ -96,3 +96,343 @@ # Tax on 50,000: 952.50 + 3,501 + 2,486 = 6,939.50. Tax on 30,000: # 952.50 + 2,457 = 3,409.50. Difference: 3,530. income_tax_main_rates: 3_530 + +# Tax at the main rates on ordinary taxable income before 2018, from each +# year's section 1 rate schedule (26 U.S.C. 1(a)-(d) as modified by 1(i)). +# Each case's comment lists the amount in each bracket times its rate, so the +# expected tax is computed by hand from the thresholds, not by the model. + +- name: 2017 single filer with 100,000 of taxable income (Rev. Proc. 2016-55) + absolute_error_margin: 0.01 + period: 2017 + input: + filing_status: SINGLE + taxable_income: 100_000 + foreign_earned_income_exclusion: 0 + capital_gains_excluded_from_taxable_income: 0 + output: + # 10% x 9,325 = 932.5; 15% x 28,625 = 4,293.75; 25% x 53,950 = 13,487.5; + # 28% x 8,100 = 2,268. + income_tax_main_rates: 20_981.75 + +- name: 2017 single filer with 500,000 of taxable income (Rev. Proc. 2016-55) + absolute_error_margin: 0.01 + period: 2017 + input: + filing_status: SINGLE + taxable_income: 500_000 + foreign_earned_income_exclusion: 0 + capital_gains_excluded_from_taxable_income: 0 + output: + # 10% x 9,325 = 932.5; 15% x 28,625 = 4,293.75; 25% x 53,950 = 13,487.5; + # 28% x 99,750 = 27,930; 33% x 225,050 = 74,266.5; 35% x 1,700 = 595; + # 39.6% x 81,600 = 32,313.6. + income_tax_main_rates: 153_818.85 + +- name: 2017 joint filer with 100,000 of taxable income (Rev. Proc. 2016-55) + absolute_error_margin: 0.01 + period: 2017 + input: + filing_status: JOINT + taxable_income: 100_000 + foreign_earned_income_exclusion: 0 + capital_gains_excluded_from_taxable_income: 0 + output: + # 10% x 18,650 = 1,865; 15% x 57,250 = 8,587.5; 25% x 24,100 = 6,025. + income_tax_main_rates: 16_477.50 + +- name: 2017 joint filer with 500,000 of taxable income (Rev. Proc. 2016-55) + absolute_error_margin: 0.01 + period: 2017 + input: + filing_status: JOINT + taxable_income: 500_000 + foreign_earned_income_exclusion: 0 + capital_gains_excluded_from_taxable_income: 0 + output: + # 10% x 18,650 = 1,865; 15% x 57,250 = 8,587.5; 25% x 77,200 = 19,300; + # 28% x 80,250 = 22,470; 33% x 183,350 = 60,505.5; 35% x 54,000 = 18,900; + # 39.6% x 29,300 = 11,602.8. + income_tax_main_rates: 143_230.80 + +- name: 2017 separate filer with 100,000 of taxable income (Rev. Proc. 2016-55) + absolute_error_margin: 0.01 + period: 2017 + input: + filing_status: SEPARATE + taxable_income: 100_000 + foreign_earned_income_exclusion: 0 + capital_gains_excluded_from_taxable_income: 0 + output: + # 10% x 9,325 = 932.5; 15% x 28,625 = 4,293.75; 25% x 38,600 = 9,650; + # 28% x 23,450 = 6,566. + income_tax_main_rates: 21_442.25 + +- name: 2017 separate filer with 500,000 of taxable income (Rev. Proc. 2016-55) + absolute_error_margin: 0.01 + period: 2017 + input: + filing_status: SEPARATE + taxable_income: 500_000 + foreign_earned_income_exclusion: 0 + capital_gains_excluded_from_taxable_income: 0 + output: + # 10% x 9,325 = 932.5; 15% x 28,625 = 4,293.75; 25% x 38,600 = 9,650; + # 28% x 40,125 = 11,235; 33% x 91,675 = 30,252.75; 35% x 27,000 = 9,450; + # 39.6% x 264,650 = 104,801.4. + income_tax_main_rates: 170_615.40 + +- name: 2017 head of household filer with 100,000 of taxable income (Rev. Proc. 2016-55) + absolute_error_margin: 0.01 + period: 2017 + input: + filing_status: HEAD_OF_HOUSEHOLD + taxable_income: 100_000 + foreign_earned_income_exclusion: 0 + capital_gains_excluded_from_taxable_income: 0 + output: + # 10% x 13,350 = 1,335; 15% x 37,450 = 5,617.5; 25% x 49,200 = 12,300. + income_tax_main_rates: 19_252.50 + +- name: 2017 head of household filer with 500,000 of taxable income (Rev. Proc. 2016-55) + absolute_error_margin: 0.01 + period: 2017 + input: + filing_status: HEAD_OF_HOUSEHOLD + taxable_income: 500_000 + foreign_earned_income_exclusion: 0 + capital_gains_excluded_from_taxable_income: 0 + output: + # 10% x 13,350 = 1,335; 15% x 37,450 = 5,617.5; 25% x 80,400 = 20,100; + # 28% x 81,300 = 22,764; 33% x 204,200 = 67,386; 35% x 27,850 = 9,747.5; + # 39.6% x 55,450 = 21,958.2. + income_tax_main_rates: 148_908.20 + +- name: 2017 surviving spouse filer with 100,000 of taxable income (Rev. Proc. 2016-55) + absolute_error_margin: 0.01 + period: 2017 + input: + filing_status: SURVIVING_SPOUSE + taxable_income: 100_000 + foreign_earned_income_exclusion: 0 + capital_gains_excluded_from_taxable_income: 0 + output: + # 10% x 18,650 = 1,865; 15% x 57,250 = 8,587.5; 25% x 24,100 = 6,025. + income_tax_main_rates: 16_477.50 + +- name: 2017 surviving spouse filer with 500,000 of taxable income (Rev. Proc. 2016-55) + absolute_error_margin: 0.01 + period: 2017 + input: + filing_status: SURVIVING_SPOUSE + taxable_income: 500_000 + foreign_earned_income_exclusion: 0 + capital_gains_excluded_from_taxable_income: 0 + output: + # 10% x 18,650 = 1,865; 15% x 57,250 = 8,587.5; 25% x 77,200 = 19,300; + # 28% x 80,250 = 22,470; 33% x 183,350 = 60,505.5; 35% x 54,000 = 18,900; + # 39.6% x 29,300 = 11,602.8. + income_tax_main_rates: 143_230.80 + +- name: 2016 single filer with 500,000 of taxable income (Rev. Proc. 2015-53) + absolute_error_margin: 0.01 + period: 2016 + input: + filing_status: SINGLE + taxable_income: 500_000 + foreign_earned_income_exclusion: 0 + capital_gains_excluded_from_taxable_income: 0 + output: + # 10% x 9,275 = 927.5; 15% x 28,375 = 4,256.25; 25% x 53,500 = 13,375; + # 28% x 99,000 = 27,720; 33% x 223,200 = 73,656; 35% x 1,700 = 595; + # 39.6% x 84,950 = 33,640.2. + income_tax_main_rates: 154_169.95 + +- name: 2016 joint filer with 500,000 of taxable income (Rev. Proc. 2015-53) + absolute_error_margin: 0.01 + period: 2016 + input: + filing_status: JOINT + taxable_income: 500_000 + foreign_earned_income_exclusion: 0 + capital_gains_excluded_from_taxable_income: 0 + output: + # 10% x 18,550 = 1,855; 15% x 56,750 = 8,512.5; 25% x 76,600 = 19,150; + # 28% x 79,550 = 22,274; 33% x 181,900 = 60,027; 35% x 53,600 = 18,760; + # 39.6% x 33,050 = 13,087.8. + income_tax_main_rates: 143_666.30 + +- name: 2016 separate filer with 500,000 of taxable income (Rev. Proc. 2015-53) + absolute_error_margin: 0.01 + period: 2016 + input: + filing_status: SEPARATE + taxable_income: 500_000 + foreign_earned_income_exclusion: 0 + capital_gains_excluded_from_taxable_income: 0 + output: + # 10% x 9,275 = 927.5; 15% x 28,375 = 4,256.25; 25% x 38,300 = 9,575; + # 28% x 39,775 = 11,137; 33% x 90,950 = 30,013.5; 35% x 26,800 = 9,380; + # 39.6% x 266,525 = 105,543.9. + income_tax_main_rates: 170_833.15 + +- name: 2016 head of household filer with 500,000 of taxable income (Rev. Proc. 2015-53) + absolute_error_margin: 0.01 + period: 2016 + input: + filing_status: HEAD_OF_HOUSEHOLD + taxable_income: 500_000 + foreign_earned_income_exclusion: 0 + capital_gains_excluded_from_taxable_income: 0 + output: + # 10% x 13,250 = 1,325; 15% x 37,150 = 5,572.5; 25% x 79,750 = 19,937.5; + # 28% x 80,650 = 22,582; 33% x 202,550 = 66,841.5; 35% x 27,650 = 9,677.5; + # 39.6% x 59,000 = 23,364. + income_tax_main_rates: 149_300 + +- name: 2016 surviving spouse filer with 500,000 of taxable income (Rev. Proc. 2015-53) + absolute_error_margin: 0.01 + period: 2016 + input: + filing_status: SURVIVING_SPOUSE + taxable_income: 500_000 + foreign_earned_income_exclusion: 0 + capital_gains_excluded_from_taxable_income: 0 + output: + # 10% x 18,550 = 1,855; 15% x 56,750 = 8,512.5; 25% x 76,600 = 19,150; + # 28% x 79,550 = 22,274; 33% x 181,900 = 60,027; 35% x 53,600 = 18,760; + # 39.6% x 33,050 = 13,087.8. + income_tax_main_rates: 143_666.30 + +- name: 2015 single filer with 100,000 of taxable income (Rev. Proc. 2014-61) + absolute_error_margin: 0.01 + period: 2015 + input: + filing_status: SINGLE + taxable_income: 100_000 + foreign_earned_income_exclusion: 0 + capital_gains_excluded_from_taxable_income: 0 + output: + # 10% x 9,225 = 922.5; 15% x 28,225 = 4,233.75; 25% x 53,300 = 13,325; + # 28% x 9,250 = 2,590. + income_tax_main_rates: 21_071.25 + +- name: 2015 single filer with 500,000 of taxable income (Rev. Proc. 2014-61) + absolute_error_margin: 0.01 + period: 2015 + input: + filing_status: SINGLE + taxable_income: 500_000 + foreign_earned_income_exclusion: 0 + capital_gains_excluded_from_taxable_income: 0 + output: + # 10% x 9,225 = 922.5; 15% x 28,225 = 4,233.75; 25% x 53,300 = 13,325; + # 28% x 98,550 = 27,594; 33% x 222,200 = 73,326; 35% x 1,700 = 595; + # 39.6% x 86,800 = 34,372.8. + income_tax_main_rates: 154_369.05 + +- name: 2015 joint filer with 100,000 of taxable income (Rev. Proc. 2014-61) + absolute_error_margin: 0.01 + period: 2015 + input: + filing_status: JOINT + taxable_income: 100_000 + foreign_earned_income_exclusion: 0 + capital_gains_excluded_from_taxable_income: 0 + output: + # 10% x 18,450 = 1,845; 15% x 56,450 = 8,467.5; 25% x 25,100 = 6,275. + income_tax_main_rates: 16_587.50 + +- name: 2015 joint filer with 500,000 of taxable income (Rev. Proc. 2014-61) + absolute_error_margin: 0.01 + period: 2015 + input: + filing_status: JOINT + taxable_income: 500_000 + foreign_earned_income_exclusion: 0 + capital_gains_excluded_from_taxable_income: 0 + output: + # 10% x 18,450 = 1,845; 15% x 56,450 = 8,467.5; 25% x 76,300 = 19,075; + # 28% x 79,250 = 22,190; 33% x 181,050 = 59,746.5; 35% x 53,350 = 18,672.5; + # 39.6% x 35,150 = 13,919.4. + income_tax_main_rates: 143_915.90 + +- name: 2015 separate filer with 100,000 of taxable income (Rev. Proc. 2014-61) + absolute_error_margin: 0.01 + period: 2015 + input: + filing_status: SEPARATE + taxable_income: 100_000 + foreign_earned_income_exclusion: 0 + capital_gains_excluded_from_taxable_income: 0 + output: + # 10% x 9,225 = 922.5; 15% x 28,225 = 4,233.75; 25% x 38,150 = 9,537.5; + # 28% x 24,400 = 6,832. + income_tax_main_rates: 21_525.75 + +- name: 2015 separate filer with 500,000 of taxable income (Rev. Proc. 2014-61) + absolute_error_margin: 0.01 + period: 2015 + input: + filing_status: SEPARATE + taxable_income: 500_000 + foreign_earned_income_exclusion: 0 + capital_gains_excluded_from_taxable_income: 0 + output: + # 10% x 9,225 = 922.5; 15% x 28,225 = 4,233.75; 25% x 38,150 = 9,537.5; + # 28% x 39,625 = 11,095; 33% x 90,525 = 29,873.25; 35% x 26,675 = 9,336.25; + # 39.6% x 267,575 = 105,959.7. + income_tax_main_rates: 170_957.95 + +- name: 2015 head of household filer with 100,000 of taxable income (Rev. Proc. 2014-61) + absolute_error_margin: 0.01 + period: 2015 + input: + filing_status: HEAD_OF_HOUSEHOLD + taxable_income: 100_000 + foreign_earned_income_exclusion: 0 + capital_gains_excluded_from_taxable_income: 0 + output: + # 10% x 13,150 = 1,315; 15% x 37,050 = 5,557.5; 25% x 49,800 = 12,450. + income_tax_main_rates: 19_322.50 + +- name: 2015 head of household filer with 500,000 of taxable income (Rev. Proc. 2014-61) + absolute_error_margin: 0.01 + period: 2015 + input: + filing_status: HEAD_OF_HOUSEHOLD + taxable_income: 500_000 + foreign_earned_income_exclusion: 0 + capital_gains_excluded_from_taxable_income: 0 + output: + # 10% x 13,150 = 1,315; 15% x 37,050 = 5,557.5; 25% x 79,400 = 19,850; + # 28% x 80,250 = 22,470; 33% x 201,650 = 66,544.5; 35% x 27,500 = 9,625; + # 39.6% x 61,000 = 24,156. + income_tax_main_rates: 149_518 + +- name: 2015 surviving spouse filer with 100,000 of taxable income (Rev. Proc. 2014-61) + absolute_error_margin: 0.01 + period: 2015 + input: + filing_status: SURVIVING_SPOUSE + taxable_income: 100_000 + foreign_earned_income_exclusion: 0 + capital_gains_excluded_from_taxable_income: 0 + output: + # 10% x 18,450 = 1,845; 15% x 56,450 = 8,467.5; 25% x 25,100 = 6,275. + income_tax_main_rates: 16_587.50 + +- name: 2015 surviving spouse filer with 500,000 of taxable income (Rev. Proc. 2014-61) + absolute_error_margin: 0.01 + period: 2015 + input: + filing_status: SURVIVING_SPOUSE + taxable_income: 500_000 + foreign_earned_income_exclusion: 0 + capital_gains_excluded_from_taxable_income: 0 + output: + # 10% x 18,450 = 1,845; 15% x 56,450 = 8,467.5; 25% x 76,300 = 19,075; + # 28% x 79,250 = 22,190; 33% x 181,050 = 59,746.5; 35% x 53,350 = 18,672.5; + # 39.6% x 35,150 = 13,919.4. + income_tax_main_rates: 143_915.90 + diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/before_credits/test_main_rate_schedule_monotone.py b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/before_credits/test_main_rate_schedule_monotone.py new file mode 100644 index 00000000000..2019a7ff2ad --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/before_credits/test_main_rate_schedule_monotone.py @@ -0,0 +1,79 @@ +"""Tax at the main rates never falls when taxable income rises. + +YAML cases pin the tax at chosen incomes; this checks the property over a +dense grid of taxable incomes, including one dollar either side of every +bracket threshold, for each filing status and each year from the first +modeled year through the projection years. It covers the pre-2018 schedule +(seven brackets topping out at 39.6%), the switch to the 2018 schedule, and +the uprated thresholds. It calls ``tax_at_main_rates``, the schedule loop that +``income_tax_main_rates`` and ``tax_on_taxable_income_at_main_rates`` use, on +the parameters of the package's shared, already-built system, so it builds no +simulation. +""" + +import numpy as np +import pytest + +from policyengine_us.system import system +from policyengine_us.tools.parameters import FIRST_MODELED_YEAR +from policyengine_us.variables.gov.irs.tax.federal_income.before_credits.tax_at_main_rates import ( + tax_at_main_rates, +) + +FILING_STATUSES = [ + "SINGLE", + "JOINT", + "SEPARATE", + "HEAD_OF_HOUSEHOLD", + "SURVIVING_SPOUSE", +] +YEARS = range(FIRST_MODELED_YEAR, 2031) + + +def bracket_node(year): + return system.parameters(f"{year}-01-01").gov.irs.income.bracket + + +def rates(bracket): + return [float(bracket.rates[b]) for b in bracket.rates.__iter__()] + + +def taxable_income_grid(bracket, status): + thresholds = [ + float(bracket.thresholds[b][status]) for b in bracket.rates.__iter__() + ] + finite = [t for t in thresholds if np.isfinite(t)] + edges = np.array([t + d for t in finite for d in (-1, 0, 1)]) + grid = np.concatenate([np.arange(0, 1_500_001, 250.0), edges]) + return np.unique(grid[grid >= 0]) + + +@pytest.mark.parametrize("year", YEARS) +@pytest.mark.parametrize("status", FILING_STATUSES) +def test_tax_never_falls_as_taxable_income_rises(year, status): + bracket = bracket_node(year) + income = taxable_income_grid(bracket, status) + tax = tax_at_main_rates(income, status, bracket) + assert tax[0] == 0 + step = np.diff(tax) + # A rise of d dollars of taxable income raises the tax by between zero + # and the top rate times d. + top_rate = max(rates(bracket)) + rise = np.diff(income) + assert np.all(step >= -1e-9), (year, status, income[1:][step < -1e-9]) + assert np.all(step <= top_rate * rise + 1e-6) + + +def test_pre_2018_schedule_is_the_seven_pre_tcja_rates(): + """2015-2017 use 10% to 39.6%; 2018 starts the 10% to 37% schedule.""" + for year in range(FIRST_MODELED_YEAR, 2018): + assert rates(bracket_node(year)) == [ + 0.10, + 0.15, + 0.25, + 0.28, + 0.33, + 0.35, + 0.396, + ], year + assert rates(bracket_node(2018)) == [0.10, 0.12, 0.22, 0.24, 0.32, 0.35, 0.37] From ef66d861ae3bc0dd59c8ed33fd22fd7490fe426f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 2 Nov 2026 19:30:34 -0500 Subject: [PATCH 2/3] Link the 2015-2017 bracket references to their tables Section 3.01 of each Rev. Proc. starts on the linked page: Rev. Proc. 2016-55 on page 7, and 2015-53 and 2014-61 on page 5. Co-Authored-By: Claude Opus 5.5 --- policyengine_us/parameters/gov/irs/income/bracket.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/policyengine_us/parameters/gov/irs/income/bracket.yaml b/policyengine_us/parameters/gov/irs/income/bracket.yaml index 108fb4be4bc..9cf4b6106c5 100644 --- a/policyengine_us/parameters/gov/irs/income/bracket.yaml +++ b/policyengine_us/parameters/gov/irs/income/bracket.yaml @@ -71,11 +71,11 @@ thresholds: - title: 2019 IRS data release href: https://www.irs.gov/pub/irs-drop/rp-18-57.pdf - title: 2017 IRS data release, Rev. Proc. 2016-55 section 3.01 - href: https://www.irs.gov/pub/irs-drop/rp-16-55.pdf + href: https://www.irs.gov/pub/irs-drop/rp-16-55.pdf#page=7 - title: 2016 IRS data release, Rev. Proc. 2015-53 section 3.01 - href: https://www.irs.gov/pub/irs-drop/rp-15-53.pdf + href: https://www.irs.gov/pub/irs-drop/rp-15-53.pdf#page=5 - title: 2015 IRS data release, Rev. Proc. 2014-61 section 3.01 - href: https://www.irs.gov/pub/irs-drop/rp-14-61.pdf + href: https://www.irs.gov/pub/irs-drop/rp-14-61.pdf#page=5 - title: CBO Tax Parameters and Effective Marginal Tax Rates | Feb 2024 # 1. Tax Parameters. href: https://www.cbo.gov/system/files/2024-02/53724-2024-02-Tax-Parameters.xlsx From 0074c0107ed2ff972db51ced4f7b6481fb5b4d35 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 2 Nov 2026 22:30:53 -0500 Subject: [PATCH 3/3] Apply the review's notes on the pre-2018 schedule Three comments said the amount taxed below 25 percent ends at the top of the 24 percent bracket; before 2018 it ends at the top of the 15 percent bracket. A 2017 case now pins that amount, a test checks that each threshold and rate exceeds the one before (the monotone-tax property can't catch a threshold typo), and the three Rev. Proc. titles give their page ranges. Co-Authored-By: Claude Opus 5.5 --- .../parameters/gov/irs/income/bracket.yaml | 6 +++--- .../test_main_rate_schedule_monotone.py | 14 ++++++++++++++ .../taxable_income_taxed_below_25_percent.yaml | 9 +++++++++ .../amt_tax_including_cg.py | 6 ++++-- .../irs/tax/federal_income/capital_gains/dwks19.py | 7 ++++--- .../taxable_income_taxed_below_25_percent.py | 12 ++++++------ 6 files changed, 40 insertions(+), 14 deletions(-) diff --git a/policyengine_us/parameters/gov/irs/income/bracket.yaml b/policyengine_us/parameters/gov/irs/income/bracket.yaml index 9cf4b6106c5..cdf5a97aa8e 100644 --- a/policyengine_us/parameters/gov/irs/income/bracket.yaml +++ b/policyengine_us/parameters/gov/irs/income/bracket.yaml @@ -70,11 +70,11 @@ thresholds: href: https://www.irs.gov/pub/irs-drop/rp-19-44.pdf - title: 2019 IRS data release href: https://www.irs.gov/pub/irs-drop/rp-18-57.pdf - - title: 2017 IRS data release, Rev. Proc. 2016-55 section 3.01 + - title: "2017 IRS data release, Rev. Proc. 2016-55 section 3.01 #page 7-8" href: https://www.irs.gov/pub/irs-drop/rp-16-55.pdf#page=7 - - title: 2016 IRS data release, Rev. Proc. 2015-53 section 3.01 + - title: "2016 IRS data release, Rev. Proc. 2015-53 section 3.01 #page 5-7" href: https://www.irs.gov/pub/irs-drop/rp-15-53.pdf#page=5 - - title: 2015 IRS data release, Rev. Proc. 2014-61 section 3.01 + - title: "2015 IRS data release, Rev. Proc. 2014-61 section 3.01 #page 5-6" href: https://www.irs.gov/pub/irs-drop/rp-14-61.pdf#page=5 - title: CBO Tax Parameters and Effective Marginal Tax Rates | Feb 2024 # 1. Tax Parameters. diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/before_credits/test_main_rate_schedule_monotone.py b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/before_credits/test_main_rate_schedule_monotone.py index 2019a7ff2ad..689d89cdf96 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/before_credits/test_main_rate_schedule_monotone.py +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/before_credits/test_main_rate_schedule_monotone.py @@ -77,3 +77,17 @@ def test_pre_2018_schedule_is_the_seven_pre_tcja_rates(): 0.396, ], year assert rates(bracket_node(2018)) == [0.10, 0.12, 0.22, 0.24, 0.32, 0.35, 0.37] + + +@pytest.mark.parametrize("year", YEARS) +@pytest.mark.parametrize("status", FILING_STATUSES) +def test_thresholds_and_rates_strictly_rise(year, status): + """The clamp in amounts_in_brackets keeps the tax monotone whatever the + thresholds are, so the property above can't catch a threshold typo. This + checks the data: each threshold and each rate exceeds the one before.""" + bracket = bracket_node(year) + keys = list(bracket.rates.__iter__()) + t = [float(bracket.thresholds[b][status]) for b in keys] + r = rates(bracket) + assert all(a < b for a, b in zip(t, t[1:])), (year, status, t) + assert all(a < b for a, b in zip(r, r[1:])), (year, r) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/taxable_income_taxed_below_25_percent.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/taxable_income_taxed_below_25_percent.yaml index 5ec59d16119..b40a612a45e 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/taxable_income_taxed_below_25_percent.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/capital_gains/taxable_income_taxed_below_25_percent.yaml @@ -89,3 +89,12 @@ output: # The 22 percent bracket ends at $103,350. taxable_income_taxed_below_25_percent: 103_350 + +- name: 2017 single filer, the pre-2018 schedule taxes income below 25 percent only to the 15 percent bracket top + period: 2017 + input: + filing_status: SINGLE + taxable_income_plus_section_911_exclusion: 43_950 + output: + # Rev. Proc. 2016-55 ends the 2017 single 15 percent bracket at $37,950. + taxable_income_taxed_below_25_percent: 37_950 diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/amt_tax_including_cg.py b/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/amt_tax_including_cg.py index 2ec9cec3888..8473d447f6c 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/amt_tax_including_cg.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/alternative_minimum_tax/amt_tax_including_cg.py @@ -81,8 +81,10 @@ def formula(tax_unit, period, parameters): # Worksheet line 21 (as figured for the regular tax), the amount the # regular tax taxes at the regular rates (26 U.S.C. 1(h)(1)(A)). # With 28 percent rate or unrecaptured section 1250 gain, Schedule D - # Tax Worksheet line 21 caps line 14 at the top of the 24 percent - # bracket (but not below line 18), so it can be less than Line 20. + # Tax Worksheet line 21 caps line 14 at the top of the highest bracket + # with a rate below 25 percent (the 15 percent bracket before 2018, the + # 24 percent bracket from 2018), but not below line 18, so it can be + # less than Line 20. # Without them, it is line 14, the same as QDCG Worksheet line 5 and # Line 20. For a Form 2555 filer it reflects the regular tax capital # gain excess, not the AMT one. diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/dwks19.py b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/dwks19.py index 4e3e4d98268..ec0745df253 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/dwks19.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/dwks19.py @@ -37,9 +37,10 @@ def formula(tax_unit, period, parameters): ).line_10 # Line 18: line 1 less line 10 (section 1(h)(1)(A)(i)). line_18 = max_(0, dwks01 - dwks10) - # Line 19: the smaller of line 1 or the top of the 24 percent - # bracket, the taxable income taxed at a rate below 25 percent - # (section 1(h)(1)(A)(ii)(I)). + # Line 19: the smaller of line 1 or the top of the highest bracket + # with a rate below 25 percent (the 15 percent bracket before 2018, + # the 24 percent bracket from 2018), the taxable income taxed at a + # rate below 25 percent (section 1(h)(1)(A)(ii)(I)). line_19 = tax_unit("taxable_income_taxed_below_25_percent", period) # Line 20: the smaller of line 14 or line 19 (section 1(h)(1)(A)(ii)). line_20 = min_(dwks14, line_19) diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/taxable_income_taxed_below_25_percent.py b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/taxable_income_taxed_below_25_percent.py index 41e8ee7d39c..9a2c8b03c98 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/taxable_income_taxed_below_25_percent.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/capital_gains/taxable_income_taxed_below_25_percent.py @@ -11,12 +11,12 @@ class taxable_income_taxed_below_25_percent(Variable): unit = USD documentation = ( "The amount of taxable income that the regular rate schedule taxes at " - "a rate below 25 percent: under the 10, 12, 22, 24, 32, 35 and 37 " - "percent schedule, taxable income up to the top of the 24 percent " - "bracket. Section 1(h)(1)(A) taxes capital gain at the regular rates " - "up to this amount, or taxable income less adjusted net capital gain " - "if less. Schedule D Tax Worksheet line 19. For a taxpayer excluding " - "foreign earned income, taxable income includes the excluded amount." + "a rate below 25 percent: taxable income up to the top of the 15 " + "percent bracket before 2018 and of the 24 percent bracket from 2018. " + "Section 1(h)(1)(A) taxes capital gain at the regular rates up to this " + "amount, or taxable income less adjusted net capital gain if less. " + "Schedule D Tax Worksheet line 19. For a taxpayer excluding foreign " + "earned income, taxable income includes the excluded amount." ) definition_period = YEAR reference = [