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.
Files changed (31) hide show
  1. policyengine/__init__.py +1 -0
  2. policyengine/constants.py +14 -0
  3. policyengine/outputs/household/comparison/calculate_household_comparison.py +53 -0
  4. policyengine/outputs/household/single/calculate_single_household.py +203 -0
  5. policyengine/outputs/macro/comparison/__init__.py +0 -0
  6. policyengine/outputs/macro/comparison/calculate_economy_comparison.py +110 -0
  7. policyengine/outputs/macro/comparison/charts/__init__.py +5 -0
  8. policyengine/outputs/macro/comparison/charts/budget.py +90 -0
  9. policyengine/outputs/macro/comparison/charts/budget_by_program.py +96 -0
  10. policyengine/outputs/macro/comparison/charts/decile.py +89 -0
  11. policyengine/outputs/macro/comparison/charts/inequality.py +77 -0
  12. policyengine/outputs/macro/comparison/charts/winners_losers.py +148 -0
  13. policyengine/outputs/macro/comparison/decile.py +209 -0
  14. policyengine/outputs/macro/single/__init__.py +2 -0
  15. policyengine/outputs/macro/single/budget.py +82 -0
  16. policyengine/outputs/macro/single/calculate_average_earnings.py +9 -0
  17. policyengine/outputs/macro/single/calculate_single_economy.py +39 -0
  18. policyengine/outputs/macro/single/inequality.py +53 -0
  19. policyengine/simulation.py +330 -0
  20. policyengine/utils/__init__.py +0 -0
  21. policyengine/utils/budget.py +72 -0
  22. policyengine/utils/calculations.py +56 -0
  23. policyengine/utils/charts.py +198 -0
  24. policyengine/utils/huggingface.py +27 -0
  25. policyengine/utils/maps.py +110 -0
  26. policyengine/utils/reforms.py +19 -0
  27. policyengine-0.1.0.dist-info/LICENSE +661 -0
  28. policyengine-0.1.0.dist-info/METADATA +689 -0
  29. policyengine-0.1.0.dist-info/RECORD +31 -0
  30. policyengine-0.1.0.dist-info/WHEEL +5 -0
  31. policyengine-0.1.0.dist-info/top_level.txt +1 -0
@@ -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
+ )