Skip to content

Commit 6dbbf9c

Browse files
committed
Wire cliff impacts into reform analyses
1 parent b787023 commit 6dbbf9c

3 files changed

Lines changed: 251 additions & 0 deletions

File tree

src/policyengine/tax_benefit_models/uk/analysis.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@
1111

1212
from policyengine.core import OutputCollection, Simulation
1313
from policyengine.outputs import (
14+
CliffImpact,
1415
LaborSupplyResponse,
1516
ProgramStatistics,
17+
calculate_cliff_impact,
1618
calculate_labor_supply_response,
19+
configure_cliff_impact_variables,
1720
configure_labor_supply_response_variables,
1821
)
1922
from policyengine.outputs.decile_impact import (
@@ -71,6 +74,7 @@ class PolicyReformAnalysis(BaseModel):
7174
baseline_inequality: Inequality
7275
reform_inequality: Inequality
7376
labor_supply_response: LaborSupplyResponse
77+
cliff_impact: CliffImpact | None = None
7478

7579

7680
def _format_missing_program_variables(missing_variables: set[str]) -> str | None:
@@ -141,13 +145,16 @@ def _validate_program_statistics_config(
141145
def economic_impact_analysis(
142146
baseline_simulation: Simulation,
143147
reform_simulation: Simulation,
148+
include_cliff_impacts: bool = False,
144149
) -> PolicyReformAnalysis:
145150
"""Perform comprehensive analysis of a UK policy reform."""
146151
configure_labor_supply_response_variables(
147152
baseline_simulation,
148153
reform_simulation,
149154
country_code="uk",
150155
)
156+
if include_cliff_impacts:
157+
configure_cliff_impact_variables(baseline_simulation, reform_simulation)
151158
_validate_program_statistics_config(baseline_simulation, reform_simulation)
152159

153160
baseline_simulation.ensure()
@@ -224,6 +231,11 @@ def economic_impact_analysis(
224231
reform_simulation,
225232
country_code="uk",
226233
)
234+
cliff_impact = (
235+
calculate_cliff_impact(baseline_simulation, reform_simulation)
236+
if include_cliff_impacts
237+
else None
238+
)
227239

228240
return PolicyReformAnalysis(
229241
decile_impacts=decile_impacts,
@@ -235,4 +247,5 @@ def economic_impact_analysis(
235247
baseline_inequality=baseline_inequality,
236248
reform_inequality=reform_inequality,
237249
labor_supply_response=labor_supply_response,
250+
cliff_impact=cliff_impact,
238251
)

src/policyengine/tax_benefit_models/us/analysis.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@
1313

1414
from policyengine.core import OutputCollection, Simulation
1515
from policyengine.outputs import (
16+
CliffImpact,
1617
LaborSupplyResponse,
1718
ProgramStatistics,
19+
calculate_cliff_impact,
1820
calculate_labor_supply_response,
21+
configure_cliff_impact_variables,
1922
configure_labor_supply_response_variables,
2023
)
2124
from policyengine.outputs.decile_impact import (
@@ -63,6 +66,7 @@ class PolicyReformAnalysis(BaseModel):
6366
baseline_inequality: Inequality
6467
reform_inequality: Inequality
6568
labor_supply_response: LaborSupplyResponse
69+
cliff_impact: CliffImpact | None = None
6670

6771

6872
def _format_missing_program_variables(missing_variables: set[str]) -> str | None:
@@ -134,6 +138,7 @@ def economic_impact_analysis(
134138
baseline_simulation: Simulation,
135139
reform_simulation: Simulation,
136140
inequality_preset: Union[USInequalityPreset, str] = USInequalityPreset.STANDARD,
141+
include_cliff_impacts: bool = False,
137142
) -> PolicyReformAnalysis:
138143
"""Perform comprehensive analysis of a US policy reform.
139144
@@ -151,6 +156,8 @@ def economic_impact_analysis(
151156
reform_simulation,
152157
country_code="us",
153158
)
159+
if include_cliff_impacts:
160+
configure_cliff_impact_variables(baseline_simulation, reform_simulation)
154161
_validate_program_statistics_config(baseline_simulation, reform_simulation)
155162

156163
baseline_simulation.ensure()
@@ -218,6 +225,11 @@ def economic_impact_analysis(
218225
reform_simulation,
219226
country_code="us",
220227
)
228+
cliff_impact = (
229+
calculate_cliff_impact(baseline_simulation, reform_simulation)
230+
if include_cliff_impacts
231+
else None
232+
)
221233

222234
return PolicyReformAnalysis(
223235
decile_impacts=decile_impacts,
@@ -227,4 +239,5 @@ def economic_impact_analysis(
227239
baseline_inequality=baseline_inequality,
228240
reform_inequality=reform_inequality,
229241
labor_supply_response=labor_supply_response,
242+
cliff_impact=cliff_impact,
230243
)
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
from unittest.mock import MagicMock
2+
3+
import pandas as pd
4+
import pytest
5+
6+
from policyengine.core import OutputCollection
7+
from policyengine.outputs import (
8+
CliffImpact,
9+
CliffImpactInSimulation,
10+
LaborSupplyResponse,
11+
ProgramStatistics,
12+
)
13+
from policyengine.outputs.inequality import Inequality
14+
from policyengine.tax_benefit_models.uk import analysis as uk_analysis
15+
from policyengine.tax_benefit_models.us import analysis as us_analysis
16+
17+
18+
def _empty_collection() -> OutputCollection:
19+
return OutputCollection(outputs=[], dataframe=pd.DataFrame())
20+
21+
22+
def _empty_labor_supply_response() -> LaborSupplyResponse:
23+
return LaborSupplyResponse.model_construct()
24+
25+
26+
def _empty_inequality(simulation) -> Inequality:
27+
return Inequality.model_construct(
28+
simulation=simulation,
29+
income_variable="household_net_income",
30+
gini=0.0,
31+
top_10_share=0.0,
32+
top_1_share=0.0,
33+
bottom_50_share=0.0,
34+
)
35+
36+
37+
def _make_simulation(simulation_id: str, events: list[str]) -> MagicMock:
38+
simulation = MagicMock()
39+
simulation.id = simulation_id
40+
simulation.dataset.data.household = pd.DataFrame({"household_id": range(101)})
41+
simulation.tax_benefit_model_version.get_variable.return_value.entity = "household"
42+
simulation.ensure.side_effect = lambda: events.append(f"{simulation_id}.ensure")
43+
return simulation
44+
45+
46+
def _patch_analysis_dependencies(
47+
monkeypatch,
48+
analysis_module,
49+
*,
50+
country_code: str,
51+
events: list[str],
52+
fail_on_cliff: bool,
53+
cliff_result: CliffImpact | None = None,
54+
) -> None:
55+
class DummyProgramStatistics(ProgramStatistics):
56+
def run(self):
57+
self.baseline_total = 0.0
58+
self.reform_total = 0.0
59+
self.change = 0.0
60+
self.baseline_count = 0.0
61+
self.reform_count = 0.0
62+
self.winners = 0.0
63+
self.losers = 0.0
64+
65+
def fake_program_statistics(**kwargs):
66+
return DummyProgramStatistics.model_construct(**kwargs)
67+
68+
monkeypatch.setattr(
69+
analysis_module,
70+
"_validate_program_statistics_config",
71+
lambda baseline_simulation, reform_simulation: None,
72+
)
73+
monkeypatch.setattr(analysis_module, "ProgramStatistics", fake_program_statistics)
74+
monkeypatch.setattr(
75+
analysis_module,
76+
"configure_labor_supply_response_variables",
77+
lambda baseline_simulation, reform_simulation, country_code: None,
78+
)
79+
monkeypatch.setattr(
80+
analysis_module,
81+
"calculate_labor_supply_response",
82+
lambda baseline_simulation, reform_simulation, country_code: (
83+
_empty_labor_supply_response()
84+
),
85+
)
86+
monkeypatch.setattr(
87+
analysis_module,
88+
"calculate_decile_impacts",
89+
lambda **kwargs: _empty_collection(),
90+
)
91+
92+
if country_code == "uk":
93+
monkeypatch.setattr(
94+
analysis_module,
95+
"compute_intra_decile_impacts",
96+
lambda **kwargs: _empty_collection(),
97+
)
98+
monkeypatch.setattr(
99+
analysis_module,
100+
"calculate_uk_poverty_rates",
101+
lambda simulation: _empty_collection(),
102+
)
103+
monkeypatch.setattr(
104+
analysis_module,
105+
"calculate_uk_inequality",
106+
_empty_inequality,
107+
)
108+
else:
109+
monkeypatch.setattr(
110+
analysis_module,
111+
"calculate_us_poverty_rates",
112+
lambda simulation: _empty_collection(),
113+
)
114+
monkeypatch.setattr(
115+
analysis_module,
116+
"calculate_us_inequality",
117+
lambda simulation, preset: _empty_inequality(simulation),
118+
)
119+
120+
if fail_on_cliff:
121+
122+
def unexpected_cliff_call(*args, **kwargs):
123+
raise AssertionError("cliff helpers should not run by default")
124+
125+
monkeypatch.setattr(
126+
analysis_module,
127+
"configure_cliff_impact_variables",
128+
unexpected_cliff_call,
129+
)
130+
monkeypatch.setattr(
131+
analysis_module,
132+
"calculate_cliff_impact",
133+
unexpected_cliff_call,
134+
)
135+
return
136+
137+
def fake_configure_cliff_impact_variables(
138+
baseline_simulation,
139+
reform_simulation,
140+
):
141+
events.append("configure_cliff")
142+
143+
def fake_calculate_cliff_impact(
144+
baseline_simulation,
145+
reform_simulation,
146+
):
147+
events.append("calculate_cliff")
148+
return cliff_result
149+
150+
monkeypatch.setattr(
151+
analysis_module,
152+
"configure_cliff_impact_variables",
153+
fake_configure_cliff_impact_variables,
154+
)
155+
monkeypatch.setattr(
156+
analysis_module,
157+
"calculate_cliff_impact",
158+
fake_calculate_cliff_impact,
159+
)
160+
161+
162+
@pytest.mark.parametrize(
163+
("analysis_module", "country_code"),
164+
[(us_analysis, "us"), (uk_analysis, "uk")],
165+
)
166+
def test_economic_impact_analysis_defaults_cliff_impact_to_none(
167+
monkeypatch,
168+
analysis_module,
169+
country_code,
170+
):
171+
events: list[str] = []
172+
baseline = _make_simulation("baseline", events)
173+
reform = _make_simulation("reform", events)
174+
_patch_analysis_dependencies(
175+
monkeypatch,
176+
analysis_module,
177+
country_code=country_code,
178+
events=events,
179+
fail_on_cliff=True,
180+
)
181+
182+
result = analysis_module.economic_impact_analysis(baseline, reform)
183+
184+
assert result.cliff_impact is None
185+
assert events == ["baseline.ensure", "reform.ensure"]
186+
187+
188+
@pytest.mark.parametrize(
189+
("analysis_module", "country_code"),
190+
[(us_analysis, "us"), (uk_analysis, "uk")],
191+
)
192+
def test_economic_impact_analysis_can_include_cliff_impacts(
193+
monkeypatch,
194+
analysis_module,
195+
country_code,
196+
):
197+
events: list[str] = []
198+
baseline = _make_simulation("baseline", events)
199+
reform = _make_simulation("reform", events)
200+
cliff_result = CliffImpact(
201+
baseline=CliffImpactInSimulation(cliff_gap=1.0, cliff_share=0.1),
202+
reform=CliffImpactInSimulation(cliff_gap=2.0, cliff_share=0.2),
203+
)
204+
_patch_analysis_dependencies(
205+
monkeypatch,
206+
analysis_module,
207+
country_code=country_code,
208+
events=events,
209+
fail_on_cliff=False,
210+
cliff_result=cliff_result,
211+
)
212+
213+
result = analysis_module.economic_impact_analysis(
214+
baseline,
215+
reform,
216+
include_cliff_impacts=True,
217+
)
218+
219+
assert result.cliff_impact == cliff_result
220+
assert events == [
221+
"configure_cliff",
222+
"baseline.ensure",
223+
"reform.ensure",
224+
"calculate_cliff",
225+
]

0 commit comments

Comments
 (0)