diff --git a/policyengine/economic_impact/budgetary_impact/by_program/__init__.py b/policyengine/economic_impact/budgetary_impact/by_program/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/policyengine/economic_impact/budgetary_impact/by_program/by_program.py b/policyengine/economic_impact/budgetary_impact/by_program/by_program.py new file mode 100644 index 0000000..a1273d7 --- /dev/null +++ b/policyengine/economic_impact/budgetary_impact/by_program/by_program.py @@ -0,0 +1,192 @@ +from policyengine.economic_impact.base_metric_calculator import BaseMetricCalculator +from policyengine_uk import Microsimulation + +class IncomeTax(BaseMetricCalculator): + def __init__(self, baseline: Microsimulation, reformed: Microsimulation, default_period: int = 2024) -> None: + super().__init__(baseline, reformed, default_period) + self.baseline = baseline + self.reformed = reformed + + def calculate(self): + + baseline = self.baseline.calculate("income_tax", map_to="household").sum() + reform = self.reformed.calculate("income_tax", map_to="household").sum() + + change = ((reform - baseline) / baseline) * 100 + + return { + "baseline": round(baseline,2), + "reform": round(reform,2), + "change": round(change,1) + } + +class NationalInsurance(BaseMetricCalculator): + def __init__(self, baseline: Microsimulation, reformed: Microsimulation, default_period: int = 2024) -> None: + super().__init__(baseline, reformed, default_period) + self.baseline = baseline + self.reformed = reformed + + def calculate(self): + + baseline = self.baseline.calculate("national_insurance", map_to="household").sum() + reform = self.reformed.calculate("national_insurance", map_to="household").sum() + + change = ((reform - baseline) / baseline) * 100 + + return { + "baseline": round(baseline,2), + "reform": round(reform,2), + "change": round(change,1) + } + +class Vat(BaseMetricCalculator): + def __init__(self, baseline: Microsimulation, reformed: Microsimulation, default_period: int = 2024) -> None: + super().__init__(baseline, reformed, default_period) + self.baseline = baseline + self.reformed = reformed + + def calculate(self): + + baseline = self.baseline.calculate("vat", map_to="household").sum() + reform = self.reformed.calculate("vat", map_to="household").sum() + + change = ((reform - baseline) / baseline) * 100 + + return { + "baseline": round(baseline,2), + "reform": round(reform,2), + "change": round(change,1) + } + +class CouncilTax(BaseMetricCalculator): + def __init__(self, baseline: Microsimulation, reformed: Microsimulation, default_period: int = 2024) -> None: + super().__init__(baseline, reformed, default_period) + self.baseline = baseline + self.reformed = reformed + + def calculate(self): + + baseline = self.baseline.calculate("council_tax", map_to="household").sum() + reform = self.reformed.calculate("council_tax", map_to="household").sum() + + change = ((reform - baseline) / baseline) * 100 + + return { + "baseline": round(baseline,2), + "reform": round(reform,2), + "change": round(change,1) + } + +class FuelDuty(BaseMetricCalculator): + def __init__(self, baseline: Microsimulation, reformed: Microsimulation, default_period: int = 2024) -> None: + super().__init__(baseline, reformed, default_period) + self.baseline = baseline + self.reformed = reformed + + def calculate(self): + + baseline = self.baseline.calculate("fuel_duty", map_to="household").sum() + reform = self.reformed.calculate("fuel_duty", map_to="household").sum() + + change = ((reform - baseline) / baseline) * 100 + + return { + "baseline": round(baseline,2), + "reform": round(reform,2), + "change": round(change,1) + } + +class TaxCredits(BaseMetricCalculator): + def __init__(self, baseline: Microsimulation, reformed: Microsimulation, default_period: int = 2024) -> None: + super().__init__(baseline, reformed, default_period) + self.baseline = baseline + self.reformed = reformed + + def calculate(self): + + baseline = self.baseline.calculate("tax_credits", map_to="household").sum() * -1 + reform = self.reformed.calculate("tax_credits", map_to="household").sum() * -1 + + change = ((reform - baseline) / baseline) * 100 + + return { + "baseline": round(baseline,2), + "reform": round(reform,2), + "change": round(change,1) + } + +class UniversalCredit(BaseMetricCalculator): + def __init__(self, baseline: Microsimulation, reformed: Microsimulation, default_period: int = 2024) -> None: + super().__init__(baseline, reformed, default_period) + self.baseline = baseline + self.reformed = reformed + + def calculate(self): + + baseline = self.baseline.calculate("universal_credit", map_to="household").sum() * -1 + reform = self.reformed.calculate("universal_credit", map_to="household").sum() * -1 + + change = ((reform - baseline) / baseline) * 100 + + return { + "baseline": round(baseline,2), + "reform": round(reform,2), + "change": round(change,1) + } + +class ChildBenefit(BaseMetricCalculator): + def __init__(self, baseline: Microsimulation, reformed: Microsimulation, default_period: int = 2024) -> None: + super().__init__(baseline, reformed, default_period) + self.baseline = baseline + self.reformed = reformed + + def calculate(self): + + baseline = self.baseline.calculate("child_benefit", map_to="household").sum() * -1 + reform = self.reformed.calculate("child_benefit", map_to="household").sum() * -1 + + change = ((reform - baseline) / baseline) * 100 + + return { + "baseline": round(baseline,2), + "reform": round(reform,2), + "change": round(change,1) + } + +class StatePension(BaseMetricCalculator): + def __init__(self, baseline: Microsimulation, reformed: Microsimulation, default_period: int = 2024) -> None: + super().__init__(baseline, reformed, default_period) + self.baseline = baseline + self.reformed = reformed + + def calculate(self): + + baseline = self.baseline.calculate("state_pension", map_to="household").sum() * -1 + reform = self.reformed.calculate("state_pension", map_to="household").sum() * -1 + + change = ((reform - baseline) / baseline) * 100 + + return { + "baseline": round(baseline,2), + "reform": round(reform,2), + "change": round(change,1) + } + +class PensionCredit(BaseMetricCalculator): + def __init__(self, baseline: Microsimulation, reformed: Microsimulation, default_period: int = 2024) -> None: + super().__init__(baseline, reformed, default_period) + self.baseline = baseline + self.reformed = reformed + + def calculate(self): + + baseline = self.baseline.calculate("pension_credit", map_to="household").sum() * -1 + reform = self.reformed.calculate("pension_credit", map_to="household").sum() * -1 + + change = ((reform - baseline) / baseline) * 100 + + return { + "baseline": round(baseline,2), + "reform": round(reform,2), + "change": round(change,1) + } \ No newline at end of file diff --git a/policyengine/economic_impact/budgetary_impact/overall/__init__.py b/policyengine/economic_impact/budgetary_impact/overall/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/policyengine/economic_impact/budgetary_impact/overall/overall.py b/policyengine/economic_impact/budgetary_impact/overall/overall.py new file mode 100644 index 0000000..c081257 --- /dev/null +++ b/policyengine/economic_impact/budgetary_impact/overall/overall.py @@ -0,0 +1,71 @@ +from policyengine.economic_impact.base_metric_calculator import BaseMetricCalculator +from policyengine_uk import Microsimulation + +class BudgetaryImpact(BaseMetricCalculator): + def __init__(self, baseline: Microsimulation, reformed: Microsimulation, default_period: int = 2024) -> None: + super().__init__(baseline, reformed, default_period) + self.baseline = baseline + self.reformed = reformed + + def calculate(self): + + baseline_total_tax = self.baseline.calculate("household_tax").sum() + reformed_total_tax = self.reformed.calculate("household_tax").sum() + + tax_revenue_impact = reformed_total_tax - baseline_total_tax + + baseline_total_benefits = self.baseline.calculate("household_benefits").sum() + reformed_total_benefits = self.reformed.calculate("household_benefits").sum() + + + benefit_spending_impact = reformed_total_benefits - baseline_total_benefits + + budgetary_impact = tax_revenue_impact - benefit_spending_impact + + + + return { + "budgetary_impact" : round(budgetary_impact,2) + } + +class BenefitSpendingImpact(BaseMetricCalculator): + def __init__(self, baseline: Microsimulation, reformed: Microsimulation, default_period: int = 2024) -> None: + super().__init__(baseline, reformed, default_period) + self.baseline = baseline + self.reformed = reformed + + def calculate(self): + + baseline_total_benefits = self.baseline.calculate("household_benefits").sum() + reformed_total_benefits = self.reformed.calculate("household_benefits").sum() + + + benefit_spending_impact = reformed_total_benefits - baseline_total_benefits + + + + return { + "baseline_total_benefits": round(baseline_total_benefits,2), + "reformed_total_benefits": round(reformed_total_benefits,2), + "benefit_spending_impact": round(benefit_spending_impact,2) + } + +class TaxRevenueImpact(BaseMetricCalculator): + def __init__(self, baseline: Microsimulation, reformed: Microsimulation, default_period: int = 2024) -> None: + super().__init__(baseline, reformed, default_period) + self.baseline = baseline + self.reformed = reformed + + def calculate(self): + + baseline_total_tax = self.baseline.calculate("household_tax").sum() + reformed_total_tax = self.reformed.calculate("household_tax").sum() + + tax_revenue_impact = reformed_total_tax - baseline_total_tax + + + return { + "baseline_total_tax": round(baseline_total_tax,2), + "reformed_total_tax": round(reformed_total_tax,2), + "tax_revenue_impact": round(tax_revenue_impact,2) + } \ No newline at end of file diff --git a/policyengine/economic_impact/economic_impact.py b/policyengine/economic_impact/economic_impact.py index ff57ebd..a539eff 100644 --- a/policyengine/economic_impact/economic_impact.py +++ b/policyengine/economic_impact/economic_impact.py @@ -22,6 +22,26 @@ FemalePoverty as DeepFemalePoverty, AllPoverty as DeepGenderAllPoverty ) + +from .budgetary_impact.by_program.by_program import ( + IncomeTax, + NationalInsurance, + Vat, + CouncilTax, + FuelDuty, + TaxCredits, + UniversalCredit, + ChildBenefit, + StatePension, + PensionCredit +) + +from .budgetary_impact.overall.overall import ( + BudgetaryImpact, + BenefitSpendingImpact, + TaxRevenueImpact +) + from typing import Dict class EconomicImpact: @@ -55,6 +75,19 @@ def __init__(self, reform: dict, country: str) -> None: # Set up metric calculators self.metric_calculators: Dict[str, object] = { + "budgetary/overall/budgetary_impact" : BudgetaryImpact(self.baseline, self.reformed), + "budgetary/overall/benefit_spending_impact" : BenefitSpendingImpact(self.baseline, self.reformed), + "budgetary/overall/tax_revenue_impact" : TaxRevenueImpact(self.baseline, self.reformed), + "budgetary/by_program/income_tax" : IncomeTax(self.baseline, self.reformed), + "budgetary/by_program/national_insurance" : NationalInsurance(self.baseline, self.reformed), + "budgetary/by_program/vat" : Vat(self.baseline, self.reformed), + "budgetary/by_program/council_tax" : CouncilTax(self.baseline, self.reformed), + "budgetary/by_program/fuel_duty" : FuelDuty(self.baseline, self.reformed), + "budgetary/by_program/tax_credits" : TaxCredits(self.baseline, self.reformed), + "budgetary/by_program/universal_credits" : UniversalCredit(self.baseline, self.reformed), + "budgetary/by_program/child_benefits" : ChildBenefit(self.baseline, self.reformed), + "budgetary/by_program/state_pension" : StatePension(self.baseline, self.reformed), + "budgetary/by_program/pension_credit" : PensionCredit(self.baseline, self.reformed), "inequality/gini": GiniCalculator(self.baseline, self.reformed), "inequality/top_1_pct_share": Top1PctShareCalculator(self.baseline, self.reformed), "inequality/top_10_pct_share": Top10PctShareCalculator(self.baseline, self.reformed), diff --git a/policyengine/tests/economic_impact/budgetary_impact/by_program/by_program.yaml b/policyengine/tests/economic_impact/budgetary_impact/by_program/by_program.yaml new file mode 100644 index 0000000..f5d2d29 --- /dev/null +++ b/policyengine/tests/economic_impact/budgetary_impact/by_program/by_program.yaml @@ -0,0 +1,100 @@ +# by_program +- test_income_tax: + reform: + gov.hmrc.income_tax.rates.uk[0].rate: + "2024-01-01.2100-12-31": 0.55 + country: uk + expected: + baseline: 291090070166.62 + reform: 496242053771.2 + change: 70.5 + +- test_national_insurance: + reform: + gov.hmrc.income_tax.rates.uk[0].rate: + "2024-01-01.2100-12-31": 0.55 + country: uk + expected: + baseline: 50826792606.89 + reform: 50826792606.89 + change: 0.0 + +- test_vat: + reform: + gov.hmrc.income_tax.rates.uk[0].rate: + "2024-01-01.2100-12-31": 0.55 + country: uk + expected: + baseline: 175581776889.21 + reform: 175581776889.21 + change: 0.0 + +- test_council_tax: + reform: + gov.hmrc.income_tax.rates.uk[0].rate: + "2024-01-01.2100-12-31": 0.55 + country: uk + expected: + baseline: 47861314826.79 + reform: 47861314826.79 + change: 0.0 + +- test_fuel_duty: + reform: + gov.hmrc.income_tax.rates.uk[0].rate: + "2024-01-01.2100-12-31": 0.55 + country: uk + expected: + baseline: 28019829809.09 + reform: 28019829809.09 + change: 0.0 + +- test_tax_credits: + reform: + gov.hmrc.income_tax.rates.uk[0].rate: + "2024-01-01.2100-12-31": 0.55 + country: uk + expected: + baseline: -208150256.01 + reform: -308166663.98 + change: 48.1 + +- test_universal_credits: + reform: + gov.hmrc.income_tax.rates.uk[0].rate: + "2024-01-01.2100-12-31": 0.55 + country: uk + expected: + baseline: -72209672284.1 + reform: -73780445681.08 + change: 2.2 + +- test_child_benefits: + reform: + gov.hmrc.income_tax.rates.uk[0].rate: + "2024-01-01.2100-12-31": 0.55 + country: uk + expected: + baseline: -15975002346.41 + reform: -15975002346.41 + change: -0.0 + +- test_state_pension: + reform: + gov.hmrc.income_tax.rates.uk[0].rate: + "2024-01-01.2100-12-31": 0.55 + country: uk + expected: + baseline: -127240166697.26 + reform: -127240166697.26 + change: -0.0 + +- test_pension_credit: + reform: + gov.hmrc.income_tax.rates.uk[0].rate: + "2024-01-01.2100-12-31": 0.55 + country: uk + expected: + baseline: -2000983943.05 + reform: -2181135212.4 + change: 9.0 \ No newline at end of file diff --git a/policyengine/tests/economic_impact/budgetary_impact/by_program/test_by_program.py b/policyengine/tests/economic_impact/budgetary_impact/by_program/test_by_program.py new file mode 100644 index 0000000..6bb48a0 --- /dev/null +++ b/policyengine/tests/economic_impact/budgetary_impact/by_program/test_by_program.py @@ -0,0 +1,53 @@ +import pytest +import yaml +import os +from policyengine import EconomicImpact + +def assert_dict_approx_equal(actual, expected, tolerance=1e3): + for key in expected: + assert abs(actual[key] - expected[key]) < tolerance, f"Key {key}: expected {expected[key]}, got {actual[key]}" + + +yaml_file_path = "policyengine/tests/economic_impact/budgetary_impact/by_program/by_program.yaml" + +# Check if the file exists +if not os.path.exists(yaml_file_path): + raise FileNotFoundError(f"The YAML file does not exist at: {yaml_file_path}") + +with open(yaml_file_path, 'r') as file: + test_cases = yaml.safe_load(file) + +@pytest.mark.parametrize("test_case", test_cases) +def test_economic_impact(test_case): + test_name = list(test_case.keys())[0] + test_data = test_case[test_name] + + economic_impact = EconomicImpact(test_data['reform'], test_data['country']) + + if 'income_tax' in test_name: + result = economic_impact.calculate("budgetary/by_program/income_tax") + elif 'national_insurance' in test_name: + result = economic_impact.calculate("budgetary/by_program/national_insurance") + elif 'vat' in test_name: + result = economic_impact.calculate("budgetary/by_program/vat") + elif 'council_tax' in test_name: + result = economic_impact.calculate("budgetary/by_program/council_tax") + elif 'fuel_duty' in test_name: + result = economic_impact.calculate("budgetary/by_program/fuel_duty") + elif 'tax_credits' in test_name: + result = economic_impact.calculate("budgetary/by_program/tax_credits") + elif 'universal_credits' in test_name: + result = economic_impact.calculate("budgetary/by_program/universal_credits") + elif 'child_benefits' in test_name: + result = economic_impact.calculate("budgetary/by_program/child_benefits") + elif 'state_pension' in test_name: + result = economic_impact.calculate("budgetary/by_program/state_pension") + elif 'pension_credit' in test_name: + result = economic_impact.calculate("budgetary/by_program/pension_credit") + else: + pytest.fail(f"Unknown test case: {test_name}") + + assert_dict_approx_equal(result, test_data['expected']) + +if __name__ == "__main__": + pytest.main([__file__]) diff --git a/policyengine/tests/economic_impact/budgetary_impact/overall/overall.yaml b/policyengine/tests/economic_impact/budgetary_impact/overall/overall.yaml new file mode 100644 index 0000000..7e9dd91 --- /dev/null +++ b/policyengine/tests/economic_impact/budgetary_impact/overall/overall.yaml @@ -0,0 +1,28 @@ +# overall +- test_budgetary_impact: + reform: + gov.hmrc.income_tax.rates.uk[0].rate: + "2024-01-01.2100-12-31": 0.55 + country: uk + expected: + budgetary_impact: 203274712297.14 + +- test_benefit_spending_impact: + reform: + gov.hmrc.income_tax.rates.uk[0].rate: + "2024-01-01.2100-12-31": 0.55 + country: uk + expected: + baseline_total_benefits: 247160184562.67 + reformed_total_benefits: 249032006583.15 + benefit_spending_impact: 1871822020.49 + +- test_tax_revenue_impact: + reform: + gov.hmrc.income_tax.rates.uk[0].rate: + "2024-01-01.2100-12-31": 0.55 + country: uk + expected: + baseline_total_tax: 447861864968.89 + reformed_total_tax: 653008399286.52 + tax_revenue_impact: 205146534317.63 \ No newline at end of file diff --git a/policyengine/tests/economic_impact/budgetary_impact/overall/test_overall.py b/policyengine/tests/economic_impact/budgetary_impact/overall/test_overall.py new file mode 100644 index 0000000..6c13d4c --- /dev/null +++ b/policyengine/tests/economic_impact/budgetary_impact/overall/test_overall.py @@ -0,0 +1,39 @@ +import pytest +import yaml +import os +from policyengine import EconomicImpact + +def assert_dict_approx_equal(actual, expected, tolerance=1e3): + for key in expected: + assert abs(actual[key] - expected[key]) < tolerance, f"Key {key}: expected {expected[key]}, got {actual[key]}" + + +yaml_file_path = "policyengine/tests/economic_impact/budgetary_impact/overall/overall.yaml" + +# Check if the file exists +if not os.path.exists(yaml_file_path): + raise FileNotFoundError(f"The YAML file does not exist at: {yaml_file_path}") + +with open(yaml_file_path, 'r') as file: + test_cases = yaml.safe_load(file) + +@pytest.mark.parametrize("test_case", test_cases) +def test_economic_impact(test_case): + test_name = list(test_case.keys())[0] + test_data = test_case[test_name] + + economic_impact = EconomicImpact(test_data['reform'], test_data['country']) + + if 'budgetary' in test_name: + result = economic_impact.calculate("budgetary/overall/budgetary_impact") + elif 'benefit' in test_name: + result = economic_impact.calculate("budgetary/overall/benefit_spending_impact") + elif 'tax' in test_name: + result = economic_impact.calculate("budgetary/overall/tax_revenue_impact") + else: + pytest.fail(f"Unknown test case: {test_name}") + + assert_dict_approx_equal(result, test_data['expected']) + +if __name__ == "__main__": + pytest.main([__file__]) \ No newline at end of file