policyengine 3.0.0__py3-none-any.whl → 3.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. policyengine/__pycache__/__init__.cpython-313.pyc +0 -0
  2. policyengine/core/__init__.py +22 -0
  3. policyengine/core/dataset.py +260 -0
  4. policyengine/core/dataset_version.py +16 -0
  5. policyengine/core/dynamic.py +43 -0
  6. policyengine/core/output.py +26 -0
  7. policyengine/{models → core}/parameter.py +4 -2
  8. policyengine/{models → core}/parameter_value.py +1 -1
  9. policyengine/core/policy.py +43 -0
  10. policyengine/{models → core}/simulation.py +10 -14
  11. policyengine/core/tax_benefit_model.py +11 -0
  12. policyengine/core/tax_benefit_model_version.py +34 -0
  13. policyengine/core/variable.py +15 -0
  14. policyengine/outputs/__init__.py +21 -0
  15. policyengine/outputs/aggregate.py +124 -0
  16. policyengine/outputs/change_aggregate.py +184 -0
  17. policyengine/outputs/decile_impact.py +140 -0
  18. policyengine/tax_benefit_models/uk/__init__.py +26 -0
  19. policyengine/tax_benefit_models/uk/analysis.py +97 -0
  20. policyengine/tax_benefit_models/uk/datasets.py +176 -0
  21. policyengine/tax_benefit_models/uk/model.py +268 -0
  22. policyengine/tax_benefit_models/uk/outputs.py +108 -0
  23. policyengine/tax_benefit_models/uk.py +33 -0
  24. policyengine/tax_benefit_models/us/__init__.py +36 -0
  25. policyengine/tax_benefit_models/us/analysis.py +99 -0
  26. policyengine/tax_benefit_models/us/datasets.py +307 -0
  27. policyengine/tax_benefit_models/us/model.py +447 -0
  28. policyengine/tax_benefit_models/us/outputs.py +108 -0
  29. policyengine/tax_benefit_models/us.py +32 -0
  30. policyengine/utils/__init__.py +3 -0
  31. policyengine/utils/dates.py +40 -0
  32. policyengine/utils/parametric_reforms.py +39 -0
  33. policyengine/utils/plotting.py +179 -0
  34. {policyengine-3.0.0.dist-info → policyengine-3.1.1.dist-info}/METADATA +185 -20
  35. policyengine-3.1.1.dist-info/RECORD +39 -0
  36. policyengine/database/__init__.py +0 -56
  37. policyengine/database/aggregate.py +0 -33
  38. policyengine/database/baseline_parameter_value_table.py +0 -66
  39. policyengine/database/baseline_variable_table.py +0 -40
  40. policyengine/database/database.py +0 -251
  41. policyengine/database/dataset_table.py +0 -41
  42. policyengine/database/dynamic_table.py +0 -34
  43. policyengine/database/link.py +0 -82
  44. policyengine/database/model_table.py +0 -27
  45. policyengine/database/model_version_table.py +0 -28
  46. policyengine/database/parameter_table.py +0 -31
  47. policyengine/database/parameter_value_table.py +0 -62
  48. policyengine/database/policy_table.py +0 -34
  49. policyengine/database/report_element_table.py +0 -48
  50. policyengine/database/report_table.py +0 -24
  51. policyengine/database/simulation_table.py +0 -50
  52. policyengine/database/user_table.py +0 -28
  53. policyengine/database/versioned_dataset_table.py +0 -28
  54. policyengine/models/__init__.py +0 -30
  55. policyengine/models/aggregate.py +0 -92
  56. policyengine/models/baseline_parameter_value.py +0 -14
  57. policyengine/models/baseline_variable.py +0 -12
  58. policyengine/models/dataset.py +0 -18
  59. policyengine/models/dynamic.py +0 -15
  60. policyengine/models/model.py +0 -124
  61. policyengine/models/model_version.py +0 -14
  62. policyengine/models/policy.py +0 -17
  63. policyengine/models/policyengine_uk.py +0 -114
  64. policyengine/models/policyengine_us.py +0 -115
  65. policyengine/models/report.py +0 -10
  66. policyengine/models/report_element.py +0 -36
  67. policyengine/models/user.py +0 -14
  68. policyengine/models/versioned_dataset.py +0 -12
  69. policyengine/utils/charts.py +0 -286
  70. policyengine/utils/compress.py +0 -20
  71. policyengine/utils/datasets.py +0 -71
  72. policyengine-3.0.0.dist-info/RECORD +0 -47
  73. policyengine-3.0.0.dist-info/entry_points.txt +0 -2
  74. {policyengine-3.0.0.dist-info → policyengine-3.1.1.dist-info}/WHEEL +0 -0
  75. {policyengine-3.0.0.dist-info → policyengine-3.1.1.dist-info}/licenses/LICENSE +0 -0
  76. {policyengine-3.0.0.dist-info → policyengine-3.1.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,124 @@
1
+ from enum import Enum
2
+ from typing import Any
3
+
4
+ from policyengine.core import Output, Simulation
5
+
6
+
7
+ class AggregateType(str, Enum):
8
+ SUM = "sum"
9
+ MEAN = "mean"
10
+ COUNT = "count"
11
+
12
+
13
+ class Aggregate(Output):
14
+ simulation: Simulation
15
+ variable: str
16
+ aggregate_type: AggregateType
17
+ entity: str | None = None
18
+
19
+ filter_variable: str | None = None
20
+ filter_variable_eq: Any | None = None
21
+ filter_variable_leq: Any | None = None
22
+ filter_variable_geq: Any | None = None
23
+ filter_variable_describes_quantiles: bool = False
24
+
25
+ # Convenient quantile specification (alternative to describes_quantiles)
26
+ quantile: int | None = (
27
+ None # Number of quantiles (e.g., 10 for deciles, 5 for quintiles)
28
+ )
29
+ quantile_eq: int | None = None # Exact quantile (e.g., 3 for 3rd decile)
30
+ quantile_leq: int | None = (
31
+ None # Maximum quantile (e.g., 5 for bottom 5 deciles)
32
+ )
33
+ quantile_geq: int | None = (
34
+ None # Minimum quantile (e.g., 9 for top 2 deciles)
35
+ )
36
+
37
+ result: Any | None = None
38
+
39
+ def run(self):
40
+ # Convert quantile specification to describes_quantiles format
41
+ if self.quantile is not None:
42
+ self.filter_variable_describes_quantiles = True
43
+ if self.quantile_eq is not None:
44
+ # For a specific quantile, filter between (quantile-1)/n and quantile/n
45
+ self.filter_variable_geq = (
46
+ self.quantile_eq - 1
47
+ ) / self.quantile
48
+ self.filter_variable_leq = self.quantile_eq / self.quantile
49
+ elif self.quantile_leq is not None:
50
+ self.filter_variable_leq = self.quantile_leq / self.quantile
51
+ elif self.quantile_geq is not None:
52
+ self.filter_variable_geq = (
53
+ self.quantile_geq - 1
54
+ ) / self.quantile
55
+
56
+ # Get variable object
57
+ var_obj = next(
58
+ v
59
+ for v in self.simulation.tax_benefit_model_version.variables
60
+ if v.name == self.variable
61
+ )
62
+
63
+ # Get the target entity data
64
+ target_entity = self.entity or var_obj.entity
65
+ data = getattr(self.simulation.output_dataset.data, target_entity)
66
+
67
+ # Map variable to target entity if needed
68
+ if var_obj.entity != target_entity:
69
+ mapped = self.simulation.output_dataset.data.map_to_entity(
70
+ var_obj.entity, target_entity, columns=[self.variable]
71
+ )
72
+ series = mapped[self.variable]
73
+ else:
74
+ series = data[self.variable]
75
+
76
+ # Apply filters
77
+ if self.filter_variable is not None:
78
+ filter_var_obj = next(
79
+ v
80
+ for v in self.simulation.tax_benefit_model_version.variables
81
+ if v.name == self.filter_variable
82
+ )
83
+
84
+ if filter_var_obj.entity != target_entity:
85
+ filter_mapped = (
86
+ self.simulation.output_dataset.data.map_to_entity(
87
+ filter_var_obj.entity,
88
+ target_entity,
89
+ columns=[self.filter_variable],
90
+ )
91
+ )
92
+ filter_series = filter_mapped[self.filter_variable]
93
+ else:
94
+ filter_series = data[self.filter_variable]
95
+
96
+ if self.filter_variable_describes_quantiles:
97
+ if self.filter_variable_eq is not None:
98
+ threshold = filter_series.quantile(self.filter_variable_eq)
99
+ series = series[filter_series <= threshold]
100
+ if self.filter_variable_leq is not None:
101
+ threshold = filter_series.quantile(
102
+ self.filter_variable_leq
103
+ )
104
+ series = series[filter_series <= threshold]
105
+ if self.filter_variable_geq is not None:
106
+ threshold = filter_series.quantile(
107
+ self.filter_variable_geq
108
+ )
109
+ series = series[filter_series >= threshold]
110
+ else:
111
+ if self.filter_variable_eq is not None:
112
+ series = series[filter_series == self.filter_variable_eq]
113
+ if self.filter_variable_leq is not None:
114
+ series = series[filter_series <= self.filter_variable_leq]
115
+ if self.filter_variable_geq is not None:
116
+ series = series[filter_series >= self.filter_variable_geq]
117
+
118
+ # Aggregate - MicroSeries will automatically apply weights
119
+ if self.aggregate_type == AggregateType.SUM:
120
+ self.result = series.sum()
121
+ elif self.aggregate_type == AggregateType.MEAN:
122
+ self.result = series.mean()
123
+ elif self.aggregate_type == AggregateType.COUNT:
124
+ self.result = series.count()
@@ -0,0 +1,184 @@
1
+ from enum import Enum
2
+ from typing import Any
3
+
4
+ from policyengine.core import Output, Simulation
5
+
6
+
7
+ class ChangeAggregateType(str, Enum):
8
+ COUNT = "count"
9
+ SUM = "sum"
10
+ MEAN = "mean"
11
+
12
+
13
+ class ChangeAggregate(Output):
14
+ baseline_simulation: Simulation
15
+ reform_simulation: Simulation
16
+ variable: str
17
+ aggregate_type: ChangeAggregateType
18
+ entity: str | None = None
19
+
20
+ # Filter by absolute change
21
+ change_geq: float | None = None # Change >= value (e.g., gain >= 500)
22
+ change_leq: float | None = None # Change <= value (e.g., loss <= -500)
23
+ change_eq: float | None = None # Change == value
24
+
25
+ # Filter by relative change (as decimal, e.g., 0.05 = 5%)
26
+ relative_change_geq: float | None = None # Relative change >= value
27
+ relative_change_leq: float | None = None # Relative change <= value
28
+ relative_change_eq: float | None = None # Relative change == value
29
+
30
+ # Filter by another variable (e.g., only count people with age >= 30)
31
+ filter_variable: str | None = None
32
+ filter_variable_eq: Any | None = None
33
+ filter_variable_leq: Any | None = None
34
+ filter_variable_geq: Any | None = None
35
+ filter_variable_describes_quantiles: bool = False
36
+
37
+ # Convenient quantile specification (alternative to describes_quantiles)
38
+ quantile: int | None = (
39
+ None # Number of quantiles (e.g., 10 for deciles, 5 for quintiles)
40
+ )
41
+ quantile_eq: int | None = None # Exact quantile (e.g., 3 for 3rd decile)
42
+ quantile_leq: int | None = (
43
+ None # Maximum quantile (e.g., 5 for bottom 5 deciles)
44
+ )
45
+ quantile_geq: int | None = (
46
+ None # Minimum quantile (e.g., 9 for top 2 deciles)
47
+ )
48
+
49
+ result: Any | None = None
50
+
51
+ def run(self):
52
+ # Convert quantile specification to describes_quantiles format
53
+ if self.quantile is not None:
54
+ self.filter_variable_describes_quantiles = True
55
+ if self.quantile_eq is not None:
56
+ # For a specific quantile, filter between (quantile-1)/n and quantile/n
57
+ self.filter_variable_geq = (
58
+ self.quantile_eq - 1
59
+ ) / self.quantile
60
+ self.filter_variable_leq = self.quantile_eq / self.quantile
61
+ elif self.quantile_leq is not None:
62
+ self.filter_variable_leq = self.quantile_leq / self.quantile
63
+ elif self.quantile_geq is not None:
64
+ self.filter_variable_geq = (
65
+ self.quantile_geq - 1
66
+ ) / self.quantile
67
+
68
+ # Get variable object
69
+ var_obj = next(
70
+ v
71
+ for v in self.baseline_simulation.tax_benefit_model_version.variables
72
+ if v.name == self.variable
73
+ )
74
+
75
+ # Get the target entity data
76
+ target_entity = self.entity or var_obj.entity
77
+ baseline_data = getattr(
78
+ self.baseline_simulation.output_dataset.data, target_entity
79
+ )
80
+ reform_data = getattr(
81
+ self.reform_simulation.output_dataset.data, target_entity
82
+ )
83
+
84
+ # Map variable to target entity if needed
85
+ if var_obj.entity != target_entity:
86
+ baseline_mapped = (
87
+ self.baseline_simulation.output_dataset.data.map_to_entity(
88
+ var_obj.entity, target_entity
89
+ )
90
+ )
91
+ baseline_series = baseline_mapped[self.variable]
92
+
93
+ reform_mapped = (
94
+ self.reform_simulation.output_dataset.data.map_to_entity(
95
+ var_obj.entity, target_entity
96
+ )
97
+ )
98
+ reform_series = reform_mapped[self.variable]
99
+ else:
100
+ baseline_series = baseline_data[self.variable]
101
+ reform_series = reform_data[self.variable]
102
+
103
+ # Calculate change (reform - baseline)
104
+ change_series = reform_series - baseline_series
105
+
106
+ # Calculate relative change (handling division by zero)
107
+ # Where baseline is 0, relative change is undefined; we'll mask these out if relative filters are used
108
+ import numpy as np
109
+
110
+ with np.errstate(divide="ignore", invalid="ignore"):
111
+ relative_change_series = change_series / baseline_series
112
+ relative_change_series = relative_change_series.replace(
113
+ [np.inf, -np.inf], np.nan
114
+ )
115
+
116
+ # Start with all rows
117
+ mask = baseline_series.notna()
118
+
119
+ # Apply absolute change filters
120
+ if self.change_eq is not None:
121
+ mask &= change_series == self.change_eq
122
+ if self.change_leq is not None:
123
+ mask &= change_series <= self.change_leq
124
+ if self.change_geq is not None:
125
+ mask &= change_series >= self.change_geq
126
+
127
+ # Apply relative change filters
128
+ if self.relative_change_eq is not None:
129
+ mask &= relative_change_series == self.relative_change_eq
130
+ if self.relative_change_leq is not None:
131
+ mask &= relative_change_series <= self.relative_change_leq
132
+ if self.relative_change_geq is not None:
133
+ mask &= relative_change_series >= self.relative_change_geq
134
+
135
+ # Apply filter_variable filters
136
+ if self.filter_variable is not None:
137
+ filter_var_obj = next(
138
+ v
139
+ for v in self.baseline_simulation.tax_benefit_model_version.variables
140
+ if v.name == self.filter_variable
141
+ )
142
+
143
+ if filter_var_obj.entity != target_entity:
144
+ filter_mapped = (
145
+ self.baseline_simulation.output_dataset.data.map_to_entity(
146
+ filter_var_obj.entity, target_entity
147
+ )
148
+ )
149
+ filter_series = filter_mapped[self.filter_variable]
150
+ else:
151
+ filter_series = baseline_data[self.filter_variable]
152
+
153
+ if self.filter_variable_describes_quantiles:
154
+ if self.filter_variable_eq is not None:
155
+ threshold = filter_series.quantile(self.filter_variable_eq)
156
+ mask &= filter_series <= threshold
157
+ if self.filter_variable_leq is not None:
158
+ threshold = filter_series.quantile(
159
+ self.filter_variable_leq
160
+ )
161
+ mask &= filter_series <= threshold
162
+ if self.filter_variable_geq is not None:
163
+ threshold = filter_series.quantile(
164
+ self.filter_variable_geq
165
+ )
166
+ mask &= filter_series >= threshold
167
+ else:
168
+ if self.filter_variable_eq is not None:
169
+ mask &= filter_series == self.filter_variable_eq
170
+ if self.filter_variable_leq is not None:
171
+ mask &= filter_series <= self.filter_variable_leq
172
+ if self.filter_variable_geq is not None:
173
+ mask &= filter_series >= self.filter_variable_geq
174
+
175
+ # Apply mask to get filtered data
176
+ filtered_change = change_series[mask]
177
+
178
+ # Aggregate
179
+ if self.aggregate_type == ChangeAggregateType.COUNT:
180
+ self.result = filtered_change.count()
181
+ elif self.aggregate_type == ChangeAggregateType.SUM:
182
+ self.result = filtered_change.sum()
183
+ elif self.aggregate_type == ChangeAggregateType.MEAN:
184
+ self.result = filtered_change.mean()
@@ -0,0 +1,140 @@
1
+ import pandas as pd
2
+ from pydantic import ConfigDict
3
+
4
+ from policyengine.core import Output, OutputCollection, Simulation
5
+
6
+
7
+ class DecileImpact(Output):
8
+ """Single decile's impact from a policy reform - represents one database row."""
9
+
10
+ model_config = ConfigDict(arbitrary_types_allowed=True)
11
+
12
+ baseline_simulation: Simulation
13
+ reform_simulation: Simulation
14
+ income_variable: str = "equiv_hbai_household_net_income"
15
+ entity: str | None = None
16
+ decile: int
17
+ quantiles: int = 10
18
+
19
+ # Results populated by run()
20
+ baseline_mean: float | None = None
21
+ reform_mean: float | None = None
22
+ absolute_change: float | None = None
23
+ relative_change: float | None = None
24
+ count_better_off: float | None = None
25
+ count_worse_off: float | None = None
26
+ count_no_change: float | None = None
27
+
28
+ def run(self):
29
+ """Calculate impact for this specific decile."""
30
+ # Get variable object to determine entity
31
+ var_obj = next(
32
+ v
33
+ for v in self.baseline_simulation.tax_benefit_model_version.variables
34
+ if v.name == self.income_variable
35
+ )
36
+
37
+ # Get target entity
38
+ target_entity = self.entity or var_obj.entity
39
+
40
+ # Get data from both simulations
41
+ baseline_data = getattr(
42
+ self.baseline_simulation.output_dataset.data, target_entity
43
+ )
44
+ reform_data = getattr(
45
+ self.reform_simulation.output_dataset.data, target_entity
46
+ )
47
+
48
+ # Map income variable to target entity if needed
49
+ if var_obj.entity != target_entity:
50
+ baseline_mapped = (
51
+ self.baseline_simulation.output_dataset.data.map_to_entity(
52
+ var_obj.entity, target_entity
53
+ )
54
+ )
55
+ baseline_income = baseline_mapped[self.income_variable]
56
+
57
+ reform_mapped = (
58
+ self.reform_simulation.output_dataset.data.map_to_entity(
59
+ var_obj.entity, target_entity
60
+ )
61
+ )
62
+ reform_income = reform_mapped[self.income_variable]
63
+ else:
64
+ baseline_income = baseline_data[self.income_variable]
65
+ reform_income = reform_data[self.income_variable]
66
+
67
+ # Calculate deciles based on baseline income
68
+ decile_series = (
69
+ pd.qcut(
70
+ baseline_income,
71
+ self.quantiles,
72
+ labels=False,
73
+ duplicates="drop",
74
+ )
75
+ + 1
76
+ )
77
+
78
+ # Calculate changes
79
+ absolute_change = reform_income - baseline_income
80
+ relative_change = (absolute_change / baseline_income) * 100
81
+
82
+ # Filter to this decile
83
+ mask = decile_series == self.decile
84
+
85
+ # Populate results
86
+ self.baseline_mean = float(baseline_income[mask].mean())
87
+ self.reform_mean = float(reform_income[mask].mean())
88
+ self.absolute_change = float(absolute_change[mask].mean())
89
+ self.relative_change = float(relative_change[mask].mean())
90
+ self.count_better_off = float((absolute_change[mask] > 0).sum())
91
+ self.count_worse_off = float((absolute_change[mask] < 0).sum())
92
+ self.count_no_change = float((absolute_change[mask] == 0).sum())
93
+
94
+
95
+ def calculate_decile_impacts(
96
+ baseline_simulation: Simulation,
97
+ reform_simulation: Simulation,
98
+ income_variable: str = "equiv_hbai_household_net_income",
99
+ entity: str | None = None,
100
+ quantiles: int = 10,
101
+ ) -> OutputCollection[DecileImpact]:
102
+ """Calculate decile-by-decile impact of a reform.
103
+
104
+ Returns:
105
+ OutputCollection containing list of DecileImpact objects and DataFrame
106
+ """
107
+ results = []
108
+ for decile in range(1, quantiles + 1):
109
+ impact = DecileImpact(
110
+ baseline_simulation=baseline_simulation,
111
+ reform_simulation=reform_simulation,
112
+ income_variable=income_variable,
113
+ entity=entity,
114
+ decile=decile,
115
+ quantiles=quantiles,
116
+ )
117
+ impact.run()
118
+ results.append(impact)
119
+
120
+ # Create DataFrame
121
+ df = pd.DataFrame(
122
+ [
123
+ {
124
+ "baseline_simulation_id": r.baseline_simulation.id,
125
+ "reform_simulation_id": r.reform_simulation.id,
126
+ "income_variable": r.income_variable,
127
+ "decile": r.decile,
128
+ "baseline_mean": r.baseline_mean,
129
+ "reform_mean": r.reform_mean,
130
+ "absolute_change": r.absolute_change,
131
+ "relative_change": r.relative_change,
132
+ "count_better_off": r.count_better_off,
133
+ "count_worse_off": r.count_worse_off,
134
+ "count_no_change": r.count_no_change,
135
+ }
136
+ for r in results
137
+ ]
138
+ )
139
+
140
+ return OutputCollection(outputs=results, dataframe=df)
@@ -0,0 +1,26 @@
1
+ """PolicyEngine UK tax-benefit model."""
2
+
3
+ from .analysis import general_policy_reform_analysis
4
+ from .datasets import PolicyEngineUKDataset, UKYearData, create_datasets
5
+ from .model import PolicyEngineUK, PolicyEngineUKLatest, uk_latest, uk_model
6
+ from .outputs import ProgrammeStatistics
7
+
8
+ __all__ = [
9
+ "UKYearData",
10
+ "PolicyEngineUKDataset",
11
+ "create_datasets",
12
+ "PolicyEngineUK",
13
+ "PolicyEngineUKLatest",
14
+ "uk_model",
15
+ "uk_latest",
16
+ "general_policy_reform_analysis",
17
+ "ProgrammeStatistics",
18
+ ]
19
+
20
+ # Rebuild models to resolve forward references
21
+ from policyengine.core import Dataset
22
+
23
+ Dataset.model_rebuild()
24
+ UKYearData.model_rebuild()
25
+ PolicyEngineUKDataset.model_rebuild()
26
+ PolicyEngineUKLatest.model_rebuild()
@@ -0,0 +1,97 @@
1
+ """General utility functions for UK policy reform analysis."""
2
+
3
+ import pandas as pd
4
+ from pydantic import BaseModel
5
+
6
+ from policyengine.core import OutputCollection, Simulation
7
+ from policyengine.outputs.decile_impact import (
8
+ DecileImpact,
9
+ calculate_decile_impacts,
10
+ )
11
+
12
+ from .outputs import ProgrammeStatistics
13
+
14
+
15
+ class PolicyReformAnalysis(BaseModel):
16
+ """Complete policy reform analysis result."""
17
+
18
+ decile_impacts: OutputCollection[DecileImpact]
19
+ programme_statistics: OutputCollection[ProgrammeStatistics]
20
+
21
+
22
+ def general_policy_reform_analysis(
23
+ baseline_simulation: Simulation,
24
+ reform_simulation: Simulation,
25
+ ) -> PolicyReformAnalysis:
26
+ """Perform comprehensive analysis of a policy reform.
27
+
28
+ Returns:
29
+ PolicyReformAnalysis containing decile impacts and programme statistics
30
+ """
31
+ # Decile impact
32
+ decile_impacts = calculate_decile_impacts(
33
+ baseline_simulation=baseline_simulation,
34
+ reform_simulation=reform_simulation,
35
+ )
36
+
37
+ # Major programmes to analyse
38
+ programmes = {
39
+ # Tax
40
+ "income_tax": {"entity": "person", "is_tax": True},
41
+ "national_insurance": {"entity": "person", "is_tax": True},
42
+ "vat": {"entity": "household", "is_tax": True},
43
+ "council_tax": {"entity": "household", "is_tax": True},
44
+ # Benefits
45
+ "universal_credit": {"entity": "person", "is_tax": False},
46
+ "child_benefit": {"entity": "person", "is_tax": False},
47
+ "pension_credit": {"entity": "person", "is_tax": False},
48
+ "income_support": {"entity": "person", "is_tax": False},
49
+ "working_tax_credit": {"entity": "person", "is_tax": False},
50
+ "child_tax_credit": {"entity": "person", "is_tax": False},
51
+ }
52
+
53
+ programme_statistics = []
54
+
55
+ for programme_name, programme_info in programmes.items():
56
+ entity = programme_info["entity"]
57
+ is_tax = programme_info["is_tax"]
58
+
59
+ stats = ProgrammeStatistics(
60
+ baseline_simulation=baseline_simulation,
61
+ reform_simulation=reform_simulation,
62
+ programme_name=programme_name,
63
+ entity=entity,
64
+ is_tax=is_tax,
65
+ )
66
+ stats.run()
67
+ programme_statistics.append(stats)
68
+
69
+ # Create DataFrame
70
+ programme_df = pd.DataFrame(
71
+ [
72
+ {
73
+ "baseline_simulation_id": p.baseline_simulation.id,
74
+ "reform_simulation_id": p.reform_simulation.id,
75
+ "programme_name": p.programme_name,
76
+ "entity": p.entity,
77
+ "is_tax": p.is_tax,
78
+ "baseline_total": p.baseline_total,
79
+ "reform_total": p.reform_total,
80
+ "change": p.change,
81
+ "baseline_count": p.baseline_count,
82
+ "reform_count": p.reform_count,
83
+ "winners": p.winners,
84
+ "losers": p.losers,
85
+ }
86
+ for p in programme_statistics
87
+ ]
88
+ )
89
+
90
+ programme_collection = OutputCollection(
91
+ outputs=programme_statistics, dataframe=programme_df
92
+ )
93
+
94
+ return PolicyReformAnalysis(
95
+ decile_impacts=decile_impacts,
96
+ programme_statistics=programme_collection,
97
+ )