diff --git a/policyengine/economic_impact/distributional_impact/__init__.py b/policyengine/economic_impact/distributional_impact/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/policyengine/economic_impact/distributional_impact/by_income_decile/__init__.py b/policyengine/economic_impact/distributional_impact/by_income_decile/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/policyengine/economic_impact/distributional_impact/by_income_decile/average/__init__.py b/policyengine/economic_impact/distributional_impact/by_income_decile/average/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/policyengine/economic_impact/distributional_impact/by_income_decile/average/average.py b/policyengine/economic_impact/distributional_impact/by_income_decile/average/average.py new file mode 100644 index 0000000..de64b2c --- /dev/null +++ b/policyengine/economic_impact/distributional_impact/by_income_decile/average/average.py @@ -0,0 +1,37 @@ +from policyengine.economic_impact.base_metric_calculator import BaseMetricCalculator +from policyengine_uk import Microsimulation +from microdf import MicroDataFrame, MicroSeries + + +class Average(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_income = MicroSeries(self.baseline.calculate("household_net_income"), weights = self.baseline.calculate("household_weight")) + reform_income = MicroSeries(self.reformed.calculate("household_net_income") , weights = baseline_income.weights) + + decile = self.baseline.calculate("household_income_decile") + income_change = reform_income - baseline_income + + + avg_income_change_by_decile = ( + income_change.groupby(decile).sum() + / baseline_income.groupby(decile).count() + ) + + + avg_decile_dict = avg_income_change_by_decile.to_dict() + result = dict( + + average={int(k): v for k, v in avg_decile_dict.items() if k > 0}, + ) + + + return { + "average": result["average"], + + } \ No newline at end of file diff --git a/policyengine/economic_impact/distributional_impact/by_income_decile/relative/__init__.py b/policyengine/economic_impact/distributional_impact/by_income_decile/relative/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/policyengine/economic_impact/distributional_impact/by_income_decile/relative/relative.py b/policyengine/economic_impact/distributional_impact/by_income_decile/relative/relative.py new file mode 100644 index 0000000..33f6098 --- /dev/null +++ b/policyengine/economic_impact/distributional_impact/by_income_decile/relative/relative.py @@ -0,0 +1,38 @@ +from policyengine.economic_impact.base_metric_calculator import BaseMetricCalculator +from policyengine_uk import Microsimulation +from microdf import MicroDataFrame, MicroSeries + + +class Relative(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_income = MicroSeries(self.baseline.calculate("household_net_income"), weights = self.baseline.calculate("household_weight")) + reform_income = MicroSeries(self.reformed.calculate("household_net_income") , weights = baseline_income.weights) + + decile = self.baseline.calculate("household_income_decile") + income_change = reform_income - baseline_income + + + rel_income_change_by_decile = ( + income_change.groupby(decile).sum() + / baseline_income.groupby(decile).sum() + ) + + + rel_decile_dict = rel_income_change_by_decile.to_dict() + + result = dict( + relative={int(k): v for k, v in rel_decile_dict.items() if k > 0} + + ) + + + return { + "relative": result["relative"], + + } \ No newline at end of file diff --git a/policyengine/economic_impact/economic_impact.py b/policyengine/economic_impact/economic_impact.py index b1ddd89..d230196 100644 --- a/policyengine/economic_impact/economic_impact.py +++ b/policyengine/economic_impact/economic_impact.py @@ -36,6 +36,9 @@ PensionCredit ) +from .distributional_impact.by_income_decile.average.average import Average +from .distributional_impact.by_income_decile.relative.relative import Relative + from .budgetary_impact.overall.overall import ( BudgetaryImpact, BenefitSpendingImpact, @@ -108,9 +111,10 @@ def __init__(self, reform: dict, country: str) -> None: "poverty/deep/male": DeepMalePoverty(self.baseline, self.reformed), "poverty/deep/female": DeepFemalePoverty(self.baseline, self.reformed), "poverty/deep/gender/all": DeepGenderAllPoverty(self.baseline, self.reformed), + "distributional/by_income/average": Average(self.baseline, self.reformed), + "distributional/by_income/relative": Relative(self.baseline, self.reformed), "winners_and_losers/by_income_decile": ByIncomeDecile(self.baseline, self.reformed), "winners_and_losers/by_wealth_decile": ByWealthDecile(self.baseline, self.reformed), - } def _get_simulation_class(self) -> type: diff --git a/policyengine/tests/economic_impact/distributional_impact/by_income/average/average.yaml b/policyengine/tests/economic_impact/distributional_impact/by_income/average/average.yaml new file mode 100644 index 0000000..0b2e8ac --- /dev/null +++ b/policyengine/tests/economic_impact/distributional_impact/by_income/average/average.yaml @@ -0,0 +1,19 @@ +# Regular poverty by age +- test_by_income_average: + reform: + gov.hmrc.income_tax.rates.uk[0].rate: + "2024-01-01.2100-12-31": 0.55 + country: uk + expected: + average: + 1: -288.4419422028393 + 2: -936.7849231542535 + 3: -1655.8066942572789 + 4: -3040.3849413280304 + 5: -5082.727697456715 + 6: -8483.137124053015 + 7: -11510.447500088267 + 8: -14567.684605663524 + 9: -17960.33204429504 + 10: -22070.767989875396 + 11: -13195.0 \ No newline at end of file diff --git a/policyengine/tests/economic_impact/distributional_impact/by_income/average/test_average.py b/policyengine/tests/economic_impact/distributional_impact/by_income/average/test_average.py new file mode 100644 index 0000000..7ee0bdd --- /dev/null +++ b/policyengine/tests/economic_impact/distributional_impact/by_income/average/test_average.py @@ -0,0 +1,38 @@ +import pytest +import yaml +import os +from policyengine import EconomicImpact + +def assert_dict_approx_equal(actual, expected, tolerance=1e-4): + assert set(actual.keys()) == set(expected.keys()), f"Keys don't match. Actual: {actual.keys()}, Expected: {expected.keys()}" + for key in expected: + if isinstance(expected[key], dict): + assert_dict_approx_equal(actual[key], expected[key], tolerance) + else: + assert abs(actual[key] - expected[key]) < tolerance, f"Key {key}: expected {expected[key]}, got {actual[key]}" + +yaml_file_path = "policyengine/tests/economic_impact/distributional_impact/by_income/average/average.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 'average' in test_name: + result = economic_impact.calculate("distributional/by_income/average") + 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 diff --git a/policyengine/tests/economic_impact/distributional_impact/by_income/relative/relative.yaml b/policyengine/tests/economic_impact/distributional_impact/by_income/relative/relative.yaml new file mode 100644 index 0000000..8ab7b48 --- /dev/null +++ b/policyengine/tests/economic_impact/distributional_impact/by_income/relative/relative.yaml @@ -0,0 +1,19 @@ +# Regular poverty by age +- test_by_income_average: + reform: + gov.hmrc.income_tax.rates.uk[0].rate: + "2024-01-01.2100-12-31": 0.55 + country: uk + expected: + relative: + 1: -0.026265868982450032 + 2: -0.05321454070727573 + 3: -0.07293061898874185 + 4: -0.1046825029478255 + 5: -0.1407466605531786 + 6: -0.19129333726780962 + 7: -0.2185085314786923 + 8: -0.22920465056330844 + 9: -0.21991139800089637 + 10: -0.12790076021888797 + 11: -0.007019584080998393 \ No newline at end of file diff --git a/policyengine/tests/economic_impact/distributional_impact/by_income/relative/test_relative.py b/policyengine/tests/economic_impact/distributional_impact/by_income/relative/test_relative.py new file mode 100644 index 0000000..bbe6fa5 --- /dev/null +++ b/policyengine/tests/economic_impact/distributional_impact/by_income/relative/test_relative.py @@ -0,0 +1,38 @@ +import pytest +import yaml +import os +from policyengine import EconomicImpact + +def assert_dict_approx_equal(actual, expected, tolerance=1e-4): + assert set(actual.keys()) == set(expected.keys()), f"Keys don't match. Actual: {actual.keys()}, Expected: {expected.keys()}" + for key in expected: + if isinstance(expected[key], dict): + assert_dict_approx_equal(actual[key], expected[key], tolerance) + else: + assert abs(actual[key] - expected[key]) < tolerance, f"Key {key}: expected {expected[key]}, got {actual[key]}" + +yaml_file_path = "policyengine/tests/economic_impact/distributional_impact/by_income/relative/relative.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 'average' in test_name: + result = economic_impact.calculate("distributional/by_income/relative") + 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