Skip to content

Commit

Permalink
Add API handling for reforms
Browse files Browse the repository at this point in the history
  • Loading branch information
nikhilwoodruff committed Nov 1, 2023
1 parent 922c2f4 commit 2bb8b86
Show file tree
Hide file tree
Showing 4 changed files with 331 additions and 9 deletions.
3 changes: 2 additions & 1 deletion policyengine_core/charts/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from .formatting import (
BLUE,
BLUE_COLOR_SCALE,
BLUE_COLOUR_SCALE,
DARK_GREEN,
LIGHT_GREEN,
DARK_GRAY,
Expand All @@ -12,3 +12,4 @@
)

from .bar import *
from .api import *
228 changes: 228 additions & 0 deletions policyengine_core/charts/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import requests
import time
import plotly.graph_objects as go
from .formatting import (
DARK_GRAY,
MEDIUM_DARK_GRAY,
WHITE,
BLUE_LIGHT,
BLUE_PRIMARY,
format_fig,
)


def get_api_chart_data(
country_id: str,
reform_policy_id: int,
chart_key: str,
region: str,
time_period: str,
baseline_policy_id: int = None,
version: str = None,
) -> dict:
if baseline_policy_id is None or version is None:
response = requests.get(
f"https://api.policyengine.org/{country_id}/metadata"
)
result = response.json().get("result", {})
baseline_policy_id = result.get("current_law_id")
version = result.get("version")
url = f"https://api.policyengine.org/{country_id}/economy/{reform_policy_id}/over/{baseline_policy_id}?region={region}&time_period={time_period}&version={version}"
response = requests.get(url)
if not response.ok:
raise ValueError(response.text)
json = response.json()
while json.get("status") == "computing":
time.sleep(1)
response = requests.get(url)
if not response.ok:
raise ValueError(response.text)
json = response.json()
return json.get("result", {}).get(chart_key)


# Specific chart definitions


def intra_decile_chart(
country_id: str,
reform_policy_id: int,
region: str,
time_period: str,
baseline_policy_id: int = None,
) -> go.Figure:
impact = get_api_chart_data(
country_id=country_id,
reform_policy_id=reform_policy_id,
chart_key="intra_decile",
baseline_policy_id=baseline_policy_id,
region=region,
time_period=time_period,
)
impact = {"intra_decile": impact}

decile_numbers = list(range(1, 11))
outcome_labels = [
"Gain more than 5%",
"Gain less than 5%",
"No change",
"Lose less than 5%",
"Lose more than 5%",
]
outcome_colours = [
DARK_GRAY,
MEDIUM_DARK_GRAY,
WHITE,
BLUE_LIGHT,
BLUE_PRIMARY,
][::-1]

data = []

for outcome_label, outcome_colour in zip(outcome_labels, outcome_colours):
data.append(
{
"type": "bar",
"y": ["All"],
"x": [impact["intra_decile"]["all"][outcome_label]],
"name": outcome_label,
"legendgroup": outcome_label,
"offsetgroup": outcome_label,
"marker": {
"color": outcome_colour,
},
"orientation": "h",
"text": [
f"{impact['intra_decile']['all'][outcome_label] * 100:.0f}%"
],
"textposition": "inside",
"textangle": 0,
"xaxis": "x",
"yaxis": "y",
"showlegend": False,
"hoverinfo": "none",
}
)

for outcome_label, outcome_colour in zip(outcome_labels, outcome_colours):
data.append(
{
"type": "bar",
"y": decile_numbers,
"x": impact["intra_decile"]["deciles"][outcome_label],
"name": outcome_label,
"marker": {
"color": outcome_colour,
},
"orientation": "h",
"text": [
f"{value * 100:.0f}%"
for value in impact["intra_decile"]["deciles"][
outcome_label
]
],
"textposition": "inside",
"textangle": 0,
"xaxis": "x2",
"yaxis": "y2",
"hoverinfo": "none",
}
)

layout = {
"barmode": "stack",
"grid": {
"rows": 2,
"columns": 1,
},
"yaxis": {
"title": "",
"tickvals": ["All"],
"domain": [0.91, 1],
},
"xaxis": {
"title": "",
"tickformat": ".0%",
"anchor": "y",
"matches": "x2",
"showgrid": False,
"showticklabels": False,
},
"xaxis2": {
"title": "Population share",
"tickformat": ".0%",
"anchor": "y2",
},
"yaxis2": {
"title": "Income decile",
"tickvals": decile_numbers,
"anchor": "x2",
"domain": [0, 0.85],
},
"uniformtext": {
"mode": "hide",
"minsize": 10,
},
}

return format_fig(go.Figure(data=data, layout=layout)).update_traces(
marker_line_width=0,
width=[0.9] * 10,
)


def decile_chart(
country_id: str,
reform_policy_id: int,
region: str,
time_period: str,
baseline_policy_id: int = None,
) -> go.Figure:
impact = get_api_chart_data(
country_id=country_id,
reform_policy_id=reform_policy_id,
chart_key="decile",
baseline_policy_id=baseline_policy_id,
region=region,
time_period=time_period,
)
impact = {"decile": impact}

decile_numbers = list(range(1, 11))
# Sort deciles by key order 1 to 10
decile_values = []
for i in decile_numbers:
decile_values.append(impact["decile"]["relative"][str(i)])

data = [
{
"x": decile_numbers,
"y": decile_values,
"type": "bar",
"marker": {
"color": [
DARK_GRAY if value < 0 else BLUE_PRIMARY
for value in decile_values
],
},
"text": [f"{value:+.1%}" for value in decile_values],
"textangle": 0,
}
]

layout = {
"xaxis": {
"title": "Income decile",
"tickvals": decile_numbers,
},
"yaxis": {
"title": "Relative change",
"tickformat": "+,.0%",
},
"uniformtext": {
"mode": "hide",
"minsize": 8,
},
}

return format_fig(go.Figure(data=data, layout=layout))
28 changes: 20 additions & 8 deletions policyengine_core/charts/formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,30 @@
from IPython.display import HTML


WHITE = "#FFF"
BLUE = "#2C6496"
GRAY = "#BDBDBD"
MEDIUM_DARK_GRAY = "#D2D2D2"
DARK_GRAY = "#616161"
GREEN = "#29d40f"
LIGHT_GRAY = "#F2F2F2"
LIGHT_GREEN = "#C5E1A5"
DARK_GREEN = "#558B2F"
BLACK = "#000"
BLUE_LIGHT = "#D8E6F3"
BLUE_PRIMARY = BLUE = "#2C6496"
BLUE_PRESSED = "#17354F"
BLUE_98 = "#F7FAFD"
TEAL_LIGHT = "#D7F4F2"
TEAL_ACCENT = "#39C6C0"
TEAL_PRESSED = "#227773"
DARKEST_BLUE = "#0C1A27"
DARK_GRAY = "#616161"
GRAY = "#808080"
LIGHT_GRAY = "#F2F2F2"
MEDIUM_DARK_GRAY = "#D2D2D2"
WHITE = "#FFFFFF"
TEAL_98 = "#F7FDFC"
BLACK = "#000000"

BLUE_COLOR_SCALE = [BLUE, "#265782", "#20496E", "#1A3C5A"]
BLUE_COLOUR_SCALE = [
BLUE_LIGHT,
BLUE_PRIMARY,
BLUE_PRESSED,
]


def format_fig(fig: go.Figure) -> go.Figure:
Expand Down
81 changes: 81 additions & 0 deletions policyengine_core/reforms/reform.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@

from policyengine_core.parameters import ParameterNode, Parameter
from policyengine_core.taxbenefitsystems import TaxBenefitSystem
from policyengine_core.periods import period as period_

import requests


class classproperty(object):
def __init__(self, f):
self.f = f

def __get__(self, obj, owner):
return self.f(owner)

Check warning on line 18 in policyengine_core/reforms/reform.py

View check run for this annotation

Codecov / codecov/patch

policyengine_core/reforms/reform.py#L18

Added line #L18 was not covered by tests


class Reform(TaxBenefitSystem):
Expand Down Expand Up @@ -37,6 +48,13 @@ class Reform(TaxBenefitSystem):
"""

name: str = None
"""The name of the reform. This is used to identify the reform in the UI."""

country_id: str = None
"""The country id of the reform. This is used to inform any calls to the PolicyEngine API."""

parameter_values: dict = None
"""The parameter values of the reform. This is used to inform any calls to the PolicyEngine API."""

def __init__(self, baseline: TaxBenefitSystem):
"""
Expand Down Expand Up @@ -94,6 +112,69 @@ def modify_parameters(
self.parameters = reform_parameters
self._parameters_at_instant_cache = {}

@staticmethod
def from_dict(
parameter_values: dict,
country_id: str = None,
name: str = None,
) -> Reform:
"""Create a reform from a dictionary of parameters.
Args:
parameters: A dictionary of parameter -> { period -> value } pairs.
Returns:
A reform.
"""

class reform(Reform):
def apply(self):

Check warning on line 131 in policyengine_core/reforms/reform.py

View check run for this annotation

Codecov / codecov/patch

policyengine_core/reforms/reform.py#L130-L131

Added lines #L130 - L131 were not covered by tests
for path, period_values in parameter_values.items():
for period, value in period_values.items():
self.modify_parameters(

Check warning on line 134 in policyengine_core/reforms/reform.py

View check run for this annotation

Codecov / codecov/patch

policyengine_core/reforms/reform.py#L134

Added line #L134 was not covered by tests
set_parameter(
path, value, period, return_modifier=True
)
)

reform.country_id = country_id
reform.parameter_values = parameter_values
reform.name = name

Check warning on line 142 in policyengine_core/reforms/reform.py

View check run for this annotation

Codecov / codecov/patch

policyengine_core/reforms/reform.py#L140-L142

Added lines #L140 - L142 were not covered by tests

return reform

Check warning on line 144 in policyengine_core/reforms/reform.py

View check run for this annotation

Codecov / codecov/patch

policyengine_core/reforms/reform.py#L144

Added line #L144 was not covered by tests

@classproperty
def api_id(self):
if self.country_id is None:
raise ValueError(

Check warning on line 149 in policyengine_core/reforms/reform.py

View check run for this annotation

Codecov / codecov/patch

policyengine_core/reforms/reform.py#L149

Added line #L149 was not covered by tests
"`country_id` is not set. This is required to use the API."
)
if self.parameter_values is None:
raise ValueError(

Check warning on line 153 in policyengine_core/reforms/reform.py

View check run for this annotation

Codecov / codecov/patch

policyengine_core/reforms/reform.py#L153

Added line #L153 was not covered by tests
"`parameter_values` is not set. This is required to use the API."
)

sanitised_parameter_values = {}

Check warning on line 157 in policyengine_core/reforms/reform.py

View check run for this annotation

Codecov / codecov/patch

policyengine_core/reforms/reform.py#L157

Added line #L157 was not covered by tests

for path, period_values in self.parameter_values.items():
sanitised_period_values = {}

Check warning on line 160 in policyengine_core/reforms/reform.py

View check run for this annotation

Codecov / codecov/patch

policyengine_core/reforms/reform.py#L160

Added line #L160 was not covered by tests
for period, value in period_values.items():
period = period_(period)
sanitised_period_values[

Check warning on line 163 in policyengine_core/reforms/reform.py

View check run for this annotation

Codecov / codecov/patch

policyengine_core/reforms/reform.py#L162-L163

Added lines #L162 - L163 were not covered by tests
f"{period.start}.{period.stop}"
] = value
sanitised_parameter_values[path] = sanitised_period_values

Check warning on line 166 in policyengine_core/reforms/reform.py

View check run for this annotation

Codecov / codecov/patch

policyengine_core/reforms/reform.py#L166

Added line #L166 was not covered by tests

response = requests.post(

Check warning on line 168 in policyengine_core/reforms/reform.py

View check run for this annotation

Codecov / codecov/patch

policyengine_core/reforms/reform.py#L168

Added line #L168 was not covered by tests
f"https://api.policyengine.org/{self.country_id}/policy",
json={
"data": sanitised_parameter_values,
"name": self.name,
},
)

return response.json().get("result", {}).get("policy_id")

Check warning on line 176 in policyengine_core/reforms/reform.py

View check run for this annotation

Codecov / codecov/patch

policyengine_core/reforms/reform.py#L176

Added line #L176 was not covered by tests


def set_parameter(
path: Union[Parameter, str],
Expand Down

0 comments on commit 2bb8b86

Please sign in to comment.