diff --git a/Makefile b/Makefile index a80704697..d7a25ec8e 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" @@ -51,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 @@ -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/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/_toc.yml b/docs/_toc.yml index d451362dc..e09066608 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 @@ -24,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/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/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..eb9095665 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 @@ -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 @@ -226,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 -Execution time is 33.2 seconds +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. @@ -254,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, @@ -312,21 +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 -Execution time is 37.3 seconds - -% 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) @@ -360,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) @@ -411,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 @@ -485,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 --------- --------- @@ -533,26 +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 -Execution time is 55.6 seconds +Write tabular output to file tmd-35-#-ext-#-#-tables.text ``` [PR @@ -567,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 ``` @@ -606,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/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/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.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/__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/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 +} 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/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/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 new file mode 100755 index 000000000..4339dca6c --- /dev/null +++ b/taxcalc/cli/behavioral_responses_tests/tests.sh @@ -0,0 +1,53 @@ +#!/bin/zsh +# CLI tests of behavior responses logic + +ERRORS=0 + +tc cps.csv 2035 --numyears 1 --runid 10 \ + --reform ref.json --exact --tables --silent +tc cps.csv 2028 --numyears 8 --runid 11 \ + --reform ref.json --exact --tables --silent +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 +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 +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 +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 +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/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/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..aee8ebe78 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 @@ -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 @@ -55,6 +56,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 +72,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 @@ -236,13 +241,40 @@ def __init__(self, input_data, tax_year, baseline, reform, assump, else: 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 = '-#' + 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' 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 @@ -262,15 +294,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 @@ -313,6 +345,35 @@ def init(self, input_data, tax_year, baseline, reform, assump, return # get assumption sub-dictionaries assumpdict = Calculator.read_json_param_objects(None, assump) + # get behavior dictionary + if behavior: + with open(behavior, 'r', encoding='utf-8') as jfile: + json_text = jfile.read() + try: + 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(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 self.behvdict['sub'] < 0.0: + msg = f'{behavior} contains negative "sub" elasticity' + self.errmsg = f'ERROR: BEHAVIOR file {msg}\n' + if self.behvdict['inc'] > 0.0: + msg = f'{behavior} contains positive "inc" elasticity' + self.errmsg += f'ERROR: BEHAVIOR file {msg}\n' + if self.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: @@ -353,84 +414,84 @@ def init(self, input_data, tax_year, baseline, reform, assump, 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, @@ -438,21 +499,21 @@ def init(self, input_data, tax_year, baseline, reform, assump, 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, @@ -460,21 +521,26 @@ def init(self, input_data, tax_year, baseline, reform, assump, 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): @@ -492,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('-') @@ -504,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=False, + 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=False, + 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 @@ -562,24 +652,50 @@ def analyze( if not doing_calcs: return # do output calculations - self.calc_bas.calc_all() - self.calc_ref.calc_all() + 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, + ) + # move returned dump dataframe values back into calc objects + 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].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_ref.array(vname, br_dump_ref[vname].to_numpy()) + del br_dump_ref + 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: 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 + mtr_output = ( + 'mtr_itax' in dump_varlist or + 'mtr_ptax' 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_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() @@ -881,11 +997,10 @@ def dump_output(calcx, dumpvars, mtr_itax, mtr_ptax): """ 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) else: odict[var] = pd.Series(calcx.array(var)) odf = pd.concat(odict, axis=1) @@ -931,14 +1046,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, ) 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, ) assert len(outdf.index) == self.calc_ref.array_len outdf.to_sql('reform', dbcon, index=False) 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 diff --git a/taxcalc/tests/test_taxcalcio.py b/taxcalc/tests/test_taxcalcio.py index bee6a5517..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 @@ -261,20 +262,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) + baseline=baseline, reform=reform, + assump=assump, behavior=behavior) assert tcio.errmsg @@ -310,16 +313,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 +337,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 +351,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 +387,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 +411,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 +447,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 +488,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 +496,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 +526,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 +563,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 +598,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 +643,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 +711,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 +720,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 = ( @@ -698,3 +728,128 @@ 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 + + +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(): + """ + 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 + + +def test_init_behavior1_errors(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 + + +@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. + """ + tcio = TaxCalcIO( + 'cps.csv', 2020, baseline=None, reform=reformfile1.name, + assump=None, behavior=behvfile2.name, + runid=11, + ) + tcio.init( + 'cps.csv', 2020, baseline=None, reform=reformfile1.name, + assump=None, behavior=behvfile2.name, + exact_calculations=True, + ) + assert not tcio.errmsg + assert tcio.tax_year() == 2020 + tcio.analyze(output_tables=True) + assert tcio.tax_year() == 2020 + table_path = Path('run11-20.tables') + table_path.unlink(missing_ok=True)