diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 205ae88..a9af4f6 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -1,4 +1,4 @@ -name: Build and test [Python 3.7, 3.8, 3.9] +name: Build and test [Python 3.9, 3.10, 3.11] on: [push, pull_request] @@ -7,11 +7,11 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8, 3.9] + python-version: [3.9, "3.10", "3.11"] steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@master with: persist-credentials: false @@ -35,8 +35,10 @@ jobs: run: | pytest -m 'not full_run and not regression' --cov=./ --cov-report=xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v2 + if: contains(github.repository, 'PSLmodels/InverseOptimalTax') + uses: codecov/codecov-action@v4 with: + token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.xml flags: unittests name: codecov-umbrella diff --git a/.github/workflows/check_black.yml b/.github/workflows/check_black.yml index 9b013e0..4d8a326 100644 --- a/.github/workflows/check_black.yml +++ b/.github/workflows/check_black.yml @@ -6,7 +6,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@master - uses: psf/black@stable with: options: "--line-length 79 --check --verbose" diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml index 8efc711..7929be5 100644 --- a/.github/workflows/deploy_docs.yml +++ b/.github/workflows/deploy_docs.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly. + uses: actions/checkout@master with: persist-credentials: false @@ -18,14 +18,12 @@ jobs: with: activate-environment: iot-dev environment-file: environment.yml - python-version: 3.8 + python-version: 3.11 auto-activate-base: false - name: Build # Build Jupyter Book shell: bash -l {0} run: | - pip install jupyter-book - pip install sphinxcontrib-bibtex==2.2.0 pip install -e . cd docs jb build ./book diff --git a/.github/workflows/docs_check.yml b/.github/workflows/docs_check.yml index f86b9e1..8d3366f 100644 --- a/.github/workflows/docs_check.yml +++ b/.github/workflows/docs_check.yml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly. + uses: actions/checkout@master with: persist-credentials: false @@ -15,14 +15,12 @@ jobs: with: activate-environment: iot-dev environment-file: environment.yml - python-version: 3.8 + python-version: 3.11 auto-activate-base: false - name: Build # Build Jupyter Book shell: bash -l {0} run: | - pip install jupyter-book - pip install sphinxcontrib-bibtex==2.2.0 pip install -e . cd docs jb build ./book diff --git a/PSL_catalog.json b/PSL_catalog.json new file mode 100644 index 0000000..11fb9b5 --- /dev/null +++ b/PSL_catalog.json @@ -0,0 +1,29 @@ +{ + "name": "Inverse Optimal Tax", + "img": "", + "banner_title": "Inverse Optimal Tax", + "banner_subtitle": "A model of inverse optimal taxation.", + "detailed_description": "A model of inverse optimal taxation.", + "policy_area": "Optimal Taxation, Inverse Optimal Taxation, Individual Income Tax", + "geography": "United States", + "language": "Python", + "maintainers": [ + { + "name": "Jason DeBacker", + "image": "https://avatars.githubusercontent.com/u/10715924?v=4", + "link": "https://github.com/jdebacker/" + }, + { + "name": "John P. Ryan", + "image": "https://avatars.githubusercontent.com/u/83313096?v=4", + "link": "https://github.com/john-p-ryan" + } + ], + "links": { + "code_repository": "https://github.com/PSLmodels/InverseOptimalTax", + "user_documentation": "https://pslmodels.github.io/InverseOptimalTax/content/intro/intro.html", + "contributor_documentation": "https://pslmodels.github.io/InverseOptimalTax/content/intro/contributing.html", + "webapp": "", + "recent_changes": "" + } + } diff --git a/environment.yml b/environment.yml index 02f8c50..58600bd 100644 --- a/environment.yml +++ b/environment.yml @@ -18,4 +18,7 @@ dependencies: - jupyter-book>=0.9.1 - black - plotly-express - - kaleido \ No newline at end of file + - kaleido + - typing_extensions + - jupyter-book + - sphinxcontrib-bibtex \ No newline at end of file diff --git a/examples/NTA_presentation_figures.ipynb b/examples/NTA_presentation_figures.ipynb new file mode 100644 index 0000000..02c9c30 --- /dev/null +++ b/examples/NTA_presentation_figures.ipynb @@ -0,0 +1,673 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# NTA 2023 Presentation Materials\n", + "This notebook contains the code used to generate the figures for the NTA 2023 presentation of DeBacker and Ryan (2023)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# imports\n", + "import iot\n", + "from iot import iot_user\n", + "import numpy as np\n", + "import pandas as pd\n", + "import json\n", + "import plotly.express as px\n", + "import os\n", + "from plotly.colors import n_colors\n", + "from scipy.interpolate import UnivariateSpline\n", + "import plotly.graph_objects as go\n", + "\n", + "# set plotly template\n", + "template = \"plotly_white\"\n", + "COLOR_SEQUENCE = [\"red\", \"blue\"] # Republican, Democrat\n", + "dash_sequence = ['dash', 'dot', 'dashdot', \"solid\"] # 2012, 2016, 2020\n", + "\n", + "# Create path for plots to be saved to\n", + "CUR_DIR = os.getcwd()\n", + "path = os.path.join(CUR_DIR, \"plots\")\n", + "if not os.path.exists(path):\n", + " os.makedirs(path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Read in candidate platform JSON files\n", + "obama2015_path = \"https://raw.githubusercontent.com/jdebacker/examples/pres_proposals/psl_examples/taxcalc/Obama2015.json\"\n", + "romney2012_path = \"https://raw.githubusercontent.com/jdebacker/examples/pres_proposals/psl_examples/taxcalc/Romney2012.json\"\n", + "clinton2016_path = \"https://raw.githubusercontent.com/jdebacker/examples/pres_proposals/psl_examples/taxcalc/Clinton2016.json\"\n", + "trump2016_path = \"https://raw.githubusercontent.com/PSLmodels/examples/main/psl_examples/taxcalc/Trump2016.json\"\n", + "biden2020_path = \"https://raw.githubusercontent.com/PSLmodels/examples/main/psl_examples/taxcalc/Biden2020.json\"\n", + "trump2020_path = \"https://raw.githubusercontent.com/PSLmodels/examples/main/psl_examples/taxcalc/TCJA.json\"\n", + "\n", + "pre_2020_baseline = \"https://raw.githubusercontent.com/PSLmodels/examples/main/psl_examples/taxcalc/2017_law.json\"\n", + "baseline_2020 = \"https://raw.githubusercontent.com/PSLmodels/examples/main/psl_examples/taxcalc/TCJA.json\"\n", + "\n", + "\n", + "candidate_dict = {\n", + " \"Obama 2015\": {\"policy_path\": obama2015_path, \"baseline_path\": [pre_2020_baseline], \"start_year\": 2016},\n", + " \"Romney 2012\": {\"policy_path\": romney2012_path, \"baseline_path\": [pre_2020_baseline], \"start_year\": 2014}, #wanted to do 13, but taxcalc with CPS only goes to 13\n", + " \"Clinton 2016\": {\"policy_path\": clinton2016_path, \"baseline_path\": [pre_2020_baseline], \"start_year\": 2017},\n", + " \"Trump 2016\": {\"policy_path\": trump2016_path, \"baseline_path\": [pre_2020_baseline], \"start_year\": 2017},\n", + " \"Biden 2020\": {\"policy_path\": biden2020_path, \"baseline_path\": [pre_2020_baseline, baseline_2020], \"start_year\": 2021},\n", + " \"Trump 2020\": {\"policy_path\": trump2020_path, \"baseline_path\": [pre_2020_baseline], \"start_year\": 2021}}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create IOT objects for each candidate platform\n", + "policies = []\n", + "baseline_policies = []\n", + "labels = list(candidate_dict.keys())\n", + "# get years from start_year in candidate_dict\n", + "years = [v[\"start_year\"] for v in candidate_dict.values()]\n", + "for k, v in candidate_dict.items():\n", + " # with open(v[\"policy_path\"], \"r\") as file:\n", + " # json1 = file.read()\n", + " json1 = v[\"policy_path\"]#json.load(open(v[\"policy_path\"]))\n", + " print(json1)\n", + " policies.append(json1)\n", + " if v[\"baseline_path\"] is None:\n", + " json2 = {}\n", + " else:\n", + " for ii, vv in enumerate(v[\"baseline_path\"]):\n", + " list_json = []\n", + " # with open(vv, \"r\") as file:\n", + " # json2 = file.read()\n", + " # list_json.append(json2)\n", + " json2 = v[\"baseline_path\"]#open(v[\"baseline_path\"])\n", + " print(json2)\n", + " baseline_policies.append(list_json)\n", + "\n", + "iot_all = iot_user.iot_comparison(\n", + " policies=policies,\n", + " baseline_policies=baseline_policies,\n", + " labels=labels,\n", + " years=years,\n", + " data=\"CPS\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Plots of f(z) for each year/candidate\n", + "fplot = iot_all.plot(var=\"f\")\n", + "fplot.update_layout(\n", + " template=template,\n", + ")\n", + "fplot.write_image(\n", + " os.path.join(path, \"income_dist.png\")\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Plots of theta(z) for each year/candidate\n", + "theta_plot = iot_all.plot(var=\"theta_z\")\n", + "theta_plot.update_layout(\n", + " template=template,\n", + ")\n", + "theta_plot.write_image(\n", + " os.path.join(path, \"theta.png\"),\n", + " scale=4\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Plots of MTRs for each year/candidate\n", + "mtr_plot = iot_all.plot(var=\"mtr\")\n", + "mtr_plot.update_layout(\n", + " template=template,\n", + ")\n", + "mtr_plot.update_traces(\n", + " line=dict(dash=\"dot\", color=\"blue\"),\n", + " selector=dict(name=\"Obama 2015\")\n", + ")\n", + "mtr_plot.update_traces(\n", + " line=dict(dash=\"dot\", color=\"red\"),\n", + " selector=dict(name=\"Romney 2012\")\n", + ")\n", + "mtr_plot.update_traces(\n", + " line=dict(dash=\"dash\", color=\"blue\"),\n", + " selector=dict(name=\"Clinton 2016\")\n", + ")\n", + "mtr_plot.update_traces(\n", + " line=dict(dash=\"dash\", color=\"red\"),\n", + " selector=dict(name=\"Trump 2016\")\n", + ")\n", + "mtr_plot.update_traces(\n", + " line=dict(dash=\"solid\", color=\"blue\"),\n", + " selector=dict(name=\"Biden 2020\")\n", + ")\n", + "mtr_plot.update_traces(\n", + " line=dict(dash=\"solid\", color=\"red\"),\n", + " selector=dict(name=\"Trump 2020\")\n", + ")\n", + "mtr_plot.write_image(\n", + " os.path.join(path, \"mtrs_all.png\"),\n", + " scale=4\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# plots of g(z) for each year/candidate\n", + "gz_plot = iot_all.plot(var=\"g_z\")\n", + "gz_plot.update_layout(\n", + " template=template,\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"dot\", color=\"blue\"),\n", + " selector=dict(name=\"Obama 2015\")\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"dot\", color=\"red\"),\n", + " selector=dict(name=\"Romney 2012\")\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"dash\", color=\"blue\"),\n", + " selector=dict(name=\"Clinton 2016\")\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"dash\", color=\"red\"),\n", + " selector=dict(name=\"Trump 2016\")\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"solid\", color=\"blue\"),\n", + " selector=dict(name=\"Biden 2020\")\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"solid\", color=\"red\"),\n", + " selector=dict(name=\"Trump 2020\")\n", + ")\n", + "# gz_plot.update_xaxes(range=[0, 850000])\n", + "\n", + "gz_plot.write_image(\n", + " os.path.join(path, \"gz_all.png\"),\n", + " scale=4\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# plots of g(z) for each year/candidate, numerical approach\n", + "gz_plot = iot_all.plot(var=\"g_z_numerical\")\n", + "gz_plot.update_layout(\n", + " template=template,\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"dot\", color=\"blue\"),\n", + " selector=dict(name=\"Obama 2015\")\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"dot\", color=\"red\"),\n", + " selector=dict(name=\"Romney 2012\")\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"dash\", color=\"blue\"),\n", + " selector=dict(name=\"Clinton 2016\")\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"dash\", color=\"red\"),\n", + " selector=dict(name=\"Trump 2016\")\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"solid\", color=\"blue\"),\n", + " selector=dict(name=\"Biden 2020\")\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"solid\", color=\"red\"),\n", + " selector=dict(name=\"Trump 2020\")\n", + ")\n", + "# gz_plot.update_xaxes(range=[0, 850000])\n", + "\n", + "gz_plot.write_image(\n", + " os.path.join(path, \"gz_numerical_all.png\"),\n", + " scale=4\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# plots of g(z) for each year/candidate, numerical approach,\n", + "# HIGHlighting the Democrats\n", + "gz_plot = iot_all.plot(var=\"g_z_numerical\")\n", + "gz_plot.update_layout(\n", + " template=template,\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"dot\", color=\"blue\"),\n", + " selector=dict(name=\"Obama 2015\")\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"dot\", color=\"gray\"),\n", + " selector=dict(name=\"Romney 2012\")\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"dash\", color=\"blue\"),\n", + " selector=dict(name=\"Clinton 2016\")\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"dash\", color=\"gray\"),\n", + " selector=dict(name=\"Trump 2016\")\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"solid\", color=\"blue\"),\n", + " selector=dict(name=\"Biden 2020\")\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"solid\", color=\"gray\"),\n", + " selector=dict(name=\"Trump 2020\")\n", + ")\n", + "# gz_plot.update_xaxes(range=[0, 850000])\n", + "\n", + "gz_plot.write_image(\n", + " os.path.join(path, \"gz_numerical_democrats.png\"),\n", + " scale=4\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# plots of g(z) for each year/candidate, numerical approach\n", + "# HIGHlighting the Republicans\n", + "gz_plot = iot_all.plot(var=\"g_z_numerical\")\n", + "gz_plot.update_layout(\n", + " template=template,\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"dot\", color=\"gray\"),\n", + " selector=dict(name=\"Obama 2015\")\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"dot\", color=\"red\"),\n", + " selector=dict(name=\"Romney 2012\")\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"dash\", color=\"gray\"),\n", + " selector=dict(name=\"Clinton 2016\")\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"dash\", color=\"red\"),\n", + " selector=dict(name=\"Trump 2016\")\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"solid\", color=\"gray\"),\n", + " selector=dict(name=\"Biden 2020\")\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"solid\", color=\"red\"),\n", + " selector=dict(name=\"Trump 2020\")\n", + ")\n", + "# gz_plot.update_xaxes(range=[0, 850000])\n", + "\n", + "gz_plot.write_image(\n", + " os.path.join(path, \"gz_numerical_republicans.png\"),\n", + " scale=4\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Show how MTRs vs tax base elasticity affecting g(z) for 2 candidates (separate plots,\n", + "# which will be put in a 2 panel figure)\n", + "fig = iot_all.JJZFig4(policy='Biden 2020')\n", + "fig.update_layout(\n", + " template=template,\n", + ")\n", + "# fig.update_xaxes(range=[0, 850000])\n", + "\n", + "fig.write_image(\n", + " os.path.join(path, \"composition_Biden2020_gz.png\"),\n", + " scale=4\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Loop over values of epsilon and plot Biden under these alternative values\"\n", + "eps_values = [0.2, 0.3, 0.4, 0.5, 0.6]\n", + "biden_eps_dict = {}\n", + "biden_eps_dict_numerical = {}\n", + "label_list = []\n", + "for i, v in enumerate(eps_values):\n", + " label = r\"$\\varepsilon$ = \" + str(v)\n", + " iot_b = iot_user.iot_comparison(\n", + " policies=[candidate_dict[\"Biden 2020\"][\"policy_path\"]],\n", + " baseline_policies=[candidate_dict[\"Biden 2020\"][\"baseline_path\"]],\n", + " labels=[label],\n", + " years=[candidate_dict[\"Biden 2020\"][\"start_year\"]],\n", + " eti=v,\n", + " data=\"CPS\"\n", + " )\n", + " label_list.append(label)\n", + " biden_eps_dict[v] = iot_b.iot[0].df().g_z\n", + " biden_eps_dict_numerical[v] = iot_b.iot[0].df().g_z_numerical" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# plot each g_z\n", + "label_dict = {}\n", + "redVSblue = n_colors('rgb(0, 0, 255)', 'rgb(255, 0, 0)', len(label_list), colortype = 'rgb')\n", + "for i, v in enumerate(label_list):\n", + " label_dict[\"wide_variable_\" + str(i)] = str(eps_values[i])#v\n", + "fig = px.line(\n", + " x=iot_b.iot[0].df().z,\n", + " y=[\n", + " biden_eps_dict[0.2],\n", + " biden_eps_dict[0.3],\n", + " biden_eps_dict[0.4],\n", + " biden_eps_dict[0.5],\n", + " biden_eps_dict[0.6]\n", + " ],\n", + " color_discrete_sequence=redVSblue,\n", + " labels=label_dict)\n", + "fig.for_each_trace(lambda t: t.update(name = label_dict[t.name], legendgroup = label_dict[t.name],\n", + " hovertemplate = t.hovertemplate.replace(t.name, label_dict[t.name])))\n", + "fig.update_layout(\n", + " template=template,\n", + " xaxis_title=\"Wages and Salaries\",\n", + " yaxis_title=r\"$g_z$\",\n", + " legend=dict(\n", + " title=\"ETI value:\",\n", + " ),\n", + ")\n", + "\n", + "# fig.update_xaxes(range=[0, 850000])\n", + "fig.write_image(\n", + " os.path.join(path, \"vary_ETI_Biden2020_gz.png\"),\n", + " scale=4\n", + " )\n", + "\n", + "# plot each g_z_numerical\n", + "label_dict = {}\n", + "redVSblue = n_colors('rgb(0, 0, 255)', 'rgb(255, 0, 0)', len(label_list), colortype = 'rgb')\n", + "for i, v in enumerate(label_list):\n", + " label_dict[\"wide_variable_\" + str(i)] = str(eps_values[i])#v\n", + "fig = px.line(\n", + " x=iot_b.iot[0].df().z[50:],\n", + " y=[\n", + " biden_eps_dict_numerical[0.2][50:],\n", + " biden_eps_dict_numerical[0.3][50:],\n", + " biden_eps_dict_numerical[0.4][50:],\n", + " biden_eps_dict_numerical[0.5][50:],\n", + " biden_eps_dict_numerical[0.6][50:]\n", + " ],\n", + " color_discrete_sequence=redVSblue,\n", + " labels=label_dict)\n", + "fig.for_each_trace(lambda t: t.update(name = label_dict[t.name], legendgroup = label_dict[t.name],\n", + " hovertemplate = t.hovertemplate.replace(t.name, label_dict[t.name])))\n", + "fig.update_layout(\n", + " template=template,\n", + " xaxis_title=\"Wages and Salaries\",\n", + " yaxis_title=r\"$g_z$\",\n", + " legend=dict(\n", + " title=\"ETI value:\",\n", + " ),\n", + ")\n", + "\n", + "# fig.update_xaxes(range=[0, 850000])\n", + "fig.write_image(\n", + " os.path.join(path, \"vary_ETI_Biden2020_gz_numerical.png\"),\n", + " scale=4\n", + " )\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Redo above with varying epsilon(z) according to some empirical studies\n", + "# Required modification of model\n", + "eti_dict = {\n", + " \"eti_values\": [0.18, 0.106, 0.567, 1.83, 1.9],\n", + " \"knot_points\": [30000, 75000, 250000, 2000000, 10000000]\n", + "}\n", + "iot_all_vary = iot_user.iot_comparison(\n", + " policies=policies,\n", + " baseline_policies=baseline_policies,\n", + " labels=labels,\n", + " years=years,\n", + " eti=eti_dict,\n", + " data=\"CPS\"\n", + ")\n", + "# plots of g(z) for each year/candidate, numerical approach\n", + "gz_plot = iot_all_vary.plot(var=\"g_z_numerical\")\n", + "gz_plot.update_layout(\n", + " template=template,\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"dot\", color=\"blue\"),\n", + " selector=dict(name=\"Obama 2015\")\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"dot\", color=\"red\"),\n", + " selector=dict(name=\"Romney 2012\")\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"dash\", color=\"blue\"),\n", + " selector=dict(name=\"Clinton 2016\")\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"dash\", color=\"red\"),\n", + " selector=dict(name=\"Trump 2016\")\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"solid\", color=\"blue\"),\n", + " selector=dict(name=\"Biden 2020\")\n", + ")\n", + "gz_plot.update_traces(\n", + " line=dict(dash=\"solid\", color=\"red\"),\n", + " selector=dict(name=\"Trump 2020\")\n", + ")\n", + "# gz_plot.update_xaxes(range=[0, 850000])\n", + "\n", + "gz_plot.write_image(\n", + " os.path.join(path, \"gz_numerical_all_vary_eti.png\"),\n", + " scale=4\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Plot how ETI varies with income\n", + "z_line = np.linspace(1, 1000000, 100000)\n", + "eti_dict = {\n", + " \"eti_values\": [0.18, 0.106, 0.567, 1.83, 1.9],\n", + " \"knot_points\": [30000, 75000, 250000, 2000000, 10000000]\n", + "}\n", + "eti_spl = UnivariateSpline(\n", + " eti_dict[\"knot_points\"], eti_dict[\"eti_values\"], k=3, s=0\n", + ")\n", + "eti = eti_spl(z_line)\n", + "fig = px.line(x=z_line, y=eti, labels={\"x\": \"Wages and Salaries\", \"y\": r\"$\\varepsilon$\"})\n", + "# add special markers without hoverinfo\n", + "fig.add_traces(\n", + " go.Scatter(\n", + " x=eti_dict[\"knot_points\"][:-2], y=eti_dict[\"eti_values\"][:-2], mode=\"markers\", name=\"Gruber and Saez (2022)\", hoverinfo=\"skip\"\n", + " )\n", + ")\n", + "# put legend at bottom\n", + "fig.update_layout(legend=dict(yanchor=\"bottom\", y=0.7, xanchor=\"left\", x=0.1))\n", + "fig.update_layout(\n", + " template=template,\n", + ")\n", + "fig.write_image(\n", + " os.path.join(path, \"ETI_spline.png\"),\n", + " scale=4,\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Do experiment where hold constant g(z) - pick a candidate as baseline - then plot epsilon(z)\n", + "# that would recover those g(z) given the tax rates of each candidate\n", + "# one plot with epsilon(z) for each candidate\n", + "# Will pick Trump and Clinton (2016) for example\n", + "\n", + "# First, plot just their g(z)\n", + "fig = px.line(\n", + " x=iot_all.iot[2].df().z[10:],\n", + " y=[iot_all.iot[2].df().g_z_numerical[10:], iot_all.iot[3].df().g_z_numerical[10:]],\n", + " labels={\"x\": \"Wages and Salaries\", \"y\": r\"$g_z$\"},\n", + " )\n", + "fig.update_layout(\n", + " template=template,\n", + " legend=dict(\n", + " title=\"Candidate:\",\n", + " ),\n", + ")\n", + "candidate_name = [\"Clinton 2016\", \"Trump 2016\"]\n", + "label_dict = {}\n", + "for i, v in enumerate(candidate_name):\n", + " label_dict[\"wide_variable_\" + str(i)] = str(candidate_name[i])\n", + "fig.for_each_trace(lambda t: t.update(name = label_dict[t.name], legendgroup = label_dict[t.name],\n", + " hovertemplate = t.hovertemplate.replace(t.name, label_dict[t.name])))\n", + "\n", + "fig.write_image(\n", + " os.path.join(path, \"trump_clinton_g_z_numerical.png\"),\n", + " scale=4\n", + " )\n", + "# Now find the epsilon(z) that would give Trump's policies the same g(z) as Clinton\n", + "eti_beliefs_lw, eti_beliefs_jjz = iot.inverse_optimal_tax.find_eti(iot_all.iot[2], iot_all.iot[3], g_z_type=\"g_z\")\n", + "idx = np.where(np.absolute(eti_beliefs_jjz[1:]) < 10)[0]\n", + "fig2 = px.line(\n", + " x=iot_all.iot[2].df().z[idx],\n", + " y=eti_beliefs_jjz[idx],\n", + " labels={\"x\": \"Wages and Salaries\", \"y\": r\"$\\text{Implied } \\varepsilon$\"},\n", + " )\n", + "fig2.update_layout(\n", + " template=template,\n", + ")\n", + "fig2.write_image(\n", + " os.path.join(path, \"trump_eti.png\"),\n", + " scale=4\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "eti_dict = {\n", + " \"eti_values\": [0.18, 0.106, 0.567, 1.83, 1.9],\n", + " \"knot_points\": [30000, 75000, 250000, 2000000, 10000000]\n", + "}\n", + "# ETI values from Gruber and Saez (2002) (Table 3) and Saez (2004) (Tables 2, 4, 5)\n", + "# Compute MTR schedule under current law\n", + "iot_2023 = iot_user.iot_comparison(\n", + " policies=[{}],\n", + " baseline_policies=[None],\n", + " labels=[\"2023 Law\"],\n", + " years=[2023],\n", + " data=\"CPS\",\n", + " eti=eti_dict\n", + " )\n", + "fig = px.line(\n", + " x=iot_2023.iot[0].df().z,\n", + " y=iot_2023.iot[0].df().mtr\n", + " )\n", + "fig.update_layout(\n", + " template=template,\n", + " xaxis_title=\"Wages and Salaries\",\n", + " yaxis_title=r\"$T'(z)$\",\n", + ")\n", + "fig.write_image(\n", + " os.path.join(path, \"MTR_2023.png\"),\n", + " scale=4\n", + " )" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "iot-dev", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/iot/inverse_optimal_tax.py b/iot/inverse_optimal_tax.py index 5898de5..7288897 100644 --- a/iot/inverse_optimal_tax.py +++ b/iot/inverse_optimal_tax.py @@ -233,19 +233,22 @@ def compute_income_dist( ) f = f_function.pdf(z_line) F = np.cumsum(f) - f_prime = np.gradient(f, edge_order=2) + f_prime = np.gradient(f, edge_order=2) elif dist_type == "Pln": + def pln_pdf(y, mu, sigma, alpha): x1 = alpha * sigma - (np.log(y) - mu) / sigma phi = st.norm.pdf((np.log(y) - mu) / sigma) - R = (1 - st.norm.cdf(x1)) / (st.norm.pdf(x1) + 1e-15) + R = (1 - st.norm.cdf(x1)) / (st.norm.pdf(x1) + 1e-15) # 1e-15 to avoid division by zero pdf = alpha / y * phi * R return pdf def neg_weighted_log_likelihood(params, data, weights): mu, sigma, alpha = params - likelihood = np.sum(weights * np.log(pln_pdf(data, mu, sigma, alpha) + 1e-15)) + likelihood = np.sum( + weights * np.log(pln_pdf(data, mu, sigma, alpha) + 1e-15) + ) # 1e-15 to avoid log(0) return -likelihood @@ -261,9 +264,8 @@ def fit_pln(data, weights, initial_guess): return result.x mu_initial = ( - (np.log(data[income_measure]) * data[weight_var]).sum() - / data[weight_var].sum() - ) + np.log(data[income_measure]) * data[weight_var] + ).sum() / data[weight_var].sum() sigmasq = ( ( ((np.log(data[income_measure]) - mu_initial) ** 2) @@ -273,23 +275,32 @@ def fit_pln(data, weights, initial_guess): ).sum() sigma_initial = np.sqrt(sigmasq) # Initial guess for m, sigma, alpha - initial_guess = np.array([mu_initial, sigma_initial, 1.5]) - mu, sigma, alpha = fit_pln(data[income_measure], data[weight_var], initial_guess) + initial_guess = np.array([mu_initial, sigma_initial, 1.5]) + mu, sigma, alpha = fit_pln( + data[income_measure], data[weight_var], initial_guess + ) def pln_cdf(y, mu, sigma, alpha): x1 = alpha * sigma - (np.log(y) - mu) / sigma R = (1 - st.norm.cdf(x1)) / (st.norm.pdf(x1) + 1e-12) CDF = ( - st.norm.cdf((np.log(y) - mu) / sigma) - - st.norm.pdf((np.log(y) - mu) / sigma) * R + st.norm.cdf((np.log(y) - mu) / sigma) + - st.norm.pdf((np.log(y) - mu) / sigma) * R ) return CDF def pln_dpdf(y, mu, sigma, alpha): x = (np.log(y) - mu) / sigma - R = (1 - st.norm.cdf(alpha * sigma - x)) / (st.norm.pdf(alpha * sigma - x) + 1e-15) + R = (1 - st.norm.cdf(alpha * sigma - x)) / ( + st.norm.pdf(alpha * sigma - x) + 1e-15 + ) left = (1 + x / sigma) * pln_pdf(y, mu, sigma, alpha) - right = alpha * st.norm.pdf(x) * ((alpha * sigma - x) * R - 1) / (sigma * y) + right = ( + alpha + * st.norm.pdf(x) + * ((alpha * sigma - x) * R - 1) + / (sigma * y) + ) return -(left + right) / y f = pln_pdf(z_line, mu, sigma, alpha)