diff --git a/changelog.yaml b/changelog.yaml index 4fedbef8..6cb1cd34 100644 --- a/changelog.yaml +++ b/changelog.yaml @@ -2,73 +2,4 @@ added: - Initial version of package. date: 2024-11-30 00:00:00 - version: 1.0.0 -- bump: patch - changes: - added: - - Changelog entries. - date: 2024-11-30 17:01:37 -- bump: major - changes: - added: - - Major version bump to succeed old versions of the package under a different - repo. - date: 2024-12-01 12:38:17 -- bump: minor - changes: - added: - - Budget chart. - - Budget window chart and data. - date: 2024-12-02 12:12:59 -- bump: minor - changes: - added: - - State level subsets. - - Huggingface token CLI entry. - date: 2024-12-02 13:48:27 -- bump: patch - changes: - fixed: - - Formatting in constituency charts. - date: 2024-12-03 13:16:27 -- bump: patch - changes: - fixed: - - Household simulation example broken due to a type error. - date: 2024-12-06 14:02:41 -- bump: minor - changes: - added: - - Household variation charts. - - Region filtering improvements. - date: 2024-12-06 14:07:31 -- bump: minor - changes: - fixed: - - Bug causing local authority impacts to fail. - date: 2024-12-23 08:05:21 -- bump: patch - changes: - fixed: - - Subsetting bug with time periods. - date: 2025-01-09 10:52:28 -- bump: minor - changes: - changed: - - Moved to strong typing. - date: 2025-01-24 00:08:26 -- bump: minor - changes: - added: - - General `calculate` method. - date: 2025-01-29 03:14:45 -- bump: minor - changes: - added: - - Chart generation for macro charts. - date: 2025-01-31 02:39:31 -- bump: patch - changes: - changed: - - Updated documentation link in README - date: 2025-02-12 09:19:58 + version: 0.1.0 diff --git a/docs/_toc.yml b/docs/_toc.yml index a3782d36..e060ef57 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -15,4 +15,8 @@ parts: - caption: Concepts chapters: - file: concepts/simulation + - file: concepts/extending + - caption: Outputs + chapters: + - file: outputs/calculate_average_earnings \ No newline at end of file diff --git a/docs/basic/calculate_economy_comparison.ipynb b/docs/basic/calculate_economy_comparison.ipynb index 2d6fcfd4..3cdb0cb0 100644 --- a/docs/basic/calculate_economy_comparison.ipynb +++ b/docs/basic/calculate_economy_comparison.ipynb @@ -11,7 +11,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -28,16 +28,18 @@ "source": [ "from policyengine import Simulation\n", "\n", - "sim = Simulation({\n", - " \"scope\": \"macro\", # Required for this\n", - " \"country\": \"uk\", # or \"us\"\n", - " \"time_period\": 2025,\n", - " \"reform\": {\n", - " \"gov.hmrc.income_tax.allowances.personal_allowance.amount\": 15_000,\n", + "sim = Simulation(\n", + " scope=\"macro\", # Required for this\n", + " country=\"us\", # or \"us\"\n", + " time_period=2025, # Defaults to 2025\n", + " reform={\n", + " \"gov.usda.snap.income.deductions.earned_income\": {\n", + " \"2025\": 0.05\n", + " }\n", " }\n", - "})\n", + ")\n", "\n", - "sim.calculate()" + "sim.calculate_economy_comparison()" ] }, { diff --git a/docs/basic/calculate_household_comparison.ipynb b/docs/basic/calculate_household_comparison.ipynb index f86d3f12..53041450 100644 --- a/docs/basic/calculate_household_comparison.ipynb +++ b/docs/basic/calculate_household_comparison.ipynb @@ -11,7 +11,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -28,11 +28,10 @@ "source": [ "from policyengine import Simulation\n", "\n", - "sim = Simulation({\n", - " \"scope\": \"household\", # Required for this\n", - " \"country\": \"uk\", # or \"us\"\n", - " \"time_period\": 2025,\n", - " \"data\": { # Required for this\n", + "sim = Simulation(\n", + " scope=\"household\",\n", + " country=\"us\",\n", + " data={ # Required for this\n", " \"people\": {\n", " \"person\": {\n", " \"age\": {\n", @@ -44,12 +43,14 @@ " }\n", " }\n", " },\n", - " \"reform\": {\n", - " \"gov.hmrc.income_tax.allowances.personal_allowance.amount\": 15_000,\n", + " reform={\n", + " \"gov.usda.snap.income.deductions.earned_income\": {\n", + " \"2025\": 0.05\n", + " }\n", " }\n", - "})\n", + ")\n", "\n", - "sim.calculate()" + "sim.calculate_single_household()" ] }, { diff --git a/docs/basic/calculate_single_economy.ipynb b/docs/basic/calculate_single_economy.ipynb index 0966e8ee..f65a2841 100644 --- a/docs/basic/calculate_single_economy.ipynb +++ b/docs/basic/calculate_single_economy.ipynb @@ -11,7 +11,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -28,13 +28,13 @@ "source": [ "from policyengine import Simulation\n", "\n", - "sim = Simulation({\n", - " \"scope\": \"macro\", # Required for this\n", - " \"country\": \"uk\", # or \"us\"\n", - " \"time_period\": 2025,\n", - "})\n", + "sim = Simulation(\n", + " scope=\"macro\",\n", + " country=\"us\",\n", + " time_period=2025,\n", + ")\n", "\n", - "sim.calculate()" + "sim.calculate_single_economy()" ] }, { diff --git a/docs/basic/calculate_single_household.ipynb b/docs/basic/calculate_single_household.ipynb index 7801a627..4a0bbbe6 100644 --- a/docs/basic/calculate_single_household.ipynb +++ b/docs/basic/calculate_single_household.ipynb @@ -11,7 +11,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -28,11 +28,10 @@ "source": [ "from policyengine import Simulation\n", "\n", - "sim = Simulation({\n", - " \"scope\": \"household\", # Required for this\n", - " \"country\": \"uk\", # or \"us\"\n", - " \"time_period\": 2025,\n", - " \"data\": { # Required for this\n", + "sim = Simulation(\n", + " scope=\"household\",\n", + " country=\"us\",\n", + " data={ # Required for this\n", " \"people\": {\n", " \"person\": {\n", " \"age\": {\n", @@ -44,9 +43,9 @@ " }\n", " }\n", " }\n", - "})\n", + ")\n", "\n", - "sim.calculate()" + "sim.calculate_single_household()" ] }, { diff --git a/docs/basic/create_charts.ipynb b/docs/basic/create_charts.ipynb index 16c0c188..571a3a2f 100644 --- a/docs/basic/create_charts.ipynb +++ b/docs/basic/create_charts.ipynb @@ -4227,1924 +4227,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Poverty\n", - "\n", - "The poverty chart shows the impact of the reform on various poverty rates, split out by demographic groups you can specify. You can show the change to poverty rate by age group, gender or racial group (US-only), and you can choose between regular or deep poverty." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.plotly.v1+json": { - "config": { - "plotlyServerURL": "https://plot.ly" - }, - "data": [ - { - "marker": { - "color": [ - "#2C6496", - "#2C6496", - "#2C6496", - "#2C6496" - ] - }, - "text": [ - "33,440", - "52,656", - "23,791", - "109,889" - ], - "type": "bar", - "x": [ - "child", - "working_age", - "senior", - "all" - ], - "y": [ - 33440.125, - 52656.5, - 23791.21875, - 109889 - ] - } - ], - "layout": { - "annotations": [ - { - "showarrow": false, - "text": "Source: PolicyEngine UK tax-benefit microsimulation model (version 2.18.0)", - "x": 0, - "xanchor": "left", - "xref": "paper", - "y": -0.2, - "yanchor": "bottom", - "yref": "paper" - } - ], - "font": { - "color": "black", - "family": "Roboto Serif" - }, - "height": 600, - "images": [ - { - "sizex": 0.15, - "sizey": 0.15, - "source": "https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/blue.png", - "x": 1.1, - "xanchor": "right", - "xref": "paper", - "y": -0.2, - "yanchor": "bottom", - "yref": "paper" - } - ], - "margin": { - "b": 120, - "l": 120, - "r": 120, - "t": 120 - }, - "modebar": { - "activecolor": "#F4F4F4", - "bgcolor": "#F4F4F4", - "color": "#F4F4F4" - }, - "paper_bgcolor": "#F4F4F4", - "plot_bgcolor": "#F4F4F4", - "template": { - "data": { - "bar": [ - { - "error_x": { - "color": "#2a3f5f" - }, - "error_y": { - "color": "#2a3f5f" - }, - "marker": { - "line": { - "color": "white", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "bar" - } - ], - "barpolar": [ - { - "marker": { - "line": { - "color": "white", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "barpolar" - } - ], - "carpet": [ - { - "aaxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "#C8D4E3", - "linecolor": "#C8D4E3", - "minorgridcolor": "#C8D4E3", - "startlinecolor": "#2a3f5f" - }, - "baxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "#C8D4E3", - "linecolor": "#C8D4E3", - "minorgridcolor": "#C8D4E3", - "startlinecolor": "#2a3f5f" - }, - "type": "carpet" - } - ], - "choropleth": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "choropleth" - } - ], - "contour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "contour" - } - ], - "contourcarpet": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "contourcarpet" - } - ], - "heatmap": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "heatmap" - } - ], - "heatmapgl": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "heatmapgl" - } - ], - "histogram": [ - { - "marker": { - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "histogram" - } - ], - "histogram2d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2d" - } - ], - "histogram2dcontour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2dcontour" - } - ], - "mesh3d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "mesh3d" - } - ], - "parcoords": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "parcoords" - } - ], - "pie": [ - { - "automargin": true, - "type": "pie" - } - ], - "scatter": [ - { - "fillpattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - }, - "type": "scatter" - } - ], - "scatter3d": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatter3d" - } - ], - "scattercarpet": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattercarpet" - } - ], - "scattergeo": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergeo" - } - ], - "scattergl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergl" - } - ], - "scattermapbox": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattermapbox" - } - ], - "scatterpolar": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolar" - } - ], - "scatterpolargl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolargl" - } - ], - "scatterternary": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterternary" - } - ], - "surface": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "surface" - } - ], - "table": [ - { - "cells": { - "fill": { - "color": "#EBF0F8" - }, - "line": { - "color": "white" - } - }, - "header": { - "fill": { - "color": "#C8D4E3" - }, - "line": { - "color": "white" - } - }, - "type": "table" - } - ] - }, - "layout": { - "annotationdefaults": { - "arrowcolor": "#2a3f5f", - "arrowhead": 0, - "arrowwidth": 1 - }, - "autotypenumbers": "strict", - "coloraxis": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "colorscale": { - "diverging": [ - [ - 0, - "#8e0152" - ], - [ - 0.1, - "#c51b7d" - ], - [ - 0.2, - "#de77ae" - ], - [ - 0.3, - "#f1b6da" - ], - [ - 0.4, - "#fde0ef" - ], - [ - 0.5, - "#f7f7f7" - ], - [ - 0.6, - "#e6f5d0" - ], - [ - 0.7, - "#b8e186" - ], - [ - 0.8, - "#7fbc41" - ], - [ - 0.9, - "#4d9221" - ], - [ - 1, - "#276419" - ] - ], - "sequential": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "sequentialminus": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ] - }, - "colorway": [ - "#636efa", - "#EF553B", - "#00cc96", - "#ab63fa", - "#FFA15A", - "#19d3f3", - "#FF6692", - "#B6E880", - "#FF97FF", - "#FECB52" - ], - "font": { - "color": "#2a3f5f" - }, - "geo": { - "bgcolor": "white", - "lakecolor": "white", - "landcolor": "white", - "showlakes": true, - "showland": true, - "subunitcolor": "#C8D4E3" - }, - "hoverlabel": { - "align": "left" - }, - "hovermode": "closest", - "mapbox": { - "style": "light" - }, - "paper_bgcolor": "white", - "plot_bgcolor": "white", - "polar": { - "angularaxis": { - "gridcolor": "#EBF0F8", - "linecolor": "#EBF0F8", - "ticks": "" - }, - "bgcolor": "white", - "radialaxis": { - "gridcolor": "#EBF0F8", - "linecolor": "#EBF0F8", - "ticks": "" - } - }, - "scene": { - "xaxis": { - "backgroundcolor": "white", - "gridcolor": "#DFE8F3", - "gridwidth": 2, - "linecolor": "#EBF0F8", - "showbackground": true, - "ticks": "", - "zerolinecolor": "#EBF0F8" - }, - "yaxis": { - "backgroundcolor": "white", - "gridcolor": "#DFE8F3", - "gridwidth": 2, - "linecolor": "#EBF0F8", - "showbackground": true, - "ticks": "", - "zerolinecolor": "#EBF0F8" - }, - "zaxis": { - "backgroundcolor": "white", - "gridcolor": "#DFE8F3", - "gridwidth": 2, - "linecolor": "#EBF0F8", - "showbackground": true, - "ticks": "", - "zerolinecolor": "#EBF0F8" - } - }, - "shapedefaults": { - "line": { - "color": "#2a3f5f" - } - }, - "ternary": { - "aaxis": { - "gridcolor": "#DFE8F3", - "linecolor": "#A2B1C6", - "ticks": "" - }, - "baxis": { - "gridcolor": "#DFE8F3", - "linecolor": "#A2B1C6", - "ticks": "" - }, - "bgcolor": "white", - "caxis": { - "gridcolor": "#DFE8F3", - "linecolor": "#A2B1C6", - "ticks": "" - } - }, - "title": { - "x": 0.05 - }, - "xaxis": { - "automargin": true, - "gridcolor": "#EBF0F8", - "linecolor": "#EBF0F8", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "#EBF0F8", - "zerolinewidth": 2 - }, - "yaxis": { - "automargin": true, - "gridcolor": "#EBF0F8", - "linecolor": "#EBF0F8", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "#EBF0F8", - "zerolinewidth": 2 - } - } - }, - "title": { - "text": "Lowering the personal allowance to £10,000 would raise the poverty rate by 1.9%" - }, - "uniformtext": { - "minsize": 12, - "mode": "hide" - }, - "width": 800, - "xaxis": { - "gridcolor": "#F4F4F4", - "ticksuffix": "", - "ticktext": [ - "Child", - "Working-age", - "Senior", - "All" - ], - "tickvals": [ - "child", - "working_age", - "senior", - "all" - ], - "title": { - "text": "Group" - }, - "zerolinecolor": "#F4F4F4" - }, - "yaxis": { - "gridcolor": "#F4F4F4", - "tickformat": ",.0f", - "ticksuffix": "", - "title": { - "text": "Poverty rate change" - }, - "zerolinecolor": "#616161" - } - } - } - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "create_poverty_chart(\n", - " sim,\n", - " age_group=None, # Specify None to show all age groups\n", - " gender=\"all\", # The rest are filters\n", - " racial_group=\"all\",\n", - " rate_relative=False, # Compare how many people are in poverty rather than the rate\n", - " change_relative=False, # Compare the absolute headcount rather than % change\n", - " poverty_rate=\"regular\",\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Inequality\n", - "\n", - "The inequality chart shows the impact of the reform on various inequality measures: the Gini coefficient, the share of income held by the top 10% of households (by income) and the top 1% share." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.plotly.v1+json": { - "config": { - "plotlyServerURL": "https://plot.ly" - }, - "data": [ - { - "marker": { - "color": [ - "#616161", - "#616161", - "#616161" - ] - }, - "text": [ - "0.2%", - "0.6%", - "1.4%" - ], - "type": "bar", - "x": [ - "Gini index", - "Top 10% share", - "Top 1% share" - ], - "y": [ - 0.002379218659187675, - 0.006010878001505188, - 0.014167378755875062 - ] - } - ], - "layout": { - "annotations": [ - { - "showarrow": false, - "text": "Source: PolicyEngine UK tax-benefit microsimulation model (version 2.18.0)", - "x": 0, - "xanchor": "left", - "xref": "paper", - "y": -0.2, - "yanchor": "bottom", - "yref": "paper" - } - ], - "font": { - "color": "black", - "family": "Roboto Serif" - }, - "height": 600, - "images": [ - { - "sizex": 0.15, - "sizey": 0.15, - "source": "https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/blue.png", - "x": 1.1, - "xanchor": "right", - "xref": "paper", - "y": -0.2, - "yanchor": "bottom", - "yref": "paper" - } - ], - "margin": { - "b": 120, - "l": 120, - "r": 120, - "t": 120 - }, - "modebar": { - "activecolor": "#F4F4F4", - "bgcolor": "#F4F4F4", - "color": "#F4F4F4" - }, - "paper_bgcolor": "#F4F4F4", - "plot_bgcolor": "#F4F4F4", - "template": { - "data": { - "bar": [ - { - "error_x": { - "color": "#2a3f5f" - }, - "error_y": { - "color": "#2a3f5f" - }, - "marker": { - "line": { - "color": "white", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "bar" - } - ], - "barpolar": [ - { - "marker": { - "line": { - "color": "white", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "barpolar" - } - ], - "carpet": [ - { - "aaxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "#C8D4E3", - "linecolor": "#C8D4E3", - "minorgridcolor": "#C8D4E3", - "startlinecolor": "#2a3f5f" - }, - "baxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "#C8D4E3", - "linecolor": "#C8D4E3", - "minorgridcolor": "#C8D4E3", - "startlinecolor": "#2a3f5f" - }, - "type": "carpet" - } - ], - "choropleth": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "choropleth" - } - ], - "contour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "contour" - } - ], - "contourcarpet": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "contourcarpet" - } - ], - "heatmap": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "heatmap" - } - ], - "heatmapgl": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "heatmapgl" - } - ], - "histogram": [ - { - "marker": { - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "histogram" - } - ], - "histogram2d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2d" - } - ], - "histogram2dcontour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2dcontour" - } - ], - "mesh3d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "mesh3d" - } - ], - "parcoords": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "parcoords" - } - ], - "pie": [ - { - "automargin": true, - "type": "pie" - } - ], - "scatter": [ - { - "fillpattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - }, - "type": "scatter" - } - ], - "scatter3d": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatter3d" - } - ], - "scattercarpet": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattercarpet" - } - ], - "scattergeo": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergeo" - } - ], - "scattergl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergl" - } - ], - "scattermapbox": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattermapbox" - } - ], - "scatterpolar": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolar" - } - ], - "scatterpolargl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolargl" - } - ], - "scatterternary": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterternary" - } - ], - "surface": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "surface" - } - ], - "table": [ - { - "cells": { - "fill": { - "color": "#EBF0F8" - }, - "line": { - "color": "white" - } - }, - "header": { - "fill": { - "color": "#C8D4E3" - }, - "line": { - "color": "white" - } - }, - "type": "table" - } - ] - }, - "layout": { - "annotationdefaults": { - "arrowcolor": "#2a3f5f", - "arrowhead": 0, - "arrowwidth": 1 - }, - "autotypenumbers": "strict", - "coloraxis": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "colorscale": { - "diverging": [ - [ - 0, - "#8e0152" - ], - [ - 0.1, - "#c51b7d" - ], - [ - 0.2, - "#de77ae" - ], - [ - 0.3, - "#f1b6da" - ], - [ - 0.4, - "#fde0ef" - ], - [ - 0.5, - "#f7f7f7" - ], - [ - 0.6, - "#e6f5d0" - ], - [ - 0.7, - "#b8e186" - ], - [ - 0.8, - "#7fbc41" - ], - [ - 0.9, - "#4d9221" - ], - [ - 1, - "#276419" - ] - ], - "sequential": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "sequentialminus": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ] - }, - "colorway": [ - "#636efa", - "#EF553B", - "#00cc96", - "#ab63fa", - "#FFA15A", - "#19d3f3", - "#FF6692", - "#B6E880", - "#FF97FF", - "#FECB52" - ], - "font": { - "color": "#2a3f5f" - }, - "geo": { - "bgcolor": "white", - "lakecolor": "white", - "landcolor": "white", - "showlakes": true, - "showland": true, - "subunitcolor": "#C8D4E3" - }, - "hoverlabel": { - "align": "left" - }, - "hovermode": "closest", - "mapbox": { - "style": "light" - }, - "paper_bgcolor": "white", - "plot_bgcolor": "white", - "polar": { - "angularaxis": { - "gridcolor": "#EBF0F8", - "linecolor": "#EBF0F8", - "ticks": "" - }, - "bgcolor": "white", - "radialaxis": { - "gridcolor": "#EBF0F8", - "linecolor": "#EBF0F8", - "ticks": "" - } - }, - "scene": { - "xaxis": { - "backgroundcolor": "white", - "gridcolor": "#DFE8F3", - "gridwidth": 2, - "linecolor": "#EBF0F8", - "showbackground": true, - "ticks": "", - "zerolinecolor": "#EBF0F8" - }, - "yaxis": { - "backgroundcolor": "white", - "gridcolor": "#DFE8F3", - "gridwidth": 2, - "linecolor": "#EBF0F8", - "showbackground": true, - "ticks": "", - "zerolinecolor": "#EBF0F8" - }, - "zaxis": { - "backgroundcolor": "white", - "gridcolor": "#DFE8F3", - "gridwidth": 2, - "linecolor": "#EBF0F8", - "showbackground": true, - "ticks": "", - "zerolinecolor": "#EBF0F8" - } - }, - "shapedefaults": { - "line": { - "color": "#2a3f5f" - } - }, - "ternary": { - "aaxis": { - "gridcolor": "#DFE8F3", - "linecolor": "#A2B1C6", - "ticks": "" - }, - "baxis": { - "gridcolor": "#DFE8F3", - "linecolor": "#A2B1C6", - "ticks": "" - }, - "bgcolor": "white", - "caxis": { - "gridcolor": "#DFE8F3", - "linecolor": "#A2B1C6", - "ticks": "" - } - }, - "title": { - "x": 0.05 - }, - "xaxis": { - "automargin": true, - "gridcolor": "#EBF0F8", - "linecolor": "#EBF0F8", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "#EBF0F8", - "zerolinewidth": 2 - }, - "yaxis": { - "automargin": true, - "gridcolor": "#EBF0F8", - "linecolor": "#EBF0F8", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "#EBF0F8", - "zerolinewidth": 2 - } - } - }, - "title": { - "text": "Lowering the personal allowance to £10,000 would raise inequality" - }, - "uniformtext": { - "minsize": 12, - "mode": "hide" - }, - "width": 800, - "xaxis": { - "gridcolor": "#F4F4F4", - "ticksuffix": "", - "title": { - "text": "" - }, - "zerolinecolor": "#F4F4F4" - }, - "yaxis": { - "gridcolor": "#F4F4F4", - "tickformat": ".0%", - "ticksuffix": "", - "title": { - "text": "Change (%)" - }, - "zerolinecolor": "#616161" - } - } - } - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "create_inequality_chart(sim, relative=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Labor supply\n", + "## Inequality\n", "\n", - "The labor supply chart shows the impact of the reform on labor supply if labor supply response assumptions have been set in the `reform`, split out by groups you can specify. You can show the change to labor supply by decile, the mechanism (or elasticity- e.g. filtering to only substitution effect-powered responses), and also by the unit (hours or earnings, though only earnings in the UK)." + "The inequality chart shows the impact of the reform on various inequality measures: the Gini coefficient, the share of income held by the top 10% of households (by income) and the top 1% share." ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -6159,56 +4249,24 @@ "color": [ "#616161", "#616161", - "#616161", - "#616161", - "#616161", - "#616161", - "#616161", - "#616161", - "#616161", - "#2C6496", "#616161" ] }, "text": [ - "-1.3%", - "-0.1%", - "-0.7%", - "-0.2%", - "-0.1%", - "-0.4%", - "-0.5%", - "-0.5%", - "-0.1%", - "0.4%", - "-0.1%" + "0.2%", + "0.6%", + "1.4%" ], "type": "bar", "x": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - "all" + "Gini index", + "Top 10% share", + "Top 1% share" ], "y": [ - -0.013388361268997362, - -0.0009001679620899952, - -0.006940002445757689, - -0.002096522479093097, - -0.000590021861392062, - -0.003602100638872149, - -0.005081699001099658, - -0.004559364671289845, - -0.001499923178195422, - 0.0038960671090264246, - -0.0007300784012860403 + 0.002379218659187675, + 0.006010878001505188, + 0.014167378755875062 ] } ], @@ -7073,7 +5131,7 @@ } }, "title": { - "text": "Lowering the personal allowance to £10,000 would lower labor supply by 0.1%" + "text": "Lowering the personal allowance to £10,000 would raise inequality" }, "uniformtext": { "minsize": 12, @@ -7083,33 +5141,8 @@ "xaxis": { "gridcolor": "#F4F4F4", "ticksuffix": "", - "ticktext": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10 - ], - "tickvals": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - "all" - ], "title": { - "text": "Group" + "text": "" }, "zerolinecolor": "#F4F4F4" }, @@ -7118,7 +5151,7 @@ "tickformat": ".0%", "ticksuffix": "", "title": { - "text": "Labor supply change (£bn)" + "text": "Change (%)" }, "zerolinecolor": "#616161" } @@ -7130,24 +5163,7 @@ } ], "source": [ - "sim = Simulation({\n", - " \"country\": \"uk\",\n", - " \"scope\": \"macro\",\n", - " \"reform\": {\n", - " \"gov.hmrc.income_tax.allowances.personal_allowance.amount\": 10_000,\n", - " \"gov.simulation.labor_supply_responses.substitution_elasticity\": 0.2,\n", - " },\n", - " \"title\": \"Lowering the personal allowance to £10,000\" # Required for charts\n", - "})\n", - "\n", - "create_labor_supply_chart(\n", - " sim,\n", - " decile=None,\n", - " elasticity=\"all\",\n", - " unit=\"earnings\",\n", - " change_relative=True,\n", - " change_average=False,\n", - ")" + "create_inequality_chart(sim, relative=True)" ] } ], diff --git a/docs/concepts/extending.ipynb b/docs/concepts/extending.ipynb new file mode 100644 index 00000000..f4b6a7c5 --- /dev/null +++ b/docs/concepts/extending.ipynb @@ -0,0 +1,69 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Extending this package\n", + "\n", + "You might want to add new chart types, output metrics, etc. to the `Simulation` interface in this package. This page explains how to do that.\n", + "\n", + "## Instructions\n", + "\n", + "Here are the basic steps.\n", + "\n", + "1. In `policyengine/outputs`, create a new file (anywhere will work) that defines a function that takes a `Simulation` object **named `simulation`** and returns *whatever* you want.\n", + "\n", + "That's it! It'll be automatically added. You can access it like so:\n", + "\n", + "```python\n", + "from policyengine import Simulation\n", + "\n", + "sim = Simulation(country=\"us\", scope=\"macro\")\n", + "\n", + "sim.calculate_average_mtr() # Assumes that def calculate_average_mtr(sim: Simulation) is defined in e.g. policyengine/outputs/macro/calculate_average_mtr.py\n", + "```\n", + "\n", + "As a reminder, your might look like this:\n", + "\n", + "```python\n", + "from policyengine import Simulation\n", + "\n", + "\n", + "def calculate_average_earnings(simulation: Simulation) -> float:\n", + " \"\"\"Calculate average earnings.\"\"\"\n", + " employment_income = simulation.baseline_simulation.calculate(\n", + " \"employment_income\"\n", + " )\n", + " return employment_income[employment_income > 0].median()\n", + "\n", + "```\n", + "\n", + "But there are best practices to follow.\n", + "\n", + "\n", + "## Best practices\n", + "\n", + "Look at the `outputs/` folder in the docs- `Average earnings` is a model example for this.\n", + "\n", + "1. **Put your new function in a sensible place to keep the code organized.** For example, we have `macro/` and `household/` as top-level folders depending on what the `Simulation` that calls your function is simulating over. Below that, we have `single` and `comparison` depending on whether the user has provided a reform or not. Bear in mind that your new function probably has to assume these two things and will likely break in the wrong context.\n", + "2. **Make sure your function is well-documented.** This is a public API, so it should be easy to understand how to use it. Please make sure it has a docstring and type hints, and add a Markdown file in the `docs/outputs/` folder (mirror the `calculate_average_earnings` one) that uses autodoc to expose the function, then add it in `docs/toc.yml`.\n", + "3. **Add tests**. Use pytest in the `tests/` folder to make sure your function works as expected.\n", + "\n", + "When writing a function, remember what you have to play with in `Simulation`:\n", + "\n", + "1. `Simulation.options` (everything passed to the `Simulation` constructor)\n", + "2. `Simulation.baseline_simulation` (a `policyengine_core.Simulation` object with the baseline policy)\n", + "3. `Simulation.reform_simulation` (a `policyengine_core.Simulation` object with the reform policy, if it exists)\n", + "4. The ablity to construct new `Simulation`s with different options. You have complete flexibility here- you could create an entirely different simulation." + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/index.ipynb b/docs/index.ipynb index 74d4e9d7..670d3987 100644 --- a/docs/index.ipynb +++ b/docs/index.ipynb @@ -73,7 +73,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.14" + "version": "3.12.9" } }, "nbformat": 4, diff --git a/docs/outputs/calculate_average_earnings.md b/docs/outputs/calculate_average_earnings.md new file mode 100644 index 00000000..ab1636d2 --- /dev/null +++ b/docs/outputs/calculate_average_earnings.md @@ -0,0 +1,7 @@ +# Average earnings + +This extension calculates the average earnings of a large population. + +```{eval-rst} +.. autofunction:: policyengine.outputs.macro.single.calculate_average_earnings.calculate_average_earnings +``` \ No newline at end of file diff --git a/policyengine/outputs/household/comparison/calculate_household_comparison.py b/policyengine/outputs/household/comparison/calculate_household_comparison.py index 48c28f0c..756be8e2 100644 --- a/policyengine/outputs/household/comparison/calculate_household_comparison.py +++ b/policyengine/outputs/household/comparison/calculate_household_comparison.py @@ -2,8 +2,7 @@ import typing -if typing.TYPE_CHECKING: - from policyengine import Simulation +from policyengine import Simulation from pydantic import BaseModel from policyengine.utils.calculations import get_change @@ -28,7 +27,7 @@ class HouseholdComparison(BaseModel): def calculate_household_comparison( - simulation: "Simulation", + simulation: Simulation, ) -> HouseholdComparison: """Calculate comparison statistics between two household scenarios.""" if not simulation.is_comparison: diff --git a/policyengine/outputs/household/single/calculate_single_household.py b/policyengine/outputs/household/single/calculate_single_household.py index 858a0d6c..f271ae68 100644 --- a/policyengine/outputs/household/single/calculate_single_household.py +++ b/policyengine/outputs/household/single/calculate_single_household.py @@ -2,8 +2,7 @@ import typing -if typing.TYPE_CHECKING: - from policyengine import Simulation +from policyengine import Simulation from pydantic import BaseModel from policyengine_core.simulations import Simulation as CountrySimulation @@ -34,7 +33,7 @@ class SingleHousehold(BaseModel): def calculate_single_household( - simulation: "Simulation", + simulation: Simulation, ) -> SingleHousehold: """Calculate household statistics for a single household scenario.""" if simulation.is_comparison: diff --git a/policyengine/outputs/macro/comparison/calculate_economy_comparison.py b/policyengine/outputs/macro/comparison/calculate_economy_comparison.py index f9d04fd6..41398eee 100644 --- a/policyengine/outputs/macro/comparison/calculate_economy_comparison.py +++ b/policyengine/outputs/macro/comparison/calculate_economy_comparison.py @@ -2,25 +2,19 @@ import typing -if typing.TYPE_CHECKING: - from policyengine import Simulation +from policyengine import Simulation from pydantic import BaseModel from policyengine.utils.calculations import get_change from policyengine.outputs.macro.single import ( - calculate_government_balance, + _calculate_government_balance, FiscalSummary, - calculate_inequality, + _calculate_inequality, InequalitySummary, - calculate_poverty, ) from .decile import calculate_decile_impacts, DecileImpacts -from .labor_supply import ( - calculate_labor_supply_impact, - LaborSupplyMetricImpact, -) from typing import Literal, List @@ -42,40 +36,10 @@ class InequalityComparison(BaseModel): class Headlines(BaseModel): budgetary_impact: float """The change in the (federal) government budget balance.""" - poverty_impact: float - """The relative change in the regular poverty rate.""" winner_share: float """The share of people that are better off in the reform scenario.""" -class PovertyRateMetricComparison(BaseModel): - age_group: Literal["child", "working_age", "senior", "all"] - """The age group of the population.""" - gender: Literal["male", "female", "all"] - """The gender of the population.""" - racial_group: Literal["white", "black", "hispanic", "other", "all"] - """The racial group of the population.""" - relative: bool - """Whether the poverty rate is relative to the total population, or a headcount.""" - poverty_rate: Literal[ - "regular", - "deep", - "uk_hbai_bhc", - "uk_hbai_bhc_half", - "us_spm", - "us_spm_half", - ] - """The poverty rate definition being calculated.""" - baseline: float - """The poverty rate value in the baseline scenario.""" - reform: float - """The poverty rate value in the reform scenario.""" - change: float - """The change in the poverty rate value.""" - relative_change: float - """The relative change in the poverty rate value.""" - - class EconomyComparison(BaseModel): headlines: Headlines """Headline statistics for the comparison.""" @@ -85,14 +49,10 @@ class EconomyComparison(BaseModel): """Inequality statistics for the household sector.""" distributional: DecileImpacts """Distributional impacts of the reform.""" - poverty: List[PovertyRateMetricComparison] - """Poverty rates for different demographic groups and poverty definitions.""" - labor_supply: List[LaborSupplyMetricImpact] - """Labor supply impacts for different demographic groups and labor supply metrics.""" def calculate_economy_comparison( - simulation: "Simulation", + simulation: Simulation, ) -> EconomyComparison: """Calculate comparison statistics between two economic scenarios.""" if not simulation.is_comparison: @@ -102,8 +62,8 @@ def calculate_economy_comparison( reform = simulation.reform_simulation options = simulation.options - baseline_balance = calculate_government_balance(baseline, options) - reform_balance = calculate_government_balance(reform, options) + baseline_balance = _calculate_government_balance(baseline, options) + reform_balance = _calculate_government_balance(reform, options) balance_change = get_change( baseline_balance, reform_balance, relative=False ) @@ -117,8 +77,8 @@ def calculate_economy_comparison( relative_change=balance_rel_change, ) - baseline_inequality = calculate_inequality(baseline) - reform_inequality = calculate_inequality(reform) + baseline_inequality = _calculate_inequality(baseline) + reform_inequality = _calculate_inequality(reform) inequality_change = get_change( baseline_inequality, reform_inequality, relative=False ) @@ -134,49 +94,11 @@ def calculate_economy_comparison( decile_impacts = calculate_decile_impacts(baseline, reform, options) - baseline_poverty_metrics = calculate_poverty(baseline, options) - reform_poverty_metrics = calculate_poverty(reform, options) - poverty_metrics: List[PovertyRateMetricComparison] = [] - for baseline_metric, reform_metric in zip( - baseline_poverty_metrics, reform_poverty_metrics - ): - change = reform_metric.value - baseline_metric.value - if baseline_metric.value == 0: - rel_change = 0 - else: - rel_change = change / baseline_metric.value - poverty_metrics.append( - PovertyRateMetricComparison( - age_group=baseline_metric.age_group, - gender=baseline_metric.gender, - racial_group=baseline_metric.racial_group, - relative=baseline_metric.relative, - poverty_rate=baseline_metric.poverty_rate, - baseline=baseline_metric.value, - reform=reform_metric.value, - change=change, - relative_change=rel_change, - ) - ) - - labor_supply_metrics = calculate_labor_supply_impact( - baseline, reform, options - ) - # Headlines budgetary_impact = fiscal_comparison.change.federal_balance - poverty_impact = next( - filter( - lambda metric: metric.age_group == "all" - and metric.racial_group == "all" - and metric.poverty_rate == "regular", - poverty_metrics, - ) - ).relative_change winner_share = decile_impacts.income.winners_and_losers.all.gain_share headlines = Headlines( budgetary_impact=budgetary_impact, - poverty_impact=poverty_impact, winner_share=winner_share, ) @@ -185,6 +107,4 @@ def calculate_economy_comparison( fiscal=fiscal_comparison, inequality=inequality_comparison, distributional=decile_impacts, - poverty=poverty_metrics, - labor_supply=labor_supply_metrics, ) diff --git a/policyengine/outputs/macro/comparison/charts/__init__.py b/policyengine/outputs/macro/comparison/charts/__init__.py index 2089605e..9db0bf9c 100644 --- a/policyengine/outputs/macro/comparison/charts/__init__.py +++ b/policyengine/outputs/macro/comparison/charts/__init__.py @@ -2,7 +2,4 @@ from .budget_by_program import create_budget_program_comparison_chart from .decile import create_decile_chart from .winners_losers import create_winners_losers_chart -from .poverty import create_poverty_chart from .inequality import create_inequality_chart -from .labor_supply import create_labor_supply_chart -from .create_all_charts import create_all_charts diff --git a/policyengine/outputs/macro/comparison/charts/budget.py b/policyengine/outputs/macro/comparison/charts/budget.py index d746b077..f7e453fd 100644 --- a/policyengine/outputs/macro/comparison/charts/budget.py +++ b/policyengine/outputs/macro/comparison/charts/budget.py @@ -2,15 +2,14 @@ import plotly.graph_objects as go import typing -if typing.TYPE_CHECKING: - from policyengine import Simulation +from policyengine import Simulation from pydantic import BaseModel from policyengine.utils.charts import * def create_budget_comparison_chart( - simulation: "Simulation", + simulation: Simulation, ) -> go.Figure: """Create a budget comparison chart.""" if not simulation.is_comparison: diff --git a/policyengine/outputs/macro/comparison/charts/budget_by_program.py b/policyengine/outputs/macro/comparison/charts/budget_by_program.py index 27eb23de..382cdfcf 100644 --- a/policyengine/outputs/macro/comparison/charts/budget_by_program.py +++ b/policyengine/outputs/macro/comparison/charts/budget_by_program.py @@ -2,15 +2,14 @@ import plotly.graph_objects as go import typing -if typing.TYPE_CHECKING: - from policyengine import Simulation +from policyengine import Simulation from pydantic import BaseModel from policyengine.utils.charts import * def create_budget_program_comparison_chart( - simulation: "Simulation", + simulation: Simulation, ) -> go.Figure: """Create a budget comparison chart.""" if not simulation.is_comparison: diff --git a/policyengine/outputs/macro/comparison/charts/create_all_charts.py b/policyengine/outputs/macro/comparison/charts/create_all_charts.py deleted file mode 100644 index eb6b2e3a..00000000 --- a/policyengine/outputs/macro/comparison/charts/create_all_charts.py +++ /dev/null @@ -1,65 +0,0 @@ -import plotly.express as px -import plotly.graph_objects as go -import typing - -if typing.TYPE_CHECKING: - from policyengine import Simulation - -from pydantic import BaseModel -from policyengine.utils.charts import * -from .budget import create_budget_comparison_chart -from .budget_by_program import create_budget_program_comparison_chart -from .decile import create_decile_chart -from .winners_losers import create_winners_losers_chart -from .poverty import create_poverty_chart -from .inequality import create_inequality_chart -from .labor_supply import create_labor_supply_chart -from typing import Any - - -class MacroCharts(BaseModel): - budget: Any - budget_programs: Any - decile_income_relative: Any - decile_income_average: Any - decile_wealth_relative: Any - decile_wealth_average: Any - winners_and_losers_income_decile: Any - winners_and_losers_wealth_decile: Any - - -def create_all_charts( - simulation: "Simulation", -) -> MacroCharts: - """Create all charts.""" - if not simulation.is_comparison: - raise ValueError("Simulation must be a comparison simulation.") - if not simulation.options.scope == "macro": - raise ValueError( - "This function is only available for macro simulations." - ) - - return MacroCharts( - budget=create_budget_comparison_chart(simulation).to_dict(), - budget_programs=create_budget_program_comparison_chart( - simulation - ).to_dict(), - decile_income_relative=create_decile_chart( - simulation, "income", True - ).to_dict(), - decile_income_average=create_decile_chart( - simulation, "income", False - ).to_dict(), - decile_wealth_relative=create_decile_chart( - simulation, "wealth", True - ).to_dict(), - decile_wealth_average=create_decile_chart( - simulation, "wealth", True - ).to_dict(), - winners_and_losers_income_decile=create_winners_losers_chart( - simulation, "income" - ).to_dict(), - winners_and_losers_wealth_decile=create_winners_losers_chart( - simulation, "wealth" - ).to_dict(), - ) diff --git a/policyengine/outputs/macro/comparison/charts/decile.py b/policyengine/outputs/macro/comparison/charts/decile.py index d17c5bf2..bd2c69e7 100644 --- a/policyengine/outputs/macro/comparison/charts/decile.py +++ b/policyengine/outputs/macro/comparison/charts/decile.py @@ -2,8 +2,7 @@ import plotly.graph_objects as go import typing -if typing.TYPE_CHECKING: - from policyengine import Simulation +from policyengine import Simulation from pydantic import BaseModel from policyengine.utils.charts import * @@ -11,7 +10,7 @@ def create_decile_chart( - simulation: "Simulation", + simulation: Simulation, decile_variable: Literal["income", "wealth"], relative: bool, ) -> go.Figure: diff --git a/policyengine/outputs/macro/comparison/charts/inequality.py b/policyengine/outputs/macro/comparison/charts/inequality.py index 5fa00d39..e0db5c29 100644 --- a/policyengine/outputs/macro/comparison/charts/inequality.py +++ b/policyengine/outputs/macro/comparison/charts/inequality.py @@ -2,15 +2,14 @@ import plotly.graph_objects as go import typing -if typing.TYPE_CHECKING: - from policyengine import Simulation +from policyengine import Simulation from pydantic import BaseModel from policyengine.utils.charts import * def create_inequality_chart( - simulation: "Simulation", + simulation: Simulation, relative: bool, ) -> go.Figure: """Create a budget comparison chart.""" diff --git a/policyengine/outputs/macro/comparison/charts/labor_supply.py b/policyengine/outputs/macro/comparison/charts/labor_supply.py deleted file mode 100644 index c3946e60..00000000 --- a/policyengine/outputs/macro/comparison/charts/labor_supply.py +++ /dev/null @@ -1,126 +0,0 @@ -import plotly.express as px -import plotly.graph_objects as go -import typing - -if typing.TYPE_CHECKING: - from policyengine import Simulation - from policyengine.outputs.macro.comparison.calculate_economy_comparison import ( - LaborSupplyMetricImpact, - ) - -from pydantic import BaseModel -from policyengine.utils.charts import * -from typing import Literal - - -def create_labor_supply_chart( - simulation: "Simulation", - decile: Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, "all"] | None, - elasticity: Literal["income", "substitution", "all"] | None, - unit: Literal["earnings", "hours"] | None, - change_relative: bool = True, - change_average: bool = False, -) -> go.Figure: - """Create a budget comparison chart.""" - if not simulation.is_comparison: - raise ValueError("Simulation must be a comparison simulation.") - - economy = simulation.calculate_economy_comparison() - - def lsr_filter(comparison: "LaborSupplyMetricImpact"): - if decile is not None: - if comparison.decile != decile: - return False - if elasticity is not None: - if comparison.elasticity != elasticity: - return False - if unit is not None: - if comparison.unit != unit: - return False - return True - - overall_lsr_impact = list( - filter( - lambda comparison: comparison.decile == "all" - and comparison.elasticity == "all" - and comparison.unit == "earnings", - economy.labor_supply, - ) - )[0].relative_change - - overall_lsr_impact = round(overall_lsr_impact, 3) - - if overall_lsr_impact > 0: - description = f"raise labor supply by {overall_lsr_impact:.1%}" - elif overall_lsr_impact < 0: - description = f"lower labor supply by {-overall_lsr_impact:.1%}" - else: - description = "have no effect on labor supply" - - lsr_impacts = list( - filter( - lsr_filter, - economy.labor_supply, - ) - ) - - if decile is None: - x_values = [lsr.decile for lsr in lsr_impacts] - x_titles = list(range(1, 11)) - elif elasticity is None: - x_values = [lsr.elasticity for lsr in lsr_impacts] - x_titles = ["Income", "Substitution", "All"] - elif unit is None: - x_values = [lsr.unit for lsr in lsr_impacts] - x_titles = ["Earnings", "Hours"] - - if change_relative: - y_values = [lsr.relative_change for lsr in lsr_impacts] - elif change_average: - y_values = [lsr.average_change for lsr in lsr_impacts] - else: - y_values = [lsr.change / 1e9 for lsr in lsr_impacts] - - colors = [BLUE if value > 0 else DARK_GRAY for value in y_values] - text = [ - ( - f"{value:.1%}" - if change_relative - else ( - f"{value:,.1%}" - if change_relative - else ( - f"${value:.1f}bn" - if not change_average - else f"${value:,.0f}" - ) - ) - ) - for value in y_values - ] - - fig = go.Figure( - data=[ - go.Bar( - x=x_values, - y=y_values, - marker=dict(color=colors), - text=text, - ) - ] - ).update_layout( - title=f"{simulation.options.title} would {description}", - yaxis_title="Labor supply change" - + (" ($bn)" if not change_average else ""), - yaxis_tickformat=(".0%" if change_relative else ",.0f"), - yaxis_ticksuffix=( - " ($bn)" if not change_average and not change_relative else "" - ), - xaxis_title="Group", - xaxis_tickvals=x_values, - xaxis_ticktext=x_titles, - ) - - return format_fig( - fig, country=simulation.options.country, add_zero_line=True - ) diff --git a/policyengine/outputs/macro/comparison/charts/poverty.py b/policyengine/outputs/macro/comparison/charts/poverty.py deleted file mode 100644 index 271071fb..00000000 --- a/policyengine/outputs/macro/comparison/charts/poverty.py +++ /dev/null @@ -1,139 +0,0 @@ -import plotly.express as px -import plotly.graph_objects as go -import typing - -if typing.TYPE_CHECKING: - from policyengine import Simulation - from policyengine.outputs.macro.comparison.calculate_economy_comparison import ( - PovertyRateMetricComparison, - ) - -from pydantic import BaseModel -from policyengine.utils.charts import * -from typing import Literal - - -def create_poverty_chart( - simulation: "Simulation", - age_group: str, - gender: str, - racial_group: str, - poverty_rate: str, - rate_relative: bool, - change_relative: bool = True, -) -> go.Figure: - """Create a budget comparison chart.""" - if not simulation.is_comparison: - raise ValueError("Simulation must be a comparison simulation.") - - economy = simulation.calculate_economy_comparison() - - def poverty_filter(comparison: "PovertyRateMetricComparison"): - if age_group is not None: - if comparison.age_group != age_group: - return False - if racial_group is not None: - if comparison.racial_group != racial_group: - return False - if gender is not None: - if comparison.gender != gender: - return False - if poverty_rate is not None: - if comparison.poverty_rate != poverty_rate: - return False - if rate_relative is not None: - if comparison.relative != rate_relative: - return False - return True - - poverty_rates = list( - filter( - poverty_filter, - economy.poverty, - ) - ) - - if len(poverty_rates) == 0: - raise ValueError("No data found for the selected filters.") - - overall_poverty_rate = list( - filter( - lambda comparison: comparison.age_group == "all" - and comparison.racial_group == "all" - and comparison.gender == "all" - and comparison.poverty_rate == (poverty_rate or "regular"), - economy.poverty, - ) - )[0].relative_change - - overall_poverty_rate = round(overall_poverty_rate, 3) - - if overall_poverty_rate > 0: - description = f"raise the {'deep ' if poverty_rate == 'deep' else ''}poverty rate by {overall_poverty_rate:.1%}" - elif overall_poverty_rate < 0: - description = f"lower the {'deep ' if poverty_rate == 'deep' else ''}poverty rate by {-overall_poverty_rate:.1%}" - else: - description = "have no effect on the poverty rate" - - if age_group is None: - x_values = [poverty.age_group for poverty in poverty_rates] - x_titles = ["Child", "Working-age", "Senior", "All"] - elif gender is None: - x_values = [poverty.gender for poverty in poverty_rates] - x_titles = ["Male", "Female", "All"] - elif racial_group is None: - x_values = [poverty.racial_group for poverty in poverty_rates] - x_titles = ["White", "Black", "Hispanic", "Other", "All"] - elif poverty_rate is None: - x_values = [poverty.poverty_rate for poverty in poverty_rates] - x_titles = ["Regular", "Deep", "All"] - elif rate_relative is None: - x_values = [poverty.relative for poverty in poverty_rates] - x_titles = ["Relative", "Headcount", "All"] - - if change_relative: - y_values = [poverty.relative_change for poverty in poverty_rates] - elif not rate_relative: - y_values = [poverty.change for poverty in poverty_rates] - else: - y_values = [poverty.change * 100 for poverty in poverty_rates] - - colors = [BLUE if value > 0 else DARK_GRAY for value in y_values] - text = [ - ( - f"{value:.1%}" - if change_relative - else (f"{value:,.0f}" if not rate_relative else f"{value:.1f}pp") - ) - for value in y_values - ] - - fig = go.Figure( - data=[ - go.Bar( - x=x_values, - y=y_values, - marker=dict(color=colors), - text=text, - ) - ] - ).update_layout( - title=f"{simulation.options.title} would {description}", - yaxis_title="Poverty rate change" - + (" (%)" if rate_relative and not change_relative else ""), - yaxis_tickformat=( - ".0%" - if change_relative - else (",.0f" if not rate_relative else ".1f") - ), - yaxis_ticksuffix=( - "pp" if (rate_relative and not change_relative) else "" - ), - xaxis_title="Group", - xaxis_tickvals=x_values, - xaxis_ticktext=x_titles, - ) - - return format_fig( - fig, country=simulation.options.country, add_zero_line=True - ) diff --git a/policyengine/outputs/macro/comparison/charts/winners_losers.py b/policyengine/outputs/macro/comparison/charts/winners_losers.py index 9d17d495..db21e18d 100644 --- a/policyengine/outputs/macro/comparison/charts/winners_losers.py +++ b/policyengine/outputs/macro/comparison/charts/winners_losers.py @@ -2,8 +2,7 @@ import plotly.graph_objects as go import typing -if typing.TYPE_CHECKING: - from policyengine import Simulation +from policyengine import Simulation from pydantic import BaseModel from policyengine.utils.charts import * @@ -28,7 +27,7 @@ def create_winners_losers_chart( - simulation: "Simulation", + simulation: Simulation, decile_variable: Literal["income", "wealth"], ) -> go.Figure: """Create a budget comparison chart.""" diff --git a/policyengine/outputs/macro/comparison/decile.py b/policyengine/outputs/macro/comparison/decile.py index a7041e7c..9c84b295 100644 --- a/policyengine/outputs/macro/comparison/decile.py +++ b/policyengine/outputs/macro/comparison/decile.py @@ -1,7 +1,6 @@ import typing -if typing.TYPE_CHECKING: - from policyengine import Simulation, SimulationOptions +from policyengine import Simulation, SimulationOptions from policyengine_core.simulations import Microsimulation diff --git a/policyengine/outputs/macro/comparison/labor_supply.py b/policyengine/outputs/macro/comparison/labor_supply.py deleted file mode 100644 index fd8269e6..00000000 --- a/policyengine/outputs/macro/comparison/labor_supply.py +++ /dev/null @@ -1,130 +0,0 @@ -import typing - -if typing.TYPE_CHECKING: - from policyengine import Simulation, SimulationOptions - -from policyengine_core.simulations import Microsimulation - -from pydantic import BaseModel -from typing import Literal, List -import numpy as np - - -class LaborSupplyMetricImpact(BaseModel): - elasticity: Literal["income", "substitution", "all"] - """Filter to the effects of a specific elasticity.""" - decile: Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, "all"] - """The income decile of the household, if filtering.""" - unit: Literal["earnings", "hours"] - """The unit of the labor supply metric.""" - baseline: float - """The labor supply metric value in the baseline scenario.""" - reform: float - """The labor supply metric value in the reform scenario.""" - change: float - """The change in the labor supply metric value.""" - relative_change: float - """The relative change in the labor supply metric value.""" - average_change: float - """The average change in the labor supply metric value (per household).""" - - -def calculate_labor_supply_impact( - baseline: Microsimulation, - reformed: Microsimulation, - options: "SimulationOptions", -) -> List[LaborSupplyMetricImpact]: - """Calculate labor supply impact statistics for a set of households.""" - if not _has_behavioral_response(reformed): - return [] - - lsr_metrics = [] - for elasticity in ["income", "substitution", "all"]: - for decile in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, "all"]: - for unit in ["earnings", "hours"]: - if options.country != "us" and unit == "hours": - # Hours not yet supported for the UK. - continue - lsr_metrics.append( - calculate_specific_lsr_metric( - baseline, - reformed, - elasticity, - decile, - unit, - ) - ) - - return lsr_metrics - - -def calculate_specific_lsr_metric( - baseline: Microsimulation, - reformed: Microsimulation, - elasticity: Literal["income", "substitution", "all"], - decile: Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, "all"], - unit: Literal["earnings", "hours"], -) -> LaborSupplyMetricImpact: - """Calculate a specific labor supply metric for a set of households.""" - - if unit == "earnings": - baseline_variable = "employment_income" - if elasticity == "income": - variable = "income_elasticity_lsr" - elif elasticity == "substitution": - variable = "substitution_elasticity_lsr" - else: - variable = "employment_income_behavioral_response" - else: - baseline_variable = "weekly_hours_worked" - if elasticity == "income": - variable = ( - "weekly_hours_worked_behavioural_response_income_elasticity" - ) - elif elasticity == "substitution": - variable = "weekly_hours_worked_behavioural_response_substitution_elasticity" - else: - variable = "weekly_hours_worked" - - baseline_values = baseline.calculate(baseline_variable, map_to="household") - reform_values = reformed.calculate( - baseline_variable, map_to="household" - ) + reformed.calculate(variable, map_to="household") - - if decile == "all": - in_decile = np.ones_like(baseline_values, dtype=bool) - else: - in_decile = ( - reformed.calculate("household_income_decile").values == decile - ) - - baseline_total = (baseline_values * in_decile).sum() - reform_total = (reform_values * in_decile).sum() - households = ( - in_decile * baseline.calculate("household_weight").values - ).sum() - change = reform_total - baseline_total - average_change = change / households - relative_change = change / baseline_total - - return LaborSupplyMetricImpact( - elasticity=elasticity, - decile=decile, - unit=unit, - baseline=baseline_total, - reform=reform_total, - change=change, - average_change=average_change, - relative_change=relative_change, - ) - - -def _has_behavioral_response(simulation: Microsimulation) -> bool: - """Check if the simulation has a behavioral response to labor supply.""" - return ( - "employment_income_behavioral_response" - in simulation.tax_benefit_system.variables - and any( - simulation.calculate("employment_income_behavioral_response") != 0 - ) - ) diff --git a/policyengine/outputs/macro/single/__init__.py b/policyengine/outputs/macro/single/__init__.py index 50b6e74a..132080a5 100644 --- a/policyengine/outputs/macro/single/__init__.py +++ b/policyengine/outputs/macro/single/__init__.py @@ -1,3 +1,2 @@ -from .budget import calculate_government_balance, FiscalSummary -from .inequality import calculate_inequality, InequalitySummary -from .poverty import calculate_poverty, PovertyRateMetric +from .budget import _calculate_government_balance, FiscalSummary +from .inequality import _calculate_inequality, InequalitySummary diff --git a/policyengine/outputs/macro/single/budget.py b/policyengine/outputs/macro/single/budget.py index d5fbdb1a..468ec6c8 100644 --- a/policyengine/outputs/macro/single/budget.py +++ b/policyengine/outputs/macro/single/budget.py @@ -1,7 +1,6 @@ import typing -if typing.TYPE_CHECKING: - from policyengine import Simulation, SimulationOptions +from policyengine import Simulation, SimulationOptions from policyengine_core.simulations import Microsimulation @@ -47,7 +46,7 @@ class FiscalSummary(BaseModel): """The total net income of the households in the simulation.""" -def calculate_government_balance( +def _calculate_government_balance( simulation: Microsimulation, options: "SimulationOptions", ) -> FiscalSummary: diff --git a/policyengine/outputs/macro/single/calculate_average_earnings.py b/policyengine/outputs/macro/single/calculate_average_earnings.py new file mode 100644 index 00000000..764071e1 --- /dev/null +++ b/policyengine/outputs/macro/single/calculate_average_earnings.py @@ -0,0 +1,9 @@ +from policyengine import Simulation + + +def calculate_average_earnings(simulation: Simulation) -> float: + """Calculate average earnings.""" + employment_income = simulation.baseline_simulation.calculate( + "employment_income" + ) + return employment_income[employment_income > 0].median() diff --git a/policyengine/outputs/macro/single/calculate_single_economy.py b/policyengine/outputs/macro/single/calculate_single_economy.py index 12f88986..206fd489 100644 --- a/policyengine/outputs/macro/single/calculate_single_economy.py +++ b/policyengine/outputs/macro/single/calculate_single_economy.py @@ -2,14 +2,12 @@ import typing -if typing.TYPE_CHECKING: - from policyengine import Simulation +from policyengine import Simulation from pydantic import BaseModel -from .budget import FiscalSummary, calculate_government_balance -from .inequality import InequalitySummary, calculate_inequality -from .poverty import PovertyRateMetric, calculate_poverty +from .budget import FiscalSummary, _calculate_government_balance +from .inequality import InequalitySummary, _calculate_inequality from typing import List @@ -18,12 +16,10 @@ class SingleEconomy(BaseModel): """Government budgets and other top-level fiscal statistics.""" inequality: InequalitySummary """Inequality statistics for the household sector.""" - poverty: List[PovertyRateMetric] - """Poverty rates for different demographic groups and poverty definitions.""" def calculate_single_economy( - simulation: "Simulation", + simulation: Simulation, ) -> SingleEconomy: """Calculate economy statistics for a single economic scenario.""" options = simulation.options @@ -32,14 +28,12 @@ def calculate_single_economy( "This function is for single economy simulations only." ) - fiscal = calculate_government_balance( + fiscal = _calculate_government_balance( simulation.baseline_simulation, options ) - inequality = calculate_inequality(simulation.baseline_simulation) - poverty = calculate_poverty(simulation.baseline_simulation, options) + inequality = _calculate_inequality(simulation.baseline_simulation) return SingleEconomy( fiscal=fiscal, inequality=inequality, - poverty=poverty, ) diff --git a/policyengine/outputs/macro/single/inequality.py b/policyengine/outputs/macro/single/inequality.py index 23cb66f2..827a9b6a 100644 --- a/policyengine/outputs/macro/single/inequality.py +++ b/policyengine/outputs/macro/single/inequality.py @@ -1,7 +1,6 @@ import typing -if typing.TYPE_CHECKING: - from policyengine import Simulation, SimulationOptions +from policyengine import Simulation, SimulationOptions from policyengine_core.simulations import Microsimulation @@ -17,7 +16,7 @@ class InequalitySummary(BaseModel): """The share of total income held by the top 1% of households.""" -def calculate_inequality( +def _calculate_inequality( simulation: Microsimulation, ): """Calculate inequality statistics for a set of households.""" @@ -26,7 +25,11 @@ def calculate_inequality( household_count_people = simulation.calculate("household_count_people") income.weights *= household_count_people personal_hh_equiv_income = income - gini = personal_hh_equiv_income.gini() + try: + gini = personal_hh_equiv_income.gini() + except: + print("WARNING: Gini calculation failed. Setting to 0.4.") + gini = 0.4 in_top_10_pct = personal_hh_equiv_income.decile_rank() == 10 in_top_1_pct = personal_hh_equiv_income.percentile_rank() == 100 diff --git a/policyengine/outputs/macro/single/poverty.py b/policyengine/outputs/macro/single/poverty.py deleted file mode 100644 index f0f2c93c..00000000 --- a/policyengine/outputs/macro/single/poverty.py +++ /dev/null @@ -1,121 +0,0 @@ -import typing - -if typing.TYPE_CHECKING: - from policyengine import Simulation, SimulationOptions - -from policyengine_core.simulations import Microsimulation - -from pydantic import BaseModel -from typing import Literal, List -import numpy as np - - -class PovertyRateMetric(BaseModel): - age_group: Literal["child", "working_age", "senior", "all"] - """The age group of the population.""" - racial_group: Literal["white", "black", "hispanic", "other", "all"] - """The racial group of the population.""" - gender: Literal["male", "female", "all"] - """The gender of the population.""" - relative: bool - """Whether the poverty rate is relative to the total population, or a headcount.""" - poverty_rate: Literal[ - "regular", - "deep", - "uk_hbai_bhc", - "uk_hbai_bhc_half", - "us_spm", - "us_spm_half", - ] - """The poverty rate definition being calculated.""" - value: float - """The poverty rate value.""" - - -AGE_BOUNDS = { - "child": (0, 18), - "working_age": (18, 65), - "senior": (65, np.inf), - "all": (0, np.inf), -} - - -def calculate_poverty( - simulation: Microsimulation, - options: "SimulationOptions", -) -> List[PovertyRateMetric]: - """Calculate poverty statistics for a set of households.""" - - poverty_metrics = [] - age = simulation.calculate("age") - if options.country == "uk": - gender = simulation.calculate("gender") - else: - gender = simulation.calculate("is_male").map( - { - True: "MALE", - False: "FEMALE", - } - ) - person_weight = simulation.calculate("person_weight").values - if options.country == "us": - racial_groups = ["white", "black", "hispanic", "other", "all"] - else: - racial_groups = ["all"] - for age_group in ["child", "working_age", "senior", "all"]: - lower_age, upper_age = AGE_BOUNDS[age_group] - in_age_group = (age >= lower_age) & (age < upper_age) - for gender_group in ["male", "female", "all"]: - in_gender = (gender_group == "all") | ( - gender == gender_group.upper() - ) - for racial_group in racial_groups: - if racial_group != "all": - in_racial_group = ( - simulation.calculate("race") == racial_group - ) - else: - in_racial_group = np.ones_like(age, dtype=bool) - for relative in [True, False]: - for poverty_rate in ["regular", "deep"]: - if poverty_rate in ( - "regular", - "uk_hbai_bhc", - "us_spm", - ): - in_poverty = simulation.calculate( - "in_poverty", map_to="person" - ) - elif poverty_rate in ( - "deep", - "uk_hbai_bhc_half", - "us_spm_half", - ): - in_poverty = simulation.calculate( - "in_deep_poverty", map_to="person" - ) - - in_group = np.array( - in_age_group & in_racial_group & in_gender - ) - total_in_group = (in_group * person_weight).sum() - total_in_group_in_poverty = ( - in_group * in_poverty * person_weight - ).sum() - if relative: - result = total_in_group_in_poverty / total_in_group - else: - result = total_in_group_in_poverty - - poverty_metrics.append( - PovertyRateMetric( - age_group=age_group, - gender=gender_group, - racial_group=racial_group, - relative=relative, - poverty_rate=poverty_rate, - value=result, - ) - ) - - return poverty_metrics diff --git a/policyengine/simulation.py b/policyengine/simulation.py index 0e3e409b..bfadd921 100644 --- a/policyengine/simulation.py +++ b/policyengine/simulation.py @@ -23,30 +23,9 @@ from pathlib import Path import pandas as pd from typing import Type -from functools import wraps -from .outputs.macro.comparison.calculate_economy_comparison import ( - calculate_economy_comparison, - EconomyComparison, -) -from .outputs.macro.single.calculate_single_economy import ( - SingleEconomy, - calculate_single_economy, -) - -from .outputs.household.single.calculate_single_household import ( - SingleHousehold, - calculate_single_household, -) - -from .outputs.household.comparison.calculate_household_comparison import ( - HouseholdComparison, - calculate_household_comparison, -) -from .outputs.macro.comparison.charts.create_all_charts import ( - create_all_charts, - MacroCharts, -) -from typing import Any, Tuple +from functools import wraps, partial +from typing import Dict, Any, Callable +import importlib CountryType = Literal["uk", "us"] ScopeType = Literal["household", "macro"] @@ -93,7 +72,7 @@ class Simulation: reform_simulation: CountrySimulation | None = None """The reform tax-benefit simulation.""" - def __init__(self, options: SimulationOptions): + def __init__(self, **options: SimulationOptions): self.options = SimulationOptions(**options) if self.options.data is None: @@ -102,6 +81,38 @@ def __init__(self, options: SimulationOptions): ] self._initialise_simulations() + self._add_output_functions() + + def _add_output_functions(self): + folder = Path(__file__).parent / "outputs" + + for module in folder.glob("**/*.py"): + if module.stem == "__init__": + continue + python_module = ( + module.relative_to(folder.parent) + .with_suffix("") + .as_posix() + .replace("/", ".") + ) + module = importlib.import_module("policyengine." + python_module) + for name in dir(module): + func = getattr(module, name) + if isinstance(func, Callable): + if hasattr(func, "__annotations__"): + if ( + func.__annotations__.get("simulation") + == Simulation + ): + wrapped_func = wraps(func)( + partial(func, simulation=self) + ) + wrapped_func.__annotations__ = func.__annotations__ + setattr( + self, + func.__name__, + wrapped_func, + ) def _set_data(self): if self.options.data is None: @@ -317,43 +328,3 @@ def _data_handle_cps_special_case(self): version=version, ) self.data = Dataset.from_file(self.data, "2023") - - def calculate( - self, - ) -> ( - SingleEconomy - | EconomyComparison - | SingleHousehold - | HouseholdComparison - ): - """Calculate the default output statistics for the simulation type.""" - if self.options.scope == "macro": - if self.is_comparison: - return self.calculate_economy_comparison() - else: - return self.calculate_single_economy() - elif self.options.scope == "household": - if self.is_comparison: - return self.calculate_household_comparison() - else: - return self.calculate_single_household() - - def calculate_economy_comparison(self) -> EconomyComparison: - """Calculate comparison statistics between two economic scenarios.""" - return calculate_economy_comparison(self) - - def calculate_single_economy(self) -> SingleEconomy: - """Calculate economy statistics for a single economic scenario.""" - return calculate_single_economy(self) - - def calculate_single_household(self) -> SingleHousehold: - """Calculate household statistics for a single household scenario.""" - return calculate_single_household(self) - - def calculate_household_comparison(self) -> HouseholdComparison: - """Calculate comparison statistics between two household scenarios.""" - return calculate_household_comparison(self) - - def create_all_charts(self) -> MacroCharts: - """Create all macro charts for the simulation.""" - return create_all_charts(self) diff --git a/pyproject.toml b/pyproject.toml index 2867418b..a173d5e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "policyengine" -version = "2.7.1" +version = "0.1.0" description = "A package to conduct policy analysis using PolicyEngine tax-benefit models." readme = "README.md" authors = [ diff --git a/tests/country/test_average_earnings.py b/tests/country/test_average_earnings.py new file mode 100644 index 00000000..8ce4b641 --- /dev/null +++ b/tests/country/test_average_earnings.py @@ -0,0 +1,11 @@ +def test_average_earnings(): + from policyengine import Simulation + + sim = Simulation( + country="uk", + scope="macro", + ) + + average_earnings = sim.calculate_average_earnings() + + assert (average_earnings < 50_000) and (average_earnings > 20_000) diff --git a/tests/country/test_uk.py b/tests/country/test_uk.py index 27a1022d..6f083aaa 100644 --- a/tests/country/test_uk.py +++ b/tests/country/test_uk.py @@ -2,10 +2,8 @@ def test_uk_macro_single(): from policyengine import Simulation sim = Simulation( - { - "scope": "macro", - "country": "uk", - } + scope="macro", + country="uk", ) sim.calculate_single_economy() @@ -15,13 +13,11 @@ def test_uk_macro_comparison(): from policyengine import Simulation sim = Simulation( - { - "scope": "macro", - "country": "uk", - "reform": { - "gov.hmrc.income_tax.allowances.personal_allowance.amount": 15_000, - }, - } + scope="macro", + country="uk", + reform={ + "gov.hmrc.income_tax.allowances.personal_allowance.amount": 15_000, + }, ) sim.calculate_economy_comparison() diff --git a/tests/country/test_us.py b/tests/country/test_us.py index 54add4d5..c4b8f6f5 100644 --- a/tests/country/test_us.py +++ b/tests/country/test_us.py @@ -2,10 +2,8 @@ def test_us_macro_single(): from policyengine import Simulation sim = Simulation( - { - "scope": "macro", - "country": "us", - } + scope="macro", + country="us", ) sim.calculate_single_economy() @@ -15,11 +13,11 @@ def test_us_macro_comparison(): from policyengine import Simulation sim = Simulation( - { - "scope": "macro", - "country": "us", - "reform": {}, - } + scope="macro", + country="us", + reform={ + "gov.usda.snap.income.deductions.earned_income": {"2025": 0.05} + }, ) sim.calculate_economy_comparison()