Recipe 6: Analyzing a Non-Parametric Reform#
This is an advanced recipe that should be followed only after mastering the basic recipe. This recipe shows how to customize the Calculator class so that it can analyze a tax reform that cannot be characterized using existing policy parameters (in this case a pseudo Cost-of-Living Refund reform). It uses, in a more extensive way, the object-oriented programming inheritance technique introduced in Redefining Expanded Income, so you might want to read that recipe first.
Recipe that illustrates how to customize the taxcalc Calculator class so that it can analyze a non-parametric reform (that is, a reform that cannot be represented using existing taxcalc Policy parameters.
The technique for doing this customization is standard in object-oriented programming: a child class is derived from a parent class and then customized. The derived child class inherits all the data and methods of the parent class, but can be customized by adding new data and methods or by overriding inherited methods.
The reform used to illustrate this programming technique is somewhat like the Cost-of-Living Refund, a refundable credit that was being discussed in tax policy circles during 2019 as a replacement for the EITC. But the reform analyzed here is not exactly like the Cost-of-Living Refund, so we call it a pseudo cost-of-living refund to emphasize that it is not meant to be an accurate representation of the Cost-of-Living Refund proposal.
import numpy as np
import taxcalc as tc
class Calculator(tc.Calculator):
"""
Customized Calculator class that inherits all tc.Calculator data and
methods, adding or overriding some to get the desired customization.
"""
def __init__(self, policy=None, records=None, verbose=False,
sync_years=True, consumption=None,
# above are the tc.Calculator constructor arguments
# below is new constructor argument used in customization
colr_active=False):
# call the parent class constructor
super().__init__(policy=policy, records=records,
verbose=verbose, sync_years=sync_years,
consumption=consumption)
# remember whether pseudo_COLR policy is active or not
self.colr_active = colr_active
# declare colr_param dictionary that will contain pseudo COLR policy
self.colr_param = dict()
def specify_pseudo_colr_policy(self):
"""
Specify policy parameters for the COLR policy in the current_year.
See use of these parameters below in the pseudo_colr_amount method.
"""
# reform implementation year
reform_year = 2020
# specify dictionary of parameter names and values for reform_year
self.colr_param = {
# credit phase-in rate on earnings
'COLR_rt': 1.0,
# ceiling on refundable credit varies by filing-unit type, MARS
'COLR_c': np.array([4000, 8000, 4000, 4000, 4000],
dtype=np.float64),
# credit phase-out start AGI level varies by filing-unit type, MARS
'COLR_ps': np.array([30000, 50000, 30000, 30000, 30000],
dtype=np.float64),
# credit phase-out rate per dollar of AGI above COLR_ps level
'COLR_prt': 0.2
}
# set pseudo COLR parameter values for current year
this_year = self.current_year
if self.colr_active and this_year >= reform_year:
# set inflation-indexed values of COLR_c and COLR_ps for this year
irates = self.__policy.inflation_rates()
syr = tc.Policy.JSON_START_YEAR
for name in ['COLR_c', 'COLR_ps']:
value = self.colr_param[name]
for year in range(reform_year, this_year):
value *= (1.0 + irates[year - syr])
self.colr_param[name] = np.round(value, 2) # to nearest penny
else: # if policy not active or if this year is before the reform year
# set ceiling to zero
self.colr_param['COLR_c'] = np.array([0.0, 0.0, 0.0, 0.0, 0.0],
dtype=np.float64)
tracing = False # set to True to see parameter values for this year
if tracing:
for name in self.colr_param:
print('> {} {} {}'.format(
this_year, name, self.colr_param[name]
))
def pseudo_colr_amount(self):
"""
Calculate pseudo Cost-of-Living Refund amount.
Note this is simply meant to illustrate a Python programming technique;
this function does NOT calculate an exact Cost-of-Living Refund amount.
See setting of parameters above in specify_pseudo_COLR_policy method.
"""
recs = self.__records
# create MARS-specific policy parameter arrays
mars_indicators = [recs.MARS == 1, recs.MARS == 2, recs.MARS == 3,
recs.MARS == 4, recs.MARS == 5]
colr_c = np.select(mars_indicators, self.colr_param['COLR_c'])
colr_ps = np.select(mars_indicators, self.colr_param['COLR_ps'])
colr_rt = self.colr_param['COLR_rt']
colr_prt = self.colr_param['COLR_prt']
# compute colr_amt
amt_pre_phaseout = np.minimum(recs.e00200 * colr_rt, colr_c)
phaseout = np.maximum((recs.c00100 - colr_ps) * colr_prt, 0.)
colr_amt = np.maximum(amt_pre_phaseout - phaseout, 0.)
setattr(recs, 'colr_amount', colr_amt)
# reduce income and combined taxes because COLR is a refundable credit
recs.iitax -= colr_amt
recs.combined -= colr_amt
# delete local arrays used only in this method
del mars_indicators
del colr_c
del colr_ps
del amt_pre_phaseout
del phaseout
del colr_amt
def calc_all(self, zero_out_calc_vars=False):
"""
Call all tax-calculation functions for the current_year.
"""
tc.BenefitPrograms(self)
self._calc_one_year(zero_out_calc_vars)
tc.BenefitSurtax(self)
tc.BenefitLimitation(self)
tc.FairShareTax(self.__policy, self.__records)
tc.LumpSumTax(self.__policy, self.__records)
# specify new method to set pseudo COLR policy parameters
self.specify_pseudo_colr_policy() # (see above)
# call new method to calculate pseudo COLR amount
self.pseudo_colr_amount() # (see above)
tc.ExpandIncome(self.__policy, self.__records)
tc.AfterTaxIncome(self.__policy, self.__records)
# end of customized Calculator class definition
# top-level logic of program that uses customized Calculator class
policy1 = tc.Policy() # baseline policy is current-law policy
policy2 = tc.Policy() # parametric reform, reformC.json, eliminates EITC
# TODO: Move to the web so this can be done standalone.
policy2.implement_reform(tc.Policy.read_json_reform('github://PSLmodels:Tax-Calculator@master/docs/recipes/_static/reformC.json'))
# specify customized Calculator objects for baseline and reform:
# baseline calc1 uses policy1 (current-law) and colr_active=False
# reform calc2 uses policy2 (no EITC) and colr_active=True
cps_records = tc.Records.cps_constructor()
calc1 = Calculator(policy=policy1, records=cps_records, colr_active=False)
calc2 = Calculator(policy=policy2, records=cps_records, colr_active=True)
# calculate tax liabilities for years around the reform year
CYR_FIRST = 2019
CYR_LAST = 2022
for cyr in range(CYR_FIRST, CYR_LAST + 1):
# advance to and calculate for specified cyr
calc1.advance_to_year(cyr)
calc1.calc_all()
calc2.advance_to_year(cyr)
calc2.calc_all()
# tabulate weighted amounts
funits = calc1.total_weight()
itax1 = calc1.weighted_total('iitax')
itax2 = calc2.weighted_total('iitax')
eitc1 = calc1.weighted_total('eitc')
eitc2 = calc2.weighted_total('eitc')
colr1 = calc1.weighted_total('colr_amount')
colr2 = calc2.weighted_total('colr_amount')
# print weighted amounts for cyr
if cyr == CYR_FIRST:
print('YEAR UNITS ITAX1 ITAX2 EITC1 EITC2 COLR1 COLR2')
line = '{} {:.1f} {:.1f} {:.1f} {:5.1f} {:5.1f} {:5.1f} {:5.1f}'
print(line.format(cyr, funits * 1e-6,
itax1 * 1e-9, itax2 * 1e-9,
eitc1 * 1e-9, eitc2 * 1e-9,
colr1 * 1e-9, colr2 * 1e-9))
YEAR UNITS ITAX1 ITAX2 EITC1 EITC2 COLR1 COLR2
2019 201.9 1258.9 1258.9 63.3 63.3 0.0 0.0
2020 204.5 756.3 465.7 65.5 0.0 0.0 356.1
2021 207.1 951.0 697.1 76.4 0.0 0.0 330.3
2022 209.7 1646.2 1360.0 60.9 0.0 0.0 347.0