"""
Partial-equilibrium elasticity-based behavioral-responses logic.
"""
# CODING-STYLE CHECKS:
# pycodestyle behresp.py
# pylint --disable=locally-disabled behresp.py
import copy
import numpy as np
from taxcalc.calculator import Calculator
from taxcalc.records import Records
from taxcalc.utils import DIST_VARIABLES
__all__ = ['response', 'quantity_response', 'labor_response']
[docs]
def response(calc_1, calc_2, elasticities, dump=False):
"""
Implements "Partial Equilibrium Simulation" conventional analysis,
returning results as a tuple of Pandas DataFrame objects (df1, df2)
where df1 is extracted from a baseline-policy calc_1 copy, and df2 is
extracted from a reform-policy calc_2 copy that incorporates the
behavioral responses given by the nature of the baseline-to-reform
change in policy and elasticities in the specified behavior dictionary.
Note: this function internally modifies a copy of calc_2 records to
account for behavioral responses that arise from the policy reform that
involves moving from calc1 policy to calc2 policy. Neither calc_1 nor
calc_2 need to have had calc_all() executed before calling the response
function. And neither calc_1 nor calc_2 are affected by this response
function.
The elasticities argument is a dictionary containing the assumed response
elasticities. Omitting an elasticity key:value pair in the dictionary
implies the omitted elasticity is assumed to be zero. Here is the full
dictionary content and each elasticity's internal name:
be_sub = elasticities['sub']
Substitution elasticity of taxable income.
Defined as proportional change in taxable income divided by
proportional change in marginal net-of-tax rate (1-MTR) on taxpayer
earnings caused by the reform. Must be zero or positive.
be_inc = elasticities['inc']
Income elasticity of taxable income.
Defined as dollar change in taxable income divided by dollar change
in after-tax income caused by the reform. Must be zero or negative.
be_cg = elasticities['cg']
Semi-elasticity of long-term capital gains.
Defined as change in logarithm of long-term capital gains divided by
change in marginal tax rate (MTR) on long-term capital gains caused by
the reform. Must be zero or negative.
Read response function documentation (see below) for discussion of
appropriate values.
The optional dump argument controls the number of variables included
in the two returned DataFrame objects. When dump=False (its default
value), the variables in the two returned DataFrame objects include
just the variables in the Tax-Calculator DIST_VARIABLES list, which
is sufficient for constructing the standard Tax-Calculator tables.
When dump=True, the variables in the two returned DataFrame objects
include all the Tax-Calculator input and calculated output variables,
which is the same output as produced by the Tax-Calculator tc --dump
option except for one difference: the tc --dump option provides two
calculated variables, mtr_inctax and mtr_paytax, that are replaced
in the dump output of this response function by mtr_combined, which
is the sum of mtr_inctax and mtr_paytax.
Note: the use here of a dollar-change income elasticity (rather than
a proportional-change elasticity) is consistent with Feldstein and
Feenberg, "The Taxation of Two Earner Families", NBER Working Paper
No. 5155 (June 1995). A proportional-change elasticity was used by
Gruber and Saez, "The elasticity of taxable income: evidence and
implications", Journal of Public Economics 84:1-32 (2002) [see
equation 2 on page 10].
Note: the nature of the capital-gains elasticity used here is similar
to that used in Joint Committee on Taxation, "New Evidence on the
Tax Elasticity of Capital Gains: A Joint Working Paper of the Staff
of the Joint Committee on Taxation and the Congressional Budget
Office", (JCX-56-12), June 2012. In particular, the elasticity
use here is equivalent to the term inside the square brackets on
the right-hand side of equation (4) on page 11 --- not the epsilon
variable on the left-hand side of equation (4), which is equal to
the elasticity used here times the weighted average marginal tax
rate on long-term capital gains. So, the JCT-CBO estimate of
-0.792 for the epsilon elasticity (see JCT-CBO, Table 5) translates
into a much larger absolute value for the be_cg semi-elasticity
used by Tax-Calculator.
To calculate the elasticity from a semi-elasticity, we multiply by
MTRs from TC and weight by shares of taxable gains. To avoid those
with zero MTRs, we restrict this to the top 40% of tax units by AGI.
Using this function, a semi-elasticity of -3.45 corresponds to a tax
rate elasticity of -0.792.
"""
# pylint: disable=too-many-locals,too-many-statements,too-many-branches
# Check function argument types and elasticity values
calc1 = copy.deepcopy(calc_1)
calc2 = copy.deepcopy(calc_2)
assert isinstance(calc1, Calculator)
assert isinstance(calc2, Calculator)
assert isinstance(elasticities, dict)
be_sub = elasticities['sub'] if 'sub' in elasticities else 0.0
be_inc = elasticities['inc'] if 'inc' in elasticities else 0.0
be_cg = elasticities['cg'] if 'cg' in elasticities else 0.0
assert be_sub >= 0.0
assert be_inc <= 0.0
assert be_cg <= 0.0
# Begin nested functions used only in this response function
def _update_ordinary_income(taxinc_change, calc):
"""
Implement total taxable income change induced by behavioral response.
"""
# compute AGI minus itemized deductions, agi_m_ided
agi = calc.array('c00100')
ided = np.where(calc.array('c04470') < calc.array('standard'),
0., calc.array('c04470'))
agi_m_ided = agi - ided
# assume behv response only for filing units with positive agi_m_ided
pos = np.array(agi_m_ided > 0., dtype=bool)
delta_income = np.where(pos, taxinc_change, 0.)
# allocate delta_income into three parts
# pylint: disable=unsupported-assignment-operation
winc = calc.array('e00200')
delta_winc = np.zeros_like(agi)
delta_winc[pos] = delta_income[pos] * winc[pos] / agi_m_ided[pos]
oinc = agi - winc
delta_oinc = np.zeros_like(agi)
delta_oinc[pos] = delta_income[pos] * oinc[pos] / agi_m_ided[pos]
delta_ided = np.zeros_like(agi)
delta_ided[pos] = delta_income[pos] * ided[pos] / agi_m_ided[pos]
# confirm that the three parts are consistent with delta_income
assert np.allclose(delta_income, delta_winc + delta_oinc - delta_ided)
# add the three parts to different records variables embedded in calc
calc.incarray('e00200', delta_winc)
calc.incarray('e00200p', delta_winc)
calc.incarray('e00300', delta_oinc)
calc.incarray('e19200', delta_ided)
return calc
def _update_cap_gain_income(cap_gain_change, calc):
"""
Implement capital gain change induced by behavioral responses.
"""
calc.incarray('p23250', cap_gain_change)
return calc
def _mtr12(calc__1, calc__2, mtr_of='e00200p', tax_type='combined'):
"""
Computes marginal tax rates for Calculator objects calc__1 and calc__2
for specified mtr_of income type and specified tax_type.
"""
assert tax_type in ('combined', 'iitax')
_, iitax1, combined1 = calc__1.mtr(mtr_of, wrt_full_compensation=True)
_, iitax2, combined2 = calc__2.mtr(mtr_of, wrt_full_compensation=True)
if tax_type == 'combined':
return (combined1, combined2)
return (iitax1, iitax2)
# End nested functions used only in this response function
# Begin main logic of response function
calc1.calc_all()
calc2.calc_all()
assert calc1.array_len == calc2.array_len
assert calc1.current_year == calc2.current_year
mtr_cap = 0.99
if dump:
recs_vinfo = Records(data=None) # contains records VARINFO only
dvars = list(recs_vinfo.USABLE_READ_VARS | recs_vinfo.CALCULATED_VARS)
# Calculate sum of substitution and income effects
if be_sub == 0.0 and be_inc == 0.0:
zero_sub_and_inc = True
si_chg = None # is not used when zero_sub_and_inc is True
if dump:
wage_mtr1 = np.zeros(calc1.array_len)
wage_mtr2 = np.zeros(calc2.array_len)
else:
zero_sub_and_inc = False
# calculate marginal combined tax rates on taxpayer wages+salary
# (e00200p is taxpayer's wages+salary)
wage_mtr1, wage_mtr2 = _mtr12(calc1, calc2,
mtr_of='e00200p',
tax_type='combined')
# calculate magnitude of substitution effect
if be_sub == 0.0:
sub = np.zeros(calc1.array_len)
else:
# proportional change in marginal net-of-tax rates on earnings
mtr1 = np.where(wage_mtr1 > mtr_cap, mtr_cap, wage_mtr1)
mtr2 = np.where(wage_mtr2 > mtr_cap, mtr_cap, wage_mtr2)
pch = ((1. - mtr2) / (1. - mtr1)) - 1.
# Note: c04800 is filing unit's taxable income
sub = be_sub * pch * calc1.array('c04800')
# calculate magnitude of income effect
if be_inc == 0.0:
inc = np.zeros(calc1.array_len)
else:
# dollar change in after-tax income
# Note: combined is f.unit's income+payroll tax liability
dch = calc1.array('combined') - calc2.array('combined')
inc = be_inc * dch
# calculate sum of substitution and income effects
si_chg = sub + inc
# Calculate long-term capital-gains effect
if be_cg == 0.0:
ltcg_chg = np.zeros(calc1.array_len)
else:
# calculate marginal tax rates on long-term capital gains
# p23250 is filing units' long-term capital gains
ltcg_mtr1, ltcg_mtr2 = _mtr12(calc1, calc2,
mtr_of='p23250',
tax_type='iitax')
rch = ltcg_mtr2 - ltcg_mtr1
exp_term = np.exp(be_cg * rch)
new_ltcg = calc1.array('p23250') * exp_term
ltcg_chg = new_ltcg - calc1.array('p23250')
# Extract dataframe from calc1
if dump:
df1 = calc1.dataframe(dvars)
df1.drop('mtr_inctax', axis='columns', inplace=True)
df1.drop('mtr_paytax', axis='columns', inplace=True)
df1['mtr_combined'] = wage_mtr1 * 100
else:
df1 = calc1.dataframe(DIST_VARIABLES)
del calc1
# Add behavioral-response changes to income sources
calc2_behv = copy.deepcopy(calc2)
del calc2
if not zero_sub_and_inc:
calc2_behv = _update_ordinary_income(si_chg, calc2_behv)
calc2_behv = _update_cap_gain_income(ltcg_chg, calc2_behv)
# Recalculate post-reform taxes incorporating behavioral responses
calc2_behv.calc_all()
# Extract dataframe from calc2_behv
if dump:
df2 = calc2_behv.dataframe(dvars)
df2.drop('mtr_inctax', axis='columns', inplace=True)
df2.drop('mtr_paytax', axis='columns', inplace=True)
df2['mtr_combined'] = wage_mtr2 * 100
else:
df2 = calc2_behv.dataframe(DIST_VARIABLES)
del calc2_behv
# Return the two dataframes
return (df1, df2)
def pch_response(elasticity=np.zeros(1),
val1=np.zeros(1),
val2=np.zeros(1)):
"""
Calculate the percentage change response, given an elasticity and
original/new values. Can be used to calculate substitution or
income effects.
Parameters
----------
elasticity: value or numpy array representing elasticity(ies).
Defaults to zero.
val1: value or numpy array representing original value(s).
Defaults to zero.
val2: value or numpy array representing new value(s).
Defaults to zero.
Returns
-------
pch_response: numpy array
Percentage change in the response, calculated essentially as:
elasticity * (val2 / val1 - 1).
"""
val1 = np.where(val1 == 0, np.nan, val1) # Avoids a warning.
pch = np.where(np.isnan(val1), 0, val2 / val1 - 1.)
return elasticity * pch
[docs]
def quantity_response(quantity=np.array([1]),
price_elasticity=np.zeros(1),
aftertax_price1=np.zeros(1),
aftertax_price2=np.zeros(1),
income_elasticity=np.zeros(1),
aftertax_income1=np.zeros(1),
aftertax_income2=np.zeros(1)):
"""
Calculate dollar change in quantity using a log-log response equation,
which assumes that the proportional change in the quantity is equal to
the sum of two terms:
(1) the proportional change in the quantity's marginal aftertax price
times an assumed price elasticity, and
(2) the proportional change in aftertax income
times an assumed income elasticity.
Not all inputs are required, so it's possible to calculate only the price
or income effects by providing a subset of arguments. Accepts arrays.
Parameters
----------
quantity: numpy array
pre-response quantity whose response is being calculated.
Defaults to 1.
price_elasticity: float
coefficient of the percentage change in aftertax price of
the quantity in the log-log response equation. Defaults to 0.
aftertax_price1: numpy array
marginal aftertax price of the quantity under baseline policy.
Note that this function forces prices to be in [0.01, inf] range,
but the caller of this function may want to constrain negative
or very small prices to be somewhat larger in order to avoid extreme
proportional changes in price. Defaults to 0.
Note this is NOT an array of marginal tax rates (MTR), but rather
usually 1-MTR (or in the case of quantities, like charitable
giving, whose MTR values are non-positive, 1+MTR).
aftertax_price2: numpy array
marginal aftertax price of the quantity under reform policy.
Note that this function forces prices to be in [0.01, inf] range,
but the caller of this function may want to constrain negative
or very small prices to be somewhat larger in order to avoid extreme
proportional changes in price. Defaults to 0.
Note this is NOT an array of marginal tax rates (MTR), but rather
usually 1-MTR (or in the case of quantities, like charitable
giving, whose MTR values are non-positive, 1+MTR).
income_elasticity: float
coefficient of the percentage change in aftertax income in the
log-log response equation. Defaults to 0.
aftertax_income1: numpy array
aftertax income under baseline policy.
Note that this function forces income to be in [1, inf] range,
but the caller of this function may want to constrain negative
or small incomes to be somewhat larger in order to avoid extreme
proportional changes in aftertax income. Defaults to 0.
aftertax_income2: numpy array
aftertax income under reform policy.
Note that this function forces income to be in [1, inf] range,
but the caller of this function may want to constrain negative
or small incomes to be somewhat larger in order to avoid extreme
proportional changes in aftertax income. Defaults to 0.
Returns
-------
response: numpy array
dollar change in quantity calculated from log-log response equation
"""
# pylint: disable=too-many-arguments,too-many-positional-arguments
substitution_effect = pch_response(
price_elasticity,
np.maximum(aftertax_price1, 0.01),
np.maximum(aftertax_price2, 0.01))
income_effect = pch_response(
income_elasticity,
np.maximum(aftertax_income1, 1.0),
np.maximum(aftertax_income2, 1.0))
return quantity * (substitution_effect + income_effect)
[docs]
def labor_response(earnings=np.array([1]),
substitution_eti=np.zeros(1),
mtr1=np.zeros(1),
mtr2=np.zeros(1),
income_elasticity=np.zeros(1),
aftertax_income1=np.zeros(1),
aftertax_income2=np.zeros(1)):
"""
Calculate labor response given earnings, substitution elasticity of taxable
income, initial and new marginal tax rates, income elasticity, and initial
and new after-tax income. Accepts arrays.
Parameters
----------
earnings: numpy array
pre-response earnings whose response is being calculated.
Defaults to 1.
substitution_eti: float or numpy array
coefficient of the substitution elasticity of taxable income.
Defaults to 0.
mtr1: numpy array
marginal tax rate of earnings under baseline policy.
Note that this function forces MTRs to be in [-inf, 0.99] range,
but the caller of this function may want to constrain large MTRs
to be somewhat smaller in order to avoid extreme
proportional changes in earnings. Defaults to 0.
mtr2: numpy array
marginal tax rate of earnings under reform policy.
Note that this function forces MTRs to be in [-inf, 0.99] range,
but the caller of this function may want to constrain large MTRs
to be somewhat smaller in order to avoid extreme
proportional changes in earnings. Defaults to 0.
income_elasticity: float
coefficient of the percentage change in aftertax income in the
log-log response equation. Defaults to 0.
aftertax_income1: numpy array
aftertax income under baseline policy.
Note that this function forces income to be in [1, inf] range,
but the caller of this function may want to constrain negative
or small incomes to be somewhat larger in order to avoid extreme
proportional changes in aftertax income. Defaults to 0.
aftertax_income2: numpy array
aftertax income under reform policy.
Note that this function forces income to be in [1, inf] range,
but the caller of this function may want to constrain negative
or small incomes to be somewhat larger in order to avoid extreme
proportional changes in aftertax income. Defaults to 0.
Returns
-------
response: numpy array
dollar change in earnings calculated from log-log response equation
"""
# pylint: disable=too-many-arguments,too-many-positional-arguments
return quantity_response(
quantity=earnings,
price_elasticity=substitution_eti,
aftertax_price1=1 - mtr1,
aftertax_price2=1 - mtr2,
income_elasticity=income_elasticity,
aftertax_income1=aftertax_income1,
aftertax_income2=aftertax_income2
)