diff --git a/codecov.yml b/codecov.yml index 16a28e695..70cbddd52 100644 --- a/codecov.yml +++ b/codecov.yml @@ -22,3 +22,4 @@ comment: ignore: - "setup.py" - "ppp.py" + - "extend_tcja.py" diff --git a/docs/_toc.yml b/docs/_toc.yml index 1a9cf18e8..d451362dc 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -24,6 +24,7 @@ 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/usage/tcja_after_2025.md b/docs/usage/tcja_after_2025.md new file mode 100644 index 000000000..f7788ec3c --- /dev/null +++ b/docs/usage/tcja_after_2025.md @@ -0,0 +1,133 @@ +TCJA after 2025 +=============== + +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 describes how to use the PSLmodels Tax-Calculator +command-line interface +[tc](https://taxcalc.pslmodels.org/guide/cli.html) with any compatible +input dataset to analyze the post-2025 effects of alternatives to +current-law policy (which calls for all temporary TCJA provisions to +expire). Compatible input datasets include: + +* the older `cps.csv` file included in the Tax-Calculator package + +* the older `puf.csv` file generated in the PSLmodels + [taxdata](https://github.com/PSLmodels/taxdata) repository and + available only to those with access to the 2011 IRS/SOI PUF + +* several newer CSV-formatted input files created in the PSLmodels + [tax-microdata](https://github.com/PSLmodels/tax-microdata-benchmarking) + repository that are based on the 2015 IRS/SOI PUF and on recent CPS + data, and are available only to those with access to the 2015 + IRS/SOI PUF + +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 +tables comparing taxes under the baseline policy to taxes under the +reform policy by income decile and in aggregate. + +The `--baseline` option is not commonly used, but it can be very +helpful in analyzing reforms that take effect beginning in 2026. Such +a reform could be analyzed relative to current-law policy (that is, +with temporary TCJA provisions expiring after 2025) by omitting the +`--baseline` option. But if such a reform were to be analyzed +relative to extending all the temporary TCJA provisions beyond 2025, +then the `--baseline ext.json` option would need to be used. The +[`ext.json`](../../taxcalc/reforms/ext.json) file contains the 2026 +tax policy reform provisions that would extend TCJA's temporary +provisions beyond 2025. Before using this `ext.json` reform file, be +sure to read how it is generated at the end of this document. + +Here are some concrete examples of using the `tc` tool to analyze a +reform of interest to you that begins in 2026. The examples assume +you have named your reform file `x.json` and that you are using one +of the compatible input datasets describe above. The examples will +call the input dataset `z.csv`. + +To analyze your reform relative to current-law policy (which means +temporary TCJA provisions have expired beginning in 2026), you would +execute this command: + +``` +tc z.csv 2026 --exact --tables --reform x.json +``` + +The tables would be in the `z-26-#-x-#-tab.text` output file generated +by this `tc` run. If you want to do custom tabulations of the micro +output data, use the `--dump`, `--dvars`, and `--sqldb` options as +explained by the `tc --help` documentation. + +To analyze your reform relative to a reform that extends all TCJA +temporary provisions beyond 2025, you would execute this command: + +``` +tc z.csv 2026 --exact --tables --baseline ext.json --reform x.json +``` + +The tables would be in the `z-26-ext-x-#-tab.text` output file +generated by this `tc` run. + +Also, remember that you can simulate a _compound reform_ using the +following syntax: + +``` +tc z.csv 2026 --exact --tables --baseline ext.json --reform x.json+y.json +``` + +where `y.json` contains a reform with additional provisions not +included in your `x.json` reform file. The resulting table output +would be in a file named `z-26-ext-x+y-#-tab.text`. + +And finally, you might consider creating a reform file called +`end.json` that contains just the two characters `{}`. This is a null +reform, which is equivalent to current-law policy, that could be used +as follows: + +``` +tc z.csv 2026 --exact --tables --baseline end.json --reform x.json +``` + +The resulting table output would be named `z-26-end-x-#-tab.text` and +have the same tabular output as the `z-26-#-x-#-tab.text` file. Some +people may prefer `end` to `#` as a way of naming current-law policy +in the context of discussing TCJA-related reforms. + + +**How is the `ext.json` file generated?** + +The short answer is by using the +[`extend_tcja.py`](../../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) +change or whenever new historical values of policy parameters are +added to the `policy_current_law.json` file thereby changing the +`Policy.LAST_KNOWN_YEAR`. + +The 3.5.3 version of Tax-Calculator incorporates the February 2024 CBO +economic projection and contains historical tax policy parameter values +through 2022. Future versions of Tax-Calculator that use historical +policy parameter values for 2023 and 2024 or use the February 2025 CBO +economic projection will cause the `extend_tcja.py` script to generate +somewhat different 2026 parameter values in the `ext.json` file. diff --git a/extend_tcja.py b/extend_tcja.py new file mode 100644 index 000000000..cbe971950 --- /dev/null +++ b/extend_tcja.py @@ -0,0 +1,143 @@ +""" +This script generates a JSON reform file, which could be called +extend_tcja.json, that can serve as an alternative baseline to +current-law policy (which ends TCJA temporary provisions after 2025). + +USAGE: (taxcalc-dev) ~% python extend_tcja.py + +IMPORTANT NOTE: be sure to remove the trailing comma after the last item + in the reform JSON object generated by this script. +""" + +import sys +import numpy +import taxcalc + +TCJA_CATEGORY = None # set to None to generate all TCJA temporary provisions +TCJA_PARAMETERS = { + # category 1 ... + "II_rt1": {"indexed": False, "category": 1}, + "II_brk1": {"indexed": True, "category": 1}, + "PT_rt1": {"indexed": False, "category": 1}, + "PT_brk1": {"indexed": True, "category": 1}, + "II_rt2": {"indexed": False, "category": 1}, + "II_brk2": {"indexed": True, "category": 1}, + "PT_rt2": {"indexed": False, "category": 1}, + "PT_brk2": {"indexed": True, "category": 1}, + "II_rt3": {"indexed": False, "category": 1}, + "II_brk3": {"indexed": True, "category": 1}, + "PT_rt3": {"indexed": False, "category": 1}, + "PT_brk3": {"indexed": True, "category": 1}, + "II_rt4": {"indexed": False, "category": 1}, + "II_brk4": {"indexed": True, "category": 1}, + "PT_rt4": {"indexed": False, "category": 1}, + "PT_brk4": {"indexed": True, "category": 1}, + "II_rt5": {"indexed": False, "category": 1}, + "II_brk5": {"indexed": True, "category": 1}, + "PT_rt5": {"indexed": False, "category": 1}, + "PT_brk5": {"indexed": True, "category": 1}, + "II_rt6": {"indexed": False, "category": 1}, + "II_brk6": {"indexed": True, "category": 1}, + "PT_rt6": {"indexed": False, "category": 1}, + "PT_brk6": {"indexed": True, "category": 1}, + "II_rt7": {"indexed": False, "category": 1}, + "II_brk7": {"indexed": True, "category": 1}, + "PT_rt7": {"indexed": False, "category": 1}, + "PT_brk7": {"indexed": True, "category": 1}, + # category 2 ... + "CTC_c": {"indexed": False, "category": 2}, + "ACTC_c": {"indexed": False, "category": 2}, + "ODC_c": {"indexed": False, "category": 2}, + "CTC_ps": {"indexed": False, "category": 2}, + "ACTC_Income_thd": {"indexed": False, "category": 2}, + # category 3 ... + "AMT_em": {"indexed": True, "category": 3}, + "AMT_em_ps": {"indexed": True, "category": 3}, + "AMT_em_pe": {"indexed": True, "category": 3}, + # category 4 ... + "STD": {"indexed": True, "category": 4}, + # category 5 ... + "ID_AllTaxes_c": {"indexed": False, "category": 5}, + "ID_Charity_crt_all": {"indexed": False, "category": 5}, + "ID_Casualty_hc": {"indexed": False, "category": 5}, + "ID_Miscellaneous_hc": {"indexed": False, "category": 5}, + "ID_ps": {"indexed": True, "category": 5}, + "ID_prt": {"indexed": False, "category": 5}, + "ID_crt": {"indexed": False, "category": 5}, + # category 6 ... + "II_em": {"indexed": True, "category": 6}, + "II_em_ps": {"indexed": True, "category": 6}, + # category 7 ... + "PT_qbid_rt": {"indexed": False, "category": 7}, + "PT_qbid_taxinc_thd": {"indexed": True, "category": 7}, + "PT_qbid_taxinc_gap": {"indexed": False, "category": 7}, + "PT_qbid_w2_wages_rt": {"indexed": False, "category": 7}, + "PT_qbid_alt_w2_wages_rt": {"indexed": False, "category": 7}, + "PT_qbid_alt_property_rt": {"indexed": False, "category": 7}, + # category 8 ... + "ALD_BusinessLosses_c": {"indexed": True, "category": 8}, + "ALD_DomesticProduction_hc": {"indexed": False, "category": 8}, +} + + +def main(): + """ + High-level script logic. + """ + # calculate 2025-to-2026 parameters indexing factor + pol = taxcalc.Policy() + pirates = pol.inflation_rates() + ifactor = 1.0 + pirates[2025-taxcalc.Policy.JSON_START_YEAR] + # specify extend-TCJA-beyond-2025 reform + # ... get 2025 parameter values + pol.set_year(2025) + pdata = dict(pol.items()) + # ... write reform header comments + print( '// REFORM TO EXTEND TEMPORARY TCJA PROVISIONS BEYOND 2025') + print(f'// USING TAX-CALCULATOR {taxcalc.__version__}') + print(f'// WITH 2025-to-2026 INDEXING FACTOR = {ifactor:.6f}') + if TCJA_CATEGORY: + print(f'// ONLY TCJA PROVISIONS IN CATEGORY {TCJA_CATEGORY}') + print('{') + # ... set 2026 nonreverted values for the parameters set to revert + for pname, pinfo in TCJA_PARAMETERS.items(): + if TCJA_CATEGORY and pinfo['category'] != TCJA_CATEGORY: + continue # skip this parameter + if pinfo['indexed']: + pval = pdata[pname][0] * ifactor + if isinstance(pval, numpy.ndarray): + # handle vector parameter + pval = numpy.minimum(9e99, pval.round(2)) + sys.stdout.write(f' "{pname}": ') + sys.stdout.write('{"2026": ') + sys.stdout.write(f'{pval.tolist()}') + sys.stdout.write('},\n') + else: + # handle scalar parameter + pval = min(9e99, pval) + sys.stdout.write(f' "{pname}": ') + sys.stdout.write('{"2026": ') + sys.stdout.write(f'{pval*ifactor:.2f}') + sys.stdout.write('},\n') + else: # if parameter is not indexed + pval = pdata[pname][0] + if isinstance(pval, numpy.ndarray): + # handle vector parameter + pval = numpy.minimum(9e99, pval.round(2)) + sys.stdout.write(f' "{pname}": ') + sys.stdout.write('{"2026": ') + sys.stdout.write(f'{pval.tolist()}') + sys.stdout.write('},\n') + else: + # handle scalar parameter + sys.stdout.write(f' "{pname}": ') + sys.stdout.write('{"2026": ') + sys.stdout.write(f'{pval:.2f}') + sys.stdout.write('},\n') + print('}') + return 0 +# end main function code + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/taxcalc/reforms/ext.json b/taxcalc/reforms/ext.json new file mode 100644 index 000000000..b208e1dc1 --- /dev/null +++ b/taxcalc/reforms/ext.json @@ -0,0 +1,59 @@ +// REFORM TO EXTEND TEMPORARY TCJA PROVISIONS BEYOND 2025 +// USING TAX-CALCULATOR 3.5.3 +// WITH 2025-to-2026 INDEXING FACTOR = 1.022000 +{ + "II_rt1": {"2026": 0.10}, + "II_brk1": {"2026": [12170.98, 24341.94, 12170.98, 17353.27, 24341.94]}, + "PT_rt1": {"2026": 0.10}, + "PT_brk1": {"2026": [12170.98, 24341.94, 12170.98, 17353.27, 24341.94]}, + "II_rt2": {"2026": 0.12}, + "II_brk2": {"2026": [49483.44, 98966.89, 49483.44, 66214.83, 98966.89]}, + "PT_rt2": {"2026": 0.12}, + "PT_brk2": {"2026": [49483.44, 98966.89, 49483.44, 66214.83, 98966.89]}, + "II_rt3": {"2026": 0.22}, + "II_brk3": {"2026": [105511.38, 211022.75, 105511.38, 105481.77, 211022.75]}, + "PT_rt3": {"2026": 0.22}, + "PT_brk3": {"2026": [105511.38, 211022.75, 105511.38, 105481.77, 211022.75]}, + "II_rt4": {"2026": 0.24}, + "II_brk4": {"2026": [201428.14, 402856.25, 201428.14, 201428.14, 402856.25]}, + "PT_rt4": {"2026": 0.24}, + "PT_brk4": {"2026": [201428.14, 402856.25, 201428.14, 201428.14, 402856.25]}, + "II_rt5": {"2026": 0.32}, + "II_brk5": {"2026": [255797.72, 511595.46, 255797.72, 255797.72, 511595.46]}, + "PT_rt5": {"2026": 0.32}, + "PT_brk5": {"2026": [255797.72, 511595.46, 255797.72, 255797.72, 511595.46]}, + "II_rt6": {"2026": 0.35}, + "II_brk6": {"2026": [639523.94, 767393.2, 383696.6, 639523.94, 767393.2]}, + "PT_rt6": {"2026": 0.35}, + "PT_brk6": {"2026": [639523.94, 767393.2, 383696.6, 639523.94, 767393.2]}, + "II_rt7": {"2026": 0.37}, + "II_brk7": {"2026": [9e+99, 9e+99, 9e+99, 9e+99, 9e+99]}, + "PT_rt7": {"2026": 0.37}, + "PT_brk7": {"2026": [9e+99, 9e+99, 9e+99, 9e+99, 9e+99]}, + "CTC_c": {"2026": 2000.00}, + "ACTC_c": {"2026": 1600.00}, + "ODC_c": {"2026": 500.00}, + "CTC_ps": {"2026": [200000.0, 400000.0, 200000.0, 200000.0, 400000.0]}, + "ACTC_Income_thd": {"2026": 2500.00}, + "AMT_em": {"2026": [89905.29, 139892.17, 69946.08, 89905.29, 139892.17]}, + "AMT_em_ps": {"2026": [639523.94, 1279047.88, 639523.94, 639523.94, 1279047.88]}, + "AMT_em_pe": {"2026": 939533.04}, + "STD": {"2026": [15339.57, 30679.14, 15339.57, 22979.74, 30679.14]}, + "ID_AllTaxes_c": {"2026": [10000.0, 10000.0, 5000.0, 10000.0, 10000.0]}, + "ID_Charity_crt_all": {"2026": 0.60}, + "ID_Casualty_hc": {"2026": 1.00}, + "ID_Miscellaneous_hc": {"2026": 1.00}, + "ID_ps": {"2026": [9e+99, 9e+99, 9e+99, 9e+99, 9e+99]}, + "ID_prt": {"2026": 0.00}, + "ID_crt": {"2026": 1.00}, + "II_em": {"2026": 0.00}, + "II_em_ps": {"2026": [9e+99, 9e+99, 9e+99, 9e+99, 9e+99]}, + "PT_qbid_rt": {"2026": 0.20}, + "PT_qbid_taxinc_thd": {"2026": [201428.14, 402856.25, 201428.14, 201428.14, 402856.25]}, + "PT_qbid_taxinc_gap": {"2026": [50000.0, 100000.0, 50000.0, 50000.0, 100000.0]}, + "PT_qbid_w2_wages_rt": {"2026": 0.50}, + "PT_qbid_alt_w2_wages_rt": {"2026": 0.25}, + "PT_qbid_alt_property_rt": {"2026": 0.03}, + "ALD_BusinessLosses_c": {"2026": [319821.19, 639642.39, 319821.19, 319821.19, 639642.39]}, + "ALD_DomesticProduction_hc": {"2026": 1.00} +} diff --git a/taxcalc/tests/test_reforms.py b/taxcalc/tests/test_reforms.py index b6bc20d08..9de5ff536 100644 --- a/taxcalc/tests/test_reforms.py +++ b/taxcalc/tests/test_reforms.py @@ -164,6 +164,7 @@ def test_round_trip_reforms(fyear, tests_path): raise ValueError(msg) +@pytest.mark.reforms def test_reform_json_and_output(tests_path): """ Check that each JSON reform file can be converted into a reform dictionary @@ -234,6 +235,8 @@ def res_and_out_are_same(base): reforms_path = os.path.join(tests_path, '..', 'reforms', '*.json') json_reform_files = glob.glob(reforms_path) for jrf in json_reform_files: + if jrf.endswith('ext.json'): + continue # skip ext.json, which is tested below in test_ext_reform # determine reform's baseline by reading contents of jrf with open(jrf, 'r') as rfile: jrf_text = rfile.read() @@ -349,3 +352,34 @@ def test_reforms(rid, test_reforms_init, tests_path, baseline_2017_law, with open(afile_path, 'w') as afile: afile.write('rid,res1,res2,res3,res4\n') afile.write('{}\n'.format(actual)) + + +@pytest.mark.extend_tcja +def test_ext_reform(tests_path): + """ + Test ext.json reform that extends TCJA beyond 2025. + """ + # test syntax of ext.json reform file + end = Policy() + end.set_year(2026) + ext = Policy() + reform_file = os.path.join(tests_path, '..', 'reforms', 'ext.json') + with open(reform_file, 'r') as rfile: + rtext = rfile.read() + ext.implement_reform(Policy.read_json_reform(rtext)) + assert not ext.parameter_warnings + ext.set_year(2026) + assert ext.II_em < end.II_em + # test tax output generated by ext.json reform file using public CPS data + recs = Records.cps_constructor() + calc_end = Calculator(policy=end, records=recs, verbose=False) + calc_end.advance_to_year(2026) + calc_end.calc_all() + iitax_end = calc_end.array('iitax') + calc_ext = Calculator(policy=ext, records=recs, verbose=False) + calc_ext.advance_to_year(2026) + calc_ext.calc_all() + iitax_ext = calc_ext.array('iitax') + rdiff = iitax_ext - iitax_end + weighted_sum_rdiff = (rdiff * calc_end.array('s006')).sum() * 1.0e-9 + assert np.allclose([weighted_sum_rdiff], [-224.46], rtol=0.0, atol=0.01) diff --git a/taxcalc/tests/test_stats_current.csv b/taxcalc/tests/test_stats_current.csv new file mode 100644 index 000000000..999ad51e2 --- /dev/null +++ b/taxcalc/tests/test_stats_current.csv @@ -0,0 +1,2 @@ +,test_id,pytest_obj,status,duration_ms,time_diff +0,taxcalc/tests/test_reforms.py::test_ext_reform,,passed,27372.827967978083,0.0