From d6d97284c7864ed6ce175c8254ed032dbad0a3df Mon Sep 17 00:00:00 2001 From: John Ryan Date: Fri, 6 Feb 2026 10:59:46 -0600 Subject: [PATCH 1/3] Add transversality option for inverse ETI --- iot/inverse_optimal_tax.py | 60 +++++++++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/iot/inverse_optimal_tax.py b/iot/inverse_optimal_tax.py index c262d05..09cd196 100644 --- a/iot/inverse_optimal_tax.py +++ b/iot/inverse_optimal_tax.py @@ -404,7 +404,7 @@ def sw_weights(self): return g_z, g_z_numerical -def find_eti(iot, g_z=None, eti_0=0.25): +def find_eti(iot, g_z=None, eti_0=0.25, boundary="z0"): """ This function solves for the ETI that would result in the policy represented via MTRs in IOT being consistent with the @@ -415,30 +415,56 @@ def find_eti(iot, g_z=None, eti_0=0.25): \varepsilon'(z)\left[\frac{zT'(z)}{1-T'(z)}\right] + \varepsilon(z)\left[\theta_z \frac{T'(z)}{1-T'(z)} +\frac{zT''(z)}{(1-T'(z))^2}\right]+ (1-g(z)) Args: - iot (IOT): instance of the I + iot (IOT): instance of the IOT class g_z (None or array_like): vector of social welfare weights - eti_0 (scalar): guess for ETI at z=0 + eti_0 (scalar): guess for ETI at z=0 (used when boundary="z0") + boundary (str): "z0" to use the initial condition at z=0 + (solves the ODE with eti_0), or "inf" to use the + transversality condition that + epsilon(z)*T'(z)/(1-T'(z))*z*f(z) -> 0 as z -> inf. Returns: eti_beliefs (array-like): vector of ETI beliefs over z """ - if g_z is None: g_z = iot.g_z - # we solve an ODE of the form f'(z) + P(z)f(z) = Q(z) - P_z = ( - 1 / iot.z - + iot.f_prime / iot.f - + iot.mtr_prime / (iot.mtr * (1 - iot.mtr)) - ) - # integrating factor for ODE: mu(z) * f'(z) + mu(z) * P(z) * f(z) = mu(z) * Q(z) - mu_z = np.exp(np.cumsum(P_z)) - Q_z = (g_z - 1) * (1 - iot.mtr) / (iot.mtr * iot.z) - # integrate Q(z) * mu(z), as we integrate both sides of the ODE - int_mu_Q = np.cumsum(mu_z * Q_z) - - eti_beliefs = (eti_0 + int_mu_Q) / mu_z + if boundary == "z0": + # Original ODE approach with boundary condition at z=0 + P_z = ( + 1 / iot.z + + iot.f_prime / iot.f + + iot.mtr_prime / (iot.mtr * (1 - iot.mtr)) + ) + mu_z = np.exp(np.cumsum(P_z)) + Q_z = (g_z - 1) * (1 - iot.mtr) / (iot.mtr * iot.z) + int_mu_Q = np.cumsum(mu_z * Q_z) + eti_beliefs = (eti_0 + int_mu_Q) / mu_z + + elif boundary == "inf": + # Transversality condition: eps(z)*T'/(1-T')*z*f -> 0 as z -> inf + # eps(z) = [(1-T'(z))/T'(z)] * [1/(z*f(z))] * int_z^inf (1 - g(zt)) f(zt) dzt + integrand = (1 - g_z) * iot.f + # Reverse cumulative integral: int_z^inf = int_0^inf - int_0^z + # Compute using reverse cumsum of trapezoid contributions + # Use cumulative_trapezoid from the right + dz = np.diff(iot.z) + # Trapezoidal contributions for each interval + trap_contributions = 0.5 * (integrand[:-1] + integrand[1:]) * dz + # Reverse cumulative sum to get integral from z to z_max + rev_cumsum = np.flip(np.cumsum(np.flip(trap_contributions))) + # Append 0 for the last point (integral from z_max to inf ~ 0) + tail_integral = np.append(rev_cumsum, 0.0) + + eti_beliefs = ( + ((1 - iot.mtr) / iot.mtr) + * (1 / (iot.z * iot.f)) + * tail_integral + ) + else: + raise ValueError( + f"boundary must be 'z0' or 'inf', got '{boundary}'" + ) return eti_beliefs From 743e9d2cfde3a63dd0994d5ac3b360ddb686dcb2 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Tue, 24 Mar 2026 15:48:15 -0500 Subject: [PATCH 2/3] update plots & boundary conditions for ETI --- examples/Simulate_all_policies.ipynb | 397 +++++++++++++++------------ 1 file changed, 228 insertions(+), 169 deletions(-) diff --git a/examples/Simulate_all_policies.ipynb b/examples/Simulate_all_policies.ipynb index 4de5255..af10c5e 100644 --- a/examples/Simulate_all_policies.ipynb +++ b/examples/Simulate_all_policies.ipynb @@ -10,7 +10,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -40,7 +40,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -101,14 +101,28 @@ " json2 = v[\"baseline_path\"]#open(v[\"baseline_path\"])\n", " print(json2)\n", " baseline_policies.append(list_json)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#income_measure = \"expanded_income\"\n", + "income_measure = \"e00200\" # default\n", "\n", "iot_all = iot_user.iot_comparison(\n", " policies=policies,\n", " baseline_policies=baseline_policies,\n", - " mtr_smoother=\"kreg\", # this is kreg or HSV\n", + " mtr_smoother=\"kreg\",\n", " labels=labels,\n", " years=years,\n", - " data=\"CPS\"\n", + " data=\"CPS\",\n", + " income_measure=income_measure,\n", + " dist_type=\"Pln\", # previously used default \"log_normal\"\n", + " #mtr_wrt=income_measure\n", ")" ] }, @@ -155,6 +169,45 @@ " )" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Plot distribution f(z) with histogram\n", + "z_max = 300_000\n", + "data = iot_all.iot[0].data_original\n", + "data_capped = data[data[income_measure] <= z_max]\n", + "\n", + "# Create weighted histogram\n", + "hist, bin_edges = np.histogram(\n", + " data_capped[income_measure], bins=15, weights=data_capped['s006'], density=True\n", + ")\n", + "bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2\n", + "\n", + "# Create figure\n", + "fig = go.Figure()\n", + "fig.add_trace(go.Bar(\n", + " x=bin_centers, y=hist, name='Data', opacity=0.6,\n", + " width=bin_edges[1] - bin_edges[0]\n", + "))\n", + "\n", + "# Add fitted distribution\n", + "iot_obj = iot_all.iot[0]\n", + "mask = iot_obj.z <= z_max\n", + "fig.add_trace(go.Scatter(\n", + " x=iot_obj.z[mask], y=iot_obj.f[mask], mode='lines',\n", + " name='Fitted f(z)', line=dict(color='red', width=2)\n", + "))\n", + "\n", + "fig.update_layout(\n", + " template=template, xaxis_title='Income', yaxis_title='Density f(z)',\n", + " xaxis=dict(range=[0, z_max])\n", + ")\n", + "fig.write_image(os.path.join(path, \"income_dist_histogram.png\"))" + ] + }, { "cell_type": "code", "execution_count": null, @@ -688,87 +741,42 @@ "# 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", + "from iot.inverse_optimal_tax import find_eti\n", + " \n", + "candidate_pairs = [\n", + " (0, 1, [\"Obama 2015\", \"Romney 2012\"], \"romney_obama_g_z_numerical.png\"),\n", + " (2, 3, [\"Clinton 2016\", \"Trump 2016\"], \"trump_clinton_g_z_numerical.png\"),\n", + " (4, 5, [\"Biden 2020\", \"Trump 2020\"], \"trump_biden_g_z_numerical.png\"),\n", + " (6, 7, [\"Harris 2024\", \"Trump 2024\"], \"trump_harris_g_z_numerical.png\"),\n", + "]\n", + " \n", + "for idx1, idx2, candidate_name, filename in candidate_pairs:\n", + " fig = px.line(\n", + " x=iot_all.iot[idx1].df().z[10:],\n", + " y=[iot_all.iot[idx1].df().g_z_numerical[10:], iot_all.iot[idx2].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 = [\"Obama 2012\", \"Romney 2012\"]\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, \"romney_obama_g_z_numerical.png\"),\n", - " scale=4\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", - "candidate_name = [\"Biden 2020\", \"Trump 2020\"]\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_biden_g_z_numerical.png\"),\n", - " scale=4\n", - " )\n", - "candidate_name = [\"Harris 2024\", \"Trump 2024\"]\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_harris_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:]) < 1))[0]\n", - "idx = (np.where(((iot_all.iot[3].df().g_z.values) > 1.05) | ((iot_all.iot[3].df().g_z.values) < 0.95)))[0]\n", + " fig.update_layout(template=template, legend=dict(title=\"Candidate:\"))\n", + " label_dict = {\"wide_variable_0\": candidate_name[0], \"wide_variable_1\": candidate_name[1]}\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.write_image(os.path.join(path, filename), scale=4)\n", + " \n", + "# Now find the epsilon(z) that would give Trump 2016's policies\n", + "# the same g(z) as Clinton 2016\n", + "# find_eti returns a single array; use Clinton's g_z as the target weights\n", + "clinton_gz = iot_all.iot[2].g_z\n", + "eti_beliefs_jjz = find_eti(iot_all.iot[3], g_z=clinton_gz)\n", + " \n", + "# Only plot where g_z departs meaningfully from 1\n", + "idx = np.where((clinton_gz > 1.05) | (clinton_gz < 0.95))[0]\n", "fig2 = px.line(\n", - " x=iot_all.iot[2].df().z[idx],\n", + " x=iot_all.iot[3].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": [ - "(np.where(((iot_all.iot[2].df().z.values) > 1.01) | ((iot_all.iot[2].df().z.values) < 0.99)))[0]" + "fig2.update_layout(template=template)\n", + "fig2.write_image(os.path.join(path, \"trump_eti.png\"), scale=4)" ] }, { @@ -781,29 +789,24 @@ " \"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", + " 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", + " 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", - " )" + "fig.write_image(os.path.join(path, \"MTR_2023.png\"), scale=4)" ] }, { @@ -812,89 +815,145 @@ "metadata": {}, "outputs": [], "source": [ - "import plotly.express as px\n", - "import pandas as pd\n", - "\n", - "# Sample data (replace with actual data)\n", - "data = {\n", - " \"gross_income\": [0, 25000, 50000, 75000, 100000, 120000],\n", - " \"non_constant_marginal_tax_rates\": [0, 0.5, -0.2, 0.3, 0.1, 0],\n", - " \"tax_base_elasticity\": [0, 1, 0.8, -0.5, -0.7, -0.6],\n", - " \"remainder_terms\": [0, 0.1, -0.1, 0.05, -0.05, 0]\n", - "}\n", - "\n", - "df = pd.DataFrame(data)\n", - "\n", - "# Create area plot\n", - "fig = px.area(df, x=\"gross_income\", y=[\"non_constant_marginal_tax_rates\", \"tax_base_elasticity\", \"remainder_terms\"],\n", - " labels={\"value\": \"Effect\", \"variable\": \"Category\"},\n", - " title=\"Area Plot of Tax and Welfare Components\",\n", - " color_discrete_map={\n", - " \"non_constant_marginal_tax_rates\": \"blue\",\n", - " \"tax_base_elasticity\": \"red\",\n", - " \"remainder_terms\": \"green\"\n", - " })\n", - "\n", - "fig.show()\n" + "# JJZ Fig 4 decomposition for Obama 2015 (single-policy object)\n", + "# Use a separate variable so we don't clobber the full iot_all\n", + "iot_obama = iot_user.iot_comparison(\n", + " policies=[policies[0]],\n", + " baseline_policies=[baseline_policies[0]],\n", + " mtr_smoother=\"kreg\",\n", + " labels=[labels[0]],\n", + " years=[years[0]],\n", + " data=\"CPS\",\n", + ")\n", + "fig = iot_obama.JJZFig4(policy=\"Obama 2015\")\n", + "fig.update_layout(template=template)\n", + "fig.show()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "https://raw.githubusercontent.com/jdebacker/examples/pres_proposals/psl_examples/taxcalc/Obama2015.json\n", - "https://raw.githubusercontent.com/jdebacker/examples/pres_proposals/psl_examples/taxcalc/Romney2012.json\n", - "https://raw.githubusercontent.com/jdebacker/examples/pres_proposals/psl_examples/taxcalc/Clinton2016.json\n", - "https://raw.githubusercontent.com/PSLmodels/examples/main/psl_examples/taxcalc/Trump2016.json\n", - "https://raw.githubusercontent.com/PSLmodels/examples/main/psl_examples/taxcalc/Biden2020.json\n", - "https://raw.githubusercontent.com/PSLmodels/examples/main/psl_examples/taxcalc/TCJA.json\n", - "https://raw.githubusercontent.com/PSLmodels/examples/main/psl_examples/taxcalc/Harris2024.json\n", - "https://raw.githubusercontent.com/PSLmodels/examples/main/psl_examples/taxcalc/Trump2024.json\n" - ] - } - ], + "outputs": [], "source": [ - "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", - " 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", - " baseline_policies.append(list_json)\n", + "# fig:trump_eti — Trump 2016 with Clinton's g_z, back out ETI\n", + "clinton_gz = iot_all.iot[2].g_z\n", + " \n", + "# (a) boundary at z0 with eps_0 = 0.25\n", + "eti_trump_z0 = find_eti(iot_all.iot[3], g_z=clinton_gz, eti_0=0.25, boundary=\"z0\")\n", + "# (b) transversality condition\n", + "eti_trump_inf = find_eti(iot_all.iot[3], g_z=clinton_gz, boundary=\"inf\")\n", + " \n", + "z = iot_all.iot[3].z\n", + " \n", + "fig_trump = go.Figure()\n", + "fig_trump.add_trace(go.Scatter(\n", + " x=z, y=eti_trump_z0, mode=\"lines\",\n", + " name=r\"ETI_0 = 0.25\",\n", + " line=dict(color=\"blue\", width=2),\n", + "))\n", + "fig_trump.add_trace(go.Scatter(\n", + " x=z, y=eti_trump_inf, mode=\"lines\",\n", + " name=\"Transversality\",\n", + " line=dict(color=\"red\", width=2, dash=\"dash\"),\n", + "))\n", + "fig_trump.add_hline(\n", + " y=0.25, line_dash=\"dot\", line_color=\"gray\",\n", + " annotation_text=\"ε = 0.25\", annotation_position=\"top left\",\n", + ")\n", + "fig_trump.update_layout(\n", + " template=template,\n", + " xaxis_title=\"Income\",\n", + " yaxis_title=r\"Implied ETI\",\n", + " legend=dict(title=\"Boundary condition\"),\n", + " yaxis=dict(range=[-0.5, 0.5]),\n", + " xaxis=dict(range=[1_000, 500_000]),\n", + ")\n", + "fig_trump.write_image(os.path.join(path, \"eti_trump.png\"), scale=4)\n", + "fig_trump.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# fig:eti_utilitarian — All candidates, g_z = 1, back out ETI\n", + "utilitarian_gz = np.ones_like(iot_all.iot[0].g_z)\n", + " \n", + "style_map = {\n", + " \"Obama 2015\": dict(color=\"blue\", dash=\"dot\"),\n", + " \"Romney 2012\": dict(color=\"red\", dash=\"dot\"),\n", + " \"Clinton 2016\": dict(color=\"blue\", dash=\"dash\"),\n", + " \"Trump 2016\": dict(color=\"red\", dash=\"dash\"),\n", + " \"Biden 2020\": dict(color=\"blue\", dash=\"dashdot\"),\n", + " \"Trump 2020\": dict(color=\"red\", dash=\"dashdot\"),\n", + " \"Harris 2024\": dict(color=\"blue\", dash=\"solid\"),\n", + " \"Trump 2024\": dict(color=\"red\", dash=\"solid\"),\n", + "}\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# boundary = \"z0\", eps_0 = 0.25\n", "\n", - "iot_all = iot_user.iot_comparison(\n", - " policies=[policies[0]],\n", - " baseline_policies=[baseline_policies[0]],\n", - " mtr_smoother=\"kreg\", # this is kreg or HSV\n", - " labels=[labels[0]],\n", - " years=[years[0]],\n", - " data=\"CPS\"\n", - ")\n", - "fig = iot_all.JJZFig4(policy='Obama 2015')\n", - "fig.update_layout(\n", + "fig_util_z0 = go.Figure()\n", + "for i, label in enumerate(labels):\n", + " eti_util = find_eti(iot_all.iot[i], g_z=utilitarian_gz, eti_0=0.25, boundary=\"z0\")\n", + " sty = style_map.get(label, dict(color=\"black\", dash=\"solid\"))\n", + " fig_util_z0.add_trace(go.Scatter(\n", + " x=iot_all.iot[i].z, y=eti_util, mode=\"lines\",\n", + " name=label,\n", + " line=dict(color=sty[\"color\"], dash=sty[\"dash\"], width=2),\n", + " ))\n", + " \n", + "fig_util_z0.update_layout(\n", " template=template,\n", - ")\n", - "# fig.update_xaxes(range=[0, 850000])\n", + " xaxis_title=\"Income\",\n", + " yaxis_title=r\"$\\text{Implied } \\varepsilon(z)$\",\n", + " legend=dict(title=\"Candidate\"),\n", + " yaxis=dict(range=[-0.2, 0.4]),\n", + " xaxis=dict(range=[1_000, 500_000]),\n", + " title=\"Utilitarian weights (ε₀ = 0.25 boundary)\",\n", + ")\n", + "fig_util_z0.write_image(os.path.join(path, \"eti_utilitarian_z0.png\"), scale=4)\n", + "fig_util_z0.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# boundary = \"inf\" (transversality)\n", "\n", - "fig.show()" + "fig_util_inf = go.Figure()\n", + "for i, label in enumerate(labels):\n", + " eti_util = find_eti(iot_all.iot[i], g_z=utilitarian_gz, boundary=\"inf\")\n", + " sty = style_map.get(label, dict(color=\"black\", dash=\"solid\"))\n", + " fig_util_inf.add_trace(go.Scatter(\n", + " x=iot_all.iot[i].z, y=eti_util, mode=\"lines\",\n", + " name=label,\n", + " line=dict(color=sty[\"color\"], dash=sty[\"dash\"], width=2),\n", + " ))\n", + " \n", + "fig_util_inf.update_layout(\n", + " template=template,\n", + " xaxis_title=\"Income\",\n", + " yaxis_title=r\"$\\text{Implied } \\varepsilon(z)$\",\n", + " legend=dict(title=\"Candidate\"),\n", + " yaxis=dict(range=[-0.5, 0.5]),\n", + " xaxis=dict(range=[1_000, 500_000]),\n", + " title=\"Utilitarian weights (transversality boundary)\",\n", + ")\n", + "fig_util_inf.write_image(os.path.join(path, \"eti_utilitarian_inf.png\"), scale=4)\n", + "fig_util_inf.show()" ] }, { @@ -921,7 +980,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.3" + "version": "3.11.11" } }, "nbformat": 4, From 46f5b1d5faf969e4a387b2cced4d543e41c55e3e Mon Sep 17 00:00:00 2001 From: John Ryan Date: Tue, 24 Mar 2026 15:59:41 -0500 Subject: [PATCH 3/3] Clean up doc strings --- iot/inverse_optimal_tax.py | 60 ++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 35 deletions(-) diff --git a/iot/inverse_optimal_tax.py b/iot/inverse_optimal_tax.py index 09cd196..ab19cd4 100644 --- a/iot/inverse_optimal_tax.py +++ b/iot/inverse_optimal_tax.py @@ -9,11 +9,8 @@ class IOT: """ - Constructor for the IOT class. - - This IOT class can be used to compute the social welfare weights - across the income distribution given data, tax policy parameters, - and behavioral parameters. + Computes social welfare weights across the income distribution given + data, tax policy parameters, and behavioral parameters. Args: data (Pandas DataFrame): micro data representing tax payers. @@ -21,20 +18,16 @@ class IOT: weight_var, mtr income_measure (str): name of income measure from data to use weight_var (str): name of weight measure from data to use - eti (scalar): compensated elasticity of taxable income - w.r.t. the marginal tax rate - bandwidth (scalar): size of income bins in units of income - lower_bound (scalar): minimum income to consider - upper_bound (scalar): maximum income to consider + eti (scalar or dict): compensated elasticity of taxable income + w.r.t. the marginal tax rate. If a dict, must have keys + ``knot_points`` and ``eti_values`` with equal-length lists. dist_type (None or str): type of distribution to use if - parametric, if None, then non-parametric bin weights - mtr_smoother (None or str): method used to smooth our mtr - function, if None, then use bin average mtrs + parametric; if None, then non-parametric bin weights + kde_bw (scalar or None): bandwidth for KDE estimation + mtr_smoother (None or str): method used to smooth the mtr + function; if None, then use bin average mtrs mtr_smooth_param (scalar): parameter for mtr_smoother kreg_bw (array_like): bandwidth for kernel regression - - Returns: - class instance: IOT """ def __init__( @@ -92,10 +85,7 @@ def __init__( def df(self): """ - Return all vector attributes in a DataFrame format - - Args: - None + Return all vector attributes in a DataFrame format. Returns: df (Pandas DataFrame): DataFrame with all inputs/outputs @@ -133,8 +123,9 @@ def compute_mtr_dist( Must include the following columns: income_measure, weight_var, mtr weight_var (str): name of weight measure from data to use - mtr_smoother (None or str): method used to smooth our mtr - function, if None, then use bin average mtrs + income_measure (str): name of income measure from data to use + mtr_smoother (None or str): method used to smooth the mtr + function; if None, then use bin average mtrs mtr_smooth_param (scalar): parameter for mtr_smoother kreg_bw (array_like): bandwidth for kernel regression @@ -226,11 +217,10 @@ def compute_income_dist( Returns: tuple: - * z (array_like): mean income at each bin in the income - distribution - * f (array_like): density for income bin z - * f_prime (array_like): slope of the density function for - income bin z + * z (array_like): income grid points + * F (array_like): cumulative distribution function at each z + * f (array_like): density at each z + * f_prime (array_like): slope of the density function at each z """ z_line = np.linspace(100, 1000000, 100000) # drop zero income observations @@ -362,21 +352,21 @@ def pln_dpdf(y, mu, sigma, alpha): def sw_weights(self): r""" - Returns the social welfare weights for a given tax policy. + Return the social welfare weights for a given tax policy. See Jacobs, Jongen, and Zoutman (2017) and Lockwood and Weinzierl (2016) for details. .. math:: - g_{z} = 1 + \theta_z \varepsilon^{c}\frac{T'(z)}{(1-T'(z))} + - \varepsilon^{c}\frac{zT''(z)}{(1-T''(z))^{2}} - - Args: - None + g_{z} = 1 + \theta_z \varepsilon^{c}\frac{T'(z)}{1-T'(z)} + + \varepsilon^{c}\frac{zT''(z)}{(1-T'(z))^{2}} Returns: - array_like: vector of social welfare weights across - the income distribution + tuple: + * g_z (array_like): social welfare weights via analytical + formula + * g_z_numerical (array_like): social welfare weights via + the Lockwood and Weinzierl numerical formula """ g_z = ( 1