Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/fix-ar-low-income-duplicate-threshold.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Corrected the 2024 Arkansas low-income tax table for head of household and surviving spouse filers with two or more dependents, whose $92 row shared the $24,200 threshold of the $104 row and was summed into a $196 row; removed a duplicated top bracket from the IRS SOI AGI-by-size calibration scale; and added tests that fail when any parameter scale repeats a threshold (other than +inf) or an Arkansas low-income table stops rising with AGI.
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,3 @@ brackets:
uprating: calibration.gov.cbo.income_by_source.adjusted_gross_income
values:
2015-01-01: 824_093_126_000
- threshold:
2015-01-01: 10_000_000
amount:
metadata:
uprating: calibration.gov.cbo.income_by_source.adjusted_gross_income
values:
2015-01-01: 824_093_126_000
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ metadata:
- title: Arkansas Income Tax Instructions, 2021, Low Income Tax Tables
href: https://www.dfa.arkansas.gov/wp-content/uploads/2021_AR1000F_and_AR1000NR_Instructions.pdf#page=26
- title: Arkansas Income Tax Instructions, 2023, Low Income Tax Tables
href: https://www.dfa.arkansas.gov/wp-content/uploads/2023_AR1000F_and_AR1000NR_Instructions.pdf#page=25
href: https://www.dfa.arkansas.gov/wp-content/uploads/2023_AR1000F_and_AR1000NR_Instructions.pdf#page=29
- title: Arkansas Income Tax Instructions, 2024, Low Income Tax Tables
href: https://www.dfa.arkansas.gov/wp-content/uploads/2024_AR1000F_and_AR1000NR_Instructions.pdf#page=24
href: https://www.dfa.arkansas.gov/wp-content/uploads/2024_AR1000F_and_AR1000NR_Instructions.pdf#page=28
- title: Arkansas Income Tax Instructions, 2025, Low Income Tax Tables
href: https://www.dfa.arkansas.gov/wp-content/uploads/TaxTables_FI_2025.pdf#page=2
brackets:
Expand Down Expand Up @@ -58,7 +58,7 @@ brackets:
2021-01-01: 22_125
2022-01-01: 22_789
2023-01-01: 23_472
2024-01-01: 24_200
2024-01-01: 24_176
2025-01-01: 24_818
amount:
2015-01-01: 99
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ metadata:
- title: Arkansas Income Tax Instructions, 2021, Low Income Tax Tables
href: https://www.dfa.arkansas.gov/wp-content/uploads/2021_AR1000F_and_AR1000NR_Instructions.pdf#page=26
- title: Arkansas Income Tax Instructions, 2023, Low Income Tax Tables
href: https://www.dfa.arkansas.gov/wp-content/uploads/2023_AR1000F_and_AR1000NR_Instructions.pdf#page=25
href: https://www.dfa.arkansas.gov/wp-content/uploads/2023_AR1000F_and_AR1000NR_Instructions.pdf#page=29
- title: Arkansas Income Tax Instructions, 2024, Low Income Tax Tables
href: https://www.dfa.arkansas.gov/wp-content/uploads/2024_AR1000F_and_AR1000NR_Instructions.pdf#page=24
href: https://www.dfa.arkansas.gov/wp-content/uploads/2024_AR1000F_and_AR1000NR_Instructions.pdf#page=28
- title: Arkansas Income Tax Instructions, 2025, Low Income Tax Tables
href: https://www.dfa.arkansas.gov/wp-content/uploads/TaxTables_FI_2025.pdf#page=2
brackets:
Expand Down Expand Up @@ -58,7 +58,7 @@ brackets:
2021-01-01: 22_125
2022-01-01: 22_789
2023-01-01: 23_472
2024-01-01: 24_200
2024-01-01: 24_176
2025-01-01: 24_818
amount:
2015-01-01: 99
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,3 +334,252 @@
state_code: AR
output:
ar_uses_low_income_tax_tables: false

# The 2024 Head of Household/Surviving Spouse with 2 or More Dependents table
# (2024 AR1000F/AR1000NR instructions, PDF page 28) reads: $0-$24,176 -> $0,
# $24,177-$24,200 -> $92, $24,201-$24,300 -> $104, $24,301-$24,400 -> $116,
# $24,401-$24,500 -> $129. The $92 row's threshold was once keyed at $24,200,
# equal to the next row's, and policyengine-core sums the amounts of brackets
# with equal thresholds, so $24,201-$24,300 got $196 and the $92 row vanished.
# One single-member tax unit per income covers every row edge in the range.
- name: Case 19, 2024 head of household with 2 dependents from $24,100 to $24,401.
period: 2024
input:
people:
person1:
ar_agi_joint: 24_100
person2:
ar_agi_joint: 24_176
person3:
ar_agi_joint: 24_177
person4:
ar_agi_joint: 24_200
person5:
ar_agi_joint: 24_201
person6:
ar_agi_joint: 24_250
person7:
ar_agi_joint: 24_300
person8:
ar_agi_joint: 24_301
person9:
ar_agi_joint: 24_400
person10:
ar_agi_joint: 24_401
tax_units:
tax_unit1:
members: [person1]
filing_status: HEAD_OF_HOUSEHOLD
tax_unit_dependents: 2
tax_unit2:
members: [person2]
filing_status: HEAD_OF_HOUSEHOLD
tax_unit_dependents: 2
tax_unit3:
members: [person3]
filing_status: HEAD_OF_HOUSEHOLD
tax_unit_dependents: 2
tax_unit4:
members: [person4]
filing_status: HEAD_OF_HOUSEHOLD
tax_unit_dependents: 2
tax_unit5:
members: [person5]
filing_status: HEAD_OF_HOUSEHOLD
tax_unit_dependents: 2
tax_unit6:
members: [person6]
filing_status: HEAD_OF_HOUSEHOLD
tax_unit_dependents: 2
tax_unit7:
members: [person7]
filing_status: HEAD_OF_HOUSEHOLD
tax_unit_dependents: 2
tax_unit8:
members: [person8]
filing_status: HEAD_OF_HOUSEHOLD
tax_unit_dependents: 2
tax_unit9:
members: [person9]
filing_status: HEAD_OF_HOUSEHOLD
tax_unit_dependents: 2
tax_unit10:
members: [person10]
filing_status: HEAD_OF_HOUSEHOLD
tax_unit_dependents: 2
households:
household:
members: [person1, person2, person3, person4, person5, person6, person7, person8, person9, person10]
state_code: AR
output:
ar_low_income_tax_joint: [0, 0, 92, 92, 104, 104, 104, 116, 116, 129]

- name: Case 20, 2024 surviving spouse with 2 dependents from $24,100 to $24,401.
period: 2024
input:
people:
person1:
ar_agi_joint: 24_100
person2:
ar_agi_joint: 24_176
person3:
ar_agi_joint: 24_177
person4:
ar_agi_joint: 24_200
person5:
ar_agi_joint: 24_201
person6:
ar_agi_joint: 24_250
person7:
ar_agi_joint: 24_300
person8:
ar_agi_joint: 24_301
person9:
ar_agi_joint: 24_400
person10:
ar_agi_joint: 24_401
tax_units:
tax_unit1:
members: [person1]
filing_status: SURVIVING_SPOUSE
tax_unit_dependents: 2
tax_unit2:
members: [person2]
filing_status: SURVIVING_SPOUSE
tax_unit_dependents: 2
tax_unit3:
members: [person3]
filing_status: SURVIVING_SPOUSE
tax_unit_dependents: 2
tax_unit4:
members: [person4]
filing_status: SURVIVING_SPOUSE
tax_unit_dependents: 2
tax_unit5:
members: [person5]
filing_status: SURVIVING_SPOUSE
tax_unit_dependents: 2
tax_unit6:
members: [person6]
filing_status: SURVIVING_SPOUSE
tax_unit_dependents: 2
tax_unit7:
members: [person7]
filing_status: SURVIVING_SPOUSE
tax_unit_dependents: 2
tax_unit8:
members: [person8]
filing_status: SURVIVING_SPOUSE
tax_unit_dependents: 2
tax_unit9:
members: [person9]
filing_status: SURVIVING_SPOUSE
tax_unit_dependents: 2
tax_unit10:
members: [person10]
filing_status: SURVIVING_SPOUSE
tax_unit_dependents: 2
households:
household:
members: [person1, person2, person3, person4, person5, person6, person7, person8, person9, person10]
state_code: AR
output:
ar_low_income_tax_joint: [0, 0, 92, 92, 104, 104, 104, 116, 116, 129]

- name: Case 21, 2024 head of household and surviving spouse with 2 dependents in the last table rows.
period: 2024
input:
people:
person1:
ar_agi_joint: 28_100
person2:
ar_agi_joint: 28_101
person3:
ar_agi_joint: 28_200
person4:
ar_agi_joint: 28_100
person5:
ar_agi_joint: 28_101
person6:
ar_agi_joint: 28_200
tax_units:
tax_unit1:
members: [person1]
filing_status: HEAD_OF_HOUSEHOLD
tax_unit_dependents: 2
tax_unit2:
members: [person2]
filing_status: HEAD_OF_HOUSEHOLD
tax_unit_dependents: 2
tax_unit3:
members: [person3]
filing_status: HEAD_OF_HOUSEHOLD
tax_unit_dependents: 2
tax_unit4:
members: [person4]
filing_status: SURVIVING_SPOUSE
tax_unit_dependents: 2
tax_unit5:
members: [person5]
filing_status: SURVIVING_SPOUSE
tax_unit_dependents: 2
tax_unit6:
members: [person6]
filing_status: SURVIVING_SPOUSE
tax_unit_dependents: 2
households:
household:
members: [person1, person2, person3, person4, person5, person6]
state_code: AR
output:
# $28,001-$28,100 -> $575; $28,101-$28,200 -> $588.
ar_low_income_tax_joint: [575, 588, 588, 575, 588, 588]

- name: Case 22, 2024 head of household and surviving spouse with 2 dependents above the $28,200 cutoff.
period: 2024
input:
people:
person1:
ar_agi_joint: 28_201
person2:
ar_agi_joint: 28_201
tax_units:
tax_unit1:
members: [person1]
filing_status: HEAD_OF_HOUSEHOLD
tax_unit_dependents: 2
tax_unit2:
members: [person2]
filing_status: SURVIVING_SPOUSE
tax_unit_dependents: 2
households:
household:
members: [person1, person2]
state_code: AR
output:
ar_uses_low_income_tax_tables: [false, false]

- name: Case 23, 2024 head of household with 2 dependents pays the table tax before credits.
period: 2024
input:
people:
person1:
ar_agi_joint: 24_190
person2:
ar_agi_joint: 24_250
tax_units:
tax_unit1:
members: [person1]
filing_status: HEAD_OF_HOUSEHOLD
tax_unit_dependents: 2
tax_unit2:
members: [person2]
filing_status: HEAD_OF_HOUSEHOLD
tax_unit_dependents: 2
households:
household:
members: [person1, person2]
state_code: AR
output:
ar_low_income_tax_joint: [92, 104]
ar_income_tax_before_non_refundable_credits_joint: [92, 104]
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Invariants of the Arkansas low-income tax tables, checked at every integer AGI.

`ar_low_income_tax_joint` evaluates these scales with `right=True`. The
published tables charge more tax at higher AGI until a cutoff, above which
filers use the regular tax table (encoded as an infinite amount). The head of
household and surviving spouse files encode the same published column
("Filing Status 3 or 6"), so they must agree. A 2024 data error broke the
first invariant: two brackets shared a threshold, core summed them, and the
table charged $196 at $24,201-$24,300 and then $116 at $24,301-$24,400.
"""

import numpy as np
import pytest
from policyengine_core.parameters import ParameterScale

from policyengine_us.system import system

TABLES = system.parameters.gov.states.ar.tax.income.rates.low_income_tax_tables
SCALES = {
node.name.removeprefix(TABLES.name + "."): node
for node in TABLES.get_descendants()
if isinstance(node, ParameterScale)
}
YEARS = range(2015, 2031)


def _tax_by_integer_agi(scale, year):
at_instant = scale(f"{year}-01-01")
top = max(t for t in at_instant.thresholds if np.isfinite(t))
agi = np.arange(0, int(top) + 2)
return at_instant.calc(agi, right=True)


def test_tables_cover_every_filing_status_and_dependent_tier():
assert sorted(SCALES) == [
"head_of_household.no_or_one_dependent",
"head_of_household.two_or_more_dependents",
"joint.no_or_one_dependent",
"joint.two_or_more_dependents",
"single",
"surviving_spouse.no_or_one_dependent",
"surviving_spouse.two_or_more_dependents",
]


@pytest.mark.parametrize("year", YEARS)
@pytest.mark.parametrize("table", sorted(SCALES))
def test_tax_rises_with_agi_until_the_cutoff(table, year):
tax = _tax_by_integer_agi(SCALES[table], year)
finite = np.isfinite(tax)

assert finite[0] and not finite[-1]
# Once AGI passes the cutoff it never re-enters the table.
assert not np.any(finite[1:] & ~finite[:-1])
assert np.all(np.diff(tax[finite]) >= 0)


@pytest.mark.parametrize("year", YEARS)
@pytest.mark.parametrize("tier", ["no_or_one_dependent", "two_or_more_dependents"])
def test_head_of_household_and_surviving_spouse_tables_agree(tier, year):
head_of_household = _tax_by_integer_agi(SCALES[f"head_of_household.{tier}"], year)
surviving_spouse = _tax_by_integer_agi(SCALES[f"surviving_spouse.{tier}"], year)

np.testing.assert_array_equal(head_of_household, surviving_spouse)
Loading
Loading