diff --git a/taxcalc/calcfunctions.py b/taxcalc/calcfunctions.py index eb08d982a..c73b23b6a 100644 --- a/taxcalc/calcfunctions.py +++ b/taxcalc/calcfunctions.py @@ -3,10 +3,11 @@ These functions are imported into the Calculator class. -Note: the CPI_offset policy parameter is the only policy parameter that -does not appear here; it is used in the policy.py file to possibly adjust -the price inflation rate used to index policy parameters (as would be done -in a reform that introduces chained-CPI indexing). +Note: the parameter_indexing_CPI_offset policy parameter is the only +policy parameter that does not appear here; it is used in the policy.py +file to possibly adjust the price inflation rate used to index policy +parameters (as would be done in a reform that introduces chained-CPI +indexing). """ # CODING-STYLE CHECKS: # pycodestyle calcfunctions.py diff --git a/taxcalc/parameters.py b/taxcalc/parameters.py index 7a192be49..b0408a164 100644 --- a/taxcalc/parameters.py +++ b/taxcalc/parameters.py @@ -9,6 +9,7 @@ import requests import taxcalc +from taxcalc.growfactors import GrowFactors from taxcalc.utils import json_to_dict @@ -149,12 +150,18 @@ def adjust_with_indexing(self, params_or_path, **kwargs): Custom adjust method that handles special indexing logic. The logic is: - 1. If "CPI_offset" is adjusted, revert all values of indexed parameters - to the 'known' values: + 1. If "parameter_indexing_CPI_offset" is adjusted, first set + parameter_indexing_CPI_offset to zero before implementing the + adjusted parameter_indexing_CPI_offset to avoid stacking adjustments. + Then, revert all values of indexed parameters to the 'known' values: a. The current values of parameters that are being adjusted are - deleted after the first year in which CPI_offset is adjusted. + deleted after the first year in which + parameter_indexing_CPI_offset is adjusted. b. The current values of parameters that are not being adjusted - (i.e. are not in params) are deleted after the last known year. + (i.e. are not in params) are deleted after the last known year, + with the exception of parameters that revert to their pre-TCJA + values in 2026. Instead, these (2026) parameter values are + recalculated using the new inflation rates. After the 'unknown' values have been deleted, the last known value is extrapolated through the budget window. If there are indexed parameters in the adjustment, they will be included in the final @@ -171,17 +178,11 @@ def adjust_with_indexing(self, params_or_path, **kwargs): parameter through the remaining years or until the -indexed status changes again. 3. Update all parameters that are not indexing related, i.e. they are - not "CPI_offset" or do not end with "-indexed". + not "parameter_indexing_CPI_offset" or do not end with "-indexed". 4. Return parsed adjustment with all adjustments, including "-indexed" parameters. Notable side-effects: - - All values of indexed parameters, including default values, are - wiped out after the first year in which the "CPI_offset" is - changed. This is only necessary because Tax-Calculator - hard-codes inflated values. If Tax-Calculator only hard-coded - values that were changed for non-inflation related reasons, - then this would not be necessary for default values. - All values of a parameter whose indexed status is adjusted are wiped out after the year in which the value is adjusted for the same hard-coding reason. @@ -191,29 +192,46 @@ def adjust_with_indexing(self, params_or_path, **kwargs): label_to_extend = self.label_to_extend array_first = self.array_first self.array_first = False + self._gfactors = GrowFactors() params = self.read_params(params_or_path) - # Check if CPI_offset is adjusted. If so, reset values of all indexed - # parameters after year where CPI_offset is changed. If CPI_offset is - # changed multiple times, then reset values after the first year in - # which the CPI_offset is changed. + # Check if parameter_indexing_CPI_offset is adjusted. If so, reset + # values of all indexed parameters after year where + # parameter_indexing_CPI_offset is changed. If + # parameter_indexing_CPI_offset is changed multiple times, then + # reset values after the first year in which the + # parameter_indexing_CPI_offset is changed. needs_reset = [] - if params.get("CPI_offset") is not None: - # Update CPI_offset with new value. + if params.get("parameter_indexing_CPI_offset") is not None: + # Update parameter_indexing_CPI_offset with new value. cpi_adj = super().adjust( - {"CPI_offset": params["CPI_offset"]}, **kwargs + {"parameter_indexing_CPI_offset": + params["parameter_indexing_CPI_offset"]}, **kwargs ) - # turn off extend now that CPI_offset has been updated. + # turn off extend now that parameter_indexing_CPI_offset + # has been updated. self.label_to_extend = None - # Get first year in which CPI_offset is changed. + # Get first year in which parameter_indexing_CPI_offset + # is changed. cpi_min_year = min( - cpi_adj["CPI_offset"], key=lambda vo: vo["year"] + cpi_adj["parameter_indexing_CPI_offset"], + key=lambda vo: vo["year"] ) - # Apply new CPI_offset values to inflation rates + rate_adjustment_vals = self.select_gte( - "CPI_offset", year=cpi_min_year["year"] + "parameter_indexing_CPI_offset", year=cpi_min_year["year"] ) + # "Undo" any existing parameter_indexing_CPI_offset for + # years after parameter_indexing_CPI_offset has + # been updated. + self._inflation_rates = self._inflation_rates[ + :cpi_min_year["year"] - self.start_year + ] + self._gfactors.price_inflation_rates( + cpi_min_year["year"], self.LAST_BUDGET_YEAR) + + # Then apply new parameter_indexing_CPI_offset values to + # inflation rates for cpi_vo in rate_adjustment_vals: self._inflation_rates[ cpi_vo["year"] - self.start_year @@ -223,7 +241,10 @@ def adjust_with_indexing(self, params_or_path, **kwargs): init_vals = {} to_delete = {} for param in params: - if param == "CPI_offset" or param in self._wage_indexed: + if ( + param == "parameter_indexing_CPI_offset" or + param in self._wage_indexed + ): continue if param.endswith("-indexed"): param = param.split("-indexed")[0] @@ -241,13 +262,55 @@ def adjust_with_indexing(self, params_or_path, **kwargs): super().adjust(init_vals, **kwargs) # 1.b For all others, these are years after last_known_year. + last_known_year = max(cpi_min_year["year"], self._last_known_year) + # calculate 2026 value, using new inflation rates, for parameters + # that revert to their pre-TCJA values. + long_params = ['II_brk7', 'II_brk6', 'II_brk5', 'II_brk4', + 'II_brk3', 'II_brk2', 'II_brk1', + 'PT_brk7', 'PT_brk6', 'PT_brk5', 'PT_brk4', + 'PT_brk3', 'PT_brk2', 'PT_brk1', + 'PT_qbid_taxinc_thd', + 'ALD_BusinessLosses_c', + 'STD', 'II_em', 'II_em_ps', + 'AMT_em', 'AMT_em_ps', 'AMT_em_pe', + 'ID_ps', 'ID_AllTaxes_c'] + final_ifactor = 1.0 + pyear = 2017 # prior year before TCJA first implemented + fyear = 2026 # final year in which parameter values revert to + # pre-TCJA values + # construct final-year inflation factor from prior year + # NOTE: pvalue[t+1] = pvalue[t] * ( 1 + irate[t] ) + for year in range(pyear, fyear): + final_ifactor *= 1 + \ + self._inflation_rates[year - self.start_year] + + long_param_vals = defaultdict(list) + # compute final year parameter value + for param in long_params: + # only revert param in 2026 if it's not in revision + if params.get(param) is None: + # grab param values from 2017 + vos = self.select_eq(param, year=pyear) + # use final_ifactor to inflate from 2017 to 2026 + for vo in vos: + long_param_vals[param].append( + # Create new dict to avoid modifying the original + dict( + vo, + value=min(9e99, round( + vo["value"] * final_ifactor, 0)), + year=fyear, + ) + ) + needs_reset.append(param) + super().adjust(long_param_vals, **kwargs) + init_vals = {} to_delete = {} - last_known_year = max(cpi_min_year["year"], self._last_known_year) for param in self._data: if ( param in params or - param == "CPI_offset" or + param == "parameter_indexing_CPI_offset" or param in self._wage_indexed ): continue @@ -257,8 +320,8 @@ def adjust_with_indexing(self, params_or_path, **kwargs): True, {"year": last_known_year} ) - to_delete[param] = self.select_gt( - param, year=last_known_year + to_delete[param] = self.select_eq( + param, strict=True, _auto=True ) needs_reset.append(param) diff --git a/taxcalc/policy.py b/taxcalc/policy.py index d2a7a9d18..a1ed3e5c1 100644 --- a/taxcalc/policy.py +++ b/taxcalc/policy.py @@ -53,7 +53,14 @@ class instance: Policy 'FilerCredit_c': 'is a removed parameter name', 'ALD_InvInc_ec_base_RyanBrady': 'is a removed parameter name', # TODO: following parameter renamed in PR 2292 merged on 2019-04-15 - 'cpi_offset': 'was renamed CPI_offset in release 2.0.0', + "cpi_offset": ( + "was renamed parameter_indexing_CPI_offset. " + "See documentation for change in usage." + ), + "CPI_offset": ( + "was renamed parameter_indexing_CPI_offset. " + "See documentation for change in usage." + ), # TODO: following parameters renamed in PR 2345 merged on 2019-06-24 'PT_excl_rt': 'was renamed PT_qbid_rt in release 2.4.0', @@ -124,9 +131,12 @@ def parameter_list(): def set_rates(self): """Initialize taxcalc indexing data.""" - cpi_vals = [vo["value"] for vo in self._data["CPI_offset"]["value"]] - # extend cpi_offset values through budget window if they - # have not been extended already. + cpi_vals = [ + vo["value"] for + vo in self._data["parameter_indexing_CPI_offset"]["value"] + ] + # extend parameter_indexing_CPI_offset values through budget window + # if they have not been extended already. cpi_vals = cpi_vals + cpi_vals[-1:] * ( self.end_year - self.start_year + 1 - len(cpi_vals) ) diff --git a/taxcalc/policy_current_law.json b/taxcalc/policy_current_law.json index c97c13502..df948026b 100644 --- a/taxcalc/policy_current_law.json +++ b/taxcalc/policy_current_law.json @@ -75,7 +75,7 @@ } } }, - "CPI_offset": { + "parameter_indexing_CPI_offset": { "title": "Decimal offset ADDED to unchained CPI to get parameter indexing rate", "description": "Values are zero before 2017; reforms that introduce indexing with chained CPI would have values around -0.0025 beginning in the year before the first year policy parameters will have values computed with chained CPI.", "notes": "See April 2013 CBO report entitled 'What Would Be the Effect on the Deficit of Using the Chained CPI to Index Benefit Programs and the Tax Code?', which includes this: 'The chained CPI grows more slowly than the traditional CPI does: an average of about 0.25 percentage points more slowly per year over the past decade.' ", diff --git a/taxcalc/reforms/2017_law.json b/taxcalc/reforms/2017_law.json index ee6e1ed9c..caeee6f51 100644 --- a/taxcalc/reforms/2017_law.json +++ b/taxcalc/reforms/2017_law.json @@ -13,7 +13,7 @@ // - Set pre-TCJA above the line deduction policy (7) // - Set pre-TCJA itemized deduction policy (8) // Reform_Parameter_Map: -// - 0: CPI_offset +// - 0: parameter_indexing_CPI_offset // - 1: II_rt?/II_brk? and PT_rt?/PT_brk? // - 2: six PT_qbid_* parameters // - 3: STD and II_em parameters @@ -26,7 +26,7 @@ // NOTE: this reform projects pre-TCJA 2017 parameter values forward using the // unchained CPI-U price index. { - "CPI_offset": {"2017": 0.0025}, + "parameter_indexing_CPI_offset": {"2017": 0}, "II_rt1": {"2018": 0.10}, "II_brk1": {"2017": [9325, 18650, 9325, 13350, 18650]}, "II_rt2": {"2018": 0.15}, diff --git a/taxcalc/reforms/TCJA.json b/taxcalc/reforms/TCJA.json index 5cc1f66e4..cf2fa225f 100644 --- a/taxcalc/reforms/TCJA.json +++ b/taxcalc/reforms/TCJA.json @@ -22,7 +22,7 @@ // - 6: AMT_em* // - 7: ALD_* // - 8: ID_* (can safely ignore WARNINGs about values for several parameters) -// - 9: CPI_offset +// - 9: parameter_indexing_CPI_offset // Note: II_brk*, PT_brk*, STD, II_em are rounded to nearest integer value { "II_rt1": {"2018": 0.10, @@ -153,5 +153,5 @@ "2026": 0}, "ID_Medical_frt": {"2017": 0.075, "2019": 0.075}, - "CPI_offset": {"2017": -0.0025} + "parameter_indexing_CPI_offset": {"2017": -0.0025} } diff --git a/taxcalc/tests/reforms.json b/taxcalc/tests/reforms.json index ea9a2b79b..1fc2bedd0 100644 --- a/taxcalc/tests/reforms.json +++ b/taxcalc/tests/reforms.json @@ -554,7 +554,7 @@ "56": { "baseline": "policy_current_law.json", "start_year": 2017, - "value": {"CPI_offset": 0.0025}, + "value": {"parameter_indexing_CPI_offset": 0}, "name": "Repeal TCJA chained CPI indexing", "output_type": "iitax", "compare_with": {} diff --git a/taxcalc/tests/test_policy.py b/taxcalc/tests/test_policy.py index 18773e754..9f48e8455 100644 --- a/taxcalc/tests/test_policy.py +++ b/taxcalc/tests/test_policy.py @@ -897,14 +897,21 @@ def test_reform_with_scalar_vector_errors(): def test_index_offset_reform(): """ - Test a reform that includes both a change in CPI_offset and a change in - a variable's indexed status in the same year. + Test a reform that includes both a change in parameter_indexing_CPI_offset + and a change in a variable's indexed status in the same year. """ + # create policy0 to extract inflation rates before any + # parameter_indexing_CPI_offset + policy0 = Policy() + policy0.implement_reform({'parameter_indexing_CPI_offset': {2017: 0}}) + cpiu_rates = policy0.inflation_rates() + reform1 = {'CTC_c-indexed': {2020: True}} policy1 = Policy() policy1.implement_reform(reform1) offset = -0.005 - reform2 = {'CTC_c-indexed': {2020: True}, 'CPI_offset': {2020: offset}} + reform2 = {'CTC_c-indexed': {2020: True}, + 'parameter_indexing_CPI_offset': {2020: offset}} policy2 = Policy() policy2.implement_reform(reform2) # caused T-C crash before PR#2364 # extract from policy1 and policy2 the parameter values of CTC_c @@ -924,7 +931,8 @@ def test_index_offset_reform(): assert pvalue2[2021] > pvalue2[2020] # ... calculate expected pvalue2[2021] from offset and pvalue1 values indexrate1 = pvalue1[2021] / pvalue1[2020] - 1. - expindexrate = indexrate1 + offset + syear = Policy.JSON_START_YEAR + expindexrate = cpiu_rates[2020 - syear] + offset expvalue = round(pvalue2[2020] * (1. + expindexrate), 2) # ... compare expected value with actual value of pvalue2 for 2021 assert np.allclose([expvalue], [pvalue2[2021]]) @@ -932,13 +940,15 @@ def test_index_offset_reform(): def test_cpi_offset_affect_on_prior_years(): """ - Test that CPI_offset does not have affect on inflation - rates in earlier years. + Test that parameter_indexing_CPI_offset does not have affect + on inflation rates in earlier years. """ - reform = {'CPI_offset': {2022: -0.005}} + reform1 = {'parameter_indexing_CPI_offset': {2022: 0}} + reform2 = {'parameter_indexing_CPI_offset': {2022: -0.005}} p1 = Policy() p2 = Policy() - p2.implement_reform(reform) + p1.implement_reform(reform1) + p2.implement_reform(reform2) start_year = p1.start_year p1_rates = np.array(p1.inflation_rates()) @@ -957,6 +967,39 @@ def test_cpi_offset_affect_on_prior_years(): ) +def test_cpi_offset_on_reverting_params(): + """ + Test that params that revert to their pre-TCJA values + in 2026 revert if a parameter_indexing_CPI_offset is specified. + """ + reform0 = {'parameter_indexing_CPI_offset': {2020: -0.001}} + reform1 = {'STD': {2017: [6350, 12700, 6350, 9350, 12700]}, + 'parameter_indexing_CPI_offset': {2020: -0.001}} + reform2 = {'STD': {2020: [10000, 20000, 10000, 10000, 20000]}, + 'parameter_indexing_CPI_offset': {2020: -0.001}} + + p0 = Policy() + p1 = Policy() + p2 = Policy() + p0.implement_reform(reform0) + p1.implement_reform(reform1) + p2.implement_reform(reform2) + + ryear = 2026 + syear = Policy.JSON_START_YEAR + + # STD was reverted in 2026 + # atol=0.5 because ppp.py rounds params to nearest int + assert np.allclose( + p0._STD[ryear - syear], + p1._STD[ryear - syear], atol=0.5) + + # STD was not reverted in 2026 if included in revision + assert not np.allclose( + p1._STD[ryear - syear], + p2._STD[ryear - syear], atol=0.5) + + class TestAdjust: """ Test update and indexing rules as defined in the Parameters docstring. @@ -1160,17 +1203,25 @@ def test_activate_index(self): def test_apply_cpi_offset(self): """ - Test applying the CPI_offset parameter without any other parameters. + Test applying the parameter_indexing_CPI_offset parameter + without any other parameters. """ pol1 = Policy() - pol1.implement_reform({"CPI_offset": {2021: -0.001}}) + pol1.implement_reform( + {"parameter_indexing_CPI_offset": {2021: -0.001}} + ) pol2 = Policy() - pol2.adjust({"CPI_offset": [{"year": 2021, "value": -0.001}]}) + pol2.adjust( + {"parameter_indexing_CPI_offset": [ + {"year": 2021, "value": -0.001} + ]} + ) cmp_policy_objs(pol1, pol2) pol0 = Policy() + pol0.implement_reform({"parameter_indexing_CPI_offset": {2021: 0}}) init_rates = pol0.inflation_rates() new_rates = pol2.inflation_rates() @@ -1178,7 +1229,7 @@ def test_apply_cpi_offset(self): start_ix = 2021 - pol2.start_year exp_rates = copy.deepcopy(new_rates) - exp_rates[start_ix:] -= pol2._CPI_offset[start_ix:] + exp_rates[start_ix:] -= pol2._parameter_indexing_CPI_offset[start_ix:] np.testing.assert_allclose(init_rates, exp_rates) # make sure values prior to 2021 were not affected. @@ -1351,18 +1402,20 @@ def test_multiple_cpi_swaps2(self): def test_adj_CPI_offset_and_index_status(self): """ - Test changing CPI_offset and another parameter simultaneously. + Test changing parameter_indexing_CPI_offset and another + parameter simultaneously. """ pol1 = Policy() pol1.implement_reform({ "CTC_c-indexed": {2020: True}, - "CPI_offset": {2020: -0.005}}, + "parameter_indexing_CPI_offset": {2020: -0.005}}, ) pol2 = Policy() pol2.adjust( { - "CPI_offset": [{"year": 2020, "value": -0.005}], + "parameter_indexing_CPI_offset": + [{"year": 2020, "value": -0.005}], "CTC_c-indexed": [{"year": 2020, "value": True}], } ) @@ -1371,11 +1424,12 @@ def test_adj_CPI_offset_and_index_status(self): # Check no difference prior to 2020 pol0 = Policy() + pol0.implement_reform({"parameter_indexing_CPI_offset": {2020: 0}}) cmp_policy_objs( pol0, pol2, year_range=range(pol2.start_year, 2020 + 1), - exclude=["CPI_offset"] + exclude=["parameter_indexing_CPI_offset"] ) pol2.set_state(year=[2021, 2022])