Skip to content

Commit d9f7704

Browse files
authored
Merge pull request #515 from PSLmodels/issue-502-future-revenue-analysis
Add CBO-vs-TMD revenue-level sanity test (issue #502)
2 parents ab98dfb + 7a3b674 commit d9f7704

2 files changed

Lines changed: 138 additions & 0 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Expected calendar-year levels for filer (PUF / data_source==1) records on
2+
# the TMD file, compared against CBO's individual income tax microsimulation
3+
# model output. Used by tests/test_revenue_levels_cbo.py.
4+
#
5+
# DATA SOURCE
6+
# CBO, "The Budget and Economic Outlook: 2026 to 2036," supplemental
7+
# data file 51138-2026-02-Revenue.xlsx (publication 61882), sheet
8+
# "3.Individual Income Tax Details", February 2026.
9+
# https://www.cbo.gov/publication/61882
10+
#
11+
# DEFINITIONS
12+
# n_returns_mil : "Number of returns (millions)"
13+
# (returns filed in 2022 plus estimates of future
14+
# additional filers; includes dependents).
15+
# agi_bil : "Adjusted gross income" (calendar-year, billions).
16+
# iitax_bil : "Individual income tax liability" line
17+
# = "Income tax after credits" + "Net investment
18+
# income tax". Matches TaxCalc `iitax`, which also
19+
# equals income tax after credits + NIIT.
20+
# This is NOT the FY "Individual income tax receipts"
21+
# line (the MTS-derived cash-receipts figure, which
22+
# also includes Form 1041, Form 1042, refund-netting,
23+
# and FY-vs-CY timing — see issue #502).
24+
#
25+
# COMPARABILITY NOTE
26+
# CBO's individual income tax microsimulation model is built on the
27+
# same SOI 2022 PUF (Pub. 1304) sample that underlies TMD. Both sides
28+
# are calendar-year, 1040-universe liability — apples to apples. The
29+
# 2022 actual matches TMD essentially exactly (gaps under 0.6% on
30+
# each of returns, AGI, and iitax).
31+
32+
2022: # actual
33+
n_returns_mil: 161.3
34+
agi_bil: 14821.8
35+
iitax_bil: 2149.6
36+
2026: # projected
37+
n_returns_mil: 169.5
38+
agi_bil: 18810.4
39+
iitax_bil: 2665.0
40+
2031: # projected
41+
n_returns_mil: 175.8
42+
agi_bil: 22272.4
43+
iitax_bil: 3310.2
44+
2036: # projected
45+
n_returns_mil: 180.5
46+
agi_bil: 26787.8
47+
iitax_bil: 4103.3

tests/test_revenue_levels_cbo.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"""
2+
Sanity-check that weighted PUF (filer) totals on the TMD file match CBO's
3+
individual income tax microsimulation projections at four anchor years.
4+
5+
Tested aggregates: number of returns, AGI (`c00100`), and individual
6+
income tax liability (`iitax`).
7+
8+
Comparator is the calendar-year 1040-universe liability series in
9+
CBO's February 2026 Revenue file, sheet 3 — built from the same SOI 2022
10+
PUF the TMD file uses. See `tests/expected_cbo_levels_2022_data.yaml`
11+
for the exact CBO line items and the data source.
12+
13+
Tolerances widen with the projection horizon to reflect compounding
14+
growfactor uncertainty.
15+
"""
16+
17+
import yaml
18+
import pytest
19+
import taxcalc
20+
21+
from tmd.imputation_assumptions import TAXYEAR, CREDIT_CLAIMING
22+
23+
# Per-year relative tolerance for all three aggregates.
24+
RELTOL = {
25+
2022: 0.01,
26+
2026: 0.02,
27+
2031: 0.05,
28+
2036: 0.06,
29+
}
30+
31+
VARIABLES = ("n_returns_mil", "agi_bil", "iitax_bil")
32+
33+
DUMP = False # if True, always fail with full output table
34+
35+
36+
@pytest.mark.skipif(
37+
TAXYEAR != 2022,
38+
reason="expected values are calibrated to TAXYEAR=2022",
39+
)
40+
def test_revenue_levels_cbo(
41+
tests_folder, tmd_variables, tmd_weights_path, tmd_growfactors_path
42+
):
43+
epath = tests_folder / "expected_cbo_levels_2022_data.yaml"
44+
with open(epath, "r", encoding="utf-8") as f:
45+
exp = yaml.safe_load(f)
46+
47+
pol = taxcalc.Policy()
48+
pol.implement_reform(CREDIT_CLAIMING)
49+
rec = taxcalc.Records(
50+
data=tmd_variables,
51+
start_year=TAXYEAR,
52+
gfactors=taxcalc.GrowFactors(
53+
growfactors_filename=str(tmd_growfactors_path)
54+
),
55+
weights=str(tmd_weights_path),
56+
adjust_ratios=None,
57+
exact_calculations=True,
58+
weights_scale=1.0,
59+
)
60+
sim = taxcalc.Calculator(policy=pol, records=rec)
61+
62+
actual = {}
63+
for year in sorted(RELTOL):
64+
sim.advance_to_year(year)
65+
sim.calc_all()
66+
wt = sim.array("s006")
67+
puf = sim.array("data_source") == 1
68+
wpuf = wt * puf
69+
actual[year] = {
70+
"n_returns_mil": wpuf.sum() * 1e-6,
71+
"agi_bil": (wpuf * sim.array("c00100")).sum() * 1e-9,
72+
"iitax_bil": (wpuf * sim.array("iitax")).sum() * 1e-9,
73+
}
74+
75+
emsg = ""
76+
for year, tol in RELTOL.items():
77+
for var in VARIABLES:
78+
act = actual[year][var]
79+
ex = exp[year][var]
80+
reldiff = act / ex - 1.0
81+
if abs(reldiff) >= tol or DUMP:
82+
emsg += (
83+
f"\n{var:<14s} year={year} "
84+
f"act={act:10.3f} exp={ex:10.3f} "
85+
f"reldiff={reldiff:+7.4f} tol={tol:.3f}"
86+
)
87+
88+
if DUMP:
89+
assert False, f"test_revenue_levels_cbo DUMP output: {emsg}"
90+
if emsg:
91+
raise ValueError(emsg)

0 commit comments

Comments
 (0)