Inverse Optimal Tax Example#

This notebook provides an illustrative example for working with iot an inverse optimal tax model. This model infers a generalized social welfare function from a specification of tax policy. The underlying assumption is that those choosing the tax policy are acting optimally, i.e., they are trying to maximize their social welfare function. This assumption of optimizing a social welfare function, together with data and empirical estimates of behavioral parameters, can be used to reverse engineer what the underyling social welfare function would have to look like if indeed tax policy were trying to maximize it.

Step 1: Import the iot_comparison class object#

The iot_comparison class object is the high-level user interfaces for the iot model. It will allow the user to specify tax policies to evaluate and provide a number of parameters for the iot model, such as the underlying data to use, how marginal tax rates are to be calculated, and more.

Note thaat the iot_comparison class implicitly assumes one is working with the Tax-Calculator model. Therefore, the specification of tax policy should follow the format for a parametric reform in Tax-Calculator.

# imports
from iot.iot_user import iot_comparison
from IPython.core.display import display, HTML
from plotly.offline import init_notebook_mode, plot
init_notebook_mode(connected=True)
/tmp/ipykernel_2023/995651565.py:3: DeprecationWarning: Importing display from IPython.core.display is deprecated since IPython 7.14, please import from IPython.display
  from IPython.core.display import display, HTML

Step 2: Instantiate an iot_comparison class object#

One can specify multiple parameterizations of tax policy to faciliate comparisons. Below, we instantiate the iot_comparison class object by point to two differnt parameterizations of tax law: current law in 2017 (before the TCJA was enacted) and policies from President Biden’s 2020 campaign.

iot1 = iot_comparison(
    policies=[
        "https://raw.githubusercontent.com/PSLmodels/examples/main/psl_examples/taxcalc/2017_law.json",
        "https://raw.githubusercontent.com/PSLmodels/examples/main/psl_examples/taxcalc/Biden2020.json",
    ],
    labels=["2017 Law", "Biden 2020"],
    baseline_policies=[None, None],
    years=[2023, 2023]
)
---------------------------------------------------------------------------
ValidationError                           Traceback (most recent call last)
Cell In[2], line 1
----> 1 iot1 = iot_comparison(
      2     policies=[
      3         "https://raw.githubusercontent.com/PSLmodels/examples/main/psl_examples/taxcalc/2017_law.json",
      4         "https://raw.githubusercontent.com/PSLmodels/examples/main/psl_examples/taxcalc/Biden2020.json",
      5     ],
      6     labels=["2017 Law", "Biden 2020"],
      7     baseline_policies=[None, None],
      8     years=[2023, 2023]
      9 )

File ~/work/InverseOptimalTax/InverseOptimalTax/iot/iot_user.py:77, in iot_comparison.__init__(self, years, baseline_policies, policies, labels, data, compare_default, mtr_wrt, income_measure, weight_var, eti, bandwidth, lower_bound, upper_bound, dist_type, kde_bw, mtr_smoother, mtr_smooth_param)
     74 self.labels = labels
     75 for i, v in enumerate(policies):
     76     df.append(
---> 77         gen_microdata(
     78             year=years[i],
     79             data=data,
     80             baseline_policy=baseline_policies[i],
     81             reform=v,
     82             mtr_wrt=mtr_wrt,
     83             income_measure=income_measure,
     84             weight_var=weight_var,
     85         )
     86     )
     87 # create results for current law policy
     88 if compare_default:

File ~/work/InverseOptimalTax/InverseOptimalTax/iot/generate_data.py:78, in gen_microdata(year, data, baseline_policy, reform, mtr_wrt, income_measure, weight_var)
     75     reform = tc.Policy.read_json_reform(reform)
     77 # Now layer on the reform policy on top of the baseline
---> 78 pol1.implement_reform(reform, print_warnings=False, raise_errors=False)
     80 calc1 = tc.Calculator(policy=pol1, records=recs)
     81 calc1.advance_to_year(year)

File /usr/share/miniconda/envs/iot-dev/lib/python3.11/site-packages/taxcalc/policy.py:121, in Policy.implement_reform(self, reform, print_warnings, raise_errors)
    116 """
    117 Implement reform using Tax-Calculator syled reforms/adjustments. Users
    118 may also use the adjust method with ParamTools styled reforms.
    119 """
    120 # need to do conversion:
--> 121 return self._update(reform, print_warnings, raise_errors)

File /usr/share/miniconda/envs/iot-dev/lib/python3.11/site-packages/taxcalc/parameters.py:589, in Parameters._update(self, revision, print_warnings, raise_errors)
    587     else:
    588         msg = f"Parameter {param} does not exist."
--> 589     raise pt.ValidationError(
    590         {"errors": {"schema": msg}},
    591         None
    592     )
    593 if param.endswith("-indexed"):
    594     for year, yearval in val.items():

ValidationError: {
    "errors": {
        "schema": "Parameter ID_Charity_crt_all does not exist."
    }
}

The the cell above is executed, Tax-Calculator is run for all of the specified policies, as well as its current law baseline. With the results of those Tax-Calculator runs, the iot model then reverse enginers the social welfare functions that would be optimizes from each of those specifications of tax law.

Accessing the results of the model#

The object we created, iot1, has as attributes, all of the inputs to the inverse optimal tax calculation, as well as the outputs. One can see all of these together in the df attribute of the IOT class that is an attribute of out iot1 object created above. Note that because we have a list of policies, we can slice that list to reference results from a specific policy. The current law baseline will be the first object in this list, followed by the polcies we specified above in the order we specified them.

For instance, to reference a Pandas DataFrame with the input and outputs of the inverse optimal tax model of the 2020 Biden campaign proposals, we would do:

iot1.iot[-1].df()
z f f_prime mtr mtr_prime theta_z g_z g_z_numerical
0 1.00000 2.097206e-17 1.262466e-16 0.207234 0.000017 7.019751 1.458758 75009.576106
1 11.00009 5.825540e-12 2.348657e-12 0.207251 0.000017 5.434858 1.355290 8.103262
2 21.00018 8.927957e-11 1.703724e-11 0.207268 0.000017 5.007470 1.327460 3.247817
3 31.00027 4.044039e-10 4.892010e-11 0.207286 0.000017 4.750053 1.310736 2.332161
4 41.00036 1.124440e-09 9.777772e-11 0.207303 0.000017 4.565262 1.298756 1.971017
... ... ... ... ... ... ... ... ...
99995 999959.99964 1.112090e-08 -3.460490e-14 0.557889 0.000000 -2.111576 0.333863 0.333857
99996 999969.99973 1.112055e-08 -3.460355e-14 0.557889 0.000000 -2.111583 0.333861 0.333855
99997 999979.99982 1.112020e-08 -3.460220e-14 0.557889 0.000000 -2.111589 0.333859 0.333853
99998 999989.99991 1.111986e-08 -3.460085e-14 0.557889 0.000000 -2.111596 0.333857 0.333851
99999 1000000.00000 1.111951e-08 -3.459950e-14 0.557889 0.000000 -2.111603 0.333855 0.333861

100000 rows × 8 columns

Visualizing results#

It’s often easiest to interpret their results through some visualizations. the iot_comparison class makes this easy, with built in plotly plotting functions.

Let’s plot the generalized social welfare function weights implied by the three policies (2021 current law, 2017 law, and the 2020 Biden campaign proposals):

fig = iot1.plot()
plot(fig, filename = 'gz_figure.html')
display(HTML('gz_figure.html'))

The plot above is showing us the social welfare function weights, \(g_z\), for taxpayers across the income distribution. Higher \(g_z\) imply a higher weight in the social welfare function that is optimized with that particular tax policy. Roughly speaking, \(g_z\) tells us how much the social welfare function would increase if we gave a taxpayer one more dollar of after tax income.

All three of the policies compared suggest social welfare functions that place more weight on tax payers with lower incomes. The social welfare functions begin to diverge for tax payers with more than $200,000 in income. Amongst the three policies, 2017 law provides the lowest weights on very high income taxpayers. The TCJA, consistent with the regressive distributional estimates, reveals relatively higher weights for the highest income taxpayers. Somewhat surprisingly, the Biden 2020 campaign proposal have the highest weights on those high income taxpayers of the policies compared.

Plotting inputs to the calculation#

We can also use the iot_comparison class’ plotting function to help us understand why we see the implied social welfare weights we do.

For instance, an important input in determining the social welfare weights is the marginal tax rate schedule. Let’s plot this for the three policies compared above:

fig2 = iot1.plot(var="mtr")
plot(fig2, filename = 'mtr_figure.html')
display(HTML('mtr_figure.html'))

We see that each set of policies shows tax progressivity. Marginal tax rates increase with income. But beyond about $200,000 in income, these marginal tax rate functions look very different. 2017 law shows the slope of the marginal tax rates gradually leveling off. In contrast, the current law baseline and Biden’s 2020 campaign policies have marginal tax rates remaining relatively conant from about $200,000 to $350,000 in income and then an increasing slope after that.

We can plot the derivative of these marginal tax rate schedules to see this more clealy:

fig3 = iot1.plot(var="mtr_prime")
plot(fig3, filename = 'mtr_prime_figure.html')
display(HTML('mtr_prime_figure.html'))
fig3 = iot1.plot(var="f")
plot(fig3, filename = 'f_figure.html')
display(HTML('f_figure.html'))
fig3 = iot1.plot(var="theta_z")
plot(fig3, filename = 'theta_figure.html')
display(HTML('theta_figure.html'))
fig3 = iot1.plot(var="f_prime")
plot(fig3, filename = 'fprime_figure.html')
display(HTML('fprime_figure.html'))
iot1.iot[-1].f[:10]
array([0.00521702, 0.005446  , 0.00560577, 0.0057706 , 0.00591634,
       0.00607098, 0.00622675, 0.00637578, 0.00651968, 0.00667111])
iot1.iot[-1].f_prime[:10]
array([1.59720145e-07, 1.58598982e-07, 1.57331720e-07, 1.55844796e-07,
       1.54137135e-07, 1.52092321e-07, 1.49798597e-07, 1.47315763e-07,
       1.44522129e-07, 1.41516370e-07])