From 069fa4009f067f7b0f08943cd025c403ab69e97a Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Thu, 15 May 2025 17:09:05 +0100 Subject: [PATCH 1/7] Enable cliff impacts Fixes #130 --- .../comparison/calculate_economy_comparison.py | 13 +++++++++---- .../macro/single/calculate_single_economy.py | 15 +++++++++++++-- pyproject.toml | 2 +- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/policyengine/outputs/macro/comparison/calculate_economy_comparison.py b/policyengine/outputs/macro/comparison/calculate_economy_comparison.py index f53691f9..96894ec2 100644 --- a/policyengine/outputs/macro/comparison/calculate_economy_comparison.py +++ b/policyengine/outputs/macro/comparison/calculate_economy_comparison.py @@ -793,16 +793,21 @@ class EconomyComparison(BaseModel): def calculate_economy_comparison( simulation: Simulation, + include_cliffs: bool = False, ) -> EconomyComparison: """Calculate comparison statistics between two economic scenarios.""" if not simulation.is_comparison: raise ValueError("Simulation must be a comparison simulation.") - baseline: SingleEconomy = simulation.calculate_single_economy(reform=False) - reform: SingleEconomy = simulation.calculate_single_economy(reform=True) + baseline: SingleEconomy = simulation.calculate_single_economy( + reform=False, include_cliffs=include_cliffs + ) + reform: SingleEconomy = simulation.calculate_single_economy( + reform=True, include_cliffs=include_cliffs + ) options = simulation.options country_id = options.country - if baseline.type == "general": + if not include_cliffs: budgetary_impact_data = budgetary_impact(baseline, reform) detailed_budgetary_impact_data = detailed_budgetary_impact( baseline, reform, country_id @@ -839,7 +844,7 @@ def calculate_economy_comparison( labor_supply_response=labor_supply_response_data, constituency_impact=constituency_impact_data, ) - elif baseline.type == "cliff": + elif include_cliffs: return dict( baseline=dict( cliff_gap=baseline.cliff_gap, diff --git a/policyengine/outputs/macro/single/calculate_single_economy.py b/policyengine/outputs/macro/single/calculate_single_economy.py index 3ed9f517..baf037bd 100644 --- a/policyengine/outputs/macro/single/calculate_single_economy.py +++ b/policyengine/outputs/macro/single/calculate_single_economy.py @@ -49,6 +49,8 @@ class SingleEconomy(BaseModel): weekly_hours_substitution_effect: float | None type: str programs: Dict[str, float] | None + cliff_gap: float | None = None + cliff_share: float | None = None @dataclass @@ -329,7 +331,7 @@ def calculate_uk_programs(self) -> Dict[str, float]: def calculate_single_economy( - simulation: Simulation, reform: bool = False + simulation: Simulation, reform: bool = False, include_cliffs: bool = False ) -> Dict: task_manager = GeneralEconomyTask( ( @@ -382,6 +384,13 @@ def calculate_single_economy( except: total_state_tax = 0 + if include_cliffs: + cliff_gap = task_manager.simulation.calculate("cliff_gap") + is_on_cliff = task_manager.simulation.calculate("is_on_cliff") + total_cliff_gap = cliff_gap.sum() + total_adults = task_manager.simulation.calculate("is_adult").sum() + cliff_share = is_on_cliff.sum() / total_adults + return SingleEconomy( **{ "total_net_income": total_net_income, @@ -414,7 +423,9 @@ def calculate_single_economy( "age": age, **labor_supply_responses, **lsr_working_hours, - "type": "general", + "type": "general" if include_cliffs else "cliff", "programs": uk_programs, + "cliff_gap": float(total_cliff_gap) if include_cliffs else None, + "cliff_share": float(cliff_share) if include_cliffs else None, } ) diff --git a/pyproject.toml b/pyproject.toml index e913360c..b19e6915 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "policyengine" -version = "0.3.1" +version = "0.3.2" description = "A package to conduct policy analysis using PolicyEngine tax-benefit models." readme = "README.md" authors = [ From 84879ee1fe698671466e6117ac7ae9d48fe895f4 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Thu, 15 May 2025 17:12:23 +0100 Subject: [PATCH 2/7] Bake into simulation --- .../comparison/calculate_economy_comparison.py | 13 ++++--------- .../macro/single/calculate_single_economy.py | 3 ++- policyengine/simulation.py | 4 ++++ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/policyengine/outputs/macro/comparison/calculate_economy_comparison.py b/policyengine/outputs/macro/comparison/calculate_economy_comparison.py index 96894ec2..95d4ffc5 100644 --- a/policyengine/outputs/macro/comparison/calculate_economy_comparison.py +++ b/policyengine/outputs/macro/comparison/calculate_economy_comparison.py @@ -793,21 +793,16 @@ class EconomyComparison(BaseModel): def calculate_economy_comparison( simulation: Simulation, - include_cliffs: bool = False, ) -> EconomyComparison: """Calculate comparison statistics between two economic scenarios.""" if not simulation.is_comparison: raise ValueError("Simulation must be a comparison simulation.") - baseline: SingleEconomy = simulation.calculate_single_economy( - reform=False, include_cliffs=include_cliffs - ) - reform: SingleEconomy = simulation.calculate_single_economy( - reform=True, include_cliffs=include_cliffs - ) + baseline: SingleEconomy = simulation.calculate_single_economy(reform=False) + reform: SingleEconomy = simulation.calculate_single_economy(reform=True) options = simulation.options country_id = options.country - if not include_cliffs: + if not simulation.options.include_cliffs: budgetary_impact_data = budgetary_impact(baseline, reform) detailed_budgetary_impact_data = detailed_budgetary_impact( baseline, reform, country_id @@ -844,7 +839,7 @@ def calculate_economy_comparison( labor_supply_response=labor_supply_response_data, constituency_impact=constituency_impact_data, ) - elif include_cliffs: + else: return dict( baseline=dict( cliff_gap=baseline.cliff_gap, diff --git a/policyengine/outputs/macro/single/calculate_single_economy.py b/policyengine/outputs/macro/single/calculate_single_economy.py index baf037bd..3ea39c43 100644 --- a/policyengine/outputs/macro/single/calculate_single_economy.py +++ b/policyengine/outputs/macro/single/calculate_single_economy.py @@ -331,8 +331,9 @@ def calculate_uk_programs(self) -> Dict[str, float]: def calculate_single_economy( - simulation: Simulation, reform: bool = False, include_cliffs: bool = False + simulation: Simulation, reform: bool = False ) -> Dict: + include_cliffs = simulation.options.include_cliffs task_manager = GeneralEconomyTask( ( simulation.baseline_simulation diff --git a/policyengine/simulation.py b/policyengine/simulation.py index ecb794fc..53660941 100644 --- a/policyengine/simulation.py +++ b/policyengine/simulation.py @@ -58,6 +58,10 @@ class SimulationOptions(BaseModel): "[Analysis title]", description="The title of the analysis (for charts). If not provided, a default title will be generated.", ) + include_cliffs: bool | None = Field( + False, + description="Whether to include tax-benefit cliffs in the simulation analyses. If True, cliffs will be included.", + ) class Simulation: From 7c6a12b42425f39415d56c607024f0581cd2c923 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Fri, 16 May 2025 09:18:27 +0100 Subject: [PATCH 3/7] Fix review issues --- .../calculate_economy_comparison.py | 96 +++++++++++-------- .../macro/single/calculate_single_economy.py | 2 +- 2 files changed, 56 insertions(+), 42 deletions(-) diff --git a/policyengine/outputs/macro/comparison/calculate_economy_comparison.py b/policyengine/outputs/macro/comparison/calculate_economy_comparison.py index 95d4ffc5..636717c5 100644 --- a/policyengine/outputs/macro/comparison/calculate_economy_comparison.py +++ b/policyengine/outputs/macro/comparison/calculate_economy_comparison.py @@ -775,6 +775,16 @@ def uk_constituency_breakdown( return UKConstituencyBreakdownWithValues(**output) +class CliffImpactInSimulation(BaseModel): + cliff_gap: float + cliff_share: float + + +class CliffImpact(BaseModel): + baseline: CliffImpactInSimulation + reform: CliffImpactInSimulation + + class EconomyComparison(BaseModel): country_package_version: str budget: BudgetaryImpact @@ -789,6 +799,7 @@ class EconomyComparison(BaseModel): intra_wealth_decile: IntraWealthDecileImpact labor_supply_response: LaborSupplyResponse constituency_impact: UKConstituencyBreakdown + cliff_impact: CliffImpact | None def calculate_economy_comparison( @@ -802,51 +813,54 @@ def calculate_economy_comparison( reform: SingleEconomy = simulation.calculate_single_economy(reform=True) options = simulation.options country_id = options.country - if not simulation.options.include_cliffs: - budgetary_impact_data = budgetary_impact(baseline, reform) - detailed_budgetary_impact_data = detailed_budgetary_impact( - baseline, reform, country_id - ) - decile_impact_data = decile_impact(baseline, reform) - inequality_impact_data = inequality_impact(baseline, reform) - poverty_impact_data = poverty_impact(baseline, reform) - poverty_by_gender_data = poverty_gender_breakdown(baseline, reform) - poverty_by_race_data = poverty_racial_breakdown(baseline, reform) - intra_decile_impact_data = intra_decile_impact(baseline, reform) - labor_supply_response_data = labor_supply_response(baseline, reform) - constituency_impact_data: UKConstituencyBreakdown = ( - uk_constituency_breakdown(baseline, reform, country_id) - ) - wealth_decile_impact_data = wealth_decile_impact( - baseline, reform, country_id - ) - intra_wealth_decile_impact_data = intra_wealth_decile_impact( - baseline, reform, country_id - ) - - return EconomyComparison( - country_package_version=get_country_package_version(country_id), - budget=budgetary_impact_data, - detailed_budget=detailed_budgetary_impact_data, - decile=decile_impact_data, - inequality=inequality_impact_data, - poverty=poverty_impact_data, - poverty_by_gender=poverty_by_gender_data, - poverty_by_race=poverty_by_race_data, - intra_decile=intra_decile_impact_data, - wealth_decile=wealth_decile_impact_data, - intra_wealth_decile=intra_wealth_decile_impact_data, - labor_supply_response=labor_supply_response_data, - constituency_impact=constituency_impact_data, - ) - else: - return dict( - baseline=dict( + budgetary_impact_data = budgetary_impact(baseline, reform) + detailed_budgetary_impact_data = detailed_budgetary_impact( + baseline, reform, country_id + ) + decile_impact_data = decile_impact(baseline, reform) + inequality_impact_data = inequality_impact(baseline, reform) + poverty_impact_data = poverty_impact(baseline, reform) + poverty_by_gender_data = poverty_gender_breakdown(baseline, reform) + poverty_by_race_data = poverty_racial_breakdown(baseline, reform) + intra_decile_impact_data = intra_decile_impact(baseline, reform) + labor_supply_response_data = labor_supply_response(baseline, reform) + constituency_impact_data: UKConstituencyBreakdown = ( + uk_constituency_breakdown(baseline, reform, country_id) + ) + wealth_decile_impact_data = wealth_decile_impact( + baseline, reform, country_id + ) + intra_wealth_decile_impact_data = intra_wealth_decile_impact( + baseline, reform, country_id + ) + + if simulation.options.include_cliffs: + cliff_impact = CliffImpact( + baseline=CliffImpactInSimulation( cliff_gap=baseline.cliff_gap, cliff_share=baseline.cliff_share, ), - reform=dict( + reform=CliffImpactInSimulation( cliff_gap=reform.cliff_gap, cliff_share=reform.cliff_share, ), ) + else: + cliff_impact = None + + return EconomyComparison( + country_package_version=get_country_package_version(country_id), + budget=budgetary_impact_data, + detailed_budget=detailed_budgetary_impact_data, + decile=decile_impact_data, + inequality=inequality_impact_data, + poverty=poverty_impact_data, + poverty_by_gender=poverty_by_gender_data, + poverty_by_race=poverty_by_race_data, + intra_decile=intra_decile_impact_data, + wealth_decile=wealth_decile_impact_data, + intra_wealth_decile=intra_wealth_decile_impact_data, + labor_supply_response=labor_supply_response_data, + constituency_impact=constituency_impact_data, + cliff_impact=cliff_impact, + ) diff --git a/policyengine/outputs/macro/single/calculate_single_economy.py b/policyengine/outputs/macro/single/calculate_single_economy.py index 3ea39c43..f7e5d4f9 100644 --- a/policyengine/outputs/macro/single/calculate_single_economy.py +++ b/policyengine/outputs/macro/single/calculate_single_economy.py @@ -424,7 +424,7 @@ def calculate_single_economy( "age": age, **labor_supply_responses, **lsr_working_hours, - "type": "general" if include_cliffs else "cliff", + "type": "general" if not include_cliffs else "cliff", "programs": uk_programs, "cliff_gap": float(total_cliff_gap) if include_cliffs else None, "cliff_share": float(cliff_share) if include_cliffs else None, From a476e442c68cb659c0ec54e3b3f9e342f35dce43 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Fri, 16 May 2025 09:26:54 +0100 Subject: [PATCH 4/7] Few other minor issues --- .../macro/single/calculate_single_economy.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/policyengine/outputs/macro/single/calculate_single_economy.py b/policyengine/outputs/macro/single/calculate_single_economy.py index f7e5d4f9..d8c1ec63 100644 --- a/policyengine/outputs/macro/single/calculate_single_economy.py +++ b/policyengine/outputs/macro/single/calculate_single_economy.py @@ -10,6 +10,8 @@ from policyengine_core.simulations import Microsimulation from typing import Dict from dataclasses import dataclass +from typing import Literal +from microdf import MicroSeries class SingleEconomy(BaseModel): @@ -47,7 +49,7 @@ class SingleEconomy(BaseModel): weekly_hours: float | None weekly_hours_income_effect: float | None weekly_hours_substitution_effect: float | None - type: str + type: Literal["general", "cliff"] programs: Dict[str, float] | None cliff_gap: float | None = None cliff_share: float | None = None @@ -386,11 +388,15 @@ def calculate_single_economy( total_state_tax = 0 if include_cliffs: - cliff_gap = task_manager.simulation.calculate("cliff_gap") - is_on_cliff = task_manager.simulation.calculate("is_on_cliff") - total_cliff_gap = cliff_gap.sum() - total_adults = task_manager.simulation.calculate("is_adult").sum() - cliff_share = is_on_cliff.sum() / total_adults + cliff_gap: MicroSeries = task_manager.simulation.calculate("cliff_gap") + is_on_cliff: MicroSeries = task_manager.simulation.calculate( + "is_on_cliff" + ) + total_cliff_gap: float = cliff_gap.sum() + total_adults: float = task_manager.simulation.calculate( + "is_adult" + ).sum() + cliff_share: float = is_on_cliff.sum() / total_adults return SingleEconomy( **{ From 67032c2a90333f322e465291452d2f2a7fba5208 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Mon, 19 May 2025 11:04:26 +0100 Subject: [PATCH 5/7] Enable cliff impacts Fixes #130 --- .../macro/single/calculate_single_economy.py | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/policyengine/outputs/macro/single/calculate_single_economy.py b/policyengine/outputs/macro/single/calculate_single_economy.py index d8c1ec63..34e5ee1d 100644 --- a/policyengine/outputs/macro/single/calculate_single_economy.py +++ b/policyengine/outputs/macro/single/calculate_single_economy.py @@ -331,6 +331,22 @@ def calculate_uk_programs(self) -> Dict[str, float]: for program in UKPrograms.PROGRAMS } + def calculate_cliffs(self): + cliff_gap: MicroSeries = self.simulation.calculate("cliff_gap") + is_on_cliff: MicroSeries = self.simulation.calculate("is_on_cliff") + total_cliff_gap: float = cliff_gap.sum() + total_adults: float = self.simulation.calculate("is_adult").sum() + cliff_share: float = is_on_cliff.sum() / total_adults + return CliffImpactInSimulation( + cliff_gap=total_cliff_gap, + cliff_share=cliff_share, + ) + + +class CliffImpactInSimulation(BaseModel): + cliff_gap: float + cliff_share: float + def calculate_single_economy( simulation: Simulation, reform: bool = False @@ -388,15 +404,12 @@ def calculate_single_economy( total_state_tax = 0 if include_cliffs: - cliff_gap: MicroSeries = task_manager.simulation.calculate("cliff_gap") - is_on_cliff: MicroSeries = task_manager.simulation.calculate( - "is_on_cliff" - ) - total_cliff_gap: float = cliff_gap.sum() - total_adults: float = task_manager.simulation.calculate( - "is_adult" - ).sum() - cliff_share: float = is_on_cliff.sum() / total_adults + cliffs = task_manager.calculate_cliffs() + cliff_gap = cliffs.cliff_gap + cliff_share = cliffs.cliff_share + else: + cliff_gap = None + cliff_share = None return SingleEconomy( **{ @@ -432,7 +445,7 @@ def calculate_single_economy( **lsr_working_hours, "type": "general" if not include_cliffs else "cliff", "programs": uk_programs, - "cliff_gap": float(total_cliff_gap) if include_cliffs else None, - "cliff_share": float(cliff_share) if include_cliffs else None, + "cliff_gap": cliff_gap if include_cliffs else None, + "cliff_share": cliff_share if include_cliffs else None, } ) From a03a7058a7e11d11d7e4df5e572870154042e795 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Mon, 19 May 2025 11:07:50 +0100 Subject: [PATCH 6/7] Fix bad syntax --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dd9726d8..4d5cf839 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "policyengine" -version = "0.3.7 +version = "0.3.7" description = "A package to conduct policy analysis using PolicyEngine tax-benefit models." readme = "README.md" authors = [ From 72a63eab64fb0bc5094ec05f8d135db115f5f7f7 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Mon, 19 May 2025 11:09:08 +0100 Subject: [PATCH 7/7] Changelog entry --- changelog_entry.yaml | 4 ++++ pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/changelog_entry.yaml b/changelog_entry.yaml index e69de29b..3b8d8bb3 100644 --- a/changelog_entry.yaml +++ b/changelog_entry.yaml @@ -0,0 +1,4 @@ +- bump: patch + changes: + fixed: + - Added cliff impacts. diff --git a/pyproject.toml b/pyproject.toml index 4d5cf839..dbb863c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "policyengine" -version = "0.3.7" +version = "0.3.6" description = "A package to conduct policy analysis using PolicyEngine tax-benefit models." readme = "README.md" authors = [