From 1c024623629a2a2d27a49c884d3a4ae00f2ce295 Mon Sep 17 00:00:00 2001 From: martinholmer Date: Tue, 8 Sep 2026 17:11:24 -0400 Subject: [PATCH 1/6] Grow weights using CBO returns (not CBO population) projection --- tests/expect_tmd_revenues | 28 ++++---- tests/expected_tax_exp_2022_data | 18 ++--- tests/test_imputed_variables.py | 8 +-- tests/test_weight_growth.py | 96 ++++++++++++++++++++++++++ tmd/areas/batch_weights.py | 6 +- tmd/areas/create_area_weights.py | 20 +++--- tmd/create_taxcalc_sampling_weights.py | 22 +++--- tmd/imputation_assumptions.py | 9 +++ tmd/storage/input/cbo26_returns.yaml | 51 ++++++++++++++ tmd/utils/weight_growth.py | 61 ++++++++++++++++ 10 files changed, 266 insertions(+), 53 deletions(-) create mode 100644 tests/test_weight_growth.py create mode 100644 tmd/storage/input/cbo26_returns.yaml create mode 100644 tmd/utils/weight_growth.py diff --git a/tests/expect_tmd_revenues b/tests/expect_tmd_revenues index 702f78c6..73cd3a1c 100644 --- a/tests/expect_tmd_revenues +++ b/tests/expect_tmd_revenues @@ -3,18 +3,18 @@ TMD vs CBO aggregate individual income tax revenue 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 +2023 2241.8 2171.9 +3.22 +2024 2441.3 2408.4 +1.36 +2025 2520.3 2493.7 +1.07 +2026 2685.1 2665.0 +0.75 +2027 2760.4 2776.8 -0.59 +2028 2835.8 2860.0 -0.85 +2029 2991.6 3039.1 -1.56 +2030 3158.0 3196.7 -1.21 +2031 3302.8 3310.2 -0.22 +2032 3451.1 3452.3 -0.03 +2033 3604.9 3599.4 +0.15 +2034 3765.1 3760.4 +0.12 +2035 3932.5 3929.3 +0.08 -sum over 2022-2035 of squared PCTDIFF: iitax = 174.05 +sum over 2022-2035 of squared PCTDIFF: iitax = 19.00 diff --git a/tests/expected_tax_exp_2022_data b/tests/expected_tax_exp_2022_data index 0df3c999..d4a019c5 100644 --- a/tests/expected_tax_exp_2022_data +++ b/tests/expected_tax_exp_2022_data @@ -1,9 +1,9 @@ -YR,KIND,EST= 2023 paytax 1431.0 -YR,KIND,EST= 2023 iitax 2332.3 -YR,KIND,EST= 2023 ctc 129.6 -YR,KIND,EST= 2023 eitc 66.9 -YR,KIND,EST= 2023 social_security_partial_taxability 47.3 -YR,KIND,EST= 2023 niit -47.4 -YR,KIND,EST= 2023 cgqd_tax_preference 179.6 -YR,KIND,EST= 2023 qbid 55.0 -YR,KIND,EST= 2023 salt 23.8 +YR,KIND,EST= 2023 paytax 1446.3 +YR,KIND,EST= 2023 iitax 2357.2 +YR,KIND,EST= 2023 ctc 130.9 +YR,KIND,EST= 2023 eitc 67.7 +YR,KIND,EST= 2023 social_security_partial_taxability 47.8 +YR,KIND,EST= 2023 niit -47.8 +YR,KIND,EST= 2023 cgqd_tax_preference 181.6 +YR,KIND,EST= 2023 qbid 55.6 +YR,KIND,EST= 2023 salt 24.0 diff --git a/tests/test_imputed_variables.py b/tests/test_imputed_variables.py index d0800c90..2ddd1d8a 100644 --- a/tests/test_imputed_variables.py +++ b/tests/test_imputed_variables.py @@ -56,7 +56,7 @@ def actual_results(rdf, bdf): # https://taxpolicycenter.org/taxvox/ # budget-laws-tax-cuts-overtime-and- # tips-are-popular-few-will-benefit - "exp_totben_2022": 24.42, + "exp_totben_2022": 24.86, "exp_affpct_2022": 8.78, "exp_affben_2022": 1423, }, @@ -73,7 +73,7 @@ def actual_results(rdf, bdf): # https://taxpolicycenter.org/taxvox/ # budget-laws-tax-cuts-overtime-and- # tips-are-popular-few-will-benefit - "exp_totben_2022": 7.19, + "exp_totben_2022": 7.32, "exp_affpct_2022": 2.64, "exp_affben_2022": 1397, }, @@ -88,7 +88,7 @@ def actual_results(rdf, bdf): # because the Tax Policy Center did not provide any # statistics for this new deduction. However, see the # following reform analysis for FOUR reforms. - "exp_totben_2022": 8.14, + "exp_totben_2022": 8.28, "exp_affpct_2022": 12.84, "exp_affben_2022": 324, }, @@ -108,7 +108,7 @@ def actual_results(rdf, bdf): # https://taxpolicycenter.org/model-estimates/T25-0257 # Note that the $1081 TPC estimate is derived by dividing # the all-unit average of $320 by the 0.296 affpct. - "exp_totben_2022": 60.07, + "exp_totben_2022": 61.15, "exp_affpct_2022": 28.97, "exp_affben_2022": 1061, }, diff --git a/tests/test_weight_growth.py b/tests/test_weight_growth.py new file mode 100644 index 00000000..10b70ff6 --- /dev/null +++ b/tests/test_weight_growth.py @@ -0,0 +1,96 @@ +""" +Test the cumulative weight growth factors used to extrapolate the TAXYEAR +sampling weights in tmd/create_taxcalc_sampling_weights.py and in +tmd/areas/create_area_weights.py. +""" + +import yaml +import pytest +from tmd.storage import STORAGE_FOLDER +from tmd.imputation_assumptions import TAXYEAR, POPULATION_FILE, RETURNS_FILE +from tmd.utils.weight_growth import cumulative_growth + +FIRST_YEAR = TAXYEAR +LAST_YEAR = TAXYEAR + 53 + + +def read_input_yaml(filename): + with open( + STORAGE_FOLDER / "input" / filename, "r", encoding="utf-8" + ) as ifile: + return yaml.safe_load(ifile.read()) + + +def test_growth_covers_all_weight_years(): + growth = cumulative_growth(FIRST_YEAR, LAST_YEAR) + assert set(growth) == set(range(FIRST_YEAR, LAST_YEAR + 1)) + assert growth[FIRST_YEAR] == 1.0 + # weights never shrink over the projection period + for year in range(FIRST_YEAR + 1, LAST_YEAR + 1): + assert growth[year] > 0.0 + + +@pytest.mark.skipif( + RETURNS_FILE is None, + reason="TAXYEAR has no tax return projection file", +) +def test_growth_tracks_return_projection(): + """ + Through the last projected return year, cumulative growth must equal + the growth in projected returns, because a sampling weight counts + returns. This is what keeps aggregate AGI and income tax revenue in + line with the CBO projections used in tests/test_tmd_revenues.py. + """ + rets = read_input_yaml(RETURNS_FILE) + growth = cumulative_growth(FIRST_YEAR, LAST_YEAR) + for year in sorted(rets): + expect = rets[year] / rets[FIRST_YEAR] + assert growth[year] == pytest.approx(expect, rel=1e-9) + + +@pytest.mark.skipif( + RETURNS_FILE is None, + reason="TAXYEAR has no tax return projection file", +) +def test_growth_reverts_to_population_after_projection(): + """ + After the last projected return year, returns per capita are held + fixed, so cumulative growth changes at the rate of population growth. + """ + pop = read_input_yaml(POPULATION_FILE) + rets = read_input_yaml(RETURNS_FILE) + last_rets_year = max(rets) + assert last_rets_year < LAST_YEAR, "no years left to test" + growth = cumulative_growth(FIRST_YEAR, LAST_YEAR) + for year in range(last_rets_year + 1, LAST_YEAR + 1): + expect = growth[year - 1] * pop[year] / pop[year - 1] + assert growth[year] == pytest.approx(expect, rel=1e-9) + + +def test_population_only_growth(): + """ + A None returns_file must reproduce pure population extrapolation, + which is the TAXYEAR=2021 behavior. + """ + pop = read_input_yaml(POPULATION_FILE) + growth = cumulative_growth(FIRST_YEAR, LAST_YEAR, returns_file=None) + for year in range(FIRST_YEAR, LAST_YEAR + 1): + expect = pop[year] / pop[FIRST_YEAR] + assert growth[year] == pytest.approx(expect, rel=1e-9) + + +@pytest.mark.skipif( + RETURNS_FILE is None, + reason="TAXYEAR has no tax return projection file", +) +def test_returns_grow_faster_than_population(): + """ + Guard on the premise of using returns rather than population: CBO + projects the filing share of the population to keep rising. + """ + pop = read_input_yaml(POPULATION_FILE) + rets = read_input_yaml(RETURNS_FILE) + last_rets_year = max(rets) + rets_growth = rets[last_rets_year] / rets[FIRST_YEAR] + pop_growth = pop[last_rets_year] / pop[FIRST_YEAR] + assert rets_growth > pop_growth diff --git a/tmd/areas/batch_weights.py b/tmd/areas/batch_weights.py index 0e57985b..13e9279f 100644 --- a/tmd/areas/batch_weights.py +++ b/tmd/areas/batch_weights.py @@ -317,15 +317,19 @@ def _time_of_newest_other_dependency(): """ from tmd.areas import AREAS_FOLDER from tmd.storage import STORAGE_FOLDER - from tmd.imputation_assumptions import POPULATION_FILE + from tmd.imputation_assumptions import POPULATION_FILE, RETURNS_FILE + from tmd.utils import weight_growth deps = [ AREAS_FOLDER / "create_area_weights.py", + Path(weight_growth.__file__), STORAGE_FOLDER / "output" / "tmd.csv.gz", STORAGE_FOLDER / "output" / "tmd_weights.csv.gz", STORAGE_FOLDER / "output" / "tmd_growfactors.csv", STORAGE_FOLDER / "input" / POPULATION_FILE, ] + if RETURNS_FILE: + deps.append(STORAGE_FOLDER / "input" / RETURNS_FILE) return max(dpath.stat().st_mtime for dpath in deps) diff --git a/tmd/areas/create_area_weights.py b/tmd/areas/create_area_weights.py index 249d29e7..aafabf26 100644 --- a/tmd/areas/create_area_weights.py +++ b/tmd/areas/create_area_weights.py @@ -58,13 +58,13 @@ ) from tmd.areas import AREAS_FOLDER -from tmd.imputation_assumptions import POPULATION_FILE, TAXYEAR +from tmd.imputation_assumptions import TAXYEAR from tmd.storage import STORAGE_FOLDER +from tmd.utils.weight_growth import cumulative_growth FIRST_YEAR = TAXYEAR LAST_YEAR = 2034 INFILE_PATH = STORAGE_FOLDER / "output" / "tmd.csv.gz" -POPFILE_PATH = STORAGE_FOLDER / "input" / POPULATION_FILE TAXCALC_AGI_CACHE = STORAGE_FOLDER / "output" / "cached_c00100.npy" CACHED_ALLVARS_PATH = STORAGE_FOLDER / "output" / "cached_allvars.csv" @@ -923,19 +923,17 @@ def create_area_weights_file( if not write_file: return 0 - # write area weights file with population extrapolation + # write area weights file, extrapolated the same way as the national + # weights in tmd/create_taxcalc_sampling_weights.py so that area + # weights stay consistent with national weights in every year w0 = pop_share * vdf.s006.values wght_area = x_opt * w0 - with open(POPFILE_PATH, "r", encoding="utf-8") as pf: - pop = yaml.safe_load(pf.read()) + growth = cumulative_growth(FIRST_YEAR, LAST_YEAR) - wdict = {f"WT{FIRST_YEAR}": wght_area} - cum_pop_growth = 1.0 - for year in range(FIRST_YEAR + 1, LAST_YEAR + 1): - annual_pop_growth = pop[year] / pop[year - 1] - cum_pop_growth *= annual_pop_growth - wdict[f"WT{year}"] = wght_area * cum_pop_growth + wdict = {} + for year in range(FIRST_YEAR, LAST_YEAR + 1): + wdict[f"WT{year}"] = wght_area * growth[year] wdf = pd.DataFrame.from_dict(wdict) wdf.to_csv( diff --git a/tmd/create_taxcalc_sampling_weights.py b/tmd/create_taxcalc_sampling_weights.py index 790d211f..4df1375f 100644 --- a/tmd/create_taxcalc_sampling_weights.py +++ b/tmd/create_taxcalc_sampling_weights.py @@ -4,38 +4,32 @@ create_taxcalc_input_variable.py script. """ -import yaml import pandas as pd from tmd.storage import STORAGE_FOLDER -from tmd.imputation_assumptions import TAXYEAR, POPULATION_FILE +from tmd.imputation_assumptions import TAXYEAR +from tmd.utils.weight_growth import cumulative_growth FIRST_YEAR = TAXYEAR LAST_YEAR = TAXYEAR + 53 VARFILE = STORAGE_FOLDER / "output" / "tmd.csv.gz" -POPFILE = STORAGE_FOLDER / "input" / POPULATION_FILE WGTFILE = STORAGE_FOLDER / "output" / "tmd_weights.csv.gz" -def create_weights_file(pop_file=POPFILE): +def create_weights_file(): """ Create Tax-Calculator-style weights file for FIRST_YEAR through LAST_YEAR. """ - # get population forecast - with open(pop_file, "r", encoding="utf-8") as pfile: - pop = yaml.safe_load(pfile.read()) + # get cumulative tax return growth relative to FIRST_YEAR + growth = cumulative_growth(FIRST_YEAR, LAST_YEAR) # get FIRST_YEAR weights from VARFILE vdf = pd.read_csv(VARFILE) weights = vdf.s006 # construct dictionary of weights by year - wdict = {f"WT{FIRST_YEAR}": weights} - cum_pop_growth = 1.0 - for year in range(FIRST_YEAR + 1, LAST_YEAR + 1): - annual_pop_growth = pop[year] / pop[year - 1] - cum_pop_growth *= annual_pop_growth - wght = weights.copy() * cum_pop_growth - wdict[f"WT{year}"] = wght + wdict = {} + for year in range(FIRST_YEAR, LAST_YEAR + 1): + wdict[f"WT{year}"] = weights.copy() * growth[year] # write weights to CSV-formatted file wdf = pd.DataFrame.from_dict(wdict) diff --git a/tmd/imputation_assumptions.py b/tmd/imputation_assumptions.py index 4fce124a..8431960e 100644 --- a/tmd/imputation_assumptions.py +++ b/tmd/imputation_assumptions.py @@ -59,11 +59,20 @@ ALI_scale = {2021: 4.0, 2022: 4.0} # population projection file used to extrapolate TAXYEAR sampling weights +# beyond the last year covered by the tax return projection file POP_FILE = { 2021: "cbo25_population.yaml", 2022: "cbo26_population.yaml", } POPULATION_FILE = POP_FILE[TAXYEAR] +# tax return projection file used to extrapolate TAXYEAR sampling weights; +# None means the weights grow with population in every year +RET_FILE = { + 2021: None, + 2022: "cbo26_returns.yaml", +} +RETURNS_FILE = RET_FILE[TAXYEAR] + # Tax-Calculator reform dictionary to produce SOI statistics SOI_IITAX_SPEC = {"soi_iitax": {2013: True}} diff --git a/tmd/storage/input/cbo26_returns.yaml b/tmd/storage/input/cbo26_returns.yaml new file mode 100644 index 00000000..15a0494f --- /dev/null +++ b/tmd/storage/input/cbo26_returns.yaml @@ -0,0 +1,51 @@ +# Projected number of individual income tax returns (millions) used to +# extrapolate TAXYEAR sampling weights beyond TAXYEAR. +# +# 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", addendum row "Number of returns +# (millions)", February 2026. +# URL: https://www.cbo.gov/publication/61882 +# +# Per the sheet's own footnote l, the count is based on returns filed in +# 2022 plus estimates of future additional filers, and it includes +# dependents. Only 2022 is an actual; every later year is a projection. +# +# This is the same workbook, sheet, and row that supply the n_returns_mil +# values in tests/expected_cbo_levels_2022_data.yaml, and it is the return +# count that corresponds to the AGI and liability series TMD is compared +# against in tests/test_tmd_revenues.py. The 2022, 2026, 2031, and 2036 +# values below match that file exactly. +# +# WHY RETURNS RATHER THAN POPULATION +# Sampling weights were previously extrapolated by total population +# growth (cbo26_population.yaml), which holds returns per capita fixed +# at its TAXYEAR value. CBO projects returns growing about twice as +# fast as population -- +11.9% versus +5.9% over 2022-2036 -- because +# the share of the population that files continues to rise. Using +# population growth therefore made aggregate AGI and income tax +# revenue fall progressively further below CBO's projections after +# TAXYEAR, reaching -5.1% on revenue by 2035, even though TMD's AGI +# per return and tax per dollar of AGI both track CBO within one +# percent throughout. +# +# EXTRAPOLATION BEYOND THE LAST YEAR +# This file ends in 2036, the last year of the CBO projection, while +# sampling weights extend to TAXYEAR+53. After 2036 the weights grow +# at the rate of population growth, which holds returns per capita +# fixed at its 2036 level. See tmd/utils/weight_growth.py. +2022: 161.3 # actual +2023: 164.7 # projected +2024: 167.1 +2025: 168.0 +2026: 169.5 +2027: 170.8 +2028: 172.1 +2029: 173.2 +2030: 174.3 +2031: 175.8 +2032: 176.9 +2033: 177.9 +2034: 178.8 +2035: 179.7 +2036: 180.5 diff --git a/tmd/utils/weight_growth.py b/tmd/utils/weight_growth.py new file mode 100644 index 00000000..73d3f28d --- /dev/null +++ b/tmd/utils/weight_growth.py @@ -0,0 +1,61 @@ +""" +Cumulative growth factors used to extrapolate TAXYEAR sampling weights. + +Weights for years after TAXYEAR are the TAXYEAR weights scaled by the +projected growth in the number of individual income tax returns, which is +what a sampling weight counts. Total population growth was used for this +before, but CBO projects returns growing about twice as fast as population +because the filing share of the population keeps rising, so population +growth left aggregate AGI and income tax revenue progressively below CBO's +projections after TAXYEAR. + +The return projection ends with the CBO budget window, well short of the +last weight year, so growth reverts to population growth after the last +projected return year, which holds returns per capita fixed at its final +projected value. +""" + +import yaml +from tmd.storage import STORAGE_FOLDER +from tmd.imputation_assumptions import POPULATION_FILE, RETURNS_FILE + + +def _read(filename): + with open( + STORAGE_FOLDER / "input" / filename, "r", encoding="utf-8" + ) as ifile: + return yaml.safe_load(ifile.read()) + + +def cumulative_growth( + first_year, + last_year, + population_file=POPULATION_FILE, + returns_file=RETURNS_FILE, +): + """ + Return dictionary of cumulative weight growth factors, by calendar year, + relative to first_year, for each year in [first_year, last_year]. + + Growth is the year-over-year growth in projected tax returns through the + last year covered by returns_file, and population growth thereafter. If + returns_file is None, population growth is used for every year. + """ + pop = _read(population_file) + rets = _read(returns_file) if returns_file else {} + last_rets_year = max(rets) if rets else first_year + if rets and first_year not in rets: + raise ValueError( + f"{returns_file} has no {first_year} data, " + "so it cannot anchor weight extrapolation" + ) + + growth = {first_year: 1.0} + cum = 1.0 + for year in range(first_year + 1, last_year + 1): + if year <= last_rets_year: + cum *= rets[year] / rets[year - 1] + else: + cum *= pop[year] / pop[year - 1] + growth[year] = cum + return growth From 348b7f3693a9f8fc1edbbe2d422e3652aae44c79 Mon Sep 17 00:00:00 2001 From: martinholmer Date: Tue, 8 Sep 2026 19:05:35 -0400 Subject: [PATCH 2/6] Revise batch_weights.py and sweep_params.py in tmd/areas folder --- tmd/areas/batch_weights.py | 26 +++++++++----------------- tmd/areas/sweep_params.py | 7 +------ 2 files changed, 10 insertions(+), 23 deletions(-) diff --git a/tmd/areas/batch_weights.py b/tmd/areas/batch_weights.py index 13e9279f..e9dd7802 100644 --- a/tmd/areas/batch_weights.py +++ b/tmd/areas/batch_weights.py @@ -21,12 +21,11 @@ import numpy as np import pandas as pd -import yaml from scipy.sparse import csc_matrix # Module-level globals shared across workers via fork copy-on-write. # -# _WORKER_VDF and _WORKER_POP are loaded ONCE in the parent process +# _WORKER_VDF is loaded ONCE in the parent process # (by _preload_data) before ProcessPoolExecutor forks workers. # On Linux, forked children inherit these pages read-only; the OS # shares the physical memory until a child writes to it. This saves @@ -35,7 +34,6 @@ # The remaining globals are per-worker configuration set by # _init_worker (called as the executor's initializer). _WORKER_VDF = None -_WORKER_POP = None _WORKER_TARGET_DIR = None _WORKER_WEIGHT_DIR = None _WORKER_MULTIPLIER_MAX = None @@ -49,17 +47,12 @@ def _preload_data(): pages copy-on-write. Loading here instead of per-worker avoids duplicating ~190 MB × N workers. """ - global _WORKER_VDF, _WORKER_POP + global _WORKER_VDF if _WORKER_VDF is not None: return - from tmd.areas.create_area_weights import ( - POPFILE_PATH, - _load_taxcalc_data, - ) + from tmd.areas.create_area_weights import _load_taxcalc_data _WORKER_VDF = _load_taxcalc_data() - with open(POPFILE_PATH, "r", encoding="utf-8") as pf: - _WORKER_POP = yaml.safe_load(pf.read()) def _init_worker( @@ -115,6 +108,7 @@ def _solve_one_area(area): _read_params, _solve_area_qp, ) + from tmd.utils.weight_growth import cumulative_growth t0 = time.time() out = io.StringIO() @@ -272,13 +266,11 @@ def _solve_one_area(area): w0 = pop_share * vdf.s006.values wght_area = x_opt * w0 - wdict = {f"WT{FIRST_YEAR}": wght_area} - cum_pop_growth = 1.0 - pop = _WORKER_POP - for year in range(FIRST_YEAR + 1, LAST_YEAR + 1): - annual_pop_growth = pop[year] / pop[year - 1] - cum_pop_growth *= annual_pop_growth - wdict[f"WT{year}"] = wght_area * cum_pop_growth + growth = cumulative_growth(FIRST_YEAR, LAST_YEAR) + + wdict = {} + for year in range(FIRST_YEAR, LAST_YEAR + 1): + wdict[f"WT{year}"] = wght_area * growth[year] wdf = pd.DataFrame.from_dict(wdict) awpath = wgt_dir / f"{area}_tmd_weights.csv.gz" diff --git a/tmd/areas/sweep_params.py b/tmd/areas/sweep_params.py index 6098828e..0802cafd 100644 --- a/tmd/areas/sweep_params.py +++ b/tmd/areas/sweep_params.py @@ -18,14 +18,12 @@ from concurrent.futures import ProcessPoolExecutor, as_completed import numpy as np -import yaml from tmd.areas.create_area_weights import ( AREA_CONSTRAINT_TOL, AREA_MAX_ITER, AREA_MULTIPLIER_MIN, AREA_SLACK_PENALTY, - POPFILE_PATH, STATE_TARGET_DIR, _build_constraint_matrix, _drop_impossible_targets, @@ -45,17 +43,14 @@ # Module-level cache _VDF = None -_POP = None def _init(): """Load data once.""" - global _VDF, _POP # pylint: disable=global-statement + global _VDF # pylint: disable=global-statement if _VDF is not None: return _VDF = _load_taxcalc_data() - with open(POPFILE_PATH, "r", encoding="utf-8") as pf: - _POP = yaml.safe_load(pf.read()) def _solve_one(args): From 963f56a69dfe1d388bb8a8885a404c300cf2bc9e Mon Sep 17 00:00:00 2001 From: martinholmer Date: Wed, 9 Sep 2026 10:46:35 -0400 Subject: [PATCH 3/6] Create growfactors in the first step of the high-level pipeline --- Makefile | 29 ++++++++++++++++++----------- tmd/create_taxcalc_cached_files.py | 11 +++++++++-- tmd/datasets/cps.py | 7 +++++-- tmd/storage/__init__.py | 5 +++++ tmd/utils/soi_replication.py | 6 +++++- tmd/utils/taxcalc_output.py | 6 +++++- 6 files changed, 47 insertions(+), 17 deletions(-) diff --git a/Makefile b/Makefile index e6d95b78..56898446 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ clean: rm -f tmd/storage/output/preimpute_tmd.csv.gz rm -f tmd/storage/output/make_data_*.log -# Each of the five build stages below tees its stdout+stderr to a +# Each of the six build stages below tees its stdout+stderr to a # per-stage log file in tmd/storage/output/. On-screen output during # `make data` is unchanged; the log files are a byproduct so that any # warnings emitted during the run (pandas, numpy, taxcalc, scipy, @@ -31,20 +31,26 @@ clean: # appear silent on the terminal until the buffer flushes. The `-u` # flag preserves the pre-tee behavior of seeing progress in real time. -tmd/storage/output/tmd.csv.gz: - python -u tmd/create_taxcalc_input_variables.py 2>&1 \ - | tee tmd/storage/output/make_data_tmd.log +# The two growth-factors files are built first because subsequent +# stages use growfactors.csv as the Policy-parameter indexing file and +# tmd_growfactors.csv as the Records-variable extrapolation file. -tmd/storage/output/tmd_weights.csv.gz: - python -u tmd/create_taxcalc_sampling_weights.py 2>&1 \ - | tee tmd/storage/output/make_data_weights.log - -tmd/storage/output/tmd_growfactors.csv: +tmd/storage/output/growfactors.csv: python -u tmd/create_raw_growth_factors.py 2>&1 \ | tee tmd/storage/output/make_data_raw_growfactors.log + +tmd/storage/output/tmd_growfactors.csv: tmd/storage/output/growfactors.csv python -u tmd/create_taxcalc_growth_factors.py 2>&1 \ | tee tmd/storage/output/make_data_tmd_growfactors.log +tmd/storage/output/tmd.csv.gz: tmd/storage/output/tmd_growfactors.csv + python -u tmd/create_taxcalc_input_variables.py 2>&1 \ + | tee tmd/storage/output/make_data_tmd.log + +tmd/storage/output/tmd_weights.csv.gz: tmd/storage/output/tmd.csv.gz + python -u tmd/create_taxcalc_sampling_weights.py 2>&1 \ + | tee tmd/storage/output/make_data_weights.log + tmd/storage/output/cached_files: python -u tmd/create_taxcalc_cached_files.py 2>&1 \ | tee tmd/storage/output/make_data_cached.log @@ -54,9 +60,10 @@ tmd/storage/output/preimpute_tmd.csv.gz: | tee tmd/storage/output/make_data_preimpute.log .PHONY=tmd_files -tmd_files: tmd/storage/output/tmd.csv.gz \ - tmd/storage/output/tmd_weights.csv.gz \ +tmd_files: tmd/storage/output/growfactors.csv \ tmd/storage/output/tmd_growfactors.csv \ + tmd/storage/output/tmd.csv.gz \ + tmd/storage/output/tmd_weights.csv.gz \ tmd/storage/output/cached_files \ tmd/storage/output/preimpute_tmd.csv.gz diff --git a/tmd/create_taxcalc_cached_files.py b/tmd/create_taxcalc_cached_files.py index 41871964..abf24391 100644 --- a/tmd/create_taxcalc_cached_files.py +++ b/tmd/create_taxcalc_cached_files.py @@ -5,7 +5,11 @@ import numpy as np import pandas as pd import taxcalc -from tmd.storage import STORAGE_FOLDER, CACHED_TAXCALC_VARIABLES +from tmd.storage import ( + STORAGE_FOLDER, + CACHED_TAXCALC_VARIABLES, + POLICY_GROWFACTORS_PATH, +) from tmd.imputation_assumptions import TAXYEAR, SOI_IITAX_SPEC INFILE_PATH = STORAGE_FOLDER / "output" / "tmd.csv.gz" @@ -21,7 +25,10 @@ def create_cached_files(): # calculate all Tax-Calculator variables for TAXYEAR # Construct Records directly (bypassing tmd_constructor which # hardcodes start_year=2021 in the taxcalc library). - pol = taxcalc.Policy() + 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=pd.read_csv(INFILE_PATH), diff --git a/tmd/datasets/cps.py b/tmd/datasets/cps.py index 7304fe9d..b3b77dcc 100644 --- a/tmd/datasets/cps.py +++ b/tmd/datasets/cps.py @@ -8,7 +8,7 @@ import pandas as pd from tqdm import tqdm import taxcalc -from tmd.storage import STORAGE_FOLDER +from tmd.storage import STORAGE_FOLDER, POLICY_GROWFACTORS_PATH from tmd.imputation_assumptions import ( SOI_IITAX_SPEC, CPS_FILER_MIN_INCOME, @@ -356,7 +356,10 @@ def _is_tax_filer(tcdf: pd.DataFrame, taxyear: int) -> pd.Series: exact_calculations=True, weights_scale=1.0, ) - pol = taxcalc.Policy() + policy_gf = taxcalc.GrowFactors( + growfactors_filename=str(POLICY_GROWFACTORS_PATH) + ) + pol = taxcalc.Policy(gfactor=policy_gf) pol.implement_reform(SOI_IITAX_SPEC) calc = taxcalc.Calculator(records=rec, policy=pol) calc.advance_to_year(taxyear) diff --git a/tmd/storage/__init__.py b/tmd/storage/__init__.py index 21c70194..bb7132c3 100644 --- a/tmd/storage/__init__.py +++ b/tmd/storage/__init__.py @@ -2,6 +2,11 @@ STORAGE_FOLDER = Path(__file__).parent +# Growth factors used to index Policy parameters (as distinct from +# tmd_growfactors.csv, which extrapolates Records variables). This file +# is created in the first stage of the `make tmd_files` pipeline. +POLICY_GROWFACTORS_PATH = STORAGE_FOLDER / "output" / "growfactors.csv" + CACHED_TAXCALC_VARIABLES = [ "c00100", # AGI "iitax", # individual income tax liability (including refundable credits) diff --git a/tmd/utils/soi_replication.py b/tmd/utils/soi_replication.py index 690e3607..ce4c4376 100644 --- a/tmd/utils/soi_replication.py +++ b/tmd/utils/soi_replication.py @@ -1,10 +1,14 @@ import pandas as pd import taxcalc +from tmd.storage import POLICY_GROWFACTORS_PATH from tmd.imputation_assumptions import SOI_IITAX_SPEC def taxcalc_to_soi(puf: pd.DataFrame, year: int) -> pd.DataFrame: - pol = taxcalc.Policy() + 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=puf, diff --git a/tmd/utils/taxcalc_output.py b/tmd/utils/taxcalc_output.py index 57cb4625..416b88ad 100644 --- a/tmd/utils/taxcalc_output.py +++ b/tmd/utils/taxcalc_output.py @@ -7,6 +7,7 @@ import numpy as np import pandas as pd import taxcalc +from tmd.storage import POLICY_GROWFACTORS_PATH from tmd.imputation_assumptions import SOI_IITAX_SPEC @@ -46,7 +47,10 @@ def add_taxcalc_outputs( exact_calculations=True, weights_scale=1.0, ) - pol = taxcalc.Policy() + policy_gf = taxcalc.GrowFactors( + growfactors_filename=str(POLICY_GROWFACTORS_PATH) + ) + pol = taxcalc.Policy(gfactor=policy_gf) pol.implement_reform(SOI_IITAX_SPEC) if reform: pol.implement_reform(reform) From 5dd5776110c977290dc6c111b17f90286295da37 Mon Sep 17 00:00:00 2001 From: martinholmer Date: Wed, 9 Sep 2026 11:14:35 -0400 Subject: [PATCH 4/6] Add TMD_*_PATH vars to tmd/storage/__init__.py --- tests/conftest.py | 24 ++---------------------- tests/test_imputed_variables.py | 21 +++++++++++---------- tests/test_revenue_levels_cbo.py | 19 +++++++++---------- tests/test_soi_sanity_2022.py | 20 ++++++++++---------- tests/test_tax_expenditures.py | 12 +++++++----- tests/test_taxcalc_results_2022.py | 19 +++++++++---------- tests/test_tmd_revenues.py | 19 +++++++++---------- tmd/storage/__init__.py | 5 +++++ 8 files changed, 62 insertions(+), 77 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 80d5cdce..32aa5e9b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,7 +7,7 @@ import numpy as np import pandas as pd import taxcalc -from tmd.storage import STORAGE_FOLDER +from tmd.storage import TMD_VARIABLES_PATH from tmd.imputation_assumptions import TAXYEAR # convert all numpy floating-point execeptions into errors @@ -50,24 +50,4 @@ def tests_folder(): @pytest.fixture(scope="session") def tmd_variables(): - return pd.read_csv(STORAGE_FOLDER / "output" / "tmd.csv.gz") - - -@pytest.fixture(scope="session") -def tmd_variables_path(): - return STORAGE_FOLDER / "output" / "tmd.csv.gz" - - -@pytest.fixture(scope="session") -def tmd_weights_path(): - return STORAGE_FOLDER / "output" / "tmd_weights.csv.gz" - - -@pytest.fixture(scope="session") -def tmd_growfactors_path(): - return STORAGE_FOLDER / "output" / "tmd_growfactors.csv" - - -@pytest.fixture(scope="session") -def policy_growfactors_path(): - return STORAGE_FOLDER / "output" / "growfactors.csv" + return pd.read_csv(TMD_VARIABLES_PATH) diff --git a/tests/test_imputed_variables.py b/tests/test_imputed_variables.py index 2ddd1d8a..1eb7f1e7 100644 --- a/tests/test_imputed_variables.py +++ b/tests/test_imputed_variables.py @@ -7,17 +7,18 @@ import numpy as np import pytest import taxcalc +from tmd.storage import ( + POLICY_GROWFACTORS_PATH, + TMD_GROWFACTORS_PATH, + TMD_VARIABLES_PATH, + TMD_WEIGHTS_PATH, +) from tmd.imputation_assumptions import TAXYEAR, SOI_IITAX_SPEC from tests.conftest import create_tmd_records @pytest.mark.obbba_deduction -def test_obbba_deduction_tax_benefits( - tmd_variables_path, - tmd_weights_path, - tmd_growfactors_path, - policy_growfactors_path, -): +def test_obbba_deduction_tax_benefits(): """ Estimate percent of tax-units affected by each new tax deduction and average size of the 2026 dollar effect among those affected. @@ -119,13 +120,13 @@ def actual_results(rdf, bdf): ] # create Tax-Calculator Records object recs = create_tmd_records( - data_path=tmd_variables_path, - weights_path=tmd_weights_path, - growfactors_path=tmd_growfactors_path, + data_path=TMD_VARIABLES_PATH, + weights_path=TMD_WEIGHTS_PATH, + growfactors_path=TMD_GROWFACTORS_PATH, ) # create baseline_sim Calculator object for simyear and get its output policy_gf = taxcalc.GrowFactors( - growfactors_filename=str(policy_growfactors_path) + growfactors_filename=str(POLICY_GROWFACTORS_PATH) ) pol = taxcalc.Policy(gfactor=policy_gf) pol.implement_reform(SOI_IITAX_SPEC) diff --git a/tests/test_revenue_levels_cbo.py b/tests/test_revenue_levels_cbo.py index dc253798..1a4e52ea 100644 --- a/tests/test_revenue_levels_cbo.py +++ b/tests/test_revenue_levels_cbo.py @@ -18,6 +18,11 @@ import pytest import taxcalc +from tmd.storage import ( + POLICY_GROWFACTORS_PATH, + TMD_GROWFACTORS_PATH, + TMD_WEIGHTS_PATH, +) from tmd.imputation_assumptions import TAXYEAR, SOI_IITAX_SPEC # Per-year relative tolerance for all three aggregates. @@ -37,19 +42,13 @@ TAXYEAR != 2022, reason="expected values are calibrated to TAXYEAR=2022", ) -def test_revenue_levels_cbo( - tests_folder, - tmd_variables, - tmd_weights_path, - tmd_growfactors_path, - policy_growfactors_path, -): +def test_revenue_levels_cbo(tests_folder, tmd_variables): epath = tests_folder / "expected_cbo_levels_2022_data.yaml" with open(epath, "r", encoding="utf-8") as f: exp = yaml.safe_load(f) policy_gf = taxcalc.GrowFactors( - growfactors_filename=str(policy_growfactors_path) + growfactors_filename=str(POLICY_GROWFACTORS_PATH) ) pol = taxcalc.Policy(gfactor=policy_gf) pol.implement_reform(SOI_IITAX_SPEC) @@ -57,9 +56,9 @@ def test_revenue_levels_cbo( data=tmd_variables, start_year=TAXYEAR, gfactors=taxcalc.GrowFactors( - growfactors_filename=str(tmd_growfactors_path) + growfactors_filename=str(TMD_GROWFACTORS_PATH) ), - weights=str(tmd_weights_path), + weights=str(TMD_WEIGHTS_PATH), adjust_ratios=None, exact_calculations=True, weights_scale=1.0, diff --git a/tests/test_soi_sanity_2022.py b/tests/test_soi_sanity_2022.py index 1c6f1a96..1c057536 100644 --- a/tests/test_soi_sanity_2022.py +++ b/tests/test_soi_sanity_2022.py @@ -48,7 +48,12 @@ import pandas as pd import taxcalc -from tmd.storage import STORAGE_FOLDER +from tmd.storage import ( + STORAGE_FOLDER, + POLICY_GROWFACTORS_PATH, + TMD_GROWFACTORS_PATH, + TMD_WEIGHTS_PATH, +) from tmd.imputation_assumptions import TAXYEAR, SOI_IITAX_SPEC SOI_YEAR = 2022 @@ -81,28 +86,23 @@ def _soi_target(soi_df, variable, count): return float(rows["Value"].iloc[0]) -def test_soi_sanity_2022( - tmd_variables, - tmd_weights_path, - tmd_growfactors_path, - policy_growfactors_path, -): +def test_soi_sanity_2022(tmd_variables): """Five weighted 2022 totals from TMD within 1% of SOI targets.""" # Run TaxCalc at TAXYEAR to obtain c00100 (AGI) and iitax. e00200, # e26270, and s006 are available directly on the input frame but # using the post-calc_all arrays keeps one consistent source for # all five aggregates. policy_gf = taxcalc.GrowFactors( - growfactors_filename=str(policy_growfactors_path) + growfactors_filename=str(POLICY_GROWFACTORS_PATH) ) pol = taxcalc.Policy(gfactor=policy_gf) pol.implement_reform(SOI_IITAX_SPEC) recs = taxcalc.Records( data=tmd_variables, start_year=TAXYEAR, - weights=str(tmd_weights_path), + weights=str(TMD_WEIGHTS_PATH), gfactors=taxcalc.GrowFactors( - growfactors_filename=str(tmd_growfactors_path) + growfactors_filename=str(TMD_GROWFACTORS_PATH) ), adjust_ratios=None, exact_calculations=True, diff --git a/tests/test_tax_expenditures.py b/tests/test_tax_expenditures.py index c7a457de..369439e5 100644 --- a/tests/test_tax_expenditures.py +++ b/tests/test_tax_expenditures.py @@ -5,7 +5,11 @@ import pytest import numpy as np -from tmd.storage import STORAGE_FOLDER +from tmd.storage import ( + STORAGE_FOLDER, + TMD_GROWFACTORS_PATH, + TMD_WEIGHTS_PATH, +) from tmd.imputation_assumptions import TAXYEAR from tmd.utils.tax_expenditures import get_tax_expenditure_results @@ -14,15 +18,13 @@ def test_tax_exp_diffs( tests_folder, tmd_variables, - tmd_weights_path, - tmd_growfactors_path, ): _ = get_tax_expenditure_results( tmd_variables, TAXYEAR, # input variables data year 2023, # simulation year for tax expenditure estimates - tmd_weights_path, - tmd_growfactors_path, + TMD_WEIGHTS_PATH, + TMD_GROWFACTORS_PATH, ) act_path = STORAGE_FOLDER / "output" / "tax_expenditures" with open(act_path, "r", encoding="utf-8") as actfile: diff --git a/tests/test_taxcalc_results_2022.py b/tests/test_taxcalc_results_2022.py index 58057aba..37c6763a 100644 --- a/tests/test_taxcalc_results_2022.py +++ b/tests/test_taxcalc_results_2022.py @@ -10,6 +10,11 @@ import yaml import pytest import taxcalc +from tmd.storage import ( + POLICY_GROWFACTORS_PATH, + TMD_GROWFACTORS_PATH, + TMD_WEIGHTS_PATH, +) from tmd.imputation_assumptions import TAXYEAR, SOI_IITAX_SPEC MAX_RELATIVE_TOLERANCE = { @@ -22,28 +27,22 @@ TAXYEAR != 2022, reason="expected values are calibrated to TAXYEAR=2022", ) -def test_taxcalc_results_2022( - tests_folder, - tmd_variables, - tmd_weights_path, - tmd_growfactors_path, - policy_growfactors_path, -): +def test_taxcalc_results_2022(tests_folder, tmd_variables): epath = tests_folder / "expected_taxcalc_results_2022.yaml" with open(epath, "r", encoding="utf-8") as f: expect = yaml.safe_load(f) policy_gf = taxcalc.GrowFactors( - growfactors_filename=str(policy_growfactors_path) + growfactors_filename=str(POLICY_GROWFACTORS_PATH) ) pol = taxcalc.Policy(gfactor=policy_gf) pol.implement_reform(SOI_IITAX_SPEC) recs = taxcalc.Records( data=tmd_variables, start_year=TAXYEAR, - weights=str(tmd_weights_path), + weights=str(TMD_WEIGHTS_PATH), gfactors=taxcalc.GrowFactors( - growfactors_filename=str(tmd_growfactors_path) + growfactors_filename=str(TMD_GROWFACTORS_PATH) ), adjust_ratios=None, exact_calculations=True, diff --git a/tests/test_tmd_revenues.py b/tests/test_tmd_revenues.py index ffe7b336..1cfab6c3 100644 --- a/tests/test_tmd_revenues.py +++ b/tests/test_tmd_revenues.py @@ -29,6 +29,11 @@ import yaml import pytest import taxcalc +from tmd.storage import ( + POLICY_GROWFACTORS_PATH, + TMD_GROWFACTORS_PATH, + TMD_WEIGHTS_PATH, +) from tmd.imputation_assumptions import TAXYEAR, SOI_IITAX_SPEC FIRST_YEAR = TAXYEAR @@ -69,13 +74,7 @@ def revenue_table(actual, expect): 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, -): +def test_tmd_revenues(tests_folder, tmd_variables): # read expected annual income tax revenue epath = tests_folder / "expected_cbo_revenues.yaml" with open(epath, "r", encoding="utf-8") as f: @@ -87,7 +86,7 @@ def test_tmd_revenues( # calculate actual annual income tax revenue on the TMD file policy_gf = taxcalc.GrowFactors( - growfactors_filename=str(policy_growfactors_path) + growfactors_filename=str(POLICY_GROWFACTORS_PATH) ) pol = taxcalc.Policy(gfactor=policy_gf) pol.implement_reform(SOI_IITAX_SPEC) @@ -95,9 +94,9 @@ def test_tmd_revenues( data=tmd_variables, start_year=TAXYEAR, gfactors=taxcalc.GrowFactors( - growfactors_filename=str(tmd_growfactors_path) + growfactors_filename=str(TMD_GROWFACTORS_PATH) ), - weights=str(tmd_weights_path), + weights=str(TMD_WEIGHTS_PATH), adjust_ratios=None, exact_calculations=True, weights_scale=1.0, diff --git a/tmd/storage/__init__.py b/tmd/storage/__init__.py index bb7132c3..4693c882 100644 --- a/tmd/storage/__init__.py +++ b/tmd/storage/__init__.py @@ -7,6 +7,11 @@ # is created in the first stage of the `make tmd_files` pipeline. POLICY_GROWFACTORS_PATH = STORAGE_FOLDER / "output" / "growfactors.csv" +# Paths of the files generated by the `make tmd_files` pipeline. +TMD_VARIABLES_PATH = STORAGE_FOLDER / "output" / "tmd.csv.gz" +TMD_WEIGHTS_PATH = STORAGE_FOLDER / "output" / "tmd_weights.csv.gz" +TMD_GROWFACTORS_PATH = STORAGE_FOLDER / "output" / "tmd_growfactors.csv" + CACHED_TAXCALC_VARIABLES = [ "c00100", # AGI "iitax", # individual income tax liability (including refundable credits) From 9a3ad9959897fa74465f7f247a9eb39bce44f68d Mon Sep 17 00:00:00 2001 From: martinholmer Date: Wed, 9 Sep 2026 11:26:48 -0400 Subject: [PATCH 5/6] Narrow tolerances in the test_revenue_levels_cbo.py test --- tests/test_revenue_levels_cbo.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_revenue_levels_cbo.py b/tests/test_revenue_levels_cbo.py index 1a4e52ea..a7d001d1 100644 --- a/tests/test_revenue_levels_cbo.py +++ b/tests/test_revenue_levels_cbo.py @@ -28,9 +28,9 @@ # Per-year relative tolerance for all three aggregates. RELTOL = { 2022: 0.01, - 2026: 0.02, - 2031: 0.05, - 2036: 0.06, + 2026: 0.01, + 2031: 0.01, + 2036: 0.01, } VARIABLES = ("n_returns_mil", "agi_bil", "iitax_bil") From 8b895f308d43f03f956453299bcc5ddc238f21ab Mon Sep 17 00:00:00 2001 From: martinholmer Date: Wed, 9 Sep 2026 16:18:44 -0400 Subject: [PATCH 6/6] Update top-level README.md docs wrt version 2.2.0 --- README.md | 36 ++++++++++-------------------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 97a2c57c..51d8f429 100644 --- a/README.md +++ b/README.md @@ -8,32 +8,16 @@ For Tax-Calculator results generated when using these TMD input files, see [this folder](https://github.com/PSLmodels/Tax-Calculator/tree/master/taxcalc/cli/input_data_tests). -The **current TMD version is 2.1.4**, which was released on July 13, -2026, and is the same as TMD version 2.1.3 except that a new version -of `scikit-learn` (1.9.0), which fixed a bug in its weighted imputation -algorithm, is used (see #534 for details). Tax-Calculator version 6.8.1 -or higher is required to generate the TMD files. - -When using version 2.1.4 to generate **sub-national weights**, there -will be (presumably small) differences from the sub-national weights -generated using earlier versions. Note that the 118th Congressional -district weight fingerprints have not yet been updated. - -The earlier TMD 2.0.0 version introduced the following significant -improvements: -- generate national, state, and Congressional district, input files -for **2022**: -[#470](https://github.com/PSLmodels/tax-microdata-benchmarking/pull/470) -[#471](https://github.com/PSLmodels/tax-microdata-benchmarking/pull/471) -[#472](https://github.com/PSLmodels/tax-microdata-benchmarking/pull/472) -[#473](https://github.com/PSLmodels/tax-microdata-benchmarking/pull/473) -[#474](https://github.com/PSLmodels/tax-microdata-benchmarking/pull/474) -- improve the selection of CPS tax units to represent nonfilers: -[#438](https://github.com/PSLmodels/tax-microdata-benchmarking/pull/438) -- vastly improve the reweighting algorithm: -[#416](https://github.com/PSLmodels/tax-microdata-benchmarking/pull/416) -- impute values for three variables used in new OBBBA deductions: -[#397](https://github.com/PSLmodels/tax-microdata-benchmarking/pull/397) +The **current TMD version is 2.2.0**, which was released on September +9, 2026, and is the same as TMD version 2.1.4 except that post-2022 +weights are grown using a CBO returns projection (rather than using a +CBO population projection as in prior versions). See PR #546 for details. + +When using version 2.2.0 to generate national or sub-national weights, +the 2022 weights (and hence, the fingerprints) are the same as for +version 2.1.4. However, all the post-2022 weights are larger. Note +that the 118th Congressional district weight fingerprints have not +been updated. ## Usage instructions