diff --git a/changelog.d/fix-soi-agi-year-keying.fixed.md b/changelog.d/fix-soi-agi-year-keying.fixed.md new file mode 100644 index 00000000000..2a79bd90115 --- /dev/null +++ b/changelog.d/fix-soi-agi-year-keying.fixed.md @@ -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. diff --git a/policyengine_us/parameters/calibration/gov/irs/soi/agi/number_of_returns.yaml b/policyengine_us/parameters/calibration/gov/irs/soi/agi/number_of_returns.yaml index 82ce792d802..432b2282a60 100644 --- a/policyengine_us/parameters/calibration/gov/irs/soi/agi/number_of_returns.yaml +++ b/policyengine_us/parameters/calibration/gov/irs/soi/agi/number_of_returns.yaml @@ -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: diff --git a/policyengine_us/parameters/calibration/gov/irs/soi/agi/total_agi.yaml b/policyengine_us/parameters/calibration/gov/irs/soi/agi/total_agi.yaml index a5753724b4d..df1ba4a5cc0 100644 --- a/policyengine_us/parameters/calibration/gov/irs/soi/agi/total_agi.yaml +++ b/policyengine_us/parameters/calibration/gov/irs/soi/agi/total_agi.yaml @@ -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: @@ -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 diff --git a/policyengine_us/tests/core/test_soi_agi_band_keying.py b/policyengine_us/tests/core/test_soi_agi_band_keying.py new file mode 100644 index 00000000000..511510b1042 --- /dev/null +++ b/policyengine_us/tests/core/test_soi_agi_band_keying.py @@ -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 diff --git a/policyengine_us/tests/test_parameter_files.py b/policyengine_us/tests/test_parameter_files.py index 1ce87fc239b..06f47a58368 100644 --- a/policyengine_us/tests/test_parameter_files.py +++ b/policyengine_us/tests/test_parameter_files.py @@ -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, @@ -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())