From dcd44f60c725fe220344108f1a0e90b433187d6f Mon Sep 17 00:00:00 2001 From: martinholmer Date: Fri, 28 Nov 2025 17:54:47 -0500 Subject: [PATCH 01/22] Add CLI --behavior option without its logic --- taxcalc/__init__.py | 2 +- taxcalc/cli/tc.py | 17 ++++++++-- taxcalc/taxcalcio.py | 16 +++++---- taxcalc/tests/test_taxcalcio.py | 60 ++++++++++++++++++++++++--------- 4 files changed, 69 insertions(+), 26 deletions(-) diff --git a/taxcalc/__init__.py b/taxcalc/__init__.py index 773b75d70..1d76d7763 100644 --- a/taxcalc/__init__.py +++ b/taxcalc/__init__.py @@ -14,6 +14,6 @@ from taxcalc.utils import * from taxcalc.cli import * -__version__ = '6.2.1' +__version__ = '6.2.1a' __min_python3_version__ = 11 __max_python3_version__ = 13 diff --git a/taxcalc/cli/tc.py b/taxcalc/cli/tc.py index f7dd99702..5f055f3aa 100644 --- a/taxcalc/cli/tc.py +++ b/taxcalc/cli/tc.py @@ -29,12 +29,15 @@ def cli_tc_main(): start_time = time.time() # parse command-line arguments: - usage_str = 'tc INPUT TAXYEAR {}{}{}{}'.format( + usage_str = 'tc INPUT TAXYEAR {}{}{}{}{}'.format( '[--help] [--numyears N]\n', ( ' ' - '[--baseline BASELINE] [--reform REFORM] ' - '[--assump ASSUMP] [--exact]\n' + '[--baseline BASELINE] [--reform REFORM]\n' + ), + ( + ' ' + '[--assump ASSUMP] [--behavior BEHAVIOR] [--exact]\n' ), ( ' ' @@ -91,6 +94,12 @@ def cli_tc_main(): 'assumptions file. No --assump implies use ' 'of no customized assumptions.'), default=None) + parser.add_argument('--behavior', + help=('BEHAVIOR is name of optional JSON behavioral ' + 'response elasticities file. No --behavior ' + 'implies use of zero elasticities; that is, ' + 'no response to reform.'), + default=None) parser.add_argument('--exact', help=('optional flag that suppresses the smoothing of ' '"stair-step" provisions in the tax law that ' @@ -273,6 +282,7 @@ def cli_tc_main(): baseline=args.baseline, reform=args.reform, assump=args.assump, + behavior=args.behavior, runid=args.runid, silent=args.silent, ) @@ -293,6 +303,7 @@ def cli_tc_main(): baseline=args.baseline, reform=args.reform, assump=args.assump, + behavior=args.behavior, exact_calculations=args.exact, ) if tcio.errmsg: diff --git a/taxcalc/taxcalcio.py b/taxcalc/taxcalcio.py index 8704c050b..849ae9074 100644 --- a/taxcalc/taxcalcio.py +++ b/taxcalc/taxcalcio.py @@ -55,6 +55,10 @@ class TaxCalcIO(): None implies economic assumptions are standard assumptions, or string is name of optional ASSUMP file. + behavior: None or string + None implies behavioral response elasticities are all zero, + or string is name of optional BEHAVIOR file. + runid: int run id value to use for simpler output file names @@ -67,8 +71,8 @@ class instance: TaxCalcIO """ # pylint: disable=too-many-instance-attributes - def __init__(self, input_data, tax_year, baseline, reform, assump, - runid=0, silent=True): + def __init__(self, input_data, tax_year, baseline, reform, + assump, behavior, runid=0, silent=True): # pylint: disable=too-many-arguments,too-many-positional-arguments # pylint: disable=too-many-branches,too-many-statements,too-many-locals self.silent = silent @@ -262,15 +266,15 @@ def delete_output_files(self): for ext in extensions: delete_file(self.output_filename.replace('.xxx', ext)) - def init(self, input_data, tax_year, baseline, reform, assump, - exact_calculations): + def init(self, input_data, tax_year, baseline, reform, + assump, behavior, exact_calculations): """ TaxCalcIO class post-constructor method that completes initialization. Parameters ---------- - First five are same as the first five of the TaxCalcIO constructor: - input_data, tax_year, baseline, reform, assump. + First six are same as the first six of the TaxCalcIO constructor: + input_data, tax_year, baseline, reform, assump, behavior. exact_calculations: boolean specifies whether or not exact tax calculations are done without diff --git a/taxcalc/tests/test_taxcalcio.py b/taxcalc/tests/test_taxcalcio.py index bee6a5517..9af89e5e5 100644 --- a/taxcalc/tests/test_taxcalcio.py +++ b/taxcalc/tests/test_taxcalcio.py @@ -274,7 +274,8 @@ def test_ctor_errors(input_data, baseline, reform, assump): Ensure error messages are generated by TaxCalcIO.__init__. """ tcio = TaxCalcIO(input_data=input_data, tax_year=2013, - baseline=baseline, reform=reform, assump=assump) + baseline=baseline, reform=reform, + assump=assump, behavior=None) assert tcio.errmsg @@ -310,16 +311,19 @@ def test_init_errors(reformfile0, errorreformfile, errorassumpfile, assump = errorassumpfile.name else: assump = asm + behavior = None # call TaxCalcIO constructor tcio = TaxCalcIO(input_data=recdf, tax_year=year, baseline=baseline, reform=reform, - assump=assump) + assump=assump, + behavior=behavior) assert not tcio.errmsg # test TaxCalcIO.init method tcio.init(input_data=recdf, tax_year=year, - baseline=baseline, reform=reform, assump=assump, + baseline=baseline, reform=reform, + assump=assump, behavior=behavior, exact_calculations=True) assert tcio.errmsg @@ -331,8 +335,12 @@ def test_ctor_init_with_cps_files(): # specify valid tax_year for cps.csv input data txyr = 2020 for rid in [0, 99]: - tcio = TaxCalcIO('cps.csv', txyr, None, None, None, runid=rid) - tcio.init('cps.csv', txyr, None, None, None, exact_calculations=False) + tcio = TaxCalcIO('cps.csv', txyr, + None, None, None, None, + runid=rid) + tcio.init('cps.csv', txyr, + None, None, None, None, + exact_calculations=False) assert not tcio.errmsg assert tcio.tax_year() == txyr # test advance_to_year method @@ -341,8 +349,11 @@ def test_ctor_init_with_cps_files(): assert tcio.tax_year() == txyr + 1 # specify invalid tax_year for cps.csv input data txyr = 2013 - tcio = TaxCalcIO('cps.csv', txyr, None, None, None) - tcio.init('cps.csv', txyr, None, None, None, exact_calculations=False) + tcio = TaxCalcIO('cps.csv', txyr, + None, None, None, None) + tcio.init('cps.csv', txyr, + None, None, None, None, + exact_calculations=False) assert tcio.errmsg @@ -374,10 +385,11 @@ def test_dump_variables(dumpvar_str, str_valid, num_vars): recdf = pd.DataFrame(data=recdict, index=[0]) year = 2018 tcio = TaxCalcIO(input_data=recdf, tax_year=year, - baseline=None, reform=None, assump=None) + baseline=None, reform=None, + assump=None, behavior=None) assert not tcio.errmsg tcio.init(input_data=recdf, tax_year=year, - baseline=None, reform=None, assump=None, + baseline=None, reform=None, assump=None, behavior=None, exact_calculations=False) assert not tcio.errmsg varlist = tcio.dump_variables(dumpvar_str) @@ -397,13 +409,15 @@ def test_output_options_min(reformfile1, assumpfile1): tax_year=taxyear, baseline=None, reform=reformfile1.name, - assump=assumpfile1.name) + assump=assumpfile1.name, + behavior=None) assert not tcio.errmsg tcio.init(input_data=pd.read_csv(StringIO(RAWINPUT)), tax_year=taxyear, baseline=None, reform=reformfile1.name, assump=assumpfile1.name, + behavior=None, exact_calculations=False) assert not tcio.errmsg dumppath = tcio.output_filepath().replace('.xxx', '.dumpdb') @@ -431,13 +445,15 @@ def test_output_options_mtr(reformfile1, assumpfile1): tax_year=taxyear, baseline=None, reform=reformfile1.name, - assump=assumpfile1.name) + assump=assumpfile1.name, + behavior=None) assert not tcio.errmsg tcio.init(input_data=pd.read_csv(StringIO(RAWINPUT)), tax_year=taxyear, baseline=None, reform=reformfile1.name, assump=assumpfile1.name, + behavior=None, exact_calculations=False) assert not tcio.errmsg dumppath = tcio.output_filepath().replace('.xxx', '.dumpdb') @@ -470,6 +486,7 @@ def test_write_policy_param_files(reformfile1): baseline=compound_reform, reform=compound_reform, assump=None, + behavior=None, ) assert not tcio.errmsg tcio.init(input_data=pd.read_csv(StringIO(RAWINPUT)), @@ -477,6 +494,7 @@ def test_write_policy_param_files(reformfile1): baseline=compound_reform, reform=compound_reform, assump=None, + behavior=None, exact_calculations=False) assert not tcio.errmsg tcio.write_policy_params_files() @@ -506,13 +524,15 @@ def test_no_tables_or_graphs(reformfile1): tax_year=2020, baseline=None, reform=reformfile1.name, - assump=None) + assump=None, + behavior=None) assert not tcio.errmsg tcio.init(input_data=idf, tax_year=2020, baseline=None, reform=reformfile1.name, assump=None, + behavior=None, exact_calculations=False) assert not tcio.errmsg # create several TaxCalcIO output files @@ -541,13 +561,15 @@ def test_tables(reformfile1): tax_year=2020, baseline=None, reform=reformfile1.name, - assump=None) + assump=None, + behavior=None) assert not tcio.errmsg tcio.init(input_data=idf, tax_year=2020, baseline=None, reform=reformfile1.name, assump=None, + behavior=None, exact_calculations=False) assert not tcio.errmsg # create TaxCalcIO tables file @@ -574,13 +596,15 @@ def test_graphs(reformfile1): tax_year=2020, baseline=None, reform=reformfile1.name, - assump=None) + assump=None, + behavior=None) assert not tcio.errmsg tcio.init(input_data=idf, tax_year=2020, baseline=None, reform=reformfile1.name, assump=None, + behavior=None, exact_calculations=False) assert not tcio.errmsg tcio.analyze(output_graphs=True) @@ -617,13 +641,15 @@ def test_analyze_warnings_print(warnreformfile): tax_year=taxyear, baseline=None, reform=warnreformfile.name, - assump=None) + assump=None, + behavior=None) assert not tcio.errmsg tcio.init(input_data=recdf, tax_year=taxyear, baseline=None, reform=warnreformfile.name, assump=None, + behavior=None, exact_calculations=False) assert not tcio.errmsg tcio.analyze() @@ -683,7 +709,8 @@ def test_error_message_parsed_correctly(regression_reform_file): tax_year=2022, baseline=regression_reform_file.name, reform=regression_reform_file.name, - assump=None) + assump=None, + behavior=None) assert not tcio.errmsg tcio.init(input_data=pd.read_csv(StringIO(RAWINPUT)), @@ -691,6 +718,7 @@ def test_error_message_parsed_correctly(regression_reform_file): baseline=regression_reform_file.name, reform=regression_reform_file.name, assump=None, + behavior=None, exact_calculations=False) assert isinstance(tcio.errmsg, str) and tcio.errmsg exp_errmsg = ( From 88bb16303330ce64eb638626d260e0c8a9f0277f Mon Sep 17 00:00:00 2001 From: martinholmer Date: Sat, 29 Nov 2025 11:58:53 -0500 Subject: [PATCH 02/22] Revise docs wrt new CLI --behavior option --- docs/_toc.yml | 1 + docs/guide/behavior_params.md | 16 +++++++++++++++ docs/guide/cli.md | 2 +- .../templates/assumption_params_template.md | 2 +- docs/index.md | 8 ++------ taxcalc/assumptions/ASSUMPTIONS.md | 2 +- taxcalc/behavior/BEHAVIOR.md | 20 +++++++++++++++++++ taxcalc/behavior/README.md | 16 +++++++++++++++ .../behavioral_response_template.json | 14 +++++++++++++ 9 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 docs/guide/behavior_params.md create mode 100644 taxcalc/behavior/BEHAVIOR.md create mode 100644 taxcalc/behavior/README.md create mode 100644 taxcalc/behavior/behavioral_response_template.json diff --git a/docs/_toc.yml b/docs/_toc.yml index d451362dc..61b50cbee 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -14,6 +14,7 @@ parts: - file: guide/input_vars - file: guide/output_vars - file: guide/assumption_params + - file: guide/behavior_params - file: recipes/index sections: - file: recipes/recipe00 diff --git a/docs/guide/behavior_params.md b/docs/guide/behavior_params.md new file mode 100644 index 000000000..dae229fa3 --- /dev/null +++ b/docs/guide/behavior_params.md @@ -0,0 +1,16 @@ +Behavior parameters +=================== + +Note that logic that uses assumed behavior parameters to compute +changes in input variables caused by a tax reform in a +partial-equilibrium setting is not part of Tax-Calculator. The +[`response` +function](https://github.com/PSLmodels/Behavioral-Responses/blob/232abc1e6b9f0a2b2f224ad887af3c19019d28d3/behresp/behavior.py#L13-L50) +in the PSLmodels Behavioral-Responses `behresp` package contains that +logic. + +By default Tax-Calculator assumes no behavioral responses to a tax +reform, which is the same as saying the behavior parameters (or +elasticities) are assumed to be zero by default. The elasticities can +be set to non-zero values in a JSON file that is formatted like +[this](https://github.com/PSLmodels/Tax-Calculator/blob/master/taxcalc/behavior/behavior_response_template.json)). diff --git a/docs/guide/cli.md b/docs/guide/cli.md index 493c2c702..6884df9ac 100644 --- a/docs/guide/cli.md +++ b/docs/guide/cli.md @@ -59,7 +59,7 @@ page](https://github.com/PSLmodels/Tax-Calculator/blob/master/taxcalc/reforms/RE ## Specify analysis assumptions This part explains how to specify economic assumption files used in -static tax analysis. This is an advanced topic, so if you want to +tax analysis. This is an advanced topic, so if you want to start out using the default assumptions (which are documented in [this section](https://taxcalc.pslmodels.org/guide/policy_params.html#policy-parameters)) of the guide), you can skip this part now and come back to read it diff --git a/docs/guide/templates/assumption_params_template.md b/docs/guide/templates/assumption_params_template.md index 4f6e0cc46..45394efe1 100644 --- a/docs/guide/templates/assumption_params_template.md +++ b/docs/guide/templates/assumption_params_template.md @@ -1,6 +1,6 @@ Assumption parameters ===================== -This section contains documentation of several sets of parameters that characterize responses to a tax reform. Consumption parameters are used to compute marginal tax rates and to compute the consumption value of in-kind benefits. Growdiff parameters are used to specify baseline differences and/or reform responses in the annual rate of growth in economic variables. (Note that behavior parameters used to compute changes in input variables caused by a tax reform in a partial-equilibrium setting are not part of Tax-Calculator, but can be used via the Behavioral-Response `behresp` package in a Python program.) +This section contains documentation of several sets of parameters that characterize responses to a tax reform. Consumption parameters are used to compute marginal tax rates and to compute the consumption value of in-kind benefits. Growdiff parameters are used to specify baseline differences and/or reform responses in the annual rate of growth in economic variables. The assumption parameters control advanced features of Tax-Calculator, so understanding the source code that uses them is essential. Default values of many assumption parameters are zero and are projected into the future at that value, which implies no response to the reform. The benefit value consumption parameters have a default value of one, which implies the consumption value of the in-kind benefits is equal to the government cost of providing the benefits. diff --git a/docs/index.md b/docs/index.md index a15be9706..9ce4b66c5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,7 +3,7 @@ Tax-Calculator ## What is Tax-Calculator? -Tax-Calculator is an open-source microsimulation model for static analysis of +Tax-Calculator is an open-source microsimulation model for analysis of USA federal income and payroll taxes. You can install it with [PyPI](https://pypi.org/project/taxcalc/) via: ``` @@ -17,17 +17,13 @@ conda install conda-forge::taxcalc When using sample data that represent the USA population, Tax-Calculator can estimate the aggregate revenue and distributional -effects of tax reforms under static analysis assumptions. Read +effects of tax reforms. Read {doc}`usage/data` for information about the three different prepared sample data sets that Tax-Calculator knows how to handle. Tax-Calculator can also process custom-created data on one or more filing units permitting analysis of how tax reforms affect certain people. -Tax-Calculator interacts with other models in the -[Policy Simulation Library](https://www.pslmodels.org/) to conduct non-static -analysis. - Tax-Calculator is transparent because its Python source code and embedded documentation are publicly available at the [Tax-Calculator GitHub repository](https://github.com/PSLmodels/Tax-Calculator). diff --git a/taxcalc/assumptions/ASSUMPTIONS.md b/taxcalc/assumptions/ASSUMPTIONS.md index 3883cb11d..9a798e874 100644 --- a/taxcalc/assumptions/ASSUMPTIONS.md +++ b/taxcalc/assumptions/ASSUMPTIONS.md @@ -49,5 +49,5 @@ the same as for policy reform files, which are described [here](https://github.com/PSLmodels/Tax-Calculator/blob/master/taxcalc/reforms/REFORMS.md#how-to-specify-a-tax-reform-in-a-json-policy-reform-file). The assumption parameter names recognized by Tax-Calculator, and their default values, are listed in [this -section](https://PSLmodels.github.io/Tax-Calculator/uguide.html#params) +section](https://taxcalc.pslmodels.org/guide/assumption_params.html) of the user guide. diff --git a/taxcalc/behavior/BEHAVIOR.md b/taxcalc/behavior/BEHAVIOR.md new file mode 100644 index 000000000..6572a0e6c --- /dev/null +++ b/taxcalc/behavior/BEHAVIOR.md @@ -0,0 +1,20 @@ +# HOW TO SPECIFY BEHAVIORAL RESPONSES IN A JSON BEHAVIOR FILE + +There is a way to specify in a text file the collection of behavioral +response elasticities that you want to assume about how individuals +respond to a tax reform. + +Here is an [example](behavioral_responses_template.json) of a +behavioral responses file. + +Every behavior file is a JSON file. JSON, which stands for JavaScript +Object Notation, is an easy way to specify structured information that +is widely used. + +Notice that a behavior file must always contain these top-level keys: +sub, inc, and cg. More information about these three elasticities can +be found in [here](https://github.com/PSLmodels/Behavioral-Responses/blob/232abc1e6b9f0a2b2f224ad887af3c19019d28d3/behresp/behavior.py#L13-L50). + +Also notice that the value of these three elasticities do not vary +from year to year, and thus, have no time dimension. + diff --git a/taxcalc/behavior/README.md b/taxcalc/behavior/README.md new file mode 100644 index 000000000..bb5ecc464 --- /dev/null +++ b/taxcalc/behavior/README.md @@ -0,0 +1,16 @@ +Behavioral Responses Files +-------------------------- + +This directory contains an example of a behavioral responses file that can +be modified to construct your own behavioral responses files +that are stored on your local computer. + +Such an behavioral responses file can then be used by the `tc` +command-line interface to Tax-Calculator with the `--behavior` option +or be read in a Python program that uses the Python API. + +[This +document](https://github.com/PSLmodels/Tax-Calculator/blob/master/taxcalc/behavior/BEHAVIOR.md) +provides access to a template behavioral responses file that assumes +no behavioral responses because all the elasticities are assumed to be +zero. diff --git a/taxcalc/behavior/behavioral_response_template.json b/taxcalc/behavior/behavioral_response_template.json new file mode 100644 index 000000000..47bec5f46 --- /dev/null +++ b/taxcalc/behavior/behavioral_response_template.json @@ -0,0 +1,14 @@ +// Parameters used to specify behavioral responses in tax policy analysis. +// +// This JSON file serves as a template for specifying behavioral response +// elasticities that specify how individuals respond to a tax reform. +// +// Detailed documentation on these parameters can be found in the +// response function docstring in the Behavioral-Responses repository +// at the following URL: +// https://github.com/PSLmodels/Behavioral-Responses/blob/232abc1e6b9f0a2b2f224ad887af3c19019d28d3/behresp/behavior.py#L13-L50 +{ + "sub": 0.0, + "inc": 0,0, + "cg": 0.0 +} From cd4e30f2d0adec7504a4c6154d8b5f44555d223a Mon Sep 17 00:00:00 2001 From: martinholmer Date: Sat, 29 Nov 2025 12:39:21 -0500 Subject: [PATCH 03/22] More docs changes wrt new CLI --behavior option --- README.md | 4 ++-- docs/guide/cli.md | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 487dc093f..9b38bce82 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ Tax-Calculator ============== -Tax-Calculator is an open-source microsimulation model for static -analysis of USA federal income and payroll taxes. +Tax-Calculator is an open-source microsimulation model for analysis of +USA federal income and payroll taxes. We are seeking contributors and maintainers. If you are interested in joining the project as a contributor or maintainer, open a new diff --git a/docs/guide/cli.md b/docs/guide/cli.md index 6884df9ac..879e1eb9d 100644 --- a/docs/guide/cli.md +++ b/docs/guide/cli.md @@ -77,6 +77,12 @@ For examples of assumption files and the general rules for writing JSON assumption files, go to [this page](https://github.com/PSLmodels/Tax-Calculator/blob/master/taxcalc/assumptions/README.md#economic-assumption-files). +## Specify behavioral responses + +For a JSON file template that can be used to specify you own values of +each response elasticity, go to [this +page](https://github.com/PSLmodels/Tax-Calculator/blob/master/taxcalc/behavior/README.md). + ## Specify filing units The `taxcalc` package containing `tc` does not include an @@ -227,7 +233,7 @@ on an old Mac with a slow Intel CPU chip. % tc cps.csv 2020 --dumpdb Read input data for 2014; input data were extrapolated to 2020 Write dump output to sqlite3 database file cps-20-#-#-#.db -Execution time is 33.2 seconds +Execution time is 9.6 seconds ``` The dump database contains 2020 income tax liabilities for each filing @@ -318,7 +324,6 @@ Write tabular output to file cps-24-#-ref3-#-tables.text Write graphical output to file cps-24-#-ref3-#-pch.html Write graphical output to file cps-24-#-ref3-#-atr.html Write graphical output to file cps-24-#-ref3-#-mtr.html -Execution time is 37.3 seconds % diff cps-24-#-ref3-#-params.bas cps-24-#-ref3-#-params.ref 34c34 @@ -552,7 +557,6 @@ Advance input data and policy to 2034 Write tabular output to file tmd-34-#-ext-#-tables.text Advance input data and policy to 2035 Write tabular output to file tmd-35-#-ext-#-tables.text -Execution time is 55.6 seconds ``` [PR From b78a4fed2a3238e42e6d6799abaf9b7a7dd827c5 Mon Sep 17 00:00:00 2001 From: martinholmer Date: Sun, 30 Nov 2025 12:15:53 -0500 Subject: [PATCH 04/22] Add CLI --behavior logic to TaxCalcIO constructor --- docs/_toc.yml | 1 - docs/guide/cli.md | 82 +++--- docs/usage/data.md | 8 +- docs/usage/tcja_after_2025.md | 343 -------------------------- taxcalc/cli/input_data_tests/tests.sh | 36 +-- taxcalc/taxcalcio.py | 22 +- 6 files changed, 82 insertions(+), 410 deletions(-) delete mode 100644 docs/usage/tcja_after_2025.md diff --git a/docs/_toc.yml b/docs/_toc.yml index 61b50cbee..e09066608 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -25,7 +25,6 @@ parts: - file: recipes/recipe04_pandas - file: recipes/recipe05 - file: recipes/recipe06 - - file: usage/tcja_after_2025 - caption: About chapters: - file: about/history diff --git a/docs/guide/cli.md b/docs/guide/cli.md index 879e1eb9d..eb9095665 100644 --- a/docs/guide/cli.md +++ b/docs/guide/cli.md @@ -232,13 +232,13 @@ on an old Mac with a slow Intel CPU chip. ``` % tc cps.csv 2020 --dumpdb Read input data for 2014; input data were extrapolated to 2020 -Write dump output to sqlite3 database file cps-20-#-#-#.db +Write dump output to sqlite3 database file cps-20-#-#-#-#.db Execution time is 9.6 seconds ``` The dump database contains 2020 income tax liabilities for each filing unit under both baseline and reform policy regimes. The name of the -dump database file is `cps-20-#-#-#.db`. Because we did not use the +dump database file is `cps-20-#-#-#-#.db`. Because we did not use the `--dumpvars` option, a minimal set of baseline/reform variables are included in the dump database. @@ -260,7 +260,7 @@ Anaconda Python distribution. Full `sqlite3` documentation is Here is a quick way to see the structure of the dump database: ``` -% echo ".schema" | sqlite3 cps-20-#-#-#.db +% echo ".schema" | sqlite3 cps-20-#-#-#-#.db CREATE TABLE IF NOT EXISTS "base" ( "RECID" INTEGER, "s006" REAL, @@ -318,20 +318,20 @@ should replace `cat` with `type`): ``` % tc cps.csv 2024 --reform ref3.json --params --tables --graphs Read input data for 2014; input data were extrapolated to 2024 -Write baseline policy parameter values to file cps-24-#-ref3-#-params.bas -Write reform policy parameter values to file cps-24-#-ref3-#-params.ref -Write tabular output to file cps-24-#-ref3-#-tables.text -Write graphical output to file cps-24-#-ref3-#-pch.html -Write graphical output to file cps-24-#-ref3-#-atr.html -Write graphical output to file cps-24-#-ref3-#-mtr.html - -% diff cps-24-#-ref3-#-params.bas cps-24-#-ref3-#-params.ref +Write baseline policy parameter values to file cps-24-#-ref3-#-#-params.bas +Write reform policy parameter values to file cps-24-#-ref3-#-#-params.ref +Write tabular output to file cps-24-#-ref3-#-#-tables.text +Write graphical output to file cps-24-#-ref3-#-#-pch.html +Write graphical output to file cps-24-#-ref3-#-#-atr.html +Write graphical output to file cps-24-#-ref3-#-#-mtr.html + +% diff cps-24-#-ref3-#-#-params.bas cps-24-#-ref3-#-#-params.ref 34c34 < II_em 0.0 --- > II_em 9059.7 -% cat cps-24-#-ref3-#-tables.text +% cat cps-24-#-ref3-#-#-tables.text Weighted Tax Reform Totals by Baseline Expanded-Income Decile Returns ExpInc IncTax PayTax LSTax AllTax (#m) ($b) ($b) ($b) ($b) ($b) @@ -365,15 +365,15 @@ Weighted Tax Differences by Baseline Expanded-Income Decile The graphs in the three `.html` files can be viewed in your browser. -The `cps-24-#-ref3-#-pch.html` file looks something like this: +The `cps-24-#-ref3-#-#-pch.html` file looks something like this: ![pch graph](../_static/pch.png) -The `cps-24-#-ref3-#-atr.html` file looks something like this: +The `cps-24-#-ref3-#-#-atr.html` file looks something like this: ![atr graph](../_static/atr.png) -The `cps-24-#-ref3-#-mtr.html` file looks something like this: +The `cps-24-#-ref3-#-#-mtr.html` file looks something like this: ![mtr graph](../_static/mtr.png) @@ -416,10 +416,10 @@ exploratory data analysis. ``` % tc cps.csv 2024 --reform ref3.json --dumpdb Read input data for 2014; input data were extrapolated to 2024 -Write dump output to sqlite3 database file cps-24-#-ref3-#.db +Write dump output to sqlite3 database file cps-24-#-ref3-#-#.db Execution time is 35.6 seconds -% sqlite3 cps-24-#-ref3-#.db +% sqlite3 cps-24-#-ref3-#-#.db SQLite version 3.39.5 2022-10-14 20:58:05 Enter ".help" for usage hints. sqlite> YOUR FIRST SQL COMMAND GOES HERE @@ -490,11 +490,11 @@ FROM base JOIN baseline AS b USING(RECID) JOIN reform AS r USING(RECID); .headers on ``` -Using this `tab.sql` script to tabulate the `cps-24-#-ref3-#.db` +Using this `tab.sql` script to tabulate the `cps-24-#-ref3-#-#.db` database produces these results in about 1.3 seconds: ``` -% sqlite3 cps-24-#-ref3-#.db < tab.sql +% sqlite3 cps-24-#-ref3-#-#.db < tab.sql *** unweighted and weighted tax unit counts: raw_count wgh_count --------- --------- @@ -538,25 +538,25 @@ Tax-Calculator 4.6.2 on Python 3.12 % tc ../tmd.csv 2026 --numyears 10 --reform ext.json --tables Read input data for 2021; input data were extrapolated to 2026 -Write tabular output to file tmd-26-#-ext-#-tables.text +Write tabular output to file tmd-26-#-ext-#-#-tables.text Advance input data and policy to 2027 -Write tabular output to file tmd-27-#-ext-#-tables.text +Write tabular output to file tmd-27-#-ext-#-#-tables.text Advance input data and policy to 2028 -Write tabular output to file tmd-28-#-ext-#-tables.text +Write tabular output to file tmd-28-#-ext-#-#-tables.text Advance input data and policy to 2029 -Write tabular output to file tmd-29-#-ext-#-tables.text +Write tabular output to file tmd-29-#-ext-#-#-tables.text Advance input data and policy to 2030 -Write tabular output to file tmd-30-#-ext-#-tables.text +Write tabular output to file tmd-30-#-ext-#-#-tables.text Advance input data and policy to 2031 -Write tabular output to file tmd-31-#-ext-#-tables.text +Write tabular output to file tmd-31-#-ext-#-#-tables.text Advance input data and policy to 2032 -Write tabular output to file tmd-32-#-ext-#-tables.text +Write tabular output to file tmd-32-#-ext-#-#-tables.text Advance input data and policy to 2033 -Write tabular output to file tmd-33-#-ext-#-tables.text +Write tabular output to file tmd-33-#-ext-#-#-tables.text Advance input data and policy to 2034 -Write tabular output to file tmd-34-#-ext-#-tables.text +Write tabular output to file tmd-34-#-ext-#-#-tables.text Advance input data and policy to 2035 -Write tabular output to file tmd-35-#-ext-#-tables.text +Write tabular output to file tmd-35-#-ext-#-#-tables.text ``` [PR @@ -571,34 +571,34 @@ fourth column, we can look at the ten-year results quickly: ``` % tail -1 tmd-??-*tables.text -==> tmd-26-#-ext-#-tables.text <== +==> tmd-26-#-ext-#-#-tables.text <== A 192.41 20828.3 -284.0 0.0 0.0 -284.0 -==> tmd-27-#-ext-#-tables.text <== +==> tmd-27-#-ext-#-#-tables.text <== A 193.77 21613.4 -291.7 0.0 0.0 -291.7 -==> tmd-28-#-ext-#-tables.text <== +==> tmd-28-#-ext-#-#-tables.text <== A 194.72 22404.7 -299.5 0.0 0.0 -299.5 -==> tmd-29-#-ext-#-tables.text <== +==> tmd-29-#-ext-#-#-tables.text <== A 195.66 23243.3 -291.8 0.0 0.0 -291.8 -==> tmd-30-#-ext-#-tables.text <== +==> tmd-30-#-ext-#-#-tables.text <== A 196.58 24115.1 -299.8 0.0 0.0 -299.8 -==> tmd-31-#-ext-#-tables.text <== +==> tmd-31-#-ext-#-#-tables.text <== A 197.48 25020.0 -307.8 0.0 0.0 -307.8 -==> tmd-32-#-ext-#-tables.text <== +==> tmd-32-#-ext-#-#-tables.text <== A 198.35 25952.7 -315.7 0.0 0.0 -315.7 -==> tmd-33-#-ext-#-tables.text <== +==> tmd-33-#-ext-#-#-tables.text <== A 199.21 26905.4 -323.6 0.0 0.0 -323.6 -==> tmd-34-#-ext-#-tables.text <== +==> tmd-34-#-ext-#-#-tables.text <== A 200.03 27878.9 -331.6 0.0 0.0 -331.6 -==> tmd-35-#-ext-#-tables.text <== +==> tmd-35-#-ext-#-#-tables.text <== A 200.83 28883.0 -339.6 0.0 0.0 -339.6 ``` @@ -610,7 +610,3 @@ could be called `gawk` or `gawk.exe` on your computer, as follows: % tail -1 tmd-??-*tables.text | awk '$1~/A/{n++;c+=$4}END{print n,c}' 10 -3085.1 ``` - -More examples of analyzing different versions of the TCJA after 2025 -are available in this -[document](https://taxcalc.pslmodels.org/usage/tcja_after_2025.html). diff --git a/docs/usage/data.md b/docs/usage/data.md index d75309c83..d46572be8 100644 --- a/docs/usage/data.md +++ b/docs/usage/data.md @@ -89,9 +89,9 @@ this command for tabular output under 2024 current-law policy: ``` (taxcalc-dev) myruns> TMD_AREA=nm tc tmd.csv 2024 --exact --tables Read input data for 2021; input data were extrapolated to 2024 -Write tabular output to file tmd_nm-24-#-#-#.tables +Write tabular output to file tmd_nm-24-#-#-#-#.tables Execution time is 8.2 seconds -(taxcalc-dev) myruns> awk '$1~/Ret/||$1~/A/' tmd_nm-24-#-#-#.tables | head -2 +(taxcalc-dev) myruns> awk '$1~/Ret/||$1~/A/' tmd_nm-24-#-#-#-#.tables | head -2 Returns ExpInc IncTax PayTax LSTax AllTax A 1.17 87.0 7.2 6.9 0.0 14.1 ``` @@ -102,9 +102,9 @@ files and execute this command: ``` (taxcalc-dev) myruns> TMD_AREA=nm01 tc tmd.csv 2024 --exact --tables Read input data for 2021; input data were extrapolated to 2024 -Write tabular output to file tmd_nm01-24-#-#-#.tables +Write tabular output to file tmd_nm01-24-#-#-#-#.tables Execution time is 8.3 seconds -(taxcalc-dev) myruns> awk '$1~/Ret/||$1~/A/' tmd_nm01-24-#-#-#.tables | head -2 +(taxcalc-dev) myruns> awk '$1~/Ret/||$1~/A/' tmd_nm01-24-#-#-#-#.tables | head -2 Returns ExpInc IncTax PayTax LSTax AllTax A 0.40 31.9 2.8 2.5 0.0 5.3 ``` diff --git a/docs/usage/tcja_after_2025.md b/docs/usage/tcja_after_2025.md deleted file mode 100644 index cd20e0b82..000000000 --- a/docs/usage/tcja_after_2025.md +++ /dev/null @@ -1,343 +0,0 @@ -# OBBBA: TCJA after 2025 - -TCJA was replaced by OBBBA when the latter was signed into law on July -4, 2025. Beginning with Tax-Calculator 5.2.0, OBBBA is current-law -policy. How the OBBBA changes were implemented as current-law policy -is described in detail in closed [issue -2926](https://github.com/PSLmodels/Tax-Calculator/issues/2926). - ----------------------------------------------------------------------- - -**THE FOLLOWING TEXT IS OF HISTORICAL INTEREST ONLY:** - -Many provisions of the TCJA are temporary and are scheduled to end -after 2025 under current-law policy. Tax policy parameters that are -associated with expiring provisions and that are not inflation indexed -will revert to their 2017 values in 2026. Tax policy parameters that -are associated with expiring provisions and that are inflation indexed -will revert to their 2017 values indexed to 2026 using a chained CPI-U -inflation factor. For a list of the ending TCJA provisions, see this -Congressional Research Service document: [Reference Table: Expiring -Provisions in the "Tax Cuts and Jobs Act" (TCJA, P.L. 115-97)]( -https://crsreports.congress.gov/product/pdf/R/R47846), which is dated -November 21, 2023. - -This document provides examples of using the PSLmodels Tax-Calculator -command-line-interface tool -[tc](https://taxcalc.pslmodels.org/guide/cli.html) with the newer -`tmd.csv` file generated in the PSLmodels -[tax-microdata](https://github.com/PSLmodels/tax-microdata-benchmarking) -repository. The `tmd` data and weights are based on the 2015 IRS/SOI -PUF data and on recent CPS data, and therefore, are the best data to -use with Tax-Calculator. All the examples assume you have the [three -`tmd` data -files](https://taxcalc.pslmodels.org/usage/data.html#irs-public-use-data-tmd-csv) -in the parent directory of your working directory. The `tmd` data -contain information on 225,256 tax filing units. - -Before reading the rest of this document, be sure you understand how -to use the Tax-Calculator command-line tool -[tc](https://taxcalc.pslmodels.org/guide/cli.html), particularly the -`--baseline` and `--reform` command-line options. For complete and -up-to-date `tc` documentation, enter `tc --help` at the command -prompt. Omitting the `--baseline` option means the baseline policy is -current-law policy. Omitting the `--reform` option means the reform -policy is current-law policy. The `--tables` option produces two -tables in one file: the top table contains aggregate and income decile -estimates under the reform and the bottom table contains estimates of -reform-minus-baseline differences by income decile and in aggregate. - -Nobody knows how the 2025 tax legislation will turn out, so the idea -of this document is to illustrate how to use the Tax-Calculator CLI -tool to analyze some of the TCJA revisions that were being reported in -the press in early May 2025. The basic legislative goal is to extend -TCJA beyond 2025, but there is discussion of a number of **revisions** -to the basic extension. The revisions being discussed include, but -are not limited to, raising the SALT deduction cap, making social -security benefits nontaxable, and liberalizing the child tax credit. -(Given the nature of the reconciliation rules under which the -legislation is being developed, no changes in social security -financing can be made, so there is discussion of a higher -elderly/disability standard deduction amount to proxy the nontaxable -social security benefits revision.) -These revisions all cause reductions in income tax revenue, so there -is also discussion about **enhancements** to the extended-TCJA policy -that would raise revenue to pay for revisions. The enhancement -considered here is the one that adds a new top income tax bracket with -a 39.6 percent marginal tax rate. - -The analysis examples below focus on the following policy scenarios: - -1. a strict extension of TCJA without any revisions or enhancements -2. a TCJA extension with the nontaxable social security benefits revision -3. a TCJA extension with the higher elderly/disabled standard deduction revision -4. a TCJA extension with the higher elderly/disabled standard deduction revision and the new top tax bracket enhancement - -All the examples use Tax-Calculator 4.6.2 version. -``` -% tc --version -Tax-Calculator 4.6.2 on Python 3.12 -``` - -The examples below were done on an ancient Mac with an old Intel -processor with four CPU cores. The execution times on newer computers -should be substantially less than shown below. In all the examples, -each `tc` run is using just one CPU core. - -The -[`ext.json`](https://github.com/PSLmodels/Tax-Calculator/blob/master/taxcalc/reforms/ext.json) -reform file is used all the examples. See the section at the end of -this document for more information on `ext.json` contents. - - -## 1. TCJA extension without any revisions or enhancements - -``` -% tc ../tmd.csv 2026 --numyears 10 --reform ext.json --exact --tables -Read input data for 2021; input data were extrapolated to 2026 -Write tabular output to file tmd-26-#-ext-#-tables.text -Advance input data and policy to 2027 -Write tabular output to file tmd-27-#-ext-#-tables.text -Advance input data and policy to 2028 -Write tabular output to file tmd-28-#-ext-#-tables.text -Advance input data and policy to 2029 -Write tabular output to file tmd-29-#-ext-#-tables.text -Advance input data and policy to 2030 -Write tabular output to file tmd-30-#-ext-#-tables.text -Advance input data and policy to 2031 -Write tabular output to file tmd-31-#-ext-#-tables.text -Advance input data and policy to 2032 -Write tabular output to file tmd-32-#-ext-#-tables.text -Advance input data and policy to 2033 -Write tabular output to file tmd-33-#-ext-#-tables.text -Advance input data and policy to 2034 -Write tabular output to file tmd-34-#-ext-#-tables.text -Advance input data and policy to 2035 -Write tabular output to file tmd-35-#-ext-#-tables.text -Execution time is 56.8 seconds -``` - -Because the aggregate change in taxes is displayed on the last line of -the tables file (in column four), we can look at the ten changes like -this: - -``` -% tail -1 tmd-??-#-ext-#-tables.text -==> tmd-26-#-ext-#-tables.text <== - A 192.41 20828.3 -284.1 0.0 0.0 -284.1 - -==> tmd-27-#-ext-#-tables.text <== - A 193.77 21613.4 -291.8 0.0 0.0 -291.8 - -==> tmd-28-#-ext-#-tables.text <== - A 194.72 22404.7 -299.6 0.0 0.0 -299.6 - -==> tmd-29-#-ext-#-tables.text <== - A 195.66 23243.3 -291.9 0.0 0.0 -291.9 - -==> tmd-30-#-ext-#-tables.text <== - A 196.58 24115.1 -299.9 0.0 0.0 -299.9 - -==> tmd-31-#-ext-#-tables.text <== - A 197.48 25020.0 -307.9 0.0 0.0 -307.9 - -==> tmd-32-#-ext-#-tables.text <== - A 198.35 25952.7 -315.9 0.0 0.0 -315.9 - -==> tmd-33-#-ext-#-tables.text <== - A 199.21 26905.4 -323.8 0.0 0.0 -323.8 - -==> tmd-34-#-ext-#-tables.text <== - A 200.03 27878.9 -331.7 0.0 0.0 -331.7 - -==> tmd-35-#-ext-#-tables.text <== - A 200.83 28883.0 -339.7 0.0 0.0 -339.7 -``` - -And the ten-year change in aggregate federal income tax liability can -be tabulated this way: - -``` -% tail -1 tmd-??-#-ext-#-tables.text | awk '$1~/A/{n++;c+=$4}END{print n,c}' -10 -3086.3 -``` - - -## 2. TCJA extension with the nontaxable social security benefits revision - -There is some discussion of exempting all social security benefits -from federal income taxation. Here is a JSON file that implements -that reform: - -``` -% cat no_ssben_tax.json -{ - "SS_percentage1": {"2026": 0.0}, - "SS_percentage2": {"2026": 0.0} -} -``` - -The marginal effect of adding that reform on to the TCJA-extension can be -estimated in this 2026 run: - -``` -% tc ../tmd.csv 2026 --baseline ext.json --reform ext.json+no_ssben_tax.json --exact --tables -Read input data for 2021; input data were extrapolated to 2026 -Write tabular output to file tmd-26-ext-ext+no_ssben_tax-#-tables.text -Execution time is 33.4 seconds - -% tail -1 tmd-26-ext-ext+no_ssben_tax-#-tables.text - A 192.41 20828.3 -110.7 0.0 0.0 -110.7 -``` - -So, this reform reduces federal income tax liability by $110.7 billion in -2026. - - -## 3. TCJA extension with the higher elderly/disabled standard deduction revision - -Next we find a reform that approximates the `no_ssben_tax` reform -analyzed in the previous section. Trying higher values for the -`STD_Aged` parameter, we quickly find that $31,500 for all filing -statuses produces a reasonable approximation of the effects of the -`no_ssben_tax` reform. - -``` -% cat higher_aged_std.json -{ - "STD_Aged": {"2026": [31500, 31500, 31500, 31500, 31500]} -} - -% tc ../tmd.csv 2026 --baseline ext.json --reform ext.json+higher_aged_std.json --exact --tables -Read input data for 2021; input data were extrapolated to 2026 -Write tabular output to file tmd-26-ext-ext+higher_aged_std-#-tables.text -Execution time is 32.4 seconds - -% diff tmd-26-ext-ext+higher_aged_std-#-tables.text tmd-26-ext-ext+no_ssben_tax-#-tables.text | tail -18 -22,29c22,29 -< 3 19.24 629.1 -0.4 0.0 0.0 -0.4 -< 4 19.24 876.9 -1.9 0.0 0.0 -1.9 -< 5 19.24 1173.0 -6.9 0.0 0.0 -6.9 -< 6 19.25 1581.6 -13.4 0.0 0.0 -13.4 -< 7 19.24 2191.6 -25.6 0.0 0.0 -25.6 -< 8 19.24 3238.1 -33.2 0.0 0.0 -33.2 -< 9 19.24 10787.1 -29.5 0.0 0.0 -29.5 -< A 192.41 20828.3 -111.1 0.0 0.0 -111.1 ---- -> 3 19.24 629.1 -0.1 0.0 0.0 -0.1 -> 4 19.24 876.9 -1.0 0.0 0.0 -1.0 -> 5 19.24 1173.0 -4.9 0.0 0.0 -4.9 -> 6 19.25 1581.6 -11.9 0.0 0.0 -11.9 -> 7 19.24 2191.6 -21.3 0.0 0.0 -21.3 -> 8 19.24 3238.1 -29.9 0.0 0.0 -29.9 -> 9 19.24 10787.1 -41.5 0.0 0.0 -41.5 -> A 192.41 20828.3 -110.7 0.0 0.0 -110.7 -``` - - -## 4. TCJA extension with the higher elderly/disabled standard deduction revision and the new top tax bracket enhancement - -In this last example, we look at how much of the extra cost of the -`higher_aged_std` reform can be paid for by adding a new top income -tax bracket to the TCJA-extension reform. - -``` -(taxcalc-dev) Tax-Calculator% cat new_top_bracket.json -{ - "II_brk7": {"2026": [2.5e6, 5.0e6, 2.5e6, 4.2e6, 5.0e6]}, - "II_rt8": {"2026": 0.396}, - "PT_brk7": {"2026": [2.5e6, 5.0e6, 2.5e6, 4.2e6, 5.0e6]}, - "PT_rt8": {"2026": 0.396} -} - -% tc ../tmd.csv 2026 --baseline ext.json+higher_aged_std.json --reform ext.json+higher_aged_std.json+new_top_bracket.json --exact --tables --graphs -Read input data for 2021; input data were extrapolated to 2026 -Write tabular output to file tmd-26-ext+higher_aged_std-ext+higher_aged_std+new_top_bracket-#-tables.text -Write graphical output to file tmd-26-ext+higher_aged_std-ext+higher_aged_std+new_top_bracket-#-pch.html -Write graphical output to file tmd-26-ext+higher_aged_std-ext+higher_aged_std+new_top_bracket-#-atr.html -Write graphical output to file tmd-26-ext+higher_aged_std-ext+higher_aged_std+new_top_bracket-#-mtr.html -Execution time is 39.8 seconds - -% tail -14 tmd-26-ext+higher_aged_std-ext+higher_aged_std+new_top_bracket-#-tables.text -Weighted Tax Differences by Baseline Expanded-Income Decile - Returns ExpInc IncTax PayTax LSTax AllTax - (#m) ($b) ($b) ($b) ($b) ($b) - 0 19.24 -274.9 0.1 0.0 0.0 0.1 - 1 19.24 210.9 0.0 0.0 0.0 0.0 - 2 19.24 414.9 0.0 0.0 0.0 0.0 - 3 19.24 629.1 0.0 0.0 0.0 0.0 - 4 19.24 876.9 0.0 0.0 0.0 0.0 - 5 19.24 1173.0 0.0 0.0 0.0 0.0 - 6 19.25 1581.6 0.0 0.0 0.0 0.0 - 7 19.24 2191.6 0.0 0.0 0.0 0.0 - 8 19.24 3238.1 0.0 0.0 0.0 0.0 - 9 19.24 10787.1 15.8 0.0 0.0 15.8 - A 192.41 20828.3 15.8 0.0 0.0 15.8 -``` - -So, the new top bracket (with a 39.6% marginal tax rate) raises -aggregate federal income tax liability by enough to pay for the TCJA -revision that approximates making social security benefits tax-free -and produces an additional $15.8 billion (in 2026) that could be used -to pay for other revisions. - -While those with the highest incomes do pay more tax, the reduction in -their after-tax expanded income is quite small as can be seen in one -of the standard graphs: -`tmd-26-ext+higher_aged_std-ext+higher_aged_std+new_top_bracket-#-pch.html`. -This graph shows that the top one percent of the income distribution -experiences a decline in after-tax income of about 0.36 percent (or -less than four dollars per thousand dollars). - -Here are the ten-year results for this run: - -``` -% tc ../tmd.csv 2026 --numyears 10 --baseline ext.json+higher_aged_std.json --reform ext.json+higher_aged_std.json+new_top_bracket.json --exact --tables -Read input data for 2021; input data were extrapolated to 2026 -Write tabular output to file tmd-26-ext+higher_aged_std-ext+higher_aged_std+new_top_bracket-#-tables.text -Advance input data and policy to 2027 -Write tabular output to file tmd-27-ext+higher_aged_std-ext+higher_aged_std+new_top_bracket-#-tables.text -Advance input data and policy to 2028 -Write tabular output to file tmd-28-ext+higher_aged_std-ext+higher_aged_std+new_top_bracket-#-tables.text -Advance input data and policy to 2029 -Write tabular output to file tmd-29-ext+higher_aged_std-ext+higher_aged_std+new_top_bracket-#-tables.text -Advance input data and policy to 2030 -Write tabular output to file tmd-30-ext+higher_aged_std-ext+higher_aged_std+new_top_bracket-#-tables.text -Advance input data and policy to 2031 -Write tabular output to file tmd-31-ext+higher_aged_std-ext+higher_aged_std+new_top_bracket-#-tables.text -Advance input data and policy to 2032 -Write tabular output to file tmd-32-ext+higher_aged_std-ext+higher_aged_std+new_top_bracket-#-tables.text -Advance input data and policy to 2033 -Write tabular output to file tmd-33-ext+higher_aged_std-ext+higher_aged_std+new_top_bracket-#-tables.text -Advance input data and policy to 2034 -Write tabular output to file tmd-34-ext+higher_aged_std-ext+higher_aged_std+new_top_bracket-#-tables.text -Advance input data and policy to 2035 -Write tabular output to file tmd-35-ext+higher_aged_std-ext+higher_aged_std+new_top_bracket-#-tables.text -Execution time is 57.1 seconds - -% tail -1 tmd-??-ext+higher_aged_std-ext+higher_aged_std+new_top_bracket-#-tables.text | awk '$1~/A/{n++;c+=$4}END{print n,c}' -10 187.9 -``` - -So, there is nearly $188 billion left to pay for other revisions to -the basic TCJA extension. - - -## How is the `ext.json` file generated? - -The short answer is by using the -[`extend_tcja.py`](https://github.com/PSLmodels/Tax-Calculator/blob/master/extend_tcja.py) script. - -Reading the `extend_tcja.py` script will provide details on how the -values in the `ext.json` file are generated. - -It is important to bear in mind that the `extend_tcja.py` script will -generate a different `ext.json` file whenever the CBO economic -projection (incorporated in the Tax-Calculator `growfactors.csv` file) -changes or whenever new historical values of policy parameters are -added to the `policy_current_law.json` file thereby changing the -`Policy.LAST_KNOWN_YEAR`. - -Beginning with the 4.5.0 version, Tax-Calculator incorporates the -January 2025 CBO economic projection and contains historical tax -policy parameter values through 2025. diff --git a/taxcalc/cli/input_data_tests/tests.sh b/taxcalc/cli/input_data_tests/tests.sh index 8bfb6cf00..0b989984c 100755 --- a/taxcalc/cli/input_data_tests/tests.sh +++ b/taxcalc/cli/input_data_tests/tests.sh @@ -5,43 +5,43 @@ # See Makefile target idtest for usage. tc cps.csv 2035 --exact --params --tables --silent -diff -q cps-35-#-#-#-params.baseline cps-35-params.baseline +diff -q cps-35-#-#-#-#-params.baseline cps-35-params.baseline if [ $? -eq 0 ]; then - rm cps-35-#-#-#-params.baseline + rm cps-35-#-#-#-#-params.baseline fi -diff -q cps-35-#-#-#-params.reform cps-35-params.reform +diff -q cps-35-#-#-#-#-params.reform cps-35-params.reform if [ $? -eq 0 ]; then - rm cps-35-#-#-#-params.reform + rm cps-35-#-#-#-#-params.reform fi -diff -q cps-35-#-#-#.tables cps-35.tables +diff -q cps-35-#-#-#-#.tables cps-35.tables if [ $? -eq 0 ]; then - rm cps-35-#-#-#.tables + rm cps-35-#-#-#-#.tables fi tc ../../../puf.csv 2035 --exact --params --tables --silent -diff -q puf-35-#-#-#-params.baseline puf-35-params.baseline +diff -q puf-35-#-#-#-#-params.baseline puf-35-params.baseline if [ $? -eq 0 ]; then - rm puf-35-#-#-#-params.baseline + rm puf-35-#-#-#-#-params.baseline fi -diff -q puf-35-#-#-#-params.reform puf-35-params.reform +diff -q puf-35-#-#-#-#-params.reform puf-35-params.reform if [ $? -eq 0 ]; then - rm puf-35-#-#-#-params.reform + rm puf-35-#-#-#-#-params.reform fi -diff -q puf-35-#-#-#.tables puf-35.tables +diff -q puf-35-#-#-#-#.tables puf-35.tables if [ $? -eq 0 ]; then - rm puf-35-#-#-#.tables + rm puf-35-#-#-#-#.tables fi tc ../../../tmd.csv 2035 --exact --params --tables --silent -diff -q tmd-35-#-#-#-params.baseline tmd-35-params.baseline +diff -q tmd-35-#-#-#-#-params.baseline tmd-35-params.baseline if [ $? -eq 0 ]; then - rm tmd-35-#-#-#-params.baseline + rm tmd-35-#-#-#-#-params.baseline fi -diff -q tmd-35-#-#-#-params.reform tmd-35-params.reform +diff -q tmd-35-#-#-#-#-params.reform tmd-35-params.reform if [ $? -eq 0 ]; then - rm tmd-35-#-#-#-params.reform + rm tmd-35-#-#-#-#-params.reform fi -diff -q tmd-35-#-#-#.tables tmd-35.tables +diff -q tmd-35-#-#-#-#.tables tmd-35.tables if [ $? -eq 0 ]; then - rm tmd-35-#-#-#.tables + rm tmd-35-#-#-#-#.tables fi diff --git a/taxcalc/taxcalcio.py b/taxcalc/taxcalcio.py index 849ae9074..8332607a1 100644 --- a/taxcalc/taxcalcio.py +++ b/taxcalc/taxcalcio.py @@ -240,8 +240,28 @@ def __init__(self, input_data, tax_year, baseline, reform, else: msg = 'TaxCalcIO.ctor: assump is neither None nor str' self.errmsg += f'ERROR: {msg}\n' + # check name and existence of BEHAVIOR file + beh = '-x' + if behavior is None: + beh = '-#' + elif isinstance(behavior, str): + # remove any leading directory path from BEHAVIOR filename + fname = os.path.basename(behavior) + # check if fname ends with ".json" + if fname.endswith('.json'): + beh = f'-{fname[:-5]}' + else: + msg = 'BEHAVIOR file name does not end in .json' + self.errmsg += f'ERROR: {msg}\n' + # check existence of BEHAVIOR file + if not os.path.isfile(behavior): + msg = 'BEHAVIOR file could not be found' + self.errmsg += f'ERROR: {msg}\n' + else: + msg = 'TaxCalcIO.ctor: behavior is neither None nor str' + self.errmsg += f'ERROR: {msg}\n' # create OUTPUT file name and delete any existing output files - self.output_filename = f'{inp}{bas}{ref}{asm}.xxx' + self.output_filename = f'{inp}{bas}{ref}{asm}{beh}.xxx' self.runid = runid if runid > 0: self.output_filename = f'run{runid}-{str(tax_year)[2:]}.xxx' From eb4b691eb96a42db3f20b5c98a5621c8a513c5db Mon Sep 17 00:00:00 2001 From: martinholmer Date: Sun, 30 Nov 2025 14:46:45 -0500 Subject: [PATCH 05/22] Add tests to increase code coverage --- taxcalc/tests/test_taxcalcio.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/taxcalc/tests/test_taxcalcio.py b/taxcalc/tests/test_taxcalcio.py index 9af89e5e5..f62522502 100644 --- a/taxcalc/tests/test_taxcalcio.py +++ b/taxcalc/tests/test_taxcalcio.py @@ -261,21 +261,22 @@ def fixture_assumpfile2(): pass # sometimes we can't remove a generated temporary file -@pytest.mark.parametrize('input_data, baseline, reform, assump', [ - ('no-dot-csv-filename', 'no-dot-json-filename', - 'no-dot-json-filename', - 'no-dot-json-filename'), - ([], [], [], [],), - ('no-exist.csv', 'no-exist.json', 'no-exist.json', 'no-exist.json'), - ('cps.csv', 'ereformfile', 'ereformfile', 'no-exist.json'), +@pytest.mark.parametrize('input_data, baseline, reform, assump, behavior', [ + ('no-dot-csv-filename', 'no-dot-json-filename', 'no-dot-json-filename', + 'no-dot-json-filename', 'no-dot-json-filename'), + ([], [], [], [], []), + ('no-exist.csv', 'no-exist.json', 'no-exist.json', + 'no-exist.json', 'no-exist.json'), + ('cps.csv', 'ereformfile', 'ereformfile', + 'no-exist.json', 'no-exist.json') ]) -def test_ctor_errors(input_data, baseline, reform, assump): +def test_ctor_errors(input_data, baseline, reform, assump, behavior): """ Ensure error messages are generated by TaxCalcIO.__init__. """ tcio = TaxCalcIO(input_data=input_data, tax_year=2013, baseline=baseline, reform=reform, - assump=assump, behavior=None) + assump=assump, behavior=behavior) assert tcio.errmsg From fbc42c619e4018e1c72ef72faf147be9ec2fa562 Mon Sep 17 00:00:00 2001 From: martinholmer Date: Sun, 30 Nov 2025 18:03:19 -0500 Subject: [PATCH 06/22] Read BEHAVIOR file and check its contents --- taxcalc.egg-info/PKG-INFO | 4 ++-- taxcalc.egg-info/SOURCES.txt | 1 - taxcalc/taxcalcio.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/taxcalc.egg-info/PKG-INFO b/taxcalc.egg-info/PKG-INFO index 01ad030ff..f8e73dab1 100644 --- a/taxcalc.egg-info/PKG-INFO +++ b/taxcalc.egg-info/PKG-INFO @@ -43,8 +43,8 @@ Dynamic: summary Tax-Calculator ============== -Tax-Calculator is an open-source microsimulation model for static -analysis of USA federal income and payroll taxes. +Tax-Calculator is an open-source microsimulation model for analysis of +USA federal income and payroll taxes. We are seeking contributors and maintainers. If you are interested in joining the project as a contributor or maintainer, open a new diff --git a/taxcalc.egg-info/SOURCES.txt b/taxcalc.egg-info/SOURCES.txt index 3ac64a64f..83df5a71b 100644 --- a/taxcalc.egg-info/SOURCES.txt +++ b/taxcalc.egg-info/SOURCES.txt @@ -96,7 +96,6 @@ docs/recipes/md_src/recipe06.md docs/usage/data.md docs/usage/overview.md docs/usage/starting.md -docs/usage/tcja_after_2025.md taxcalc/__init__.py taxcalc/calcfunctions.py taxcalc/calculator.py diff --git a/taxcalc/taxcalcio.py b/taxcalc/taxcalcio.py index 8332607a1..9d4df39ce 100644 --- a/taxcalc/taxcalcio.py +++ b/taxcalc/taxcalcio.py @@ -337,6 +337,36 @@ def init(self, input_data, tax_year, baseline, reform, return # get assumption sub-dictionaries assumpdict = Calculator.read_json_param_objects(None, assump) + # get behavior dictionary + behvdict = None + if behavior: + with open(behv, 'r', encoding='utf-8') as jfile: + json_text = jfile.read() + try: + behvdict = json_to_dict(json_text) + except ValueError as valerr: # pragma: no cover + msg = f'{behavior} contains invalid JSON' + self.errmsg = f'ERROR: BEHAVIOR file {msg}\n' + self.errmsg += f'{valerr}' + return + # check behavior response elasticity names and values + elasticity_set = set(behvdict.keys()) + if elasticity_set != set(['sub', 'inc', 'cg']): + msg = f'{behavior} contains invalid or missing elasticities' + self.errmsg = f'ERROR: BEHAVIOR file {msg}\n' + self.errmsg += f'Valid elasticities are "sub", "inc", "cg"' + return + if behvdict['sub'] < 0.0: + msg = f'{behavior} contains negative "sub" elasticity' + self.errmsg = f'ERROR: BEHAVIOR file {msg}\n' + if behvdict['inc'] > 0.0: + msg = f'{behavior} contains positive "inc" elasticity' + self.errmsg += f'ERROR: BEHAVIOR file {msg}\n' + if behvdict['cg'] > 0.0: + msg = f'{behavior} contains positive "cg" elasticity' + self.errmsg += f'ERROR: BEHAVIOR file {msg}\n' + if self.errmsg: + return # get policy parameter dictionaries from --baseline file(s) poldicts_bas = [] if self.specified_baseline: From c5284080033f038e6ea3580da646b705c769a387 Mon Sep 17 00:00:00 2001 From: martinholmer Date: Mon, 1 Dec 2025 09:58:46 -0500 Subject: [PATCH 07/22] Add tests of new TaxCalcIO.init behavior logic --- taxcalc/taxcalcio.py | 6 +-- taxcalc/tests/test_taxcalcio.py | 76 +++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/taxcalc/taxcalcio.py b/taxcalc/taxcalcio.py index 9d4df39ce..5e7f2506e 100644 --- a/taxcalc/taxcalcio.py +++ b/taxcalc/taxcalcio.py @@ -4,7 +4,7 @@ # CODING-STYLE CHECKS: # pycodestyle taxcalcio.py # pylint --disable=locally-disabled taxcalcio.py - +# pylint: disable=too-many-lines import os import gc import copy @@ -340,7 +340,7 @@ def init(self, input_data, tax_year, baseline, reform, # get behavior dictionary behvdict = None if behavior: - with open(behv, 'r', encoding='utf-8') as jfile: + with open(behavior, 'r', encoding='utf-8') as jfile: json_text = jfile.read() try: behvdict = json_to_dict(json_text) @@ -354,7 +354,7 @@ def init(self, input_data, tax_year, baseline, reform, if elasticity_set != set(['sub', 'inc', 'cg']): msg = f'{behavior} contains invalid or missing elasticities' self.errmsg = f'ERROR: BEHAVIOR file {msg}\n' - self.errmsg += f'Valid elasticities are "sub", "inc", "cg"' + self.errmsg += 'Valid elasticities are "sub", "inc", "cg"' return if behvdict['sub'] < 0.0: msg = f'{behavior} contains negative "sub" elasticity' diff --git a/taxcalc/tests/test_taxcalcio.py b/taxcalc/tests/test_taxcalcio.py index f62522502..633cc922d 100644 --- a/taxcalc/tests/test_taxcalcio.py +++ b/taxcalc/tests/test_taxcalcio.py @@ -727,3 +727,79 @@ def test_error_message_parsed_correctly(regression_reform_file): 'AMEDT_rt[year=2021] 1.8 > max 1 ' ) assert tcio.errmsg == exp_errmsg + + +@pytest.fixture(scope='session', name='behvfile0') +def fixture_behvfile0(): + """ + Temporary behavior file with .json extension. + """ + contents = """ + { + "sub": 0, + "inc": 0, + "cg": 0, + "extra_key": 0 + } + """ + with tempfile.NamedTemporaryFile( + suffix='.json', mode='a', delete=False + ) as bfile: + bfile.write(contents) + yield bfile + if os.path.isfile(bfile.name): + try: + os.remove(bfile.name) + except OSError: + pass # sometimes we can't remove a generated temporary file + + +@pytest.fixture(scope='session', name='behvfile1') +def fixture_behvfile1(): + """ + Temporary behavior file with .json extension. + """ + contents = """ + { + "sub": -0.3, + "inc": 0.5, + "cg": 1 + } + """ + with tempfile.NamedTemporaryFile( + suffix='.json', mode='a', delete=False + ) as bfile: + bfile.write(contents) + yield bfile + if os.path.isfile(bfile.name): + try: + os.remove(bfile.name) + except OSError: + pass # sometimes we can't remove a generated temporary file + + +@pytest.mark.parametrize('behvfile', [('behvfile0', 'behvfile1')]) +def test_init_behavior_errors(behvfile, behvfile0, behvfile1): + """ + Check behavior error messages generated correctly by TaxCalcIO.init method. + """ + recdict = {'RECID': 1, 'MARS': 1, 'e00300': 100000, 's006': 1e8} + recdf = pd.DataFrame(data=recdict, index=[0]) + if behvfile == 'behvfile0': + behavior_filename = behvfile0.name + else: + behavior_filename = behvfile1.name + # test TaxCalcIO constructor + tcio = TaxCalcIO(input_data=recdf, + tax_year=2024, + baseline=None, + reform=None, + assump=None, + behavior=behavior_filename) + assert not tcio.errmsg + # test TaxCalcIO.init method + tcio.init(input_data=recdf, tax_year=2024, + baseline=None, reform=None, + assump=None, behavior=behavior_filename, + exact_calculations=True) + assert tcio.errmsg From f357041cbe348e876bc839c978dcb7c5feb4f990 Mon Sep 17 00:00:00 2001 From: martinholmer Date: Mon, 1 Dec 2025 12:52:04 -0500 Subject: [PATCH 08/22] Edit error message in taxcalcio.py module --- taxcalc/taxcalcio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/taxcalc/taxcalcio.py b/taxcalc/taxcalcio.py index 5e7f2506e..8882d0690 100644 --- a/taxcalc/taxcalcio.py +++ b/taxcalc/taxcalcio.py @@ -352,7 +352,7 @@ def init(self, input_data, tax_year, baseline, reform, # check behavior response elasticity names and values elasticity_set = set(behvdict.keys()) if elasticity_set != set(['sub', 'inc', 'cg']): - msg = f'{behavior} contains invalid or missing elasticities' + msg = f'{behavior} contains extra or missing elasticities' self.errmsg = f'ERROR: BEHAVIOR file {msg}\n' self.errmsg += 'Valid elasticities are "sub", "inc", "cg"' return From 10452a2c9046fb83b752bef6e4e41572b5c6a10e Mon Sep 17 00:00:00 2001 From: martinholmer Date: Mon, 1 Dec 2025 15:08:19 -0500 Subject: [PATCH 09/22] Refactor new TaxCalcIO.init tests wrt --behavior option --- taxcalc/tests/test_taxcalcio.py | 39 ++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/taxcalc/tests/test_taxcalcio.py b/taxcalc/tests/test_taxcalcio.py index 633cc922d..2dbb37313 100644 --- a/taxcalc/tests/test_taxcalcio.py +++ b/taxcalc/tests/test_taxcalcio.py @@ -778,28 +778,31 @@ def fixture_behvfile1(): pass # sometimes we can't remove a generated temporary file -@pytest.mark.parametrize('behvfile', [('behvfile0', 'behvfile1')]) -def test_init_behavior_errors(behvfile, behvfile0, behvfile1): +def test_init_behavior_errors_0(behvfile0): """ Check behavior error messages generated correctly by TaxCalcIO.init method. """ recdict = {'RECID': 1, 'MARS': 1, 'e00300': 100000, 's006': 1e8} recdf = pd.DataFrame(data=recdict, index=[0]) - if behvfile == 'behvfile0': - behavior_filename = behvfile0.name - else: - behavior_filename = behvfile1.name - # test TaxCalcIO constructor - tcio = TaxCalcIO(input_data=recdf, - tax_year=2024, - baseline=None, - reform=None, - assump=None, - behavior=behavior_filename) + behv_fname = behvfile0.name + tcio = TaxCalcIO(input_data=recdf, tax_year=2024, baseline=None, + reform=None, assump=None, behavior=behv_fname) assert not tcio.errmsg - # test TaxCalcIO.init method - tcio.init(input_data=recdf, tax_year=2024, - baseline=None, reform=None, - assump=None, behavior=behavior_filename, - exact_calculations=True) + tcio.init(input_data=recdf, tax_year=2024, baseline=None, reform=None, + assump=None, behavior=behv_fname, exact_calculations=True) + assert tcio.errmsg + + +def test_init_behavior_errors_1(behvfile1): + """ + Check behavior error messages generated correctly by TaxCalcIO.init method. + """ + recdict = {'RECID': 1, 'MARS': 1, 'e00300': 100000, 's006': 1e8} + recdf = pd.DataFrame(data=recdict, index=[0]) + behv_fname = behvfile1.name + tcio = TaxCalcIO(input_data=recdf, tax_year=2024, baseline=None, + reform=None, assump=None, behavior=behv_fname) + assert not tcio.errmsg + tcio.init(input_data=recdf, tax_year=2024, baseline=None, reform=None, + assump=None, behavior=behv_fname, exact_calculations=True) assert tcio.errmsg From b3ae7ede9c875f72465c7c9e7a1c16069ce0c18b Mon Sep 17 00:00:00 2001 From: martinholmer Date: Mon, 1 Dec 2025 17:45:25 -0500 Subject: [PATCH 10/22] Step 1 in adding behavior logic to TaxCalcIO.analyze --- taxcalc/taxcalcio.py | 63 +++++++++++++++++++-------------- taxcalc/tests/test_taxcalcio.py | 32 ++++++++--------- 2 files changed, 52 insertions(+), 43 deletions(-) diff --git a/taxcalc/taxcalcio.py b/taxcalc/taxcalcio.py index 8882d0690..6d3694da1 100644 --- a/taxcalc/taxcalcio.py +++ b/taxcalc/taxcalcio.py @@ -13,6 +13,7 @@ import numpy as np import pandas as pd import paramtools +import behresp from taxcalc.policy import Policy from taxcalc.records import Records from taxcalc.consumption import Consumption @@ -241,6 +242,7 @@ def __init__(self, input_data, tax_year, baseline, reform, msg = 'TaxCalcIO.ctor: assump is neither None nor str' self.errmsg += f'ERROR: {msg}\n' # check name and existence of BEHAVIOR file + self.behvdict = None beh = '-x' if behavior is None: beh = '-#' @@ -338,31 +340,30 @@ def init(self, input_data, tax_year, baseline, reform, # get assumption sub-dictionaries assumpdict = Calculator.read_json_param_objects(None, assump) # get behavior dictionary - behvdict = None if behavior: with open(behavior, 'r', encoding='utf-8') as jfile: json_text = jfile.read() try: - behvdict = json_to_dict(json_text) + self.behvdict = json_to_dict(json_text) except ValueError as valerr: # pragma: no cover msg = f'{behavior} contains invalid JSON' self.errmsg = f'ERROR: BEHAVIOR file {msg}\n' self.errmsg += f'{valerr}' return # check behavior response elasticity names and values - elasticity_set = set(behvdict.keys()) + elasticity_set = set(self.behvdict.keys()) if elasticity_set != set(['sub', 'inc', 'cg']): msg = f'{behavior} contains extra or missing elasticities' self.errmsg = f'ERROR: BEHAVIOR file {msg}\n' self.errmsg += 'Valid elasticities are "sub", "inc", "cg"' return - if behvdict['sub'] < 0.0: + if self.behvdict['sub'] < 0.0: msg = f'{behavior} contains negative "sub" elasticity' self.errmsg = f'ERROR: BEHAVIOR file {msg}\n' - if behvdict['inc'] > 0.0: + if self.behvdict['inc'] > 0.0: msg = f'{behavior} contains positive "inc" elasticity' self.errmsg += f'ERROR: BEHAVIOR file {msg}\n' - if behvdict['cg'] > 0.0: + if self.behvdict['cg'] > 0.0: msg = f'{behavior} contains positive "cg" elasticity' self.errmsg += f'ERROR: BEHAVIOR file {msg}\n' if self.errmsg: @@ -616,24 +617,29 @@ def analyze( if not doing_calcs: return # do output calculations - self.calc_bas.calc_all() - self.calc_ref.calc_all() - if output_dump: + if self.behvdict: # if assuming behavioral responses + br_dump_bas, br_dump_ref = behresp.response( + self.calc_bas, self.calc_ref, + self.behvdict, dump=True + ) + else: # if assuming no behavioral responses + self.calc_bas.calc_all() + self.calc_ref.calc_all() + # handle MTR output variables + mtr_ptax_bas = None + mtr_itax_bas = None + mtr_ptax_ref = None + mtr_itax_ref = None + if output_dump and not self.behvdict: assert isinstance(dump_varlist, list) assert len(dump_varlist) > 0 - # might need marginal tax rates for dumpdb - (mtr_ptax_ref, mtr_itax_ref, - _) = self.calc_ref.mtr(wrt_full_compensation=False, - calc_all_already_called=True) - (mtr_ptax_bas, mtr_itax_bas, - _) = self.calc_bas.mtr(wrt_full_compensation=False, - calc_all_already_called=True) - else: - # do not need marginal tax rates for dumpdb - mtr_ptax_ref = None - mtr_itax_ref = None - mtr_ptax_bas = None - mtr_itax_bas = None + if 'mtr_itax' in dump_varlist or 'mtr_ptax' in dump_varlist: + (mtr_ptax_bas, mtr_itax_bas, + _) = self.calc_bas.mtr(wrt_full_compensation=False, + calc_all_already_called=True) + (mtr_ptax_ref, mtr_itax_ref, + _) = self.calc_ref.mtr(wrt_full_compensation=False, + calc_all_already_called=True) # optionally write --tables output to text file if output_tables: self.write_tables_file() @@ -642,11 +648,14 @@ def analyze( self.write_graph_files() # optionally write --dumpdb output to SQLite database file if output_dump: - self.write_dumpdb_file( - dump_varlist, - mtr_ptax_ref, mtr_itax_ref, - mtr_ptax_bas, mtr_itax_bas, - ) + if self.behvdict: # if assuming behavioral responses + pass # TODO --- add code here --- + else: # if assuming no behavioral responses + self.write_dumpdb_file( + dump_varlist, + mtr_ptax_ref, mtr_itax_ref, + mtr_ptax_bas, mtr_itax_bas, + ) def write_policy_params_files(self): """ diff --git a/taxcalc/tests/test_taxcalcio.py b/taxcalc/tests/test_taxcalcio.py index 2dbb37313..3272d66ff 100644 --- a/taxcalc/tests/test_taxcalcio.py +++ b/taxcalc/tests/test_taxcalcio.py @@ -754,6 +754,21 @@ def fixture_behvfile0(): pass # sometimes we can't remove a generated temporary file +def test_init_behavior0_errors(behvfile0): + """ + Check behavior error messages generated correctly by TaxCalcIO.init method. + """ + recdict = {'RECID': 1, 'MARS': 1, 'e00300': 100000, 's006': 1e8} + recdf = pd.DataFrame(data=recdict, index=[0]) + behv_fname = behvfile0.name + tcio = TaxCalcIO(input_data=recdf, tax_year=2024, baseline=None, + reform=None, assump=None, behavior=behv_fname) + assert not tcio.errmsg + tcio.init(input_data=recdf, tax_year=2024, baseline=None, reform=None, + assump=None, behavior=behv_fname, exact_calculations=True) + assert tcio.errmsg + + @pytest.fixture(scope='session', name='behvfile1') def fixture_behvfile1(): """ @@ -778,22 +793,7 @@ def fixture_behvfile1(): pass # sometimes we can't remove a generated temporary file -def test_init_behavior_errors_0(behvfile0): - """ - Check behavior error messages generated correctly by TaxCalcIO.init method. - """ - recdict = {'RECID': 1, 'MARS': 1, 'e00300': 100000, 's006': 1e8} - recdf = pd.DataFrame(data=recdict, index=[0]) - behv_fname = behvfile0.name - tcio = TaxCalcIO(input_data=recdf, tax_year=2024, baseline=None, - reform=None, assump=None, behavior=behv_fname) - assert not tcio.errmsg - tcio.init(input_data=recdf, tax_year=2024, baseline=None, reform=None, - assump=None, behavior=behv_fname, exact_calculations=True) - assert tcio.errmsg - - -def test_init_behavior_errors_1(behvfile1): +def test_init_behavior1_errors(behvfile1): """ Check behavior error messages generated correctly by TaxCalcIO.init method. """ From 1c709af19214c9827c97cf91fd5face6b6d9af72 Mon Sep 17 00:00:00 2001 From: martinholmer Date: Mon, 1 Dec 2025 21:20:53 -0500 Subject: [PATCH 11/22] Step 2 in adding behavior logic to TaxCalcIO.analyze --- taxcalc/taxcalcio.py | 64 +++++++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/taxcalc/taxcalcio.py b/taxcalc/taxcalcio.py index 6d3694da1..000def8f6 100644 --- a/taxcalc/taxcalcio.py +++ b/taxcalc/taxcalcio.py @@ -620,26 +620,40 @@ def analyze( if self.behvdict: # if assuming behavioral responses br_dump_bas, br_dump_ref = behresp.response( self.calc_bas, self.calc_ref, - self.behvdict, dump=True + self.behvdict, dump=True, ) + # move returned dump dataframe values back into calc objects + for vname, vseries in br_dump_bas.items(): + self.calc_bas.array(vname, vseries) + for vname, vseries in br_dump_ref.items(): + self.calc_ref.array(vname, vseries) else: # if assuming no behavioral responses self.calc_bas.calc_all() self.calc_ref.calc_all() # handle MTR output variables mtr_ptax_bas = None mtr_itax_bas = None + mtr_combined_bas = None mtr_ptax_ref = None mtr_itax_ref = None - if output_dump and not self.behvdict: + mtr_combined_ref = None + if output_dump: assert isinstance(dump_varlist, list) assert len(dump_varlist) > 0 - if 'mtr_itax' in dump_varlist or 'mtr_ptax' in dump_varlist: + mtr_output = ( + 'mtr_itax' in dump_varlist or + 'mtr_ptax' in dump_varlist or + 'mtr_combined' in dump_varlist + ) + if mtr_output: (mtr_ptax_bas, mtr_itax_bas, - _) = self.calc_bas.mtr(wrt_full_compensation=False, - calc_all_already_called=True) + mtr_combined_bas) = self.calc_bas.mtr( + wrt_full_compensation=False, + calc_all_already_called=True) (mtr_ptax_ref, mtr_itax_ref, - _) = self.calc_ref.mtr(wrt_full_compensation=False, - calc_all_already_called=True) + mtr_combined_ref) = self.calc_ref.mtr( + wrt_full_compensation=False, + calc_all_already_called=True) # optionally write --tables output to text file if output_tables: self.write_tables_file() @@ -648,14 +662,11 @@ def analyze( self.write_graph_files() # optionally write --dumpdb output to SQLite database file if output_dump: - if self.behvdict: # if assuming behavioral responses - pass # TODO --- add code here --- - else: # if assuming no behavioral responses - self.write_dumpdb_file( - dump_varlist, - mtr_ptax_ref, mtr_itax_ref, - mtr_ptax_bas, mtr_itax_bas, - ) + self.write_dumpdb_file( + dump_varlist, + mtr_ptax_ref, mtr_itax_ref, mtr_combined_ref, + mtr_ptax_bas, mtr_itax_bas, mtr_combined_bas, + ) def write_policy_params_files(self): """ @@ -931,24 +942,25 @@ def dump_variables(self, dumpvars_str): def write_dumpdb_file( self, dump_varlist, - mtr_ptax_ref, mtr_itax_ref, - mtr_ptax_bas, mtr_itax_bas, + mtr_ptax_ref, mtr_itax_ref, mtr_combined_ref, + mtr_ptax_bas, mtr_itax_bas, mtr_combined_bas, ): """ Write dump output to SQLite database file. """ # pylint: disable=too-many-arguments,too-many-positional-arguments - def dump_output(calcx, dumpvars, mtr_itax, mtr_ptax): + def dump_output(calcx, dumpvars, mtr_itax, mtr_ptax, mtr_combined): """ Extract dump output from calcx and return it as Pandas DataFrame. """ odict = {} for var in dumpvars: - if var in TaxCalcIO.MTR_DUMPVARS: - if var == 'mtr_itax': - odict[var] = pd.Series(mtr_itax) - elif var == 'mtr_ptax': - odict[var] = pd.Series(mtr_ptax) + if var == 'mtr_itax': + odict[var] = pd.Series(mtr_itax) + elif var == 'mtr_ptax': + odict[var] = pd.Series(mtr_ptax) + elif var == 'mtr_combined': + odict[var] = pd.Series(mtr_combined) else: odict[var] = pd.Series(calcx.array(var)) odf = pd.concat(odict, axis=1) @@ -994,14 +1006,16 @@ def dump_output(calcx, dumpvars, mtr_itax, mtr_ptax): del outdf # write baseline table outdf = dump_output( - self.calc_bas, dump_varlist, mtr_itax_bas, mtr_ptax_bas, + self.calc_bas, dump_varlist, + mtr_itax_bas, mtr_ptax_bas, mtr_combined_bas, ) assert len(outdf.index) == self.calc_bas.array_len outdf.to_sql('baseline', dbcon, index=False) del outdf # write reform table outdf = dump_output( - self.calc_ref, dump_varlist, mtr_itax_ref, mtr_ptax_ref, + self.calc_ref, dump_varlist, + mtr_itax_ref, mtr_ptax_ref, mtr_combined_ref, ) assert len(outdf.index) == self.calc_ref.array_len outdf.to_sql('reform', dbcon, index=False) From 931dc0484d7cd78df4dc72ad2564a8104958c163 Mon Sep 17 00:00:00 2001 From: martinholmer Date: Tue, 2 Dec 2025 12:26:58 -0500 Subject: [PATCH 12/22] Step 3 in adding behavior logic to TaxCalcIO.analyze --- taxcalc/taxcalcio.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/taxcalc/taxcalcio.py b/taxcalc/taxcalcio.py index 000def8f6..2ed12ccb8 100644 --- a/taxcalc/taxcalcio.py +++ b/taxcalc/taxcalcio.py @@ -623,10 +623,20 @@ def analyze( self.behvdict, dump=True, ) # move returned dump dataframe values back into calc objects - for vname, vseries in br_dump_bas.items(): - self.calc_bas.array(vname, vseries) - for vname, vseries in br_dump_ref.items(): - self.calc_ref.array(vname, vseries) + vnames = list(br_dump_bas.columns) + for mtr_vname in ['mtr_ptax', 'mtr_itax', 'mtr_combined']: + if mtr_vname in vnames: + vnames.remove(mtr_vname) + for vname in vnames: + self.calc_bas.array(vname, br_dump_bas[vname]) + del br_dump_bas + vnames = list(br_dump_ref.columns) + for mtr_vname in ['mtr_ptax', 'mtr_itax', 'mtr_combined']: + if mtr_vname in vnames: + vnames.remove(mtr_vname) + for vname in vnames: + self.calc_bas.array(vname, br_dump_ref[vname]) + del br_dump_ref else: # if assuming no behavioral responses self.calc_bas.calc_all() self.calc_ref.calc_all() From 76bf55395d682eeb034b47c4aa57e38aabeaf14c Mon Sep 17 00:00:00 2001 From: martinholmer Date: Tue, 2 Dec 2025 14:23:44 -0500 Subject: [PATCH 13/22] Step 4 in adding behavior logic to TaxCalcIO.analyze --- taxcalc/taxcalcio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/taxcalc/taxcalcio.py b/taxcalc/taxcalcio.py index 2ed12ccb8..e049dcd8f 100644 --- a/taxcalc/taxcalcio.py +++ b/taxcalc/taxcalcio.py @@ -628,14 +628,14 @@ def analyze( if mtr_vname in vnames: vnames.remove(mtr_vname) for vname in vnames: - self.calc_bas.array(vname, br_dump_bas[vname]) + self.calc_bas.array(vname, br_dump_bas[vname].to_numpy()) del br_dump_bas vnames = list(br_dump_ref.columns) for mtr_vname in ['mtr_ptax', 'mtr_itax', 'mtr_combined']: if mtr_vname in vnames: vnames.remove(mtr_vname) for vname in vnames: - self.calc_bas.array(vname, br_dump_ref[vname]) + self.calc_ref.array(vname, br_dump_ref[vname].to_numpy()) del br_dump_ref else: # if assuming no behavioral responses self.calc_bas.calc_all() From 1f32bf9ce0f987135e6160220937ff19b15d92c4 Mon Sep 17 00:00:00 2001 From: martinholmer Date: Wed, 3 Dec 2025 10:09:22 -0500 Subject: [PATCH 14/22] Add brtest target to Makefile --- Makefile | 7 ++++ .../cli/behavioral_responses_tests/br0.json | 5 +++ .../cli/behavioral_responses_tests/br1.json | 5 +++ .../cli/behavioral_responses_tests/ref.json | 10 ++++++ .../run11-35.tables-expect | 29 ++++++++++++++++ .../cli/behavioral_responses_tests/tests.sh | 33 +++++++++++++++++++ 6 files changed, 89 insertions(+) create mode 100644 taxcalc/cli/behavioral_responses_tests/br0.json create mode 100644 taxcalc/cli/behavioral_responses_tests/br1.json create mode 100644 taxcalc/cli/behavioral_responses_tests/ref.json create mode 100644 taxcalc/cli/behavioral_responses_tests/run11-35.tables-expect create mode 100755 taxcalc/cli/behavioral_responses_tests/tests.sh diff --git a/Makefile b/Makefile index a80704697..a998691b4 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,8 @@ help: @echo " pycodestyle (nee pep8) and pylint tools" @echo "idtest : generate report for and cleanup after executing" @echo " taxcalc/cli/input_data_tests/tests.sh" + @echo "brtest : generate report for and cleanup after executing" + @echo " taxcalc/cli/behavioral_responses_tests/tests.sh" @echo "coverage : generate test coverage report" @echo "git-sync : synchronize local, origin, and upstream Git repos" @echo "git-pr N=n : create local pr-n branch containing upstream PR" @@ -96,6 +98,11 @@ idtest: package @echo "Executing taxcalc/cli/input_data_tests" @cd taxcalc/cli/input_data_tests ; ./tests.sh +.PHONY=brtest +brtest: package + @echo "Executing taxcalc/cli/behavioral_responses_tests" + @cd taxcalc/cli/behavioral_responses_tests ; ./tests.sh + define coverage-cleanup rm -f .coverage htmlcov/* endef diff --git a/taxcalc/cli/behavioral_responses_tests/br0.json b/taxcalc/cli/behavioral_responses_tests/br0.json new file mode 100644 index 000000000..ba2985138 --- /dev/null +++ b/taxcalc/cli/behavioral_responses_tests/br0.json @@ -0,0 +1,5 @@ +{ + "sub": 0.0, + "inc": 0.0, + "cg": 0.0 +} diff --git a/taxcalc/cli/behavioral_responses_tests/br1.json b/taxcalc/cli/behavioral_responses_tests/br1.json new file mode 100644 index 000000000..f73bdeb48 --- /dev/null +++ b/taxcalc/cli/behavioral_responses_tests/br1.json @@ -0,0 +1,5 @@ +{ + "sub": 0.25, + "inc": 0.0, + "cg": 0.0 +} diff --git a/taxcalc/cli/behavioral_responses_tests/ref.json b/taxcalc/cli/behavioral_responses_tests/ref.json new file mode 100644 index 000000000..a15026b8c --- /dev/null +++ b/taxcalc/cli/behavioral_responses_tests/ref.json @@ -0,0 +1,10 @@ +{ + "policy": { + // raise personal exemption amount in 2028 and index in subsequent years + "II_em": {"2028": 1000}, + // raise non-AMT tax rates in the top three brackets in 2028 + "II_rt5": {"2028": 0.36}, + "II_rt6": {"2028": 0.39}, + "II_rt7": {"2028": 0.41} + } +} diff --git a/taxcalc/cli/behavioral_responses_tests/run11-35.tables-expect b/taxcalc/cli/behavioral_responses_tests/run11-35.tables-expect new file mode 100644 index 000000000..9ee1c83ee --- /dev/null +++ b/taxcalc/cli/behavioral_responses_tests/run11-35.tables-expect @@ -0,0 +1,29 @@ +Weighted Tax Reform Totals by Baseline Expanded-Income Decile for 2035 + Returns ExpInc IncTax PayTax LSTax AllTax + (#m) ($b) ($b) ($b) ($b) ($b) + 0 24.21 66.6 -1.4 5.0 0.0 3.6 + 1 24.21 584.2 -12.7 46.8 0.0 34.1 + 2 24.21 1073.3 -2.7 66.8 0.0 64.2 + 3 24.21 1429.7 14.7 78.0 0.0 92.8 + 4 24.21 1807.9 33.7 113.5 0.0 147.2 + 5 24.21 2289.4 64.1 159.8 0.0 223.9 + 6 24.21 2931.1 114.6 213.8 0.0 328.4 + 7 24.21 3853.4 206.6 305.7 0.0 512.3 + 8 24.21 5371.6 437.7 482.6 0.0 920.3 + 9 24.21 12429.6 2384.6 916.9 0.0 3301.4 + A 242.13 31836.8 3239.2 2388.9 0.0 5628.2 + +Weighted Tax Differences by Baseline Expanded-Income Decile for 2035 + Returns ExpInc IncTax PayTax LSTax AllTax + (#m) ($b) ($b) ($b) ($b) ($b) + 0 24.21 66.6 0.0 0.0 0.0 0.0 + 1 24.21 584.2 -0.7 0.0 0.0 -0.7 + 2 24.21 1073.3 -1.4 0.0 0.0 -1.4 + 3 24.21 1429.7 -1.8 0.0 0.0 -1.8 + 4 24.21 1807.9 -2.7 0.0 0.0 -2.7 + 5 24.21 2289.4 -4.3 0.0 0.0 -4.3 + 6 24.21 2931.1 -5.8 0.0 0.0 -5.8 + 7 24.21 3853.4 -7.9 0.0 0.0 -7.9 + 8 24.21 5371.6 -13.4 0.0 0.0 -13.4 + 9 24.21 12429.6 74.0 0.0 0.0 74.0 + A 242.13 31836.8 36.1 0.0 0.0 36.1 diff --git a/taxcalc/cli/behavioral_responses_tests/tests.sh b/taxcalc/cli/behavioral_responses_tests/tests.sh new file mode 100755 index 000000000..72c4b4382 --- /dev/null +++ b/taxcalc/cli/behavioral_responses_tests/tests.sh @@ -0,0 +1,33 @@ +#!/bin/zsh +# CLI tests of behavior responses logic + +tc cps.csv 2035 --numyears 1 --runid 10 \ + --reform ref.json --exact --params --tables --silent +echo finished run10 +tc cps.csv 2028 --numyears 8 --runid 11 \ + --reform ref.json --exact --params --tables --silent +echo finished run11 +echo run11-vs-run11exp-diffs: +diff run11-35.tables run11-35.tables-expect +echo run11-vs-run10-diffs: +diff run11-35.tables run10-35.tables + +tc cps.csv 2035 --numyears 1 --behavior br0.json --runid 20 \ + --reform ref.json --exact --params --tables --silent +echo finished run20 +echo run20-vs-run10-diffs: +diff run20-35.tables run10-35.tables +tc cps.csv 2028 --numyears 8 --behavior br0.json --runid 21 \ + --reform ref.json --exact --params --tables --silent +echo finished run21 +echo run21-vs-run20-diffs: +diff run21-35.tables run20-35.tables + +tc cps.csv 2035 --numyears 1 --behavior br1.json --runid 30 \ + --reform ref.json --exact --params --tables --silent +echo finished run30 +tc cps.csv 2028 --numyears 8 --behavior br1.json --runid 31 \ + --reform ref.json --exact --params --tables --silent +echo finished run31 +echo run31-vs-run30-diffs: +diff run31-35.tables run30-35.tables From d49c33be8695115a1b5e8e8cdbf0a05c4a5f8e0e Mon Sep 17 00:00:00 2001 From: martinholmer Date: Wed, 3 Dec 2025 13:09:29 -0500 Subject: [PATCH 15/22] Step 5 in adding behavior logic to TaxCalcIO.analyze --- .../cli/behavioral_responses_tests/tests.sh | 12 +- taxcalc/taxcalcio.py | 103 ++++++++++++------ 2 files changed, 75 insertions(+), 40 deletions(-) diff --git a/taxcalc/cli/behavioral_responses_tests/tests.sh b/taxcalc/cli/behavioral_responses_tests/tests.sh index 72c4b4382..f01fadc2a 100755 --- a/taxcalc/cli/behavioral_responses_tests/tests.sh +++ b/taxcalc/cli/behavioral_responses_tests/tests.sh @@ -2,10 +2,10 @@ # CLI tests of behavior responses logic tc cps.csv 2035 --numyears 1 --runid 10 \ - --reform ref.json --exact --params --tables --silent + --reform ref.json --exact --tables --silent echo finished run10 tc cps.csv 2028 --numyears 8 --runid 11 \ - --reform ref.json --exact --params --tables --silent + --reform ref.json --exact --tables --silent echo finished run11 echo run11-vs-run11exp-diffs: diff run11-35.tables run11-35.tables-expect @@ -13,21 +13,21 @@ echo run11-vs-run10-diffs: diff run11-35.tables run10-35.tables tc cps.csv 2035 --numyears 1 --behavior br0.json --runid 20 \ - --reform ref.json --exact --params --tables --silent + --reform ref.json --exact --tables --silent echo finished run20 echo run20-vs-run10-diffs: diff run20-35.tables run10-35.tables tc cps.csv 2028 --numyears 8 --behavior br0.json --runid 21 \ - --reform ref.json --exact --params --tables --silent + --reform ref.json --exact --tables --silent echo finished run21 echo run21-vs-run20-diffs: diff run21-35.tables run20-35.tables tc cps.csv 2035 --numyears 1 --behavior br1.json --runid 30 \ - --reform ref.json --exact --params --tables --silent + --reform ref.json --exact --tables --silent echo finished run30 tc cps.csv 2028 --numyears 8 --behavior br1.json --runid 31 \ - --reform ref.json --exact --params --tables --silent + --reform ref.json --exact --tables --silent echo finished run31 echo run31-vs-run30-diffs: diff run31-35.tables run30-35.tables diff --git a/taxcalc/taxcalcio.py b/taxcalc/taxcalcio.py index e049dcd8f..2b7a2e060 100644 --- a/taxcalc/taxcalcio.py +++ b/taxcalc/taxcalcio.py @@ -269,6 +269,12 @@ def __init__(self, input_data, tax_year, baseline, reform, self.output_filename = f'run{runid}-{str(tax_year)[2:]}.xxx' self.delete_output_files() # initialize variables whose values are set in init method + self.pol_ref = None + self.pol_bas = None + self.recs_ref = None + self.recs_bas = None + self.con = None + self.aging_input_data = None self.calc_ref = None self.calc_bas = None @@ -408,84 +414,84 @@ def init(self, input_data, tax_year, baseline, reform, gdiff_response.apply_to(policy_gfactors_ref) # ... the baseline Policy object if self.specified_baseline: - pol_bas = Policy( + self.pol_bas = Policy( gfactors=policy_gfactors_bas, last_budget_year=last_b_year, ) for poldict in poldicts_bas: try: - pol_bas.implement_reform( + self.pol_bas.implement_reform( poldict, print_warnings=True, raise_errors=False, ) if self.errmsg: self.errmsg += '\n' - for _, errors in pol_bas.parameter_errors.items(): + for _, errors in self.pol_bas.parameter_errors.items(): self.errmsg += '\n'.join(errors) except paramtools.ValidationError as valerr_msg: self.errmsg += str(valerr_msg) else: - pol_bas = Policy( + self.pol_bas = Policy( gfactors=policy_gfactors_bas, last_budget_year=last_b_year, ) # ... the reform Policy object if self.specified_reform: - pol_ref = Policy( + self.pol_ref = Policy( gfactors=policy_gfactors_ref, last_budget_year=last_b_year, ) for poldict in poldicts_ref: try: - pol_ref.implement_reform( + self.pol_ref.implement_reform( poldict, print_warnings=True, raise_errors=False, ) if self.errmsg: self.errmsg += '\n' - for _, errors in pol_ref.parameter_errors.items(): + for _, errors in self.pol_ref.parameter_errors.items(): self.errmsg += '\n'.join(errors) except paramtools.ValidationError as valerr_msg: self.errmsg += str(valerr_msg) else: - pol_ref = Policy( + self.pol_ref = Policy( gfactors=policy_gfactors_bas, last_budget_year=last_b_year, ) # create Consumption object - con = Consumption(last_budget_year=last_b_year) + self.con = Consumption(last_budget_year=last_b_year) try: - con.update_consumption(assumpdict['consumption']) + self.con.update_consumption(assumpdict['consumption']) except paramtools.ValidationError as valerr_msg: self.errmsg += str(valerr_msg) # any errors imply cannot proceed with calculations if self.errmsg: return # set policy to tax_year - pol_ref.set_year(tax_year) - pol_bas.set_year(tax_year) + self.pol_ref.set_year(tax_year) + self.pol_bas.set_year(tax_year) # read input file contents into Records objects - aging_input_data = True + self.aging_input_data = True if self.cps_input_data: - recs_ref = Records.cps_constructor( + self.recs_ref = Records.cps_constructor( gfactors=gfactors_ref, exact_calculations=exact_calculations, ) - recs_bas = Records.cps_constructor( + self.recs_bas = Records.cps_constructor( gfactors=gfactors_bas, exact_calculations=exact_calculations, ) elif self.puf_input_data: # pragma: no cover - recs_ref = Records.puf_constructor( + self.recs_ref = Records.puf_constructor( data=input_data, gfactors=gfactors_ref, weights=self.puf_weights, ratios=self.puf_ratios, exact_calculations=exact_calculations, ) - recs_bas = Records.puf_constructor( + self.recs_bas = Records.puf_constructor( data=input_data, gfactors=gfactors_bas, weights=self.puf_weights, @@ -493,21 +499,21 @@ def init(self, input_data, tax_year, baseline, reform, exact_calculations=exact_calculations, ) elif self.tmd_input_data: # pragma: no cover - recs_ref = Records.tmd_constructor( + self.recs_ref = Records.tmd_constructor( data_path=Path(input_data), weights_path=Path(self.tmd_weights), growfactors=gfactors_ref, exact_calculations=exact_calculations, ) - recs_bas = Records.tmd_constructor( + self.recs_bas = Records.tmd_constructor( data_path=Path(input_data), weights_path=Path(self.tmd_weights), growfactors=gfactors_bas, exact_calculations=exact_calculations, ) else: # input_data are raw data that are not being aged - aging_input_data = False - recs_ref = Records( + self.aging_input_data = False + self.recs_ref = Records( data=input_data, start_year=tax_year, gfactors=None, @@ -515,21 +521,26 @@ def init(self, input_data, tax_year, baseline, reform, adjust_ratios=None, exact_calculations=exact_calculations, ) - recs_bas = copy.deepcopy(recs_ref) + self.recs_bas = copy.deepcopy(self.recs_ref) + # extrapolate input data to tax_year if aging_input_data is True + if self.aging_input_data: + while self.recs_ref.current_year < tax_year: + self.recs_ref.increment_year() + self.recs_bas.increment_year() # create Calculator objects self.calc_ref = Calculator( - policy=pol_ref, - records=recs_ref, + policy=self.pol_ref, + records=self.recs_ref, verbose=(not self.silent), - consumption=con, - sync_years=aging_input_data, + consumption=self.con, + sync_years=self.aging_input_data, ) self.calc_bas = Calculator( - policy=pol_bas, - records=recs_bas, + policy=self.pol_bas, + records=self.recs_bas, verbose=False, - consumption=con, - sync_years=aging_input_data, + consumption=self.con, + sync_years=self.aging_input_data, ) def tax_year(self): @@ -547,7 +558,7 @@ def output_filepath(self): def advance_to_year(self, year): """ - Update self.output_filename and advance Calculator objects to year. + Update self.output_filename and create Calculator objects for year. """ # update self.output_filename and delete output files parts = self.output_filename.split('-') @@ -559,9 +570,33 @@ def advance_to_year(self, year): parts[1] = '.'.join(subparts) self.output_filename = '-'.join(parts) self.delete_output_files() - # advance baseline and reform Calculator objects to specified year - self.calc_bas.advance_to_year(year) - self.calc_ref.advance_to_year(year) + # create baseline and reform Calculator objects for specified year + # ... set policy for year + self.pol_ref.set_year(year) + self.pol_bas.set_year(year) + # ... set consumption for year + self.con.set_year(year) + # ... increment records to year + self.recs_ref.increment_year() + self.recs_bas.increment_year() + # ... delete old and create new Calculator objects + del self.calc_ref + self.calc_ref = Calculator( + policy=self.pol_ref, + records=self.recs_ref, + verbose=(not self.silent), + consumption=self.con, + sync_years=self.aging_input_data, + ) + del self.calc_bas + self.calc_bas = Calculator( + policy=self.pol_bas, + records=self.recs_bas, + verbose=(not self.silent), + consumption=self.con, + sync_years=self.aging_input_data, + ) + # report advance to new year aging_data = ( self.cps_input_data or self.puf_input_data or From d926aa774bffa5324801e504255440d567139cb1 Mon Sep 17 00:00:00 2001 From: martinholmer Date: Wed, 3 Dec 2025 15:03:42 -0500 Subject: [PATCH 16/22] Add test of CLI assuming behavioral responses --- docs/contributing/RELEASING.md | 17 ++----- .../cli/behavioral_responses_tests/tests.sh | 47 ++++++++++++------- taxcalc/tests/test_taxcalcio.py | 44 +++++++++++++++++ 3 files changed, 78 insertions(+), 30 deletions(-) diff --git a/docs/contributing/RELEASING.md b/docs/contributing/RELEASING.md index 2cac79133..b71489607 100755 --- a/docs/contributing/RELEASING.md +++ b/docs/contributing/RELEASING.md @@ -7,29 +7,18 @@ In the top-level Tax-Calculator directory, do the following: ``` --> run `git switch master` [to get on master branch] - --> run `./gitsync` [to ensure master is up-to-date with GitHub version] - --> run `make clean` [to remove any local taxcalc package] - --> run `git checkout -b X-Y-Z` [to create X-Y-Z (e.g., `4-4-1`) branch] - --> on branch X-Y-Z, edit docs/about/releases.md to finalize X.Y.Z info - --> specify release X.Y.Z in setup.py, taxcalc/__init__.py, docs/index.md - ---> run `python update_pcl.py` [to update policy_current_law.json if needed] - +--> run `python update_pcl.py` [to update policy_current_law.json] --> run `make cstest` [to confirm project coding style is being followed] - --> run `make pytest-all` [to confirm all pytest test are passing] - --> run `make idtest` [to check CLI results for CPS, PUF, TMD input data] - ---> run `make tctest-jit` [to make sure JIT decorators are not hiding bugs] - +--> run `make brtest` [to check CLI results for behavioral responses] +--> run `make tctest-jit` [to ensure JIT decorators are not hiding bugs] --> commit X-Y-Z branch and push to origin - --> open new GitHub pull request using your X-Y-Z branch ``` diff --git a/taxcalc/cli/behavioral_responses_tests/tests.sh b/taxcalc/cli/behavioral_responses_tests/tests.sh index f01fadc2a..93cfa1aac 100755 --- a/taxcalc/cli/behavioral_responses_tests/tests.sh +++ b/taxcalc/cli/behavioral_responses_tests/tests.sh @@ -1,33 +1,48 @@ #!/bin/zsh # CLI tests of behavior responses logic +ERRORS=0 + tc cps.csv 2035 --numyears 1 --runid 10 \ --reform ref.json --exact --tables --silent -echo finished run10 tc cps.csv 2028 --numyears 8 --runid 11 \ --reform ref.json --exact --tables --silent -echo finished run11 -echo run11-vs-run11exp-diffs: -diff run11-35.tables run11-35.tables-expect -echo run11-vs-run10-diffs: -diff run11-35.tables run10-35.tables +cmp run11-35.tables run11-35.tables-expect +if [ $? -ne 0 ]; then + ERRORS=1 + echo Differences between run11-35.tables run11-35.tables-expect +fi +cmp run11-35.tables run10-35.tables +if [ $? -ne 0 ]; then + ERRORS=1 + echo Differences between run11-35.tables run10-35.tables +fi tc cps.csv 2035 --numyears 1 --behavior br0.json --runid 20 \ --reform ref.json --exact --tables --silent -echo finished run20 -echo run20-vs-run10-diffs: -diff run20-35.tables run10-35.tables +cmp run20-35.tables run10-35.tables +if [ $? -ne 0 ]; then + ERRORS=1 + echo Differences between run20-35.tables run10-35.tables +fi tc cps.csv 2028 --numyears 8 --behavior br0.json --runid 21 \ --reform ref.json --exact --tables --silent -echo finished run21 -echo run21-vs-run20-diffs: -diff run21-35.tables run20-35.tables +cmp run21-35.tables run20-35.tables +if [ $? -ne 0 ]; then + ERRORS=1 + echo Differences between run21-35.tables run20-35.tables +fi tc cps.csv 2035 --numyears 1 --behavior br1.json --runid 30 \ --reform ref.json --exact --tables --silent -echo finished run30 tc cps.csv 2028 --numyears 8 --behavior br1.json --runid 31 \ --reform ref.json --exact --tables --silent -echo finished run31 -echo run31-vs-run30-diffs: -diff run31-35.tables run30-35.tables +cmp run31-35.tables run30-35.tables +if [ $? -ne 0 ]; then + ERRORS=1 + echo Differences between run31-35.tables run30-35.tables +fi + +if [ $ERRORS -eq 0 ]; then + rm -f run??-??.tables +fi diff --git a/taxcalc/tests/test_taxcalcio.py b/taxcalc/tests/test_taxcalcio.py index 3272d66ff..b9d6d6247 100644 --- a/taxcalc/tests/test_taxcalcio.py +++ b/taxcalc/tests/test_taxcalcio.py @@ -806,3 +806,47 @@ def test_init_behavior1_errors(behvfile1): tcio.init(input_data=recdf, tax_year=2024, baseline=None, reform=None, assump=None, behavior=behv_fname, exact_calculations=True) assert tcio.errmsg + + +@pytest.fixture(scope='session', name='behvfile2') +def fixture_behvfile2(): + """ + Temporary behavior file with .json extension. + """ + contents = """ + { + "sub": 0.25, + "inc": 0.0, + "cg": 0.0 + } + """ + with tempfile.NamedTemporaryFile( + suffix='.json', mode='a', delete=False + ) as bfile: + bfile.write(contents) + yield bfile + if os.path.isfile(bfile.name): + try: + os.remove(bfile.name) + except OSError: + pass # sometimes we can't remove a generated temporary file + + +def test_tc_analyze_with_behavior(reformfile1, behvfile2): + """ + Test TaxCalcIO.analyze method when assuming behavioral responses to reform. + """ + txyr = 2020 + tcio = TaxCalcIO( + 'cps.csv', txyr, baseline=None, reform=reformfile1.name, + assump=None, behavior=behvfile2.name, + ) + tcio.init( + 'cps.csv', txyr, baseline=None, reform=reformfile1.name, + assump=None, behavior=behvfile2.name, + exact_calculations=True, + ) + assert not tcio.errmsg + assert tcio.tax_year() == txyr + tcio.analyze() + assert tcio.tax_year() == txyr From 4ad29bb77b3ef6467a52877d597d10cb3c235986 Mon Sep 17 00:00:00 2001 From: martinholmer Date: Wed, 3 Dec 2025 16:46:40 -0500 Subject: [PATCH 17/22] Modify new test --- taxcalc/tests/test_taxcalcio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/taxcalc/tests/test_taxcalcio.py b/taxcalc/tests/test_taxcalcio.py index b9d6d6247..d8b734f55 100644 --- a/taxcalc/tests/test_taxcalcio.py +++ b/taxcalc/tests/test_taxcalcio.py @@ -848,5 +848,5 @@ def test_tc_analyze_with_behavior(reformfile1, behvfile2): ) assert not tcio.errmsg assert tcio.tax_year() == txyr - tcio.analyze() + tcio.analyze(output_tables=True) assert tcio.tax_year() == txyr From 06fc8ec792f8ffa686d62ff485861c00650267f1 Mon Sep 17 00:00:00 2001 From: martinholmer Date: Wed, 3 Dec 2025 17:38:45 -0500 Subject: [PATCH 18/22] Add cleanup code to new test --- taxcalc/tests/test_taxcalcio.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/taxcalc/tests/test_taxcalcio.py b/taxcalc/tests/test_taxcalcio.py index d8b734f55..f6f004e30 100644 --- a/taxcalc/tests/test_taxcalcio.py +++ b/taxcalc/tests/test_taxcalcio.py @@ -9,6 +9,7 @@ import os from io import StringIO +from pathlib import Path import tempfile import pytest import pandas as pd @@ -836,17 +837,19 @@ def test_tc_analyze_with_behavior(reformfile1, behvfile2): """ Test TaxCalcIO.analyze method when assuming behavioral responses to reform. """ - txyr = 2020 tcio = TaxCalcIO( - 'cps.csv', txyr, baseline=None, reform=reformfile1.name, + 'cps.csv', 2020, baseline=None, reform=reformfile1.name, assump=None, behavior=behvfile2.name, + runid=11, ) tcio.init( - 'cps.csv', txyr, baseline=None, reform=reformfile1.name, + 'cps.csv', 2020, baseline=None, reform=reformfile1.name, assump=None, behavior=behvfile2.name, exact_calculations=True, ) assert not tcio.errmsg - assert tcio.tax_year() == txyr + assert tcio.tax_year() == 2020 tcio.analyze(output_tables=True) - assert tcio.tax_year() == txyr + assert tcio.tax_year() == 2020 + table_path = Path('run11-20.tables') + table_path.unlink(missing_ok=True) From 5f92d2e6edfa6b24b62ac29217be1e7d72b03125 Mon Sep 17 00:00:00 2001 From: martinholmer Date: Wed, 3 Dec 2025 17:54:51 -0500 Subject: [PATCH 19/22] Modify handling of MTR variables in TaxCalcIO --- taxcalc/taxcalcio.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/taxcalc/taxcalcio.py b/taxcalc/taxcalcio.py index 2b7a2e060..eb331756c 100644 --- a/taxcalc/taxcalcio.py +++ b/taxcalc/taxcalcio.py @@ -678,25 +678,22 @@ def analyze( # handle MTR output variables mtr_ptax_bas = None mtr_itax_bas = None - mtr_combined_bas = None mtr_ptax_ref = None mtr_itax_ref = None - mtr_combined_ref = None if output_dump: assert isinstance(dump_varlist, list) assert len(dump_varlist) > 0 mtr_output = ( 'mtr_itax' in dump_varlist or - 'mtr_ptax' in dump_varlist or - 'mtr_combined' in dump_varlist + 'mtr_ptax' in dump_varlist ) if mtr_output: (mtr_ptax_bas, mtr_itax_bas, - mtr_combined_bas) = self.calc_bas.mtr( + _) = self.calc_bas.mtr( wrt_full_compensation=False, calc_all_already_called=True) (mtr_ptax_ref, mtr_itax_ref, - mtr_combined_ref) = self.calc_ref.mtr( + _) = self.calc_ref.mtr( wrt_full_compensation=False, calc_all_already_called=True) # optionally write --tables output to text file @@ -709,8 +706,8 @@ def analyze( if output_dump: self.write_dumpdb_file( dump_varlist, - mtr_ptax_ref, mtr_itax_ref, mtr_combined_ref, - mtr_ptax_bas, mtr_itax_bas, mtr_combined_bas, + mtr_ptax_ref, mtr_itax_ref, + mtr_ptax_bas, mtr_itax_bas, ) def write_policy_params_files(self): @@ -987,14 +984,14 @@ def dump_variables(self, dumpvars_str): def write_dumpdb_file( self, dump_varlist, - mtr_ptax_ref, mtr_itax_ref, mtr_combined_ref, - mtr_ptax_bas, mtr_itax_bas, mtr_combined_bas, + mtr_ptax_ref, mtr_itax_ref, + mtr_ptax_bas, mtr_itax_bas, ): """ Write dump output to SQLite database file. """ # pylint: disable=too-many-arguments,too-many-positional-arguments - def dump_output(calcx, dumpvars, mtr_itax, mtr_ptax, mtr_combined): + def dump_output(calcx, dumpvars, mtr_itax, mtr_ptax): """ Extract dump output from calcx and return it as Pandas DataFrame. """ @@ -1004,8 +1001,6 @@ def dump_output(calcx, dumpvars, mtr_itax, mtr_ptax, mtr_combined): odict[var] = pd.Series(mtr_itax) elif var == 'mtr_ptax': odict[var] = pd.Series(mtr_ptax) - elif var == 'mtr_combined': - odict[var] = pd.Series(mtr_combined) else: odict[var] = pd.Series(calcx.array(var)) odf = pd.concat(odict, axis=1) @@ -1052,7 +1047,7 @@ def dump_output(calcx, dumpvars, mtr_itax, mtr_ptax, mtr_combined): # write baseline table outdf = dump_output( self.calc_bas, dump_varlist, - mtr_itax_bas, mtr_ptax_bas, mtr_combined_bas, + mtr_itax_bas, mtr_ptax_bas, ) assert len(outdf.index) == self.calc_bas.array_len outdf.to_sql('baseline', dbcon, index=False) @@ -1060,7 +1055,7 @@ def dump_output(calcx, dumpvars, mtr_itax, mtr_ptax, mtr_combined): # write reform table outdf = dump_output( self.calc_ref, dump_varlist, - mtr_itax_ref, mtr_ptax_ref, mtr_combined_ref, + mtr_itax_ref, mtr_ptax_ref, ) assert len(outdf.index) == self.calc_ref.array_len outdf.to_sql('reform', dbcon, index=False) From 5391fc9f41adcdfd64c3efe58afb71c754e3c6bf Mon Sep 17 00:00:00 2001 From: martinholmer Date: Wed, 3 Dec 2025 19:15:44 -0500 Subject: [PATCH 20/22] Revise Calculator test to increase code coverage --- taxcalc/tests/test_calculator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/taxcalc/tests/test_calculator.py b/taxcalc/tests/test_calculator.py index e90dfb9fb..2cf896453 100644 --- a/taxcalc/tests/test_calculator.py +++ b/taxcalc/tests/test_calculator.py @@ -118,6 +118,7 @@ def test_calculator_advance_to_year(cps_subsample): """ rec = Records.cps_constructor(data=cps_subsample) pol = Policy() + pol.set_year(2015) calc = Calculator(policy=pol, records=rec) calc.advance_to_year(2016) assert calc.current_year == 2016 From 73af87def2ae601da6837c47824aa08b55a2e69d Mon Sep 17 00:00:00 2001 From: martinholmer Date: Thu, 4 Dec 2025 10:43:16 -0500 Subject: [PATCH 21/22] Strengthen 'make brtest' tests --- Makefile | 4 +-- taxcalc/calculator.py | 4 +-- .../run30-35.tables-expect | 29 +++++++++++++++++++ .../cli/behavioral_responses_tests/tests.sh | 5 ++++ 4 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 taxcalc/cli/behavioral_responses_tests/run30-35.tables-expect diff --git a/Makefile b/Makefile index a998691b4..d7a25ec8e 100644 --- a/Makefile +++ b/Makefile @@ -53,13 +53,13 @@ endef .PHONY=pytest pytest: clean @$(pytest-setup) - @cd taxcalc ; pytest -n4 --disable-warnings --durations=0 --durations-min=2 -m "not requires_puf and not requires_tmd" + @cd taxcalc ; pytest -n4 --disable-warnings --durations=0 --durations-min=4 -m "not requires_puf and not requires_tmd" @$(pytest-cleanup) .PHONY=pytest pytest-all: clean @$(pytest-setup) - @cd taxcalc ; pytest -n4 --disable-warnings --durations=0 --durations-min=2 -m "" + @cd taxcalc ; pytest -n4 --disable-warnings --durations=0 --durations-min=4 -m "" @$(pytest-cleanup) define tctest-cleanup diff --git a/taxcalc/calculator.py b/taxcalc/calculator.py index 85ae0fb91..3d8f2ce6c 100644 --- a/taxcalc/calculator.py +++ b/taxcalc/calculator.py @@ -112,9 +112,7 @@ def __init__(self, policy=None, records=None, verbose=False, raise ValueError('consumption must be None or Consumption object') if self.__consumption.current_year < self.__policy.current_year: self.__consumption.set_year(self.__policy.current_year) - current_year_is_data_year = ( - self.__records.current_year == self.__records.data_year) - if sync_years and current_year_is_data_year: + if sync_years: while self.__records.current_year < self.__policy.current_year: self.__records.increment_year() if verbose: diff --git a/taxcalc/cli/behavioral_responses_tests/run30-35.tables-expect b/taxcalc/cli/behavioral_responses_tests/run30-35.tables-expect new file mode 100644 index 000000000..0024f3055 --- /dev/null +++ b/taxcalc/cli/behavioral_responses_tests/run30-35.tables-expect @@ -0,0 +1,29 @@ +Weighted Tax Reform Totals by Baseline Expanded-Income Decile for 2035 + Returns ExpInc IncTax PayTax LSTax AllTax + (#m) ($b) ($b) ($b) ($b) ($b) + 0 24.21 66.6 -1.4 5.0 0.0 3.6 + 1 24.21 584.2 -12.7 46.8 0.0 34.1 + 2 24.21 1073.3 -2.6 66.9 0.0 64.2 + 3 24.21 1429.7 14.8 78.1 0.0 92.8 + 4 24.21 1807.9 33.8 113.5 0.0 147.3 + 5 24.21 2289.4 64.3 160.0 0.0 224.3 + 6 24.21 2931.1 114.8 213.9 0.0 328.7 + 7 24.21 3853.4 207.0 306.1 0.0 513.1 + 8 24.21 5371.6 438.1 482.8 0.0 920.9 + 9 24.21 12429.6 2353.3 913.9 0.0 3267.2 + A 242.13 31836.8 3209.3 2386.9 0.0 5596.2 + +Weighted Tax Differences by Baseline Expanded-Income Decile for 2035 + Returns ExpInc IncTax PayTax LSTax AllTax + (#m) ($b) ($b) ($b) ($b) ($b) + 0 24.21 66.6 0.0 0.0 0.0 0.0 + 1 24.21 584.2 -0.7 0.0 0.0 -0.7 + 2 24.21 1073.3 -1.3 0.0 0.0 -1.3 + 3 24.21 1429.7 -1.7 0.0 0.0 -1.7 + 4 24.21 1807.9 -2.6 0.0 0.0 -2.6 + 5 24.21 2289.4 -4.1 0.2 0.0 -4.0 + 6 24.21 2931.1 -5.6 0.1 0.0 -5.5 + 7 24.21 3853.4 -7.4 0.3 0.0 -7.1 + 8 24.21 5371.6 -13.0 0.3 0.0 -12.8 + 9 24.21 12429.6 42.7 -3.0 0.0 39.8 + A 242.13 31836.8 6.2 -2.1 0.0 4.2 diff --git a/taxcalc/cli/behavioral_responses_tests/tests.sh b/taxcalc/cli/behavioral_responses_tests/tests.sh index 93cfa1aac..4339dca6c 100755 --- a/taxcalc/cli/behavioral_responses_tests/tests.sh +++ b/taxcalc/cli/behavioral_responses_tests/tests.sh @@ -35,6 +35,11 @@ fi tc cps.csv 2035 --numyears 1 --behavior br1.json --runid 30 \ --reform ref.json --exact --tables --silent +cmp run30-35.tables run30-35.tables-expect +if [ $? -ne 0 ]; then + ERRORS=1 + echo Differences between run30-35.tables run30-35.tables-expect +fi tc cps.csv 2028 --numyears 8 --behavior br1.json --runid 31 \ --reform ref.json --exact --tables --silent cmp run31-35.tables run30-35.tables From 3028693350353c4bb66260f66965fde9b2420b77 Mon Sep 17 00:00:00 2001 From: martinholmer Date: Thu, 4 Dec 2025 12:58:10 -0500 Subject: [PATCH 22/22] Improve CLI messages --- taxcalc/taxcalcio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/taxcalc/taxcalcio.py b/taxcalc/taxcalcio.py index eb331756c..aee8ebe78 100644 --- a/taxcalc/taxcalcio.py +++ b/taxcalc/taxcalcio.py @@ -584,7 +584,7 @@ def advance_to_year(self, year): self.calc_ref = Calculator( policy=self.pol_ref, records=self.recs_ref, - verbose=(not self.silent), + verbose=False, consumption=self.con, sync_years=self.aging_input_data, ) @@ -592,7 +592,7 @@ def advance_to_year(self, year): self.calc_bas = Calculator( policy=self.pol_bas, records=self.recs_bas, - verbose=(not self.silent), + verbose=False, consumption=self.con, sync_years=self.aging_input_data, )