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
20 changes: 20 additions & 0 deletions tests/expect_tmd_revenues
Original file line number Diff line number Diff line change
@@ -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
62 changes: 62 additions & 0 deletions tests/expected_cbo_revenues.yaml
Original file line number Diff line number Diff line change
@@ -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
144 changes: 144 additions & 0 deletions tests/test_tmd_revenues.py
Original file line number Diff line number Diff line change
@@ -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()
Loading