Skip to content

Commit 88e335b

Browse files
Merge pull request #51 from PolicyEngine/dev-improv
Improve constituency mapping tools
2 parents 0297d94 + bb658c2 commit 88e335b

File tree

7 files changed

+169
-159
lines changed

7 files changed

+169
-159
lines changed

docs/_config.yml

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# _config.yml
2-
title: PolicyEngine
32
author: PolicyEngine
4-
copyright: "2022"
3+
title: PolicyEngine
4+
copyright: "2025"
55
logo: logo.png
66

77
execute:
@@ -19,11 +19,4 @@ sphinx:
1919
html_theme: furo
2020
pygments_style: default
2121
html_css_files:
22-
- style.css
23-
# extra_extensions:
24-
# - sphinx.ext.autodoc
25-
# - sphinxarg.ext
26-
# - sphinx.ext.viewcode
27-
# - sphinx.ext.napoleon
28-
# - sphinx_math_dollar
29-
# - sphinx.ext.mathjax
22+
- style.css

policyengine/outputs/macro/comparison/budget/general.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ def budget_chart(simulation: Simulation, data: dict) -> go.Figure:
104104
xaxis_title="",
105105
yaxis_title="Budgetary impact (£ billions)",
106106
yaxis_tickformat=",.0f",
107+
uniformtext_minsize=12,
108+
uniformtext_mode="hide",
107109
)
108110

109-
return format_fig(fig)
111+
return format_fig(fig, simulation.country)

policyengine/outputs/macro/comparison/local_areas/parliamentary_constituencies.py

Lines changed: 19 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -3,165 +3,44 @@
33
from policyengine.utils.huggingface import download
44
import plotly.express as px
55
from policyengine.utils.charts import *
6+
from policyengine.utils.constituency_maps import plot_hex_map
7+
from typing import Callable
8+
from policyengine_core import Microsimulation
9+
from microdf import MicroSeries
610

711

812
def parliamentary_constituencies(
913
simulation: Simulation,
1014
chart: bool = False,
11-
variable: str = None,
12-
aggregator: str = None,
13-
relative: bool = None,
15+
metric: Callable[[Microsimulation], MicroSeries] = None,
16+
comparator: bool = None,
1417
) -> dict:
1518
if not simulation.options.get("include_constituencies"):
1619
return {}
1720

18-
if chart:
19-
return heatmap(
20-
simulation=simulation,
21-
variable=variable,
22-
aggregator=aggregator,
23-
relative=relative,
24-
)
25-
26-
constituency_baseline = simulation.calculate(
27-
"macro/baseline/gov/local_areas/parliamentary_constituencies"
28-
)
29-
constituency_reform = simulation.calculate(
30-
"macro/reform/gov/local_areas/parliamentary_constituencies"
31-
)
32-
33-
result = {}
34-
35-
for constituency in constituency_baseline:
36-
result[constituency] = {}
37-
for key in constituency_baseline[constituency]:
38-
result[constituency][key] = {
39-
"change": constituency_reform[constituency][key]
40-
- constituency_baseline[constituency][key],
41-
"baseline": constituency_baseline[constituency][key],
42-
"reform": constituency_reform[constituency][key],
43-
}
44-
45-
return result
46-
47-
48-
def heatmap(
49-
simulation: Simulation,
50-
variable: str = None,
51-
aggregator: str = None,
52-
relative: bool = None,
53-
) -> dict:
54-
if not simulation.options.get("include_constituencies"):
55-
return {}
56-
57-
options = {}
21+
kwargs = {}
22+
if metric is not None:
23+
kwargs["metric"] = metric
5824

59-
if variable is not None:
60-
options["variables"] = [variable]
61-
if aggregator is not None:
62-
options["aggregator"] = aggregator
25+
if comparator is None:
26+
comparator = lambda x, y: (y / x) - 1
6327

6428
constituency_baseline = simulation.calculate(
65-
"macro/baseline/gov/local_areas/parliamentary_constituencies",
66-
**options,
29+
"macro/baseline/gov/local_areas/parliamentary_constituencies", **kwargs
6730
)
6831
constituency_reform = simulation.calculate(
69-
"macro/reform/gov/local_areas/parliamentary_constituencies", **options
32+
"macro/reform/gov/local_areas/parliamentary_constituencies", **kwargs
7033
)
7134

7235
result = {}
7336

74-
constituency_names_file_path = download(
75-
repo="policyengine/policyengine-uk-data",
76-
repo_filename="constituencies_2024.csv",
77-
local_folder=None,
78-
version=None,
79-
)
80-
constituency_names = pd.read_csv(constituency_names_file_path)
81-
82-
if variable is None:
83-
variable = "household_net_income"
84-
if relative is None:
85-
relative = True
86-
8737
for constituency in constituency_baseline:
88-
if relative:
89-
result[constituency] = (
90-
constituency_reform[constituency][variable]
91-
/ constituency_baseline[constituency][variable]
92-
- 1
93-
)
94-
else:
95-
result[constituency] = (
96-
constituency_reform[constituency][variable]
97-
- constituency_baseline[constituency][variable]
98-
)
99-
100-
x_range = constituency_names["x"].max() - constituency_names["x"].min()
101-
y_range = constituency_names["y"].max() - constituency_names["y"].min()
102-
# Expand x range to preserve aspect ratio
103-
expanded_lower_x_range = -(y_range - x_range) / 2
104-
expanded_upper_x_range = x_range - expanded_lower_x_range
105-
constituency_names.x = (
106-
constituency_names.x - (constituency_names.y % 2 == 0) * 0.5
107-
)
108-
constituency_names["Relative change"] = (
109-
pd.Series(list(result.values()), index=list(result.keys()))
110-
.loc[constituency_names["name"]]
111-
.values
112-
)
113-
114-
label = simulation.baseline.tax_benefit_system.variables[variable].label
115-
116-
fig = px.scatter(
117-
constituency_names,
118-
x="x",
119-
y="y",
120-
color="Relative change",
121-
hover_name="name",
122-
title=f"{'Relative change' if relative else 'Change'} in {label} by parliamentary constituency",
123-
)
124-
125-
format_fig(fig)
126-
127-
# Show hexagons on scatter points
128-
129-
fig.update_traces(
130-
marker=dict(
131-
symbol="hexagon", line=dict(width=0, color="lightgray"), size=15
38+
result[constituency] = comparator(
39+
constituency_baseline[constituency],
40+
constituency_reform[constituency],
13241
)
133-
)
134-
fig.update_layout(
135-
xaxis_tickvals=[],
136-
xaxis_title="",
137-
yaxis_tickvals=[],
138-
yaxis_title="",
139-
xaxis_range=[expanded_lower_x_range, expanded_upper_x_range],
140-
yaxis_range=[
141-
constituency_names["y"].min(),
142-
constituency_names["y"].max(),
143-
],
144-
).update_traces(marker_size=10).update_layout(
145-
xaxis_range=[30, 85], yaxis_range=[-50, 2]
146-
)
147-
148-
x_min = fig.data[0]["marker"]["color"].min()
149-
x_max = fig.data[0]["marker"]["color"].max()
150-
max_abs = max(abs(x_min), abs(x_max))
15142

152-
fig.update_layout(
153-
coloraxis=dict(
154-
cmin=-max_abs,
155-
cmax=max_abs,
156-
colorscale=[
157-
[0, DARK_GRAY],
158-
[0.5, "lightgray"],
159-
[1, BLUE],
160-
],
161-
colorbar=dict(
162-
tickformat=".0%" if relative else ",.0f",
163-
),
164-
)
165-
)
43+
if chart:
44+
return plot_hex_map(result)
16645

167-
return fig
46+
return result

policyengine/outputs/macro/single/gov/local_areas/parliamentary_constituencies.py

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
from policyengine import Simulation
22
from policyengine.utils.huggingface import download
33
import h5py
4-
from microdf import MicroDataFrame
4+
from microdf import MicroSeries
55
import pandas as pd
6+
from typing import Callable
7+
from policyengine_core import Microsimulation
8+
from policyengine.utils.constituency_maps import plot_hex_map
69

710
DEFAULT_VARIABLES = [
811
"household_net_income",
@@ -11,11 +14,21 @@
1114

1215
def parliamentary_constituencies(
1316
simulation: Simulation,
14-
variables: list = DEFAULT_VARIABLES,
15-
aggregator: str = "sum",
17+
metric: Callable[[Microsimulation], MicroSeries] = None,
18+
chart: bool = False,
1619
) -> dict:
20+
"""Calculate the impact of the reform on parliamentary constituencies.
21+
22+
Args:
23+
simulation (Simulation): The simulation for which the impact is to be calculated.
24+
custom_function (Callable[[Microsimulation], [float]]): A custom function to calculate the impact. This must be called on a Microsimulation and return a float (we will call it for each constituency weight set).
25+
26+
"""
1727
if not simulation.options.get("include_constituencies"):
1828
return {}
29+
30+
if metric is None:
31+
metric = lambda sim: sim.calculate("household_net_income").median()
1932
weights_file_path = download(
2033
repo="policyengine/policyengine-uk-data",
2134
repo_filename="parliamentary_constituency_weights.h5",
@@ -33,15 +46,40 @@ def parliamentary_constituencies(
3346
with h5py.File(weights_file_path, "r") as f:
3447
weights = f[str(simulation.time_period)][...]
3548

36-
sim_df = simulation.selected.calculate_dataframe(variables)
37-
3849
result = {}
3950

51+
sim = simulation.selected
52+
original_hh_weight = sim.calculate("household_weight").values
53+
4054
for constituency_id in range(weights.shape[0]):
41-
weighted_df = MicroDataFrame(sim_df, weights=weights[constituency_id])
55+
sim.set_input(
56+
"household_weight",
57+
sim.default_calculation_period,
58+
weights[constituency_id],
59+
)
60+
sim.get_holder("person_weight").delete_arrays(
61+
sim.default_calculation_period
62+
)
63+
sim.get_holder("benunit_weight").delete_arrays(
64+
sim.default_calculation_period
65+
)
66+
calculation_result = metric(simulation.selected)
4267
code = constituency_names.code.iloc[constituency_id]
4368
result[constituency_names.set_index("code").loc[code]["name"]] = (
44-
getattr(weighted_df, aggregator)().to_dict()
69+
calculation_result
4570
)
4671

72+
sim.get_holder("person_weight").delete_arrays(
73+
sim.default_calculation_period
74+
)
75+
sim.get_holder("benunit_weight").delete_arrays(
76+
sim.default_calculation_period
77+
)
78+
sim.set_input(
79+
"household_weight", sim.default_calculation_period, original_hh_weight
80+
)
81+
82+
if chart:
83+
return plot_hex_map(result)
84+
4785
return result

policyengine/outputs/macro/single/household/income_distribution.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def income_distribution_chart(simulation: Simulation) -> go.Figure:
2929
color_discrete_sequence=[BLUE],
3030
)
3131
fig.update_layout(
32-
title="Number of household by net income band",
32+
title="Number of households by net income band",
3333
xaxis_title="Household net income",
3434
yaxis_title="Number of households",
3535
)

policyengine/simulation.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ class Simulation:
3939
"""The tax-benefit simulation for the baseline scenario."""
4040
reformed: CountrySimulation = None
4141
"""The tax-benefit simulation for the reformed scenario."""
42+
selected: CountryMicrosimulation = None
43+
"""The selected simulation for the current calculation."""
4244
verbose: bool = False
4345
"""Whether to print out progress messages."""
4446

@@ -74,6 +76,11 @@ def __init__(
7476
elif isinstance(reform, int):
7577
reform = Reform.from_api(reform, country_id=country)
7678

79+
if isinstance(baseline, dict):
80+
baseline = Reform.from_dict(baseline, country_id=country)
81+
elif isinstance(baseline, int):
82+
baseline = Reform.from_api(baseline, country_id=country)
83+
7784
self.baseline = baseline
7885
self.reform = reform
7986

0 commit comments

Comments
 (0)