Source code for taxcalc.behresp

"""
Partial-equilibrium parameter-based behavioral-responses logic.

This module contains two independent sets of functions that are not
used by each other:

(1) The response function, which conducts partial-equilibrium
    analysis of a baseline-to-reform policy change: it computes the
    behavioral response implied by the assumed response parameters,
    adds that response to the reform filing units' input variables,
    recalculates reform taxes, and returns baseline and reform
    DataFrame objects.
    This is the function used by the tc CLI --behavior option and by
    cookbook recipe 2.

(2) The quantity_response and labor_response functions (and their
    pch_response helper), which are stand-alone array arithmetic that
    evaluate a log-log response equation for a caller-supplied quantity,
    elasticities, prices, and incomes.  They conduct no tax calculations
    and know nothing about Calculator objects; a caller must compute the
    marginal tax rates and incomes and apply the returned dollar change.

Beyond being unrelated in the code, the two sets of functions differ in
their economics: the response function scales its substitution effect by
taxable income (c04800), whereas labor_response and quantity_response
scale theirs by the quantity (for example, earnings) passed in by the
caller.  They also handle extreme values differently: the response
function caps nothing --- it applies no cap to marginal tax rates and no
floor to after-tax income --- whereas quantity_response forces after-tax
prices into the [0.01, inf] range and after-tax income into the
[1, inf] range.
"""
# 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']


# policy parameters that determine employer payroll tax liability on
# wages, and hence the only parameters whose reform can generate an
# earnings shift in the response function
ESF_PARAMS = ('FICA_ss_trt_employer', 'FICA_mc_trt_employer',
              'SS_Earnings_c', 'SS_Earnings_thd')

# Number of bisection steps used to solve the earnings-shift fixed-point
# equation in the response function.  The equation is solved by bisection
# on a bracket that is guaranteed to contain the solution, so this count
# is a precision setting rather than a convergence risk: each step halves
# the bracket, making sixty steps enough to locate the shifted wage to
# far less than a cent for any wage representable in the input data.
ESF_BISECTION_STEPS = 60


def employer_ptax_on_wages(ss_rate, mc_rate, cap, thd, gross_ws):
    """
    Returns array of employer payroll tax liability on the specified
    array of one earner's gross wages (wages plus employer pension
    contributions), where ss_rate and mc_rate are the employer OASDI and
    HI payroll tax rates, cap is the SS_Earnings_c value, and thd is the
    SS_Earnings_thd value.  Liability is increasing in gross wages and is
    piecewise linear with breakpoints at cap and thd.

    Note: this restates the ptax_er_p and ptax_er_s logic in the
    EI_PayrollTax function, which cannot be read from those output
    variables by the response function because it needs liability at
    trial wages that neither Calculator object has computed.  The two
    statements of the rule must be kept in agreement; see the response
    function docstring.
    """
    return (ss_rate * (np.minimum(cap, gross_ws) +
                       np.maximum(0., gross_ws - thd)) +
            mc_rate * gross_ws)


def bisect(residual, low, high, steps=ESF_BISECTION_STEPS):
    """
    Returns array containing the zero of the specified residual function
    on each of the brackets given by the low and high arrays, which must
    contain the zero: the residual must be increasing, nonpositive at
    low, and nonnegative at high.  Each of the specified number of steps
    halves every bracket, so the returned arrays are accurate to the
    initial bracket width divided by two raised to the steps power.

    Note: this function knows nothing about taxes; it is pure numerical
    solution logic that operates on whatever residual function its caller
    supplies.
    """
    for _ in range(steps):
        mid = 0.5 * (low + high)
        below = residual(mid) < 0.
        low = np.where(below, mid, low)
        high = np.where(below, high, mid)
    return 0.5 * (low + high)


[docs] def response(calc_1, calc_2, elasticities, dump=False): """ Implements conventional analysis (that is, static reform analysis plus partial-equilibrium behavior responses to a reform), 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. This function internally modifies a copy of the calc_2 records to account for behavioral responses that arise from the policy reform that involves moving from calc_1 policy to calc_2 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. Parameters ---------- calc_1: Calculator object represents baseline policy; must be advanced to the analysis year. calc_2: Calculator object represents reform policy; must be advanced to the same analysis year as calc_1 and must contain the same number of filing units. elasticities: dictionary contains the assumed response parameters/elasticities. Omitting a key:value pair implies the omitted parameter/elasticity is assumed to be zero. (Note that the tc CLI --behavior option is stricter: a JSON behavior file must contain all the keys.) Here is the full dictionary content and each parameter/elasticity's internal name: be_esf = elasticities['esf'] Earnings shift factor. Defined as the fraction of the reform-induced change in employer payroll tax liability that is shifted to wages rather than to nontaxable employee fringe benefits such as employer-provided health insurance. The shift is sign-symmetric: an increase in employer payroll tax liability decreases wages and a decrease increases them, although the two shifts are not equal in size because the employer tax rate that feeds back into the shifted wage differs between the two reforms. It is calculated per earner, and it is applied before the elasticities below are evaluated. Must be in the [0,1] range. 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. See the capital-gains note below for a discussion of appropriate values; be_cg is NOT the tax-rate elasticity usually reported in the literature. dump: boolean 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 --dumpdb option except for one difference: the tc dump output 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. Two cautions about the mtr_combined column: it is expressed in percentage points (that is, it is 100 times the rates returned by the Calculator.mtr method), and it contains all zeros when both be_sub and be_inc are zero, because in that case no earnings marginal tax rates are computed. Returns ------- (df1, df2): tuple of two Pandas DataFrame objects df1 contains baseline-policy results extracted from a copy of calc_1, and df2 contains reform-policy results, incorporating the behavioral responses, extracted from a copy of calc_2. Both have one row per filing unit, in input-data order, and contain the columns described in the dump argument documentation. Notes ----- Response equations: The earnings shift is computed first, and separately for the taxpayer and the spouse, because the OASDI cap (SS_Earnings_c) and the reform-only extra OASDI threshold (SS_Earnings_thd) apply per person rather than per filing unit, so a couple with two earners just below the cap and a couple with one earner well above it have different employer payroll tax exposure at the same filing-unit earnings. Writing ptax_er(wage) for an earner's employer payroll tax liability on wages under reform policy as a function of that earner's wages, ptax_er_1 for the same earner's baseline employer payroll tax liability on wages, and wage2 for the earner's pre-shift reform wages, the shifted wage is the solution of wage = wage2 - be_esf * (ptax_er(wage) - ptax_er_1) This holds the be_esf fraction of the earner's gross compensation --- wages plus employer payroll tax --- fixed, and it is sign-symmetric: an increase in employer payroll tax liability lowers wages and a decrease raises them. The remaining (1 - be_esf) fraction is absorbed by nontaxable fringe benefits, which are not represented in the input data. The equation is implicit in wage because ptax_er is a function of the wage being solved for, and it has no single closed-form solution because that function is piecewise linear. Writing s0 and s1 for the baseline and reform employer OASDI rates, h for the employer HI rate, and cap for SS_Earnings_c, the two interior regimes are: wages below the cap under both policies, where the employer payroll tax is proportional to the wage in both its OASDI and its HI part, so the shift is proportional: wage = wage2 * (1 + be_esf * (s0 + h)) / (1 + be_esf * (s1 + h)) wages above the cap under both policies, where the OASDI part is the flat amount s * cap and only the uncapped HI part varies with the wage, so the OASDI portion of the shift is a lump sum: wage = wage2 - be_esf * (s1 - s0) * cap / (1 + be_esf * h) In the second regime the earner's marginal wage is unchanged by an OASDI rate reform, so the shift is a pure income effect there, whereas in the first regime the marginal wage falls as well. An earner whose wage cut carries them from above the cap to below it is in neither regime; such an earner is on the kink, where the wage cut shrinks their own employer OASDI liability, which in turn feeds back into the wage. Because of these earners --- and of the band between the old and new caps under a reform to SS_Earnings_c itself --- the shift cannot be computed as a single average rate applied to all earners. The implementation therefore solves the fixed-point equation numerically, by bisection on a bracket that is guaranteed to contain the solution, which covers all three regimes above without special-casing any of them. The bracket follows from the residual of the equation being increasing in the wage with a slope of at least one, which puts the solution no farther from the pre-shift wage than the residual evaluated there. The numerical solution logic is the module-level bisect function, which knows nothing about payroll taxes, and the payroll tax rule is the module-level employer_ptax_on_wages function, which knows nothing about the equation being solved; a change in the payroll tax rule therefore leaves the solution logic untouched. The ptax_er function used here is the employer payroll tax on wages: the employer share of the OASDI tax on wages up to the SS_Earnings_c cap, plus the employer share of the reform-only extra OASDI tax on wages above the SS_Earnings_thd threshold, plus the employer share of the uncapped HI tax on wages, all of them evaluated on gross wages, which are wages plus employer pension contributions. This is exactly the rule that the EI_PayrollTax function applies to produce the ptax_er_p and ptax_er_s output variables, both of which are functions of gross wages alone; that is what makes an employer-payroll-tax incidence assumption expressible as a shift of wages. The baseline anchor ptax_er_1 is therefore read from those output variables rather than recomputed. The reform-policy function must still be stated here, as the module-level employer_ptax_on_wages function, because the bisection evaluates it at trial wages that neither Calculator object has computed, which no output variable can supply; the employer payroll tax rules are consequently stated in this module as well as in the EI_PayrollTax function, and the two statements must be kept in agreement. The test_behresp.py module tests that agreement directly. The substitution and income effects on taxable income are computed, in dollars per filing unit, as follows, where mtr1 and mtr2 are the baseline and reform combined (income plus payroll) marginal tax rates on the taxpayer's earnings (e00200p) computed with respect to full compensation (and not capped in any way), where c04800 is baseline taxable income, and where combined1 and combined2 are the baseline and reform combined income and payroll tax liabilities: sub = be_sub * (((1 - mtr2) / (1 - mtr1)) - 1) * c04800 inc = be_inc * (combined1 - combined2) The long-term capital gains response is computed, in dollars per filing unit, as follows, where ltcg_mtr1 and ltcg_mtr2 are the baseline and reform income-tax marginal tax rates on long-term capital gains (p23250): new_p23250 = p23250 * exp(be_cg * (ltcg_mtr2 - ltcg_mtr1)) ltcg_chg = new_p23250 - p23250 Note that the substitution effect is scaled by taxable income, which includes long-term capital gains, so its magnitude is not independent of the filing unit's LTCG amount. How responses are applied to input variables: The earnings shift is applied directly to the earnings variables of the two earners it is computed for: the taxpayer part is added to e00200p and the spouse part to e00200s, with e00200 incremented by their sum. Nothing else is adjusted. In particular, the pension contribution variables, pencon_p and pencon_s, are held fixed even though they are part of the employer payroll tax base, because nontaxable benefits are what the (1 - be_esf) fraction of the shift is defined to absorb. The shift is applied to a copy of calc_2 before the elasticities below are evaluated, and that copy is recalculated, so the reform marginal tax rates and tax liabilities that enter the substitution, income, and capital-gains responses are all measured at post-shift earnings. The ordering matters because the shift is an accounting adjustment that defines the reform being analyzed --- it holds gross compensation fixed --- rather than a behavioral response to it, so the elasticity-driven responses are layered on top of it. There is no double counting of the employer payroll tax: the substitution effect prices earnings using marginal tax rates computed with respect to full compensation, which is a different concept from the wage shift itself. The sum of the substitution and income effects is a change in taxable income that must be mapped back onto the input variables used in the tax calculation. The dollar change is allocated in proportion to three components --- wage and salary income (e00200), other AGI (c00100 minus e00200), and itemized deductions --- and the three parts are added to these input variables: - the wage part is added to both e00200 and e00200p - the other-income part is added to e00300 (taxable interest) - the deduction part is added to e19200 (interest paid deduction) Two consequences are worth noting. First, the spouse's earnings variable, e00200s, is not adjusted by this part of the response, so the substitution and income effects adjust e00200 and e00200p by the same amount. (The earnings shift, in contrast, is calculated per earner and does adjust e00200s.) Second, a response shows up in dump output as changes in e00300 and e19200 even for filing units whose actual behavior would involve other income or deduction items. The capital-gains response, by contrast, is applied directly to p23250. The denominator used to form the three allocation shares, called alloc_base in the code, is AGI minus itemized deductions, where itemized deductions (c04470) are counted only for filing units that actually itemize (that is, only when c04470 is no less than the standard deduction). This is NOT an approximation of taxable income, and it must not be replaced by the calculated taxable income variable, c04800. Because other AGI is defined as AGI minus wages, the three components sum to alloc_base by construction, so the three shares sum to one and the allocated parts sum to the intended dollar change. Dividing by any other quantity --- c04800 included --- would scale the delivered change by the ratio of alloc_base to that quantity. Taxable income is used where taxable income is the concept called for: c04800 scales the substitution effect, as described in the response equations above. The mapping does assume that a dollar added to e00200, e00300, or e19200 moves taxable income by a dollar. That is exact for a filing unit in the interior of the rate schedule, but not for one whose AGI change also moves an AGI-linked provision (taxable Social Security benefits, phase-outs such as the EITC, the qualified business income deduction, or the itemized deduction limitation). The realized change in aggregate taxable income therefore differs somewhat from the intended change: for a top-bracket rate reform applied to CPS data for 2026, with be_sub of 0.25 and be_inc of -0.1, the realized change exceeded the intended change by about four percent, with under one percent of responding filing units differing by more than ten dollars. Filing units excluded from the response: The earnings shift is skipped entirely when be_esf is zero or when the reform alters none of the four parameters of the employer payroll tax on wages: the four ESF_PARAMS parameters, which are FICA_ss_trt_employer, FICA_mc_trt_employer, SS_Earnings_c, and SS_Earnings_thd. Those four are the only parameters that the employer payroll tax on wages depends on; in particular the employee-share rates are not among them. That test is on the parameters themselves rather than on calculated tax amounts, so it is exact for the documented use of this function, in which calc_1 and calc_2 differ only in policy. A caller that passes two Calculator objects containing different input data would get no earnings shift when the two policies are the same, even though the two employer payroll tax liabilities would differ. Within a reform that does change one of the four parameters, the shift is applied only to earners with positive wages. That excludes the self-employed, who have no employer to shift a payroll tax to. It also excludes an earner whose gross wages are entirely employer pension contributions: such an earner does generate an employer payroll tax liability, but has no wages to shift it onto, and shifting it onto an e00200p or e00200s of zero would make that variable negative. Note that the employer payroll tax used for an included earner is nevertheless computed on gross wages, so it includes the part generated by that earner's pension contributions. The shifted wage is floored at zero. The shift is bounded by be_esf times the employer payroll tax rate times the gross wage, so under any plausible reform it is a small fraction of the wage and the floor never binds, but a reform that sets an extreme employer rate, or an earner whose gross wages are mostly pension contributions, could otherwise drive a wage negative. Where the floor does bind, the earner's gross compensation is not held fixed, because there is not enough wage income to absorb the be_esf fraction of the employer payroll tax change. The substitution and income effects are applied only to filing units with positive alloc_base; all other filing units are assumed to have no ordinary-income response. That condition is a guard on the allocation arithmetic rather than an economic screen: it keeps the denominator away from zero and prevents the negative shares that a negative alloc_base would produce. It excludes no filing unit that has positive taxable income, because c04800 is positive only when alloc_base is positive. Note that the converse does not hold: a filing unit with positive alloc_base and zero taxable income does respond, which is intended, because such a unit can still owe payroll tax and therefore can still have an income effect. For that reason the condition must not be tightened to require positive c04800. Within the responding group, filing units that do not itemize receive no change in e19200. Aside from this positive alloc_base condition, the response function applies no adhoc limits: earnings marginal tax rates are used exactly as computed, with no cap, so a marginal tax rate at or above one generates a zero or negative baseline net-of-tax rate and hence an extreme substitution effect for that filing unit. Likewise, there is no limit on the capital-gains response, whose exponential form can generate large proportional changes when the change in the capital-gains marginal tax rate is large. What is not modeled: Each analysis year is handled independently: this function contains no logic that carries a response in one year over into another year, so retiming behavior --- most notably the realization timing of capital gains --- is not modeled. There are no response margins for the spouse's earnings, for short-term capital gains, for deduction items other than the mechanical e19200 adjustment described above, or for any margin not represented by the elasticities. The earnings shift assumes full backward shifting of the be_esf fraction onto the individual earner who generated the employer payroll tax liability, so it models neither shifting across workers within an employer nor any forward shifting to consumers or to capital. Being a partial-equilibrium calculation, the analysis holds constant all prices, wages, and macroeconomic aggregates. 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 T-C 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. Specifying be_cg equal to a published tax-rate elasticity such as -0.792 is therefore a common mistake that generates a much smaller capital-gains response than intended. """ # pylint: disable=too-many-locals,too-many-statements,too-many-branches # Check function argument types and elasticity values assert isinstance(calc_1, Calculator) assert isinstance(calc_2, Calculator) assert isinstance(elasticities, dict) be_esf = elasticities['esf'] if 'esf' in elasticities else 0.0 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 0.0 <= be_esf <= 1.0 assert be_sub >= 0.0 assert be_inc <= 0.0 assert be_cg <= 0.0 calc1 = copy.deepcopy(calc_1) calc2 = copy.deepcopy(calc_2) # 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 allocation base: AGI minus itemized deductions # Note: alloc_base is not an approximation of taxable income and # must not be replaced by the c04800 taxable income variable; it # is the sum of the three components being adjusted below, which # is what makes the three allocation shares sum to one. See the # response function docstring for details. agi = calc.array('c00100') ided = np.where(calc.array('c04470') < calc.array('standard'), 0., calc.array('c04470')) alloc_base = agi - ided # apply response only where the allocation arithmetic is valid # Note: this guards against a zero denominator and against the # negative shares implied by a negative alloc_base. It is not an # economic screen and must not be tightened to require positive # c04800: a filing unit with zero taxable income can still owe # payroll tax and therefore can still have an income effect. pos = np.array(alloc_base > 0., dtype=bool) # allocate change in taxable income into three parts # Note: because oinc is agi minus winc, the three parts always # satisfy delta_winc + delta_oinc - delta_ided == taxinc_change # for the pos filing units, so there is nothing to check here. # pylint: disable=unsupported-assignment-operation winc = calc.array('e00200') oinc = agi - winc share = np.zeros_like(agi) share[pos] = taxinc_change[pos] / alloc_base[pos] delta_winc = share * winc delta_oinc = share * oinc delta_ided = share * 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 _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. Both calc__1 and calc__2 must already have had their calc_all method called, which allows the mtr method to skip one of the two calc_all calls it would otherwise make for each Calculator object. """ assert tax_type in ('combined', 'iitax') _, iitax1, combined1 = calc__1.mtr(mtr_of, calc_all_already_called=True, wrt_full_compensation=True) _, iitax2, combined2 = calc__2.mtr(mtr_of, calc_all_already_called=True, wrt_full_compensation=True) if tax_type == 'combined': return (combined1, combined2) return (iitax1, iitax2) def _shifted_wage(calc, wage, pencon, ptax_er_1, esf): """ Returns array of post-shift wages, which are the solution of the fixed-point equation described in the response function docstring, where wage is the array of pre-shift reform wages, pencon is the array of employer pension contributions, ptax_er_1 is the array of baseline employer payroll tax liability on wages, esf is the earnings shift factor, and calc is the reform-policy Calculator object. """ ss_rate = calc.policy_param('FICA_ss_trt_employer') mc_rate = calc.policy_param('FICA_mc_trt_employer') cap = calc.policy_param('SS_Earnings_c') thd = calc.policy_param('SS_Earnings_thd') def residual(wag): """ Amount by which wag falls short of solving the fixed-point equation; it is increasing in wag with a slope of at least one, because employer payroll tax rates are nonnegative. """ return wag - wage + esf * ( employer_ptax_on_wages(ss_rate, mc_rate, cap, thd, wag + pencon) - ptax_er_1 ) # bracket the solution: because the residual has a slope of at # least one and is zero at the solution, the solution is no # farther from the pre-shift wage than the residual there offset = residual(wage) low = np.minimum(wage, wage - offset) high = np.maximum(wage, wage - offset) wage_shifted = bisect(residual, low, high) # floor the shifted wage at zero, which binds only for an earner # with too little wage income to absorb the shift; see the # response function docstring return np.maximum(0., wage_shifted) # End nested functions used only in this response function # Begin main logic of response function assert calc1.array_len == calc2.array_len assert calc1.current_year == calc2.current_year # Determine whether the reform generates an earnings shift # Note: the shift depends on the wage-based part of the employer # payroll tax alone, so it can be skipped whenever the reform leaves # every parameter of that tax unchanged, which is an exact test that # requires no comparison of calculated tax amounts. has_earnings_shift = be_esf > 0. and any( calc1.policy_param(pname) != calc2.policy_param(pname) for pname in ESF_PARAMS ) # Note: calc1 must be calculated on every path, because the baseline # employer payroll tax read by the earnings shift below, the df1 # variables extracted at the end of this function, and the marginal # tax rates are all calculated variables. When the earnings shift is # active, calc2 is deliberately left uncalculated until after the # shift, because the shift reads only calc2 input arrays (e00200p, # e00200s, pencon_p, and pencon_s) and calc2 policy parameters; the # calc_all below supplies calc2 its first calculation on every path. calc1.calc_all() if has_earnings_shift: # Calculate earnings shift caused by the reform-induced change in # employer payroll tax liability, which holds each earner's gross # compensation fixed by shifting the be_esf fraction of that # change onto wages. The shift is calculated separately for the # taxpayer and the spouse, and only for earners with wages; see # the response function docstring for the fixed-point equation # solved by the nested _shifted_wage function and for why the # wageless are excluded. shift = {} for who in ('p', 's'): ptax_er_1 = calc1.array(f'ptax_er_{who}') pencon_2 = calc2.array(f'pencon_{who}') wage_2 = calc2.array(f'e00200{who}') wage_shifted = _shifted_wage(calc2, wage_2, pencon_2, ptax_er_1, be_esf) shift[who] = np.where(wage_2 > 0., wage_shifted - wage_2, 0.) calc2.incarray('e00200p', shift['p']) calc2.incarray('e00200s', shift['s']) calc2.incarray('e00200', shift['p'] + shift['s']) calc2.calc_all() # Calculate sum of substitution and income effects zero_sub_and_inc = be_sub == 0.0 and be_inc == 0.0 # Note: the wage marginal tax rates are used only by the substitution # effect and by the dump output, so they are not computed when be_sub # is zero unless they are needed for the dump output if be_sub == 0.0 and (zero_sub_and_inc or not dump): wage_mtr1 = np.zeros(calc1.array_len) wage_mtr2 = np.zeros(calc2.array_len) else: # 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') if zero_sub_and_inc: si_chg = None # is not used when zero_sub_and_inc is True else: # 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 pch = ((1. - wage_mtr2) / (1. - wage_mtr1)) - 1. # Note: c04800 is filing unit's taxable income # Scaling by taxable income (rather than by earnings, as the # labor_response and quantity_response functions do) is by # design; see the module docstring. 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: recs_vinfo = Records(data=None) # contains records VARINFO only dvars = sorted(recs_vinfo.USABLE_READ_VARS | recs_vinfo.CALCULATED_VARS) df1 = calc1.dataframe(dvars) df1.drop(['mtr_inctax', '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 # Note: calc2 is already a private deepcopy of the calc_2 argument, # so it can be modified without making another deepcopy of it calc2_behv = calc2 del calc2 if not zero_sub_and_inc: calc2_behv = _update_ordinary_income(si_chg, calc2_behv) calc2_behv.incarray('p23250', ltcg_chg) # 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', '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. This is a helper function for the quantity_response function; it is not part of the public API of this module (it is not in __all__) and it is not used by the response function. A val1 element equal to zero implies an undefined proportional change, so this function returns a zero response for such elements rather than generating a divide-by-zero warning. 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 )