Recipe 4: Estimating Differential Reform Response - pandas versionΒΆ

This is an advanced recipe that should be followed only after mastering the basic recipe. This recipe shows how to estimate the reform response in charitable giving when the response elasticities vary by earnings group. It employs the groupby technique used in the Creating a Custom Table recipe, so you might want to read that recipe first.

import taxcalc as tc
import pandas as pd
import numpy as np
import behresp

# use publicly-available CPS input file
recs = tc.Records.cps_constructor()

# specify Calculator object for static analysis of current-law policy
pol = tc.Policy()
calc1 = tc.Calculator(policy=pol, records=recs)

CYR = 2020

# calculate current-law tax liabilities for CYR
calc1.advance_to_year(CYR)
calc1.calc_all()

# calculate marginal tax rate wrt cash charitable giving
(_, _, mtr1) = calc1.mtr('e19800', calc_all_already_called=True,
                         wrt_full_compensation=False)

# specify Calculator object for static analysis of reform policy
# TODO: Move this reform online so this can be run non-locally.
pol.implement_reform(tc.Policy.read_json_reform('github://PSLmodels:[email protected]/docs/recipes/_static/reformB.json'))
calc2 = tc.Calculator(policy=pol, records=recs)

# calculate reform tax liabilities for cyr
calc2.advance_to_year(CYR)
calc2.calc_all()

# calculate marginal tax rate wrt cash charitable giving
(_, _, mtr2) = calc2.mtr('e19800', calc_all_already_called=True,
                         wrt_full_compensation=False)

# extract variables needed for quantity_response function
# (note the aftertax price is 1+mtr because mtr wrt charity is non-positive)
vdf = calc1.dataframe(['s006', 'e19800', 'e00200'])
vdf['price1'] = 1.0 + mtr1
vdf['price2'] = 1.0 + mtr2
vdf['atinc1'] = calc1.array('aftertax_income')
vdf['atinc2'] = calc2.array('aftertax_income')

# group filing units into earnings groups with different response elasticities
# (note earnings groups are just an example based on no empirical results)
EARNINGS_BINS = [-9e99, 50e3, 9e99]  # Two groups: below and above $50,000.
vdf['table_row'] = pd.cut(vdf.e00200, EARNINGS_BINS, right=False).astype(str)

vdf['price_elasticity'] = np.where(vdf.e00200 < EARNINGS_BINS[1],
                                   -0.1, -0.4)
vdf['income_elasticity'] = 0.1

# create copies of vdf and subset by price elasticity
vdf_1 = vdf.copy()
vdf_2 = vdf.copy()

vdf_1 = vdf_1.loc[vdf_1['price_elasticity']==-0.1]
vdf_2 = vdf_2.loc[vdf_2['price_elasticity']==-0.4]

# Calculate response based on features of each filing unit.
# Call quantity_response for each subset (i.e. vdf_1 and vdf_2)
vdf_1['response'] = behresp.quantity_response(quantity=vdf_1.e19800,
                                              price_elasticity=-0.1,
                                              aftertax_price1=vdf_1.price1,
                                              aftertax_price2=vdf_1.price2,
                                              income_elasticity=0.1,
                                              aftertax_income1=vdf_1.atinc1,
                                              aftertax_income2=vdf_1.atinc2)

vdf_2['response'] = behresp.quantity_response(quantity=vdf_2.e19800,
                                              price_elasticity=-0.4,
                                              aftertax_price1=vdf_2.price1,
                                              aftertax_price2=vdf_2.price2,
                                              income_elasticity=0.1,
                                              aftertax_income1=vdf_2.atinc1,
                                              aftertax_income2=vdf_2.atinc2)

vdf = pd.concat([vdf_1, vdf_2])

# Add weighted totals.
# Can also use microdf as mdf.add_weighted_totals(vdf, ['response', 'e19800'])
vdf['e19800_b'] = vdf.s006 * vdf.e19800 / 1e9
vdf['response_b'] = vdf.s006 * vdf.response / 1e9
vdf['funits_m'] = vdf.s006 / 1e6

SUM_VARS = ['funits_m', 'e19800_b', 'response_b']
# Sum weighted total columns for each income group.
grouped = vdf.groupby('table_row')[SUM_VARS].sum()
# Add a total row and make the index a column for printing.
grouped.loc['TOTAL'] = grouped.sum()
grouped.reset_index(inplace=True)

# Calculate percent response and drop unnecessary total.
grouped['pct_response'] = 100 * grouped.response_b / grouped.e19800_b
grouped.drop('e19800_b', axis=1, inplace=True)

# Rename columns for printing.
grouped.columns = ['Earnings Group', 'Num(#M)', 'Resp($B)', 'Resp(%)']
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
<ipython-input-2-07624c035916> in <module>
     15 # calculate current-law tax liabilities for CYR
     16 calc1.advance_to_year(CYR)
---> 17 calc1.calc_all()
     18 
     19 # calculate marginal tax rate wrt cash charitable giving

~/work/Tax-Calculator/Tax-Calculator/taxcalc/calculator.py in calc_all(self, zero_out_calc_vars)
    174         BenefitLimitation(self)
    175         FairShareTax(self.__policy, self.__records)
--> 176         LumpSumTax(self.__policy, self.__records)
    177         ExpandIncome(self.__policy, self.__records)
    178         AfterTaxIncome(self.__policy, self.__records)

~/work/Tax-Calculator/Tax-Calculator/taxcalc/decorators.py in wrapper(*args, **kwargs)
    323                  {"applied_f": applied_jitted_f}, fakeglobals)
    324             high_level_fn = fakeglobals['hl_func']
--> 325             ans = high_level_fn(*args, **kwargs)
    326             return ans
    327 

<string> in hl_func(pm, pf)

/usr/share/miniconda/envs/taxcalc-dev/lib/python3.8/site-packages/numba/core/dispatcher.py in _compile_for_args(self, *args, **kws)
    370         return_val = None
    371         try:
--> 372             return_val = self.compile(tuple(argtypes))
    373         except errors.ForceLiteralArg as e:
    374             # Received request for compiler re-entry with the list of arguments

/usr/share/miniconda/envs/taxcalc-dev/lib/python3.8/site-packages/numba/core/dispatcher.py in compile(self, sig)
    907                 with ev.trigger_event("numba:compile", data=ev_details):
    908                     try:
--> 909                         cres = self._compiler.compile(args, return_type)
    910                     except errors.ForceLiteralArg as e:
    911                         def folded(args, kws):

/usr/share/miniconda/envs/taxcalc-dev/lib/python3.8/site-packages/numba/core/dispatcher.py in compile(self, args, return_type)
     77 
     78     def compile(self, args, return_type):
---> 79         status, retval = self._compile_cached(args, return_type)
     80         if status:
     81             return retval

/usr/share/miniconda/envs/taxcalc-dev/lib/python3.8/site-packages/numba/core/dispatcher.py in _compile_cached(self, args, return_type)
     91 
     92         try:
---> 93             retval = self._compile_core(args, return_type)
     94         except errors.TypingError as e:
     95             self._failed_cache[key] = e

/usr/share/miniconda/envs/taxcalc-dev/lib/python3.8/site-packages/numba/core/dispatcher.py in _compile_core(self, args, return_type)
    104 
    105         impl = self._get_implementation(args, {})
--> 106         cres = compiler.compile_extra(self.targetdescr.typing_context,
    107                                       self.targetdescr.target_context,
    108                                       impl,

/usr/share/miniconda/envs/taxcalc-dev/lib/python3.8/site-packages/numba/core/compiler.py in compile_extra(typingctx, targetctx, func, args, return_type, flags, locals, library, pipeline_class)
    604     pipeline = pipeline_class(typingctx, targetctx, library,
    605                               args, return_type, flags, locals)
--> 606     return pipeline.compile_extra(func)
    607 
    608 

/usr/share/miniconda/envs/taxcalc-dev/lib/python3.8/site-packages/numba/core/compiler.py in compile_extra(self, func)
    351         self.state.lifted = ()
    352         self.state.lifted_from = None
--> 353         return self._compile_bytecode()
    354 
    355     def compile_ir(self, func_ir, lifted=(), lifted_from=None):

/usr/share/miniconda/envs/taxcalc-dev/lib/python3.8/site-packages/numba/core/compiler.py in _compile_bytecode(self)
    413         """
    414         assert self.state.func_ir is None
--> 415         return self._compile_core()
    416 
    417     def _compile_ir(self):

/usr/share/miniconda/envs/taxcalc-dev/lib/python3.8/site-packages/numba/core/compiler.py in _compile_core(self)
    384             res = None
    385             try:
--> 386                 pm.run(self.state)
    387                 if self.state.cr is not None:
    388                     break

/usr/share/miniconda/envs/taxcalc-dev/lib/python3.8/site-packages/numba/core/compiler_machinery.py in run(self, state)
    328                 pass_inst = _pass_registry.get(pss).pass_inst
    329                 if isinstance(pass_inst, CompilerPass):
--> 330                     self._runPass(idx, pass_inst, state)
    331                 else:
    332                     raise BaseException("Legacy pass in use")

/usr/share/miniconda/envs/taxcalc-dev/lib/python3.8/site-packages/numba/core/compiler_lock.py in _acquire_compile_lock(*args, **kwargs)
     33         def _acquire_compile_lock(*args, **kwargs):
     34             with self:
---> 35                 return func(*args, **kwargs)
     36         return _acquire_compile_lock
     37 

/usr/share/miniconda/envs/taxcalc-dev/lib/python3.8/site-packages/numba/core/compiler_machinery.py in _runPass(self, index, pss, internal_state)
    287             mutated |= check(pss.run_initialization, internal_state)
    288         with SimpleTimer() as pass_time:
--> 289             mutated |= check(pss.run_pass, internal_state)
    290         with SimpleTimer() as finalize_time:
    291             mutated |= check(pss.run_finalizer, internal_state)

/usr/share/miniconda/envs/taxcalc-dev/lib/python3.8/site-packages/numba/core/compiler_machinery.py in check(func, compiler_state)
    260 
    261         def check(func, compiler_state):
--> 262             mangled = func(compiler_state)
    263             if mangled not in (True, False):
    264                 msg = ("CompilerPass implementations should return True/False. "

/usr/share/miniconda/envs/taxcalc-dev/lib/python3.8/site-packages/numba/core/typed_passes.py in run_pass(self, state)
    185         # add back in dels.
    186         post_proc = postproc.PostProcessor(state.func_ir)
--> 187         post_proc.run(emit_dels=True)
    188 
    189         state.type_annotation = type_annotations.TypeAnnotation(

/usr/share/miniconda/envs/taxcalc-dev/lib/python3.8/site-packages/numba/core/postproc.py in run(self, emit_dels)
     93         # and then strips dels as part of its analysis.
     94         if emit_dels:
---> 95             self._insert_var_dels()
     96 
     97     def _populate_generator_info(self):

/usr/share/miniconda/envs/taxcalc-dev/lib/python3.8/site-packages/numba/core/postproc.py in _insert_var_dels(self)
    171         """
    172         vlt = self.func_ir.variable_lifetime
--> 173         self._patch_var_dels(vlt.deadmaps.internal, vlt.deadmaps.escaping)
    174 
    175     def _patch_var_dels(self, internal_dead_map, escaping_dead_map):

/usr/share/miniconda/envs/taxcalc-dev/lib/python3.8/site-packages/numba/core/postproc.py in _patch_var_dels(self, internal_dead_map, escaping_dead_map)
    184             for stmt in reversed(ir_block.body[:-1]):
    185                 # internal vars that are used here
--> 186                 live_set = set(v.name for v in stmt.list_vars())
    187                 dead_set = live_set & internal_dead_set
    188                 for T, def_func in ir_extension_insert_dels.items():

/usr/share/miniconda/envs/taxcalc-dev/lib/python3.8/site-packages/numba/core/ir.py in list_vars(self)
    345 
    346     def list_vars(self):
--> 347         return self._rec_list_vars(self.__dict__)
    348 
    349 

KeyboardInterrupt: 

Result: Response in Charitable Giving by Earnings Group

grouped.round(3)
Earnings Group Num(#M) Resp($B) Resp(%)
0 [-9e+99, 50000.0) 153.673 0.579 0.354
1 [50000.0, 9e+99) 51.771 2.810 1.924
2 TOTAL 205.444 3.388 1.094