policyengine 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- policyengine/__init__.py +1 -0
- policyengine/constants.py +14 -0
- policyengine/outputs/household/comparison/calculate_household_comparison.py +53 -0
- policyengine/outputs/household/single/calculate_single_household.py +203 -0
- policyengine/outputs/macro/comparison/__init__.py +0 -0
- policyengine/outputs/macro/comparison/calculate_economy_comparison.py +110 -0
- policyengine/outputs/macro/comparison/charts/__init__.py +5 -0
- policyengine/outputs/macro/comparison/charts/budget.py +90 -0
- policyengine/outputs/macro/comparison/charts/budget_by_program.py +96 -0
- policyengine/outputs/macro/comparison/charts/decile.py +89 -0
- policyengine/outputs/macro/comparison/charts/inequality.py +77 -0
- policyengine/outputs/macro/comparison/charts/winners_losers.py +148 -0
- policyengine/outputs/macro/comparison/decile.py +209 -0
- policyengine/outputs/macro/single/__init__.py +2 -0
- policyengine/outputs/macro/single/budget.py +82 -0
- policyengine/outputs/macro/single/calculate_average_earnings.py +9 -0
- policyengine/outputs/macro/single/calculate_single_economy.py +39 -0
- policyengine/outputs/macro/single/inequality.py +53 -0
- policyengine/simulation.py +330 -0
- policyengine/utils/__init__.py +0 -0
- policyengine/utils/budget.py +72 -0
- policyengine/utils/calculations.py +56 -0
- policyengine/utils/charts.py +198 -0
- policyengine/utils/huggingface.py +27 -0
- policyengine/utils/maps.py +110 -0
- policyengine/utils/reforms.py +19 -0
- policyengine-0.1.0.dist-info/LICENSE +661 -0
- policyengine-0.1.0.dist-info/METADATA +689 -0
- policyengine-0.1.0.dist-info/RECORD +31 -0
- policyengine-0.1.0.dist-info/WHEEL +5 -0
- policyengine-0.1.0.dist-info/top_level.txt +1 -0
policyengine/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .simulation import Simulation, SimulationOptions
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Mainly simulation options and parameters."""
|
|
2
|
+
|
|
3
|
+
# Datasets
|
|
4
|
+
|
|
5
|
+
ENHANCED_FRS = "hf://policyengine/policyengine-uk-data/enhanced_frs_2022_23.h5"
|
|
6
|
+
FRS = "hf://policyengine/policyengine-uk-data/frs_2022_23.h5"
|
|
7
|
+
ENHANCED_CPS = "hf://policyengine/policyengine-us-data/enhanced_cps_2024.h5"
|
|
8
|
+
CPS = "hf://policyengine/policyengine-us-data/cps_2023.h5"
|
|
9
|
+
POOLED_CPS = "hf://policyengine/policyengine-us-data/pooled_3_year_cps_2023.h5"
|
|
10
|
+
|
|
11
|
+
DEFAULT_DATASETS_BY_COUNTRY = {
|
|
12
|
+
"uk": ENHANCED_FRS,
|
|
13
|
+
"us": CPS,
|
|
14
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Calculate comparison statistics between two economic scenarios."""
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from policyengine import Simulation
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from policyengine.utils.calculations import get_change
|
|
9
|
+
from policyengine_core.simulations import Simulation as CountrySimulation
|
|
10
|
+
from policyengine.outputs.household.single.calculate_single_household import (
|
|
11
|
+
SingleHousehold,
|
|
12
|
+
fill_and_calculate,
|
|
13
|
+
FullHouseholdSpecification,
|
|
14
|
+
)
|
|
15
|
+
from typing import Literal, List
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class HouseholdComparison(BaseModel):
|
|
19
|
+
full_household_baseline: FullHouseholdSpecification
|
|
20
|
+
"""The full completion of the household under the baseline scenario."""
|
|
21
|
+
|
|
22
|
+
full_household_reform: FullHouseholdSpecification
|
|
23
|
+
"""The full completion of the household under the reform scenario."""
|
|
24
|
+
|
|
25
|
+
change: FullHouseholdSpecification
|
|
26
|
+
"""The change in the household from the baseline to the reform scenario."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def calculate_household_comparison(
|
|
30
|
+
simulation: Simulation,
|
|
31
|
+
) -> HouseholdComparison:
|
|
32
|
+
"""Calculate comparison statistics between two household scenarios."""
|
|
33
|
+
if not simulation.is_comparison:
|
|
34
|
+
raise ValueError("Simulation must be a comparison simulation.")
|
|
35
|
+
|
|
36
|
+
baseline_household = fill_and_calculate(
|
|
37
|
+
simulation.options.data, simulation.baseline_simulation
|
|
38
|
+
)
|
|
39
|
+
reform_household = fill_and_calculate(
|
|
40
|
+
simulation.options.data, simulation.reform_simulation
|
|
41
|
+
)
|
|
42
|
+
change = get_change(
|
|
43
|
+
baseline_household,
|
|
44
|
+
reform_household,
|
|
45
|
+
relative=False,
|
|
46
|
+
skip_mismatch=True,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
return HouseholdComparison(
|
|
50
|
+
full_household_baseline=baseline_household,
|
|
51
|
+
full_household_reform=reform_household,
|
|
52
|
+
change=change,
|
|
53
|
+
)
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Calculate comparison statistics between two economic scenarios."""
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from policyengine import Simulation
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from policyengine_core.simulations import Simulation as CountrySimulation
|
|
9
|
+
from typing import List, Dict
|
|
10
|
+
from datetime import date
|
|
11
|
+
from policyengine_core.variables import Variable
|
|
12
|
+
from policyengine_core.entities import Entity
|
|
13
|
+
from policyengine_core.model_api import YEAR, MONTH, ETERNITY, Enum
|
|
14
|
+
import dpath.util
|
|
15
|
+
import math
|
|
16
|
+
import json
|
|
17
|
+
|
|
18
|
+
Value = float | str | bool | list | None
|
|
19
|
+
Axes = List[List[Dict[str, str | int]]]
|
|
20
|
+
TimePeriodValues = Dict[str, Value]
|
|
21
|
+
EntityValues = Dict[str, TimePeriodValues]
|
|
22
|
+
EntityGroupValues = Dict[str, EntityValues]
|
|
23
|
+
FullHouseholdSpecification = Dict[
|
|
24
|
+
str, EntityGroupValues | Axes
|
|
25
|
+
] # {people: {person: {variable: {time_period: value}}}}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SingleHousehold(BaseModel):
|
|
29
|
+
"""Statistics for a single household scenario."""
|
|
30
|
+
|
|
31
|
+
full_household: FullHouseholdSpecification
|
|
32
|
+
"""Full variable calculations for the household."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def calculate_single_household(
|
|
36
|
+
simulation: Simulation,
|
|
37
|
+
) -> SingleHousehold:
|
|
38
|
+
"""Calculate household statistics for a single household scenario."""
|
|
39
|
+
if simulation.is_comparison:
|
|
40
|
+
raise ValueError(
|
|
41
|
+
"This function is for single economy simulations only."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return SingleHousehold(
|
|
45
|
+
full_household=fill_and_calculate(
|
|
46
|
+
simulation.options.data, simulation.baseline_simulation
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def fill_and_calculate(
|
|
52
|
+
household: FullHouseholdSpecification, simulation: CountrySimulation
|
|
53
|
+
):
|
|
54
|
+
"""Fill in missing variables and calculate all variables for a household"""
|
|
55
|
+
# Copy the household to avoid modifying the original
|
|
56
|
+
household = json.loads(json.dumps(household))
|
|
57
|
+
household = add_yearly_variables(household, simulation)
|
|
58
|
+
household = calculate_all_variables(household, simulation)
|
|
59
|
+
household.pop("axes", None)
|
|
60
|
+
return household
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_requested_computations(
|
|
64
|
+
household: FullHouseholdSpecification,
|
|
65
|
+
) -> List[tuple[str, str, str, str]]:
|
|
66
|
+
requested_computations = dpath.util.search(
|
|
67
|
+
{k: v for k, v in household.items() if k != "axes"},
|
|
68
|
+
"*/*/*/*",
|
|
69
|
+
# afilter=lambda t: t is None,
|
|
70
|
+
yielded=True,
|
|
71
|
+
)
|
|
72
|
+
requested_computation_data = []
|
|
73
|
+
|
|
74
|
+
for computation in requested_computations:
|
|
75
|
+
path = computation[0]
|
|
76
|
+
entity_plural, entity_id, variable_name, period = path.split("/")
|
|
77
|
+
requested_computation_data.append(
|
|
78
|
+
(entity_plural, entity_id, variable_name, period)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return requested_computation_data
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def calculate_all_variables(
|
|
85
|
+
household: FullHouseholdSpecification, simulation: CountrySimulation
|
|
86
|
+
) -> FullHouseholdSpecification:
|
|
87
|
+
requested_computations = get_requested_computations(household)
|
|
88
|
+
|
|
89
|
+
for (
|
|
90
|
+
entity_plural,
|
|
91
|
+
entity_id,
|
|
92
|
+
variable_name,
|
|
93
|
+
period,
|
|
94
|
+
) in requested_computations:
|
|
95
|
+
variable = simulation.tax_benefit_system.get_variable(variable_name)
|
|
96
|
+
result = simulation.calculate(variable_name, period)
|
|
97
|
+
population = simulation.get_population(entity_plural)
|
|
98
|
+
|
|
99
|
+
if "axes" in household:
|
|
100
|
+
count_entities = len(household[entity_plural])
|
|
101
|
+
entity_index = 0
|
|
102
|
+
for _entity_id in household[entity_plural].keys():
|
|
103
|
+
if _entity_id == entity_id:
|
|
104
|
+
break
|
|
105
|
+
entity_index += 1
|
|
106
|
+
try:
|
|
107
|
+
result = result.astype(float)
|
|
108
|
+
except:
|
|
109
|
+
pass
|
|
110
|
+
result = (
|
|
111
|
+
result.reshape((-1, count_entities)).T[entity_index].tolist()
|
|
112
|
+
)
|
|
113
|
+
# If the result contains infinities, throw an error
|
|
114
|
+
if any(
|
|
115
|
+
[
|
|
116
|
+
not isinstance(value, str) and math.isinf(value)
|
|
117
|
+
for value in result
|
|
118
|
+
]
|
|
119
|
+
):
|
|
120
|
+
raise ValueError("Infinite value")
|
|
121
|
+
else:
|
|
122
|
+
household[entity_plural][entity_id][variable_name][
|
|
123
|
+
period
|
|
124
|
+
] = result
|
|
125
|
+
else:
|
|
126
|
+
entity_index = population.get_index(entity_id)
|
|
127
|
+
if variable.value_type == Enum:
|
|
128
|
+
entity_result = result.decode()[entity_index].name
|
|
129
|
+
elif variable.value_type == float:
|
|
130
|
+
entity_result = float(str(result[entity_index]))
|
|
131
|
+
# Convert infinities to JSON infinities
|
|
132
|
+
if entity_result == float("inf"):
|
|
133
|
+
entity_result = "Infinity"
|
|
134
|
+
elif entity_result == float("-inf"):
|
|
135
|
+
entity_result = "-Infinity"
|
|
136
|
+
elif variable.value_type == str:
|
|
137
|
+
entity_result = str(result[entity_index])
|
|
138
|
+
else:
|
|
139
|
+
entity_result = result.tolist()[entity_index]
|
|
140
|
+
|
|
141
|
+
household[entity_plural][entity_id][variable_name][
|
|
142
|
+
period
|
|
143
|
+
] = entity_result
|
|
144
|
+
|
|
145
|
+
return household
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_household_year(household: FullHouseholdSpecification) -> str:
|
|
149
|
+
"""Given a household dict, get the household's year
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
household (dict): The household itself
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
# Set household_year based on current year
|
|
156
|
+
household_year = date.today().year
|
|
157
|
+
|
|
158
|
+
# Determine if "age" variable present within household and return list of values at it
|
|
159
|
+
household_age_list = list(
|
|
160
|
+
household.get("people", {}).get("you", {}).get("age", {}).keys()
|
|
161
|
+
)
|
|
162
|
+
# If it is, overwrite household_year with the value present
|
|
163
|
+
if len(household_age_list) > 0:
|
|
164
|
+
household_year = household_age_list[0]
|
|
165
|
+
|
|
166
|
+
return str(household_year)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def add_yearly_variables(
|
|
170
|
+
household: FullHouseholdSpecification, simulation: CountrySimulation
|
|
171
|
+
) -> FullHouseholdSpecification:
|
|
172
|
+
"""
|
|
173
|
+
Add yearly variables to a household dict before enqueueing calculation
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
variables: Dict[str, Variable] = simulation.tax_benefit_system.variables
|
|
177
|
+
entities: Dict[str, Entity] = (
|
|
178
|
+
simulation.tax_benefit_system.entities_by_singular()
|
|
179
|
+
)
|
|
180
|
+
household_year = get_household_year(household)
|
|
181
|
+
|
|
182
|
+
for variable in variables:
|
|
183
|
+
if variables[variable].definition_period in (YEAR, MONTH, ETERNITY):
|
|
184
|
+
entity_plural = entities[variables[variable].entity.key].plural
|
|
185
|
+
if entity_plural in household:
|
|
186
|
+
possible_entities = household[entity_plural].keys()
|
|
187
|
+
for entity in possible_entities:
|
|
188
|
+
if (
|
|
189
|
+
not variables[variable].name
|
|
190
|
+
in household[entity_plural][entity]
|
|
191
|
+
):
|
|
192
|
+
if variables[variable].is_input_variable():
|
|
193
|
+
value = variables[variable].default_value
|
|
194
|
+
if isinstance(value, Enum):
|
|
195
|
+
value = value.name
|
|
196
|
+
household[entity_plural][entity][
|
|
197
|
+
variables[variable].name
|
|
198
|
+
] = {household_year: value}
|
|
199
|
+
else:
|
|
200
|
+
household[entity_plural][entity][
|
|
201
|
+
variables[variable].name
|
|
202
|
+
] = {household_year: None}
|
|
203
|
+
return household
|
|
File without changes
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Calculate comparison statistics between two economic scenarios."""
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from policyengine import Simulation
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from policyengine.utils.calculations import get_change
|
|
9
|
+
|
|
10
|
+
from policyengine.outputs.macro.single import (
|
|
11
|
+
_calculate_government_balance,
|
|
12
|
+
FiscalSummary,
|
|
13
|
+
_calculate_inequality,
|
|
14
|
+
InequalitySummary,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from .decile import calculate_decile_impacts, DecileImpacts
|
|
18
|
+
|
|
19
|
+
from typing import Literal, List
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FiscalComparison(BaseModel):
|
|
23
|
+
baseline: FiscalSummary
|
|
24
|
+
reform: FiscalSummary
|
|
25
|
+
change: FiscalSummary
|
|
26
|
+
relative_change: FiscalSummary
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class InequalityComparison(BaseModel):
|
|
30
|
+
baseline: InequalitySummary
|
|
31
|
+
reform: InequalitySummary
|
|
32
|
+
change: InequalitySummary
|
|
33
|
+
relative_change: InequalitySummary
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Headlines(BaseModel):
|
|
37
|
+
budgetary_impact: float
|
|
38
|
+
"""The change in the (federal) government budget balance."""
|
|
39
|
+
winner_share: float
|
|
40
|
+
"""The share of people that are better off in the reform scenario."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class EconomyComparison(BaseModel):
|
|
44
|
+
headlines: Headlines
|
|
45
|
+
"""Headline statistics for the comparison."""
|
|
46
|
+
fiscal: FiscalComparison
|
|
47
|
+
"""Government budgets and other top-level fiscal statistics."""
|
|
48
|
+
inequality: InequalityComparison
|
|
49
|
+
"""Inequality statistics for the household sector."""
|
|
50
|
+
distributional: DecileImpacts
|
|
51
|
+
"""Distributional impacts of the reform."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def calculate_economy_comparison(
|
|
55
|
+
simulation: Simulation,
|
|
56
|
+
) -> EconomyComparison:
|
|
57
|
+
"""Calculate comparison statistics between two economic scenarios."""
|
|
58
|
+
if not simulation.is_comparison:
|
|
59
|
+
raise ValueError("Simulation must be a comparison simulation.")
|
|
60
|
+
|
|
61
|
+
baseline = simulation.baseline_simulation
|
|
62
|
+
reform = simulation.reform_simulation
|
|
63
|
+
options = simulation.options
|
|
64
|
+
|
|
65
|
+
baseline_balance = _calculate_government_balance(baseline, options)
|
|
66
|
+
reform_balance = _calculate_government_balance(reform, options)
|
|
67
|
+
balance_change = get_change(
|
|
68
|
+
baseline_balance, reform_balance, relative=False
|
|
69
|
+
)
|
|
70
|
+
balance_rel_change = get_change(
|
|
71
|
+
baseline_balance, reform_balance, relative=True
|
|
72
|
+
)
|
|
73
|
+
fiscal_comparison = FiscalComparison(
|
|
74
|
+
baseline=baseline_balance,
|
|
75
|
+
reform=reform_balance,
|
|
76
|
+
change=balance_change,
|
|
77
|
+
relative_change=balance_rel_change,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
baseline_inequality = _calculate_inequality(baseline)
|
|
81
|
+
reform_inequality = _calculate_inequality(reform)
|
|
82
|
+
inequality_change = get_change(
|
|
83
|
+
baseline_inequality, reform_inequality, relative=False
|
|
84
|
+
)
|
|
85
|
+
inequality_rel_change = get_change(
|
|
86
|
+
baseline_inequality, reform_inequality, relative=True
|
|
87
|
+
)
|
|
88
|
+
inequality_comparison = InequalityComparison(
|
|
89
|
+
baseline=baseline_inequality,
|
|
90
|
+
reform=reform_inequality,
|
|
91
|
+
change=inequality_change,
|
|
92
|
+
relative_change=inequality_rel_change,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
decile_impacts = calculate_decile_impacts(baseline, reform, options)
|
|
96
|
+
|
|
97
|
+
# Headlines
|
|
98
|
+
budgetary_impact = fiscal_comparison.change.federal_balance
|
|
99
|
+
winner_share = decile_impacts.income.winners_and_losers.all.gain_share
|
|
100
|
+
headlines = Headlines(
|
|
101
|
+
budgetary_impact=budgetary_impact,
|
|
102
|
+
winner_share=winner_share,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return EconomyComparison(
|
|
106
|
+
headlines=headlines,
|
|
107
|
+
fiscal=fiscal_comparison,
|
|
108
|
+
inequality=inequality_comparison,
|
|
109
|
+
distributional=decile_impacts,
|
|
110
|
+
)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
from .budget import create_budget_comparison_chart
|
|
2
|
+
from .budget_by_program import create_budget_program_comparison_chart
|
|
3
|
+
from .decile import create_decile_chart
|
|
4
|
+
from .winners_losers import create_winners_losers_chart
|
|
5
|
+
from .inequality import create_inequality_chart
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import plotly.express as px
|
|
2
|
+
import plotly.graph_objects as go
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from policyengine import Simulation
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from policyengine.utils.charts import *
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def create_budget_comparison_chart(
|
|
12
|
+
simulation: Simulation,
|
|
13
|
+
) -> go.Figure:
|
|
14
|
+
"""Create a budget comparison chart."""
|
|
15
|
+
if not simulation.is_comparison:
|
|
16
|
+
raise ValueError("Simulation must be a comparison simulation.")
|
|
17
|
+
|
|
18
|
+
economy = simulation.calculate_economy_comparison()
|
|
19
|
+
|
|
20
|
+
if simulation.options.country == "uk":
|
|
21
|
+
x_values = [
|
|
22
|
+
"Tax revenues",
|
|
23
|
+
"Government spending",
|
|
24
|
+
"Public sector net worth",
|
|
25
|
+
]
|
|
26
|
+
y_values = [
|
|
27
|
+
economy.fiscal.change.federal_tax,
|
|
28
|
+
-economy.fiscal.change.government_spending,
|
|
29
|
+
economy.fiscal.change.federal_balance,
|
|
30
|
+
]
|
|
31
|
+
else:
|
|
32
|
+
x_values = [
|
|
33
|
+
"Federal tax revenues",
|
|
34
|
+
"State tax revenues",
|
|
35
|
+
"Federal government spending",
|
|
36
|
+
]
|
|
37
|
+
y_values = [
|
|
38
|
+
economy.fiscal.change.federal_tax,
|
|
39
|
+
economy.fiscal.change.state_tax,
|
|
40
|
+
-economy.fiscal.change.government_spending,
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
y_values = [value / 1e9 for value in y_values]
|
|
44
|
+
|
|
45
|
+
net_change = round(economy.fiscal.change.federal_balance / 1e9, 1)
|
|
46
|
+
|
|
47
|
+
if net_change > 0:
|
|
48
|
+
description = f"raise ${net_change}bn"
|
|
49
|
+
elif net_change < 0:
|
|
50
|
+
description = f"cost ${-net_change}bn"
|
|
51
|
+
else:
|
|
52
|
+
description = "have no effect on government finances"
|
|
53
|
+
|
|
54
|
+
chart = go.Figure(
|
|
55
|
+
data=[
|
|
56
|
+
go.Waterfall(
|
|
57
|
+
x=x_values,
|
|
58
|
+
y=y_values,
|
|
59
|
+
measure=["relative"] * (len(x_values) - 1) + ["total"],
|
|
60
|
+
textposition="inside",
|
|
61
|
+
text=[f"${value:.1f}bn" for value in y_values],
|
|
62
|
+
increasing=dict(
|
|
63
|
+
marker=dict(
|
|
64
|
+
color=BLUE,
|
|
65
|
+
)
|
|
66
|
+
),
|
|
67
|
+
decreasing=dict(
|
|
68
|
+
marker=dict(
|
|
69
|
+
color=DARK_GRAY,
|
|
70
|
+
)
|
|
71
|
+
),
|
|
72
|
+
totals=dict(
|
|
73
|
+
marker=dict(
|
|
74
|
+
color=BLUE if net_change > 0 else DARK_GRAY,
|
|
75
|
+
)
|
|
76
|
+
),
|
|
77
|
+
),
|
|
78
|
+
]
|
|
79
|
+
).update_layout(
|
|
80
|
+
title=f"{simulation.options.title} would {description}",
|
|
81
|
+
yaxis_title="Budgetary impact ($bn)",
|
|
82
|
+
uniformtext=dict(
|
|
83
|
+
mode="hide",
|
|
84
|
+
minsize=12,
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return format_fig(
|
|
89
|
+
chart, country=simulation.options.country, add_zero_line=True
|
|
90
|
+
)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import plotly.express as px
|
|
2
|
+
import plotly.graph_objects as go
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from policyengine import Simulation
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from policyengine.utils.charts import *
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def create_budget_program_comparison_chart(
|
|
12
|
+
simulation: Simulation,
|
|
13
|
+
) -> go.Figure:
|
|
14
|
+
"""Create a budget comparison chart."""
|
|
15
|
+
if not simulation.is_comparison:
|
|
16
|
+
raise ValueError("Simulation must be a comparison simulation.")
|
|
17
|
+
|
|
18
|
+
if not simulation.options.country == "uk":
|
|
19
|
+
raise ValueError("This chart is only available for the UK.")
|
|
20
|
+
|
|
21
|
+
economy = simulation.calculate_economy_comparison()
|
|
22
|
+
|
|
23
|
+
change_programs = economy.fiscal.change.tax_benefit_programs
|
|
24
|
+
|
|
25
|
+
change_programs = {
|
|
26
|
+
program: change_programs[program]
|
|
27
|
+
for program in change_programs
|
|
28
|
+
if round(change_programs[program] / 1e9, 1) != 0
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
labels = [
|
|
32
|
+
simulation.baseline_simulation.tax_benefit_system.variables.get(
|
|
33
|
+
program
|
|
34
|
+
).label
|
|
35
|
+
for program in change_programs
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
x_values = labels
|
|
39
|
+
y_values = [
|
|
40
|
+
round(change_programs[program] / 1e9, 1) for program in change_programs
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
total_from_programs = round(sum(y_values))
|
|
44
|
+
total_overall = round(economy.fiscal.change.federal_balance / 1e9)
|
|
45
|
+
|
|
46
|
+
if total_from_programs != total_overall:
|
|
47
|
+
x_values.append("Other")
|
|
48
|
+
y_values.append(total_overall - total_from_programs)
|
|
49
|
+
|
|
50
|
+
x_values.append("Combined")
|
|
51
|
+
y_values.append(total_overall)
|
|
52
|
+
|
|
53
|
+
if total_overall > 0:
|
|
54
|
+
description = f"raise ${total_overall}bn"
|
|
55
|
+
elif total_overall < 0:
|
|
56
|
+
description = f"cost ${-total_overall}bn"
|
|
57
|
+
else:
|
|
58
|
+
description = "have no effect on government finances"
|
|
59
|
+
|
|
60
|
+
chart = go.Figure(
|
|
61
|
+
data=[
|
|
62
|
+
go.Waterfall(
|
|
63
|
+
x=x_values,
|
|
64
|
+
y=y_values,
|
|
65
|
+
measure=["relative"] * (len(x_values) - 1) + ["total"],
|
|
66
|
+
textposition="inside",
|
|
67
|
+
text=[f"${value:.1f}bn" for value in y_values],
|
|
68
|
+
increasing=dict(
|
|
69
|
+
marker=dict(
|
|
70
|
+
color=BLUE,
|
|
71
|
+
)
|
|
72
|
+
),
|
|
73
|
+
decreasing=dict(
|
|
74
|
+
marker=dict(
|
|
75
|
+
color=DARK_GRAY,
|
|
76
|
+
)
|
|
77
|
+
),
|
|
78
|
+
totals=dict(
|
|
79
|
+
marker=dict(
|
|
80
|
+
color=BLUE if total_overall > 0 else DARK_GRAY,
|
|
81
|
+
)
|
|
82
|
+
),
|
|
83
|
+
),
|
|
84
|
+
]
|
|
85
|
+
).update_layout(
|
|
86
|
+
title=f"{simulation.options.title} would {description}",
|
|
87
|
+
yaxis_title="Budgetary impact (bn)",
|
|
88
|
+
uniformtext=dict(
|
|
89
|
+
mode="hide",
|
|
90
|
+
minsize=12,
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return format_fig(
|
|
95
|
+
chart, country=simulation.options.country, add_zero_line=True
|
|
96
|
+
)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import plotly.express as px
|
|
2
|
+
import plotly.graph_objects as go
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from policyengine import Simulation
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from policyengine.utils.charts import *
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_decile_chart(
|
|
13
|
+
simulation: Simulation,
|
|
14
|
+
decile_variable: Literal["income", "wealth"],
|
|
15
|
+
relative: bool,
|
|
16
|
+
) -> go.Figure:
|
|
17
|
+
"""Create a budget comparison chart."""
|
|
18
|
+
if not simulation.is_comparison:
|
|
19
|
+
raise ValueError("Simulation must be a comparison simulation.")
|
|
20
|
+
|
|
21
|
+
economy = simulation.calculate_economy_comparison()
|
|
22
|
+
|
|
23
|
+
if decile_variable == "income":
|
|
24
|
+
data = economy.distributional.income.income_change
|
|
25
|
+
else:
|
|
26
|
+
data = economy.distributional.wealth.income_change
|
|
27
|
+
|
|
28
|
+
if relative:
|
|
29
|
+
data = data.relative
|
|
30
|
+
else:
|
|
31
|
+
data = data.average
|
|
32
|
+
|
|
33
|
+
avg_change = sum(data.values()) / len(data)
|
|
34
|
+
|
|
35
|
+
if relative:
|
|
36
|
+
text = [f"{value:.1%}" for value in data.values()]
|
|
37
|
+
avg_change = round(avg_change, 3)
|
|
38
|
+
else:
|
|
39
|
+
text = [f"${value:,.0f}" for value in data.values()]
|
|
40
|
+
avg_change = round(avg_change)
|
|
41
|
+
|
|
42
|
+
avg_change_str = (
|
|
43
|
+
f"${abs(avg_change):,.0f}"
|
|
44
|
+
if not relative
|
|
45
|
+
else f"{abs(avg_change):.1%}"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if avg_change > 0:
|
|
49
|
+
description = (
|
|
50
|
+
f"increase the net income of households by {avg_change_str}"
|
|
51
|
+
)
|
|
52
|
+
elif avg_change < 0:
|
|
53
|
+
description = (
|
|
54
|
+
f"decrease the net income of households by {avg_change_str}"
|
|
55
|
+
)
|
|
56
|
+
else:
|
|
57
|
+
description = "have no effect on household net income"
|
|
58
|
+
|
|
59
|
+
chart = go.Figure(
|
|
60
|
+
data=[
|
|
61
|
+
go.Bar(
|
|
62
|
+
x=list(data.keys()),
|
|
63
|
+
y=list(data.values()),
|
|
64
|
+
text=text,
|
|
65
|
+
marker=dict(
|
|
66
|
+
color=[
|
|
67
|
+
BLUE if value > 0 else DARK_GRAY
|
|
68
|
+
for value in data.values()
|
|
69
|
+
]
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
]
|
|
73
|
+
).update_layout(
|
|
74
|
+
title=f"{simulation.options.title} would {description}",
|
|
75
|
+
yaxis_title=f"Average change to net income ({'%' if relative else '$'})",
|
|
76
|
+
yaxis_tickformat="$,.0f" if not relative else ".0%",
|
|
77
|
+
xaxis_title=(
|
|
78
|
+
"Income decile" if decile_variable == "income" else "Wealth decile"
|
|
79
|
+
),
|
|
80
|
+
uniformtext=dict(
|
|
81
|
+
mode="hide",
|
|
82
|
+
minsize=12,
|
|
83
|
+
),
|
|
84
|
+
xaxis_tickvals=list(data.keys()),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return format_fig(
|
|
88
|
+
chart, country=simulation.options.country, add_zero_line=True
|
|
89
|
+
)
|