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-soi-agi-year-keying.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Keyed the IRS SOI AGI-by-size calibration amounts to 2020, the tax year of the IRS SOI Table 1.1 values they hold, instead of 2015, where uprating had put the 2020 amounts 40% above the published table and every later year 40% above its correctly uprated value; and corrected the source citations for the SOI AGI and return-count scales.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ metadata:
amount_unit: return
threshold_unit: currency-USD
reference:
- title: IRS SOI 2021 (20in11si.xls)
- title: IRS SOI Tax Stats - Publication 1304 Table 1.1, tax year 2021, number of returns
href: https://www.irs.gov/pub/irs-soi/21in11si.xls
# From https://www.irs.gov/statistics/soi-tax-stats-individual-statistical-tables-by-size-of-adjusted-gross-income
brackets:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ metadata:
amount_unit: currency-USD
threshold_unit: currency-USD
reference:
- title: IRS SOI PUF 2015 (20in11.xls)
- title: IRS SOI Tax Stats - Publication 1304 Table 1.1, tax year 2020, adjusted gross income less deficit
href: https://www.irs.gov/pub/irs-soi/20in11si.xls
# From https://www.irs.gov/statistics/soi-tax-stats-individual-statistical-tables-by-size-of-adjusted-gross-income
brackets:
- threshold:
2015-01-01: 1
amount:
values:
2015-01-01: 24_087_842_000
2020-01-01: 24_087_842_000
metadata:
uprating: calibration.gov.cbo.income_by_source.adjusted_gross_income
- threshold:
Expand All @@ -19,116 +21,116 @@ brackets:
metadata:
uprating: calibration.gov.cbo.income_by_source.adjusted_gross_income
values:
2015-01-01: 76_144_251_000
2020-01-01: 76_144_251_000
- threshold:
2015-01-01: 10_000
amount:
metadata:
uprating: calibration.gov.cbo.income_by_source.adjusted_gross_income
values:
2015-01-01: 131_879_612_000
2020-01-01: 131_879_612_000
- threshold:
2015-01-01: 15_000
amount:
metadata:
uprating: calibration.gov.cbo.income_by_source.adjusted_gross_income
values:
2015-01-01: 175_213_815_000
2020-01-01: 175_213_815_000
- threshold:
2015-01-01: 20_000
amount:
metadata:
uprating: calibration.gov.cbo.income_by_source.adjusted_gross_income
values:
2015-01-01: 218_827_831_000
2020-01-01: 218_827_831_000
- threshold:
2015-01-01: 25_000
amount:
metadata:
uprating: calibration.gov.cbo.income_by_source.adjusted_gross_income
values:
2015-01-01: 261_305_433_000
2020-01-01: 261_305_433_000
- threshold:
2015-01-01: 30_000
amount:
metadata:
uprating: calibration.gov.cbo.income_by_source.adjusted_gross_income
values:
2015-01-01: 583_890_556_000
2020-01-01: 583_890_556_000
- threshold:
2015-01-01: 40_000
amount:
metadata:
uprating: calibration.gov.cbo.income_by_source.adjusted_gross_income
values:
2015-01-01: 589_155_857_000
2020-01-01: 589_155_857_000
- threshold:
2015-01-01: 50_000
amount:
metadata:
uprating: calibration.gov.cbo.income_by_source.adjusted_gross_income
values:
2015-01-01: 1_388_153_537_000
2020-01-01: 1_388_153_537_000
- threshold:
2015-01-01: 75_000
amount:
metadata:
uprating: calibration.gov.cbo.income_by_source.adjusted_gross_income
values:
2015-01-01: 1_247_263_301_000
2020-01-01: 1_247_263_301_000
- threshold:
2015-01-01: 100_000
amount:
metadata:
uprating: calibration.gov.cbo.income_by_source.adjusted_gross_income
values:
2015-01-01: 3_060_065_884_000
2020-01-01: 3_060_065_884_000
- threshold:
2015-01-01: 200_000
amount:
metadata:
uprating: calibration.gov.cbo.income_by_source.adjusted_gross_income
values:
2015-01-01: 2_187_228_752_000
2020-01-01: 2_187_228_752_000
- threshold:
2015-01-01: 500_000
amount:
metadata:
uprating: calibration.gov.cbo.income_by_source.adjusted_gross_income
values:
2015-01-01: 833_075_077_000
2020-01-01: 833_075_077_000
- threshold:
2015-01-01: 1_000_000
amount:
metadata:
uprating: calibration.gov.cbo.income_by_source.adjusted_gross_income
values:
2015-01-01: 328_832_716_000
2020-01-01: 328_832_716_000
- threshold:
2015-01-01: 1_500_000
amount:
metadata:
uprating: calibration.gov.cbo.income_by_source.adjusted_gross_income
values:
2015-01-01: 187_713_632_000
2020-01-01: 187_713_632_000
- threshold:
2015-01-01: 2_000_000
amount:
metadata:
uprating: calibration.gov.cbo.income_by_source.adjusted_gross_income
values:
2015-01-01: 471_209_618_000
2020-01-01: 471_209_618_000
- threshold:
2015-01-01: 5_000_000
amount:
metadata:
uprating: calibration.gov.cbo.income_by_source.adjusted_gross_income
values:
2015-01-01: 278_473_308_000
2020-01-01: 278_473_308_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
2020-01-01: 824_093_126_000
83 changes: 83 additions & 0 deletions policyengine_us/tests/core/test_soi_agi_band_keying.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""The IRS SOI AGI-band calibration scales hold their published values in the
tax year they come from.

``calibration.gov.irs.soi.agi.total_agi`` holds tax year 2020 values from IRS
SOI Publication 1304 Table 1.1 and ``number_of_returns`` holds tax year 2021
values. ``uprate_parameters`` grows each amount forward from its latest dated
value, so keying a scale to a year before its data inflates every later year:
``total_agi`` sat at 2015-01-01 and its 2020 amounts were about 40% above the
published table. ``CountryTaxBenefitSystem`` then backdates each parameter's
earliest value to 2015-01-01, so years before the first dated amount still
evaluate.
"""

from datetime import date
from pathlib import Path

import pytest
import yaml

from policyengine_us.system import system as SYSTEM

SOI_AGI_DIR = (
Path(__file__).resolve().parents[2] / "parameters/calibration/gov/irs/soi/agi"
)
CASES = {
"total_agi": (2020, "calibration.gov.cbo.income_by_source.adjusted_gross_income"),
"number_of_returns": (2021, "calibration.gov.census.populations.total"),
}


def _amount_values(name):
parameter = yaml.safe_load((SOI_AGI_DIR / f"{name}.yaml").read_text())
return [bracket["amount"]["values"] for bracket in parameter["brackets"]]


def _scale(name):
return getattr(SYSTEM.parameters.calibration.gov.irs.soi.agi, name)


def _uprating_index(path):
node = SYSTEM.parameters
for key in path.split("."):
node = getattr(node, key)
return node


@pytest.mark.parametrize("name", CASES)
def test_soi_agi_band_holds_published_values_in_its_tax_year(name):
tax_year, _ = CASES[name]
key = date(tax_year, 1, 1)
amounts = _amount_values(name)

assert all(key in values for values in amounts), f"{name} lacks {key} amounts"
assert _scale(name)(f"{tax_year}-01-01").amounts == [
values[key] for values in amounts
]


@pytest.mark.parametrize("name", CASES)
def test_soi_agi_band_uprates_from_its_latest_value(name):
_, index_path = CASES[name]
latest = max(max(values) for values in _amount_values(name)).year
scale = _scale(name)
index = _uprating_index(index_path)
anchor = f"{latest}-01-01"
later = f"{latest + 5}-01-01"
growth = index(later) / index(anchor)

assert scale(later).amounts == pytest.approx(
[amount * growth for amount in scale(anchor).amounts]
)


@pytest.mark.parametrize("name", CASES)
def test_soi_agi_band_evaluates_before_its_first_value(name):
earliest = min(min(values) for values in _amount_values(name)).year
scale = _scale(name)
at_earliest = scale(f"{earliest}-01-01")

for year in range(2015, earliest):
earlier = scale(f"{year}-01-01")
assert earlier.thresholds == at_earliest.thresholds, year
assert earlier.amounts == at_earliest.amounts, year
33 changes: 33 additions & 0 deletions policyengine_us/tests/test_parameter_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@
SOI_LONG_TERM_CAPITAL_GAINS_PATH = (
PARAMETERS_DIR / "calibration/gov/irs/soi/long_term_capital_gains.yaml"
)
SOI_AGI_DIR = PARAMETERS_DIR / "calibration/gov/irs/soi/agi"
# IRS SOI Publication 1304 Table 1.1, "$1 under $5,000" and "$10,000,000 or
# more" rows: column 3 (AGI less deficit, published in thousands, stored here in
# dollars) for tax year 2020 and column 1 (number of returns) for tax year 2021.
SOI_AGI_BAND_TABLES = {
"total_agi.yaml": (
date(2020, 1, 1),
"https://www.irs.gov/pub/irs-soi/20in11si.xls",
24_087_842_000,
824_093_126_000,
),
"number_of_returns.yaml": (
date(2021, 1, 1),
"https://www.irs.gov/pub/irs-soi/21in11si.xls",
8_487_025,
45_404,
),
}
MI_INCOME_TAX_RATE_PATH = PARAMETERS_DIR / "gov/states/mi/tax/income/rate.yaml"
SOI_LONG_TERM_CAPITAL_GAINS_ANCHORS = {
date(2015, 1, 1): 733_313_255_000,
Expand Down Expand Up @@ -150,6 +168,21 @@ def test_soi_long_term_capital_gains_uses_latest_publication_1304_anchor():
)


def test_soi_agi_bands_are_keyed_to_their_table_1_1_tax_year():
for filename, (period, href, lowest, highest) in SOI_AGI_BAND_TABLES.items():
parameter = yaml.safe_load((SOI_AGI_DIR / filename).read_text())
amounts = [bracket["amount"]["values"] for bracket in parameter["brackets"]]

assert len(amounts) == 18, filename
for index, values in enumerate(amounts):
assert period in values, f"{filename} bracket {index} lacks {period}"
assert amounts[0][period] == lowest, filename
assert amounts[-1][period] == highest, filename
reference = parameter["metadata"]["reference"][0]
assert reference["href"] == href, filename
assert f"tax year {period.year}" in reference["title"], filename


def test_mi_2026_income_tax_rate_uses_official_annual_determination():
parameter = yaml.safe_load(MI_INCOME_TAX_RATE_PATH.read_text())

Expand Down
Loading