diff --git a/tests/expect_tmd_revenues b/tests/expect_tmd_revenues new file mode 100644 index 00000000..702f78c6 --- /dev/null +++ b/tests/expect_tmd_revenues @@ -0,0 +1,20 @@ +TMD vs CBO aggregate individual income tax revenue +(revenue in billions of calendar-year dollars) + +YEAR ACTUAL EXPECT PCTDIFF +2022 2149.4 2149.6 -0.01 +2023 2218.1 2171.9 +2.13 +2024 2407.9 2408.4 -0.02 +2025 2491.9 2493.7 -0.07 +2026 2637.4 2665.0 -1.04 +2027 2697.5 2776.8 -2.85 +2028 2757.4 2860.0 -3.59 +2029 2898.3 3039.1 -4.63 +2030 3049.1 3196.7 -4.62 +2031 3171.2 3310.2 -4.20 +2032 3302.1 3452.3 -4.35 +2033 3438.6 3599.4 -4.47 +2034 3581.8 3760.4 -4.75 +2035 3730.6 3929.3 -5.06 + +sum over 2022-2035 of squared PCTDIFF: iitax = 174.05 diff --git a/tests/expected_cbo_revenues.yaml b/tests/expected_cbo_revenues.yaml new file mode 100644 index 00000000..2bb6c85f --- /dev/null +++ b/tests/expected_cbo_revenues.yaml @@ -0,0 +1,62 @@ +# Expected calendar-year aggregate individual income tax revenue for each +# year in the 2022 through 2035 range. +# +# DATA SOURCE +# CBO, "The Budget and Economic Outlook: 2026 to 2036," supplemental +# data file 51138-2026-02-Revenue.xlsx (publication 61882), sheet +# "3.Individual Income Tax Details", February 2026. +# https://www.cbo.gov/publication/61882 +# This is the same workbook and the same sheet that underlie +# tests/expected_cbo_levels_2022_data.yaml. +# +# DEFINITIONS +# iitax_bil : "Individual income tax liability" line +# = "Income tax after credits" + "Net investment +# income tax". Calendar-year liability in +# billions of dollars. Matches TaxCalc `iitax`, +# which also equals income tax after credits +# plus NIIT. +# This is NOT the fiscal-year "Individual income tax +# receipts" line (the MTS-derived cash-receipts +# figure, which also includes Form 1041, Form +# 1042, refund-netting, and FY-vs-CY timing -- +# see issue #502). +# +# COMPARABILITY NOTE +# CBO's individual income tax microsimulation model is built on the +# same SOI 2022 PUF (Pub. 1304) sample that underlies TMD. Both +# sides are calendar-year, 1040-universe liability -- apples to +# apples. The 2022 actual matches TMD essentially exactly. +# +# Per the sheet's own column headings, only 2022 is an actual; every +# later year is a projection under CBO's February 2026 baseline, +# which reflects law in effect at that time. + +2022: # actual + iitax_bil: 2149.6 +2023: # projected + iitax_bil: 2171.9 +2024: # projected + iitax_bil: 2408.4 +2025: # projected + iitax_bil: 2493.7 +2026: # projected + iitax_bil: 2665.0 +2027: # projected + iitax_bil: 2776.8 +2028: # projected + iitax_bil: 2860.0 +2029: # projected + iitax_bil: 3039.1 +2030: # projected + iitax_bil: 3196.7 +2031: # projected + iitax_bil: 3310.2 +2032: # projected + iitax_bil: 3452.3 +2033: # projected + iitax_bil: 3599.4 +2034: # projected + iitax_bil: 3760.4 +2035: # projected + iitax_bil: 3929.3 diff --git a/tests/test_tmd_revenues.py b/tests/test_tmd_revenues.py new file mode 100644 index 00000000..ffe7b336 --- /dev/null +++ b/tests/test_tmd_revenues.py @@ -0,0 +1,144 @@ +""" +Compare weighted TMD aggregate individual income tax revenue against CBO's +individual income tax microsimulation projections for each calendar year +in the 2022 through 2035 range. + +Expected values are the calendar-year 1040-universe liability series in +CBO's February 2026 Revenue file, sheet 3, read from +`tests/expected_cbo_revenues.yaml` (see that file for the exact CBO line +item and the data source). Actual values are weighted totals of the +TaxCalc `iitax` variable over the filer (PUF / data_source==1) records on +the TMD file, which is the same universe the CBO series covers and the +same universe used by tests/test_revenue_levels_cbo.py. + +Unlike test_revenue_levels_cbo.py, this test applies no per-year +tolerance. Instead it writes the full year-by-year comparison to +`tests/actual_tmd_revenues` and requires that file to be identical to the +version-controlled `tests/expect_tmd_revenues` file. Any change in the +TMD file, its weights, or its growfactors that moves annual revenue will +therefore fail this test, showing exactly which years moved and by how +much. When the change is intended, copy `tests/actual_tmd_revenues` over +`tests/expect_tmd_revenues` and commit the difference. + +The written `tests/actual_tmd_revenues` file is removed when the test +passes, and is left in place when the test fails so that it can be +inspected and, if the new results are correct, copied. +""" + +import difflib +import yaml +import pytest +import taxcalc +from tmd.imputation_assumptions import TAXYEAR, SOI_IITAX_SPEC + +FIRST_YEAR = TAXYEAR +LAST_YEAR = 2035 + +ACTUAL_FILENAME = "actual_tmd_revenues" +EXPECT_FILENAME = "expect_tmd_revenues" + + +def revenue_table(actual, expect): + """ + Return, as a list of text lines, the year-by-year comparison of the + actual and expected income tax revenue dictionaries, followed by the + sum over years of the squared percentage differences. + """ + lines = [ + "TMD vs CBO aggregate individual income tax revenue", + "(revenue in billions of calendar-year dollars)", + "", + "YEAR" + "ACTUAL".rjust(11) + "EXPECT".rjust(11) + "PCTDIFF".rjust(9), + ] + sum_sq_pctdiff = 0.0 + for year in range(FIRST_YEAR, LAST_YEAR + 1): + act = actual[year] + exp = expect[year] + pctdiff = 100.0 * (act / exp - 1.0) + sum_sq_pctdiff += pctdiff * pctdiff + lines.append(f"{year:4d} {act:10.1f} {exp:10.1f} {pctdiff:+8.2f}") + lines.append("") + lines.append( + f"sum over {FIRST_YEAR}-{LAST_YEAR} of squared PCTDIFF: " + f"iitax = {sum_sq_pctdiff:.2f}" + ) + return lines + + +@pytest.mark.skipif( + TAXYEAR != 2022, + reason="expected values are calibrated to TAXYEAR=2022", +) +def test_tmd_revenues( + tests_folder, + tmd_variables, + tmd_weights_path, + tmd_growfactors_path, + policy_growfactors_path, +): + # read expected annual income tax revenue + epath = tests_folder / "expected_cbo_revenues.yaml" + with open(epath, "r", encoding="utf-8") as f: + edict = yaml.safe_load(f) + expect = {} + for year in range(FIRST_YEAR, LAST_YEAR + 1): + assert year in edict, f"{epath.name} has no {year} data" + expect[year] = edict[year]["iitax_bil"] + + # calculate actual annual income tax revenue on the TMD file + policy_gf = taxcalc.GrowFactors( + growfactors_filename=str(policy_growfactors_path) + ) + pol = taxcalc.Policy(gfactor=policy_gf) + pol.implement_reform(SOI_IITAX_SPEC) + rec = taxcalc.Records( + data=tmd_variables, + start_year=TAXYEAR, + gfactors=taxcalc.GrowFactors( + growfactors_filename=str(tmd_growfactors_path) + ), + weights=str(tmd_weights_path), + adjust_ratios=None, + exact_calculations=True, + weights_scale=1.0, + ) + sim = taxcalc.Calculator(policy=pol, records=rec) + + actual = {} + for year in range(FIRST_YEAR, LAST_YEAR + 1): + sim.advance_to_year(year) + sim.calc_all() + wght = sim.array("s006") * (sim.array("data_source") == 1) + actual[year] = (wght * sim.array("iitax")).sum() * 1e-9 + + # write actual output file + apath = tests_folder / ACTUAL_FILENAME + atext = "\n".join(revenue_table(actual, expect)) + "\n" + with open(apath, "w", encoding="utf-8") as f: + f.write(atext) + + # compare actual output file with expected output file + xpath = tests_folder / EXPECT_FILENAME + if not xpath.exists(): + raise ValueError( + f"tests/{EXPECT_FILENAME} file does not exist:\n" + f" if tests/{ACTUAL_FILENAME} contains correct results,\n" + f" copy it to tests/{EXPECT_FILENAME} and add it to git" + ) + with open(xpath, "r", encoding="utf-8") as f: + xtext = f.read() + if atext != xtext: + diffs = difflib.unified_diff( + xtext.splitlines(keepends=True), + atext.splitlines(keepends=True), + fromfile=f"tests/{EXPECT_FILENAME}", + tofile=f"tests/{ACTUAL_FILENAME}", + n=0, + ) + raise ValueError( + f"tests/{ACTUAL_FILENAME} differs from " + f"tests/{EXPECT_FILENAME}:\n" + "".join(diffs) + ) + + # remove actual output file because it is identical to expected file + apath.unlink()