policyengine 3.1.14__tar.gz → 3.1.16__tar.gz

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 (103) hide show
  1. {policyengine-3.1.14 → policyengine-3.1.16}/CHANGELOG.md +14 -0
  2. {policyengine-3.1.14 → policyengine-3.1.16}/PKG-INFO +5 -5
  3. {policyengine-3.1.14 → policyengine-3.1.16}/changelog.yaml +10 -0
  4. policyengine-3.1.16/examples/household_impact_example.py +128 -0
  5. {policyengine-3.1.14 → policyengine-3.1.16}/pyproject.toml +5 -5
  6. policyengine-3.1.16/src/policyengine/__pycache__/__init__.cpython-313.pyc +0 -0
  7. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/core/tax_benefit_model_version.py +9 -1
  8. policyengine-3.1.16/src/policyengine/outputs/__init__.py +49 -0
  9. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/outputs/decile_impact.py +22 -2
  10. policyengine-3.1.16/src/policyengine/outputs/inequality.py +276 -0
  11. policyengine-3.1.16/src/policyengine/outputs/poverty.py +238 -0
  12. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/tax_benefit_models/uk/__init__.py +10 -2
  13. policyengine-3.1.16/src/policyengine/tax_benefit_models/uk/analysis.py +292 -0
  14. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/tax_benefit_models/uk/model.py +15 -4
  15. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/tax_benefit_models/us/__init__.py +10 -2
  16. policyengine-3.1.16/src/policyengine/tax_benefit_models/us/analysis.py +313 -0
  17. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/tax_benefit_models/us/model.py +15 -4
  18. policyengine-3.1.16/src/policyengine/utils/__init__.py +7 -0
  19. policyengine-3.1.16/src/policyengine/utils/parameter_labels.py +213 -0
  20. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine.egg-info/PKG-INFO +5 -5
  21. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine.egg-info/SOURCES.txt +13 -2
  22. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine.egg-info/requires.txt +4 -4
  23. policyengine-3.1.16/tests/__init__.py +1 -0
  24. policyengine-3.1.16/tests/fixtures/__init__.py +1 -0
  25. policyengine-3.1.16/tests/fixtures/parameter_labels_fixtures.py +165 -0
  26. policyengine-3.1.16/tests/test_household_impact.py +206 -0
  27. policyengine-3.1.16/tests/test_inequality.py +288 -0
  28. policyengine-3.1.16/tests/test_models.py +148 -0
  29. policyengine-3.1.16/tests/test_pandas3_compatibility.py +19 -0
  30. policyengine-3.1.16/tests/test_parameter_labels.py +894 -0
  31. policyengine-3.1.16/tests/test_poverty.py +283 -0
  32. {policyengine-3.1.14 → policyengine-3.1.16}/uv.lock +1 -1
  33. policyengine-3.1.14/CLAUDE.md +0 -17
  34. policyengine-3.1.14/src/policyengine/__pycache__/__init__.cpython-313.pyc +0 -0
  35. policyengine-3.1.14/src/policyengine/outputs/__init__.py +0 -21
  36. policyengine-3.1.14/src/policyengine/tax_benefit_models/uk/analysis.py +0 -97
  37. policyengine-3.1.14/src/policyengine/tax_benefit_models/us/analysis.py +0 -99
  38. policyengine-3.1.14/src/policyengine/utils/__init__.py +0 -3
  39. policyengine-3.1.14/tests/test_models.py +0 -68
  40. {policyengine-3.1.14 → policyengine-3.1.16}/.claude/policyengine-guide.md +0 -0
  41. {policyengine-3.1.14 → policyengine-3.1.16}/.claude/quick-reference.md +0 -0
  42. {policyengine-3.1.14 → policyengine-3.1.16}/.github/CONTRIBUTING.md +0 -0
  43. {policyengine-3.1.14 → policyengine-3.1.16}/.github/changelog_template.md +0 -0
  44. {policyengine-3.1.14 → policyengine-3.1.16}/.github/fetch_version.py +0 -0
  45. {policyengine-3.1.14 → policyengine-3.1.16}/.github/get-changelog-diff.sh +0 -0
  46. {policyengine-3.1.14 → policyengine-3.1.16}/.github/has-functional-changes.sh +0 -0
  47. {policyengine-3.1.14 → policyengine-3.1.16}/.github/is-version-number-acceptable.sh +0 -0
  48. {policyengine-3.1.14 → policyengine-3.1.16}/.github/publish-git-tag.sh +0 -0
  49. {policyengine-3.1.14 → policyengine-3.1.16}/.github/workflows/code_changes.yaml +0 -0
  50. {policyengine-3.1.14 → policyengine-3.1.16}/.github/workflows/docs.yml +0 -0
  51. {policyengine-3.1.14 → policyengine-3.1.16}/.github/workflows/pr_code_changes.yaml +0 -0
  52. {policyengine-3.1.14 → policyengine-3.1.16}/.github/workflows/pr_docs_changes.yaml +0 -0
  53. {policyengine-3.1.14 → policyengine-3.1.16}/.github/workflows/versioning.yaml +0 -0
  54. {policyengine-3.1.14 → policyengine-3.1.16}/.gitignore +0 -0
  55. {policyengine-3.1.14 → policyengine-3.1.16}/LICENSE +0 -0
  56. {policyengine-3.1.14 → policyengine-3.1.16}/Makefile +0 -0
  57. {policyengine-3.1.14 → policyengine-3.1.16}/README.md +0 -0
  58. {policyengine-3.1.14 → policyengine-3.1.16}/changelog_entry.yaml +0 -0
  59. {policyengine-3.1.14 → policyengine-3.1.16}/docs/.gitignore +0 -0
  60. {policyengine-3.1.14 → policyengine-3.1.16}/docs/core-concepts.md +0 -0
  61. {policyengine-3.1.14 → policyengine-3.1.16}/docs/country-models-uk.md +0 -0
  62. {policyengine-3.1.14 → policyengine-3.1.16}/docs/country-models-us.md +0 -0
  63. {policyengine-3.1.14 → policyengine-3.1.16}/docs/dev.md +0 -0
  64. {policyengine-3.1.14 → policyengine-3.1.16}/docs/index.md +0 -0
  65. {policyengine-3.1.14 → policyengine-3.1.16}/docs/myst.yml +0 -0
  66. {policyengine-3.1.14 → policyengine-3.1.16}/docs/visualisation.md +0 -0
  67. {policyengine-3.1.14 → policyengine-3.1.16}/examples/employment_income_variation_uk.py +0 -0
  68. {policyengine-3.1.14 → policyengine-3.1.16}/examples/employment_income_variation_us.py +0 -0
  69. {policyengine-3.1.14 → policyengine-3.1.16}/examples/income_bands_uk.py +0 -0
  70. {policyengine-3.1.14 → policyengine-3.1.16}/examples/income_distribution_us.py +0 -0
  71. {policyengine-3.1.14 → policyengine-3.1.16}/examples/policy_change_uk.py +0 -0
  72. {policyengine-3.1.14 → policyengine-3.1.16}/examples/speedtest_us_simulation.py +0 -0
  73. {policyengine-3.1.14 → policyengine-3.1.16}/setup.cfg +0 -0
  74. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/__init__.py +0 -0
  75. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/core/__init__.py +0 -0
  76. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/core/cache.py +0 -0
  77. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/core/dataset.py +0 -0
  78. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/core/dataset_version.py +0 -0
  79. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/core/dynamic.py +0 -0
  80. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/core/output.py +0 -0
  81. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/core/parameter.py +0 -0
  82. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/core/parameter_value.py +0 -0
  83. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/core/policy.py +0 -0
  84. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/core/simulation.py +0 -0
  85. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/core/tax_benefit_model.py +0 -0
  86. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/core/variable.py +0 -0
  87. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/outputs/aggregate.py +0 -0
  88. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/outputs/change_aggregate.py +0 -0
  89. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/tax_benefit_models/uk/datasets.py +0 -0
  90. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/tax_benefit_models/uk/outputs.py +0 -0
  91. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/tax_benefit_models/uk.py +0 -0
  92. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/tax_benefit_models/us/datasets.py +0 -0
  93. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/tax_benefit_models/us/outputs.py +0 -0
  94. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/tax_benefit_models/us.py +0 -0
  95. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/utils/dates.py +0 -0
  96. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/utils/parametric_reforms.py +0 -0
  97. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine/utils/plotting.py +0 -0
  98. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine.egg-info/dependency_links.txt +0 -0
  99. {policyengine-3.1.14 → policyengine-3.1.16}/src/policyengine.egg-info/top_level.txt +0 -0
  100. {policyengine-3.1.14 → policyengine-3.1.16}/tests/test_aggregate.py +0 -0
  101. {policyengine-3.1.14 → policyengine-3.1.16}/tests/test_cache.py +0 -0
  102. {policyengine-3.1.14 → policyengine-3.1.16}/tests/test_change_aggregate.py +0 -0
  103. {policyengine-3.1.14 → policyengine-3.1.16}/tests/test_entity_mapping.py +0 -0
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.1.16] - 2026-01-25 14:20:29
9
+
10
+ ### Changed
11
+
12
+ - Bumped policyengine-core minimum version to 3.23.5 for pandas 3.0 compatibility
13
+
14
+ ## [3.1.15] - 2025-12-14 23:51:27
15
+
16
+ ### Added
17
+
18
+ - Household impacts
19
+
8
20
  ## [3.1.14] - 2025-12-10 21:59:24
9
21
 
10
22
  ### Fixed
@@ -281,6 +293,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
281
293
 
282
294
 
283
295
 
296
+ [3.1.16]: https://github.com/PolicyEngine/policyengine.py/compare/3.1.15...3.1.16
297
+ [3.1.15]: https://github.com/PolicyEngine/policyengine.py/compare/3.1.14...3.1.15
284
298
  [3.1.14]: https://github.com/PolicyEngine/policyengine.py/compare/3.1.13...3.1.14
285
299
  [3.1.13]: https://github.com/PolicyEngine/policyengine.py/compare/3.1.12...3.1.13
286
300
  [3.1.12]: https://github.com/PolicyEngine/policyengine.py/compare/3.1.11...3.1.12
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: policyengine
3
- Version: 3.1.14
3
+ Version: 3.1.16
4
4
  Summary: A package to conduct policy analysis using PolicyEngine tax-benefit models.
5
5
  Author-email: PolicyEngine <hello@policyengine.org>
6
6
  License: GNU AFFERO GENERAL PUBLIC LICENSE
@@ -670,15 +670,15 @@ Description-Content-Type: text/markdown
670
670
  License-File: LICENSE
671
671
  Requires-Dist: pydantic>=2.0.0
672
672
  Requires-Dist: pandas>=2.0.0
673
- Requires-Dist: microdf_python
673
+ Requires-Dist: microdf_python>=1.2.1
674
674
  Requires-Dist: plotly>=5.0.0
675
675
  Requires-Dist: requests>=2.31.0
676
676
  Requires-Dist: psutil>=5.9.0
677
677
  Provides-Extra: uk
678
- Requires-Dist: policyengine_core>=3.10; extra == "uk"
678
+ Requires-Dist: policyengine_core>=3.23.6; extra == "uk"
679
679
  Requires-Dist: policyengine-uk>=2.51.0; extra == "uk"
680
680
  Provides-Extra: us
681
- Requires-Dist: policyengine_core>=3.10; extra == "us"
681
+ Requires-Dist: policyengine_core>=3.23.6; extra == "us"
682
682
  Requires-Dist: policyengine-us>=1.213.1; extra == "us"
683
683
  Provides-Extra: dev
684
684
  Requires-Dist: black; extra == "dev"
@@ -691,7 +691,7 @@ Requires-Dist: itables; extra == "dev"
691
691
  Requires-Dist: build; extra == "dev"
692
692
  Requires-Dist: pytest-asyncio>=0.26.0; extra == "dev"
693
693
  Requires-Dist: ruff>=0.5.0; extra == "dev"
694
- Requires-Dist: policyengine_core>=3.10; extra == "dev"
694
+ Requires-Dist: policyengine_core>=3.23.6; extra == "dev"
695
695
  Requires-Dist: policyengine-uk>=2.51.0; extra == "dev"
696
696
  Requires-Dist: policyengine-us>=1.213.1; extra == "dev"
697
697
  Dynamic: license-file
@@ -230,3 +230,13 @@
230
230
  fixed:
231
231
  - Improvements to loading taxbenefitsystems.
232
232
  date: 2025-12-10 21:59:24
233
+ - bump: patch
234
+ changes:
235
+ added:
236
+ - Household impacts
237
+ date: 2025-12-14 23:51:27
238
+ - bump: patch
239
+ changes:
240
+ changed:
241
+ - Bumped policyengine-core minimum version to 3.23.5 for pandas 3.0 compatibility
242
+ date: 2026-01-25 14:20:29
@@ -0,0 +1,128 @@
1
+ """Example: Calculate household tax and benefit impacts.
2
+
3
+ This script demonstrates using calculate_household_impact for both UK and US
4
+ to compute taxes and benefits for custom households.
5
+
6
+ Run: python examples/household_impact_example.py
7
+ """
8
+
9
+ from policyengine.tax_benefit_models.uk import (
10
+ UKHouseholdInput,
11
+ )
12
+ from policyengine.tax_benefit_models.uk import (
13
+ calculate_household_impact as calculate_uk_impact,
14
+ )
15
+ from policyengine.tax_benefit_models.us import (
16
+ USHouseholdInput,
17
+ )
18
+ from policyengine.tax_benefit_models.us import (
19
+ calculate_household_impact as calculate_us_impact,
20
+ )
21
+
22
+
23
+ def uk_example():
24
+ """UK household impact example."""
25
+ print("=" * 60)
26
+ print("UK HOUSEHOLD IMPACT")
27
+ print("=" * 60)
28
+
29
+ # Single adult earning £50,000
30
+ household = UKHouseholdInput(
31
+ people=[{"age": 35, "employment_income": 50_000}],
32
+ year=2026,
33
+ )
34
+ result = calculate_uk_impact(household)
35
+
36
+ print("\nSingle adult, £50k income:")
37
+ print(
38
+ f" Net income: £{result.household['hbai_household_net_income']:,.0f}"
39
+ )
40
+ print(f" Income tax: £{result.person[0]['income_tax']:,.0f}")
41
+ print(
42
+ f" National Insurance: £{result.person[0]['national_insurance']:,.0f}"
43
+ )
44
+ print(f" Total tax: £{result.household['household_tax']:,.0f}")
45
+
46
+ # Family with two children, £30k income, renting
47
+ household = UKHouseholdInput(
48
+ people=[
49
+ {"age": 35, "employment_income": 30_000},
50
+ {"age": 33},
51
+ {"age": 8},
52
+ {"age": 5},
53
+ ],
54
+ benunit={
55
+ "would_claim_uc": True,
56
+ "would_claim_child_benefit": True,
57
+ },
58
+ household={
59
+ "rent": 12_000, # £1k/month
60
+ "region": "NORTH_WEST",
61
+ },
62
+ year=2026,
63
+ )
64
+ result = calculate_uk_impact(household)
65
+
66
+ print("\nFamily (2 adults, 2 children), £30k income, renting:")
67
+ print(
68
+ f" Net income: £{result.household['hbai_household_net_income']:,.0f}"
69
+ )
70
+ print(f" Income tax: £{result.person[0]['income_tax']:,.0f}")
71
+ print(f" Child benefit: £{result.benunit[0]['child_benefit']:,.0f}")
72
+ print(f" Universal credit: £{result.benunit[0]['universal_credit']:,.0f}")
73
+ print(f" Total benefits: £{result.household['household_benefits']:,.0f}")
74
+
75
+
76
+ def us_example():
77
+ """US household impact example."""
78
+ print("\n" + "=" * 60)
79
+ print("US HOUSEHOLD IMPACT")
80
+ print("=" * 60)
81
+
82
+ # Single adult earning $50,000
83
+ household = USHouseholdInput(
84
+ people=[
85
+ {"age": 35, "employment_income": 50_000, "is_tax_unit_head": True}
86
+ ],
87
+ tax_unit={"filing_status": "SINGLE"},
88
+ household={"state_code_str": "CA"},
89
+ year=2024,
90
+ )
91
+ result = calculate_us_impact(household)
92
+
93
+ print("\nSingle adult, $50k income (California):")
94
+ print(f" Net income: ${result.household['household_net_income']:,.0f}")
95
+ print(f" Income tax: ${result.tax_unit[0]['income_tax']:,.0f}")
96
+ print(f" Payroll tax: ${result.tax_unit[0]['employee_payroll_tax']:,.0f}")
97
+
98
+ # Married couple with children, lower income
99
+ household = USHouseholdInput(
100
+ people=[
101
+ {"age": 35, "employment_income": 40_000, "is_tax_unit_head": True},
102
+ {"age": 33, "is_tax_unit_spouse": True},
103
+ {"age": 8, "is_tax_unit_dependent": True},
104
+ {"age": 5, "is_tax_unit_dependent": True},
105
+ ],
106
+ tax_unit={"filing_status": "JOINT"},
107
+ household={"state_code_str": "TX"},
108
+ year=2024,
109
+ )
110
+ result = calculate_us_impact(household)
111
+
112
+ print("\nMarried couple with 2 children, $40k income (Texas):")
113
+ print(f" Net income: ${result.household['household_net_income']:,.0f}")
114
+ print(f" Federal income tax: ${result.tax_unit[0]['income_tax']:,.0f}")
115
+ print(f" EITC: ${result.tax_unit[0]['eitc']:,.0f}")
116
+ print(f" Child tax credit: ${result.tax_unit[0]['ctc']:,.0f}")
117
+ print(f" SNAP: ${result.spm_unit[0]['snap']:,.0f}")
118
+
119
+
120
+ def main():
121
+ uk_example()
122
+ us_example()
123
+ print("\n" + "=" * 60)
124
+ print("Done!")
125
+
126
+
127
+ if __name__ == "__main__":
128
+ main()
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "policyengine"
7
- version = "3.1.14"
7
+ version = "3.1.16"
8
8
  description = "A package to conduct policy analysis using PolicyEngine tax-benefit models."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -15,7 +15,7 @@ requires-python = ">=3.13"
15
15
  dependencies = [
16
16
  "pydantic>=2.0.0",
17
17
  "pandas>=2.0.0",
18
- "microdf_python",
18
+ "microdf_python>=1.2.1",
19
19
  "plotly>=5.0.0",
20
20
  "requests>=2.31.0",
21
21
  "psutil>=5.9.0",
@@ -23,11 +23,11 @@ dependencies = [
23
23
 
24
24
  [project.optional-dependencies]
25
25
  uk = [
26
- "policyengine_core>=3.10",
26
+ "policyengine_core>=3.23.6",
27
27
  "policyengine-uk>=2.51.0",
28
28
  ]
29
29
  us = [
30
- "policyengine_core>=3.10",
30
+ "policyengine_core>=3.23.6",
31
31
  "policyengine-us>=1.213.1",
32
32
  ]
33
33
  dev = [
@@ -41,7 +41,7 @@ dev = [
41
41
  "build",
42
42
  "pytest-asyncio>=0.26.0",
43
43
  "ruff>=0.5.0",
44
- "policyengine_core>=3.10",
44
+ "policyengine_core>=3.23.6",
45
45
  "policyengine-uk>=2.51.0",
46
46
  "policyengine-us>=1.213.1",
47
47
  ]
@@ -24,7 +24,15 @@ class TaxBenefitModelVersion(BaseModel):
24
24
 
25
25
  variables: list["Variable"] = Field(default_factory=list)
26
26
  parameters: list["Parameter"] = Field(default_factory=list)
27
- parameter_values: list["ParameterValue"] = Field(default_factory=list)
27
+
28
+ @property
29
+ def parameter_values(self) -> list["ParameterValue"]:
30
+ """Aggregate all parameter values from all parameters."""
31
+ return [
32
+ pv
33
+ for parameter in self.parameters
34
+ for pv in parameter.parameter_values
35
+ ]
28
36
 
29
37
  # Lookup dicts for O(1) access (excluded from serialization)
30
38
  variables_by_name: dict[str, "Variable"] = Field(
@@ -0,0 +1,49 @@
1
+ from policyengine.core import Output, OutputCollection
2
+ from policyengine.outputs.aggregate import Aggregate, AggregateType
3
+ from policyengine.outputs.change_aggregate import (
4
+ ChangeAggregate,
5
+ ChangeAggregateType,
6
+ )
7
+ from policyengine.outputs.decile_impact import (
8
+ DecileImpact,
9
+ calculate_decile_impacts,
10
+ )
11
+ from policyengine.outputs.inequality import (
12
+ UK_INEQUALITY_INCOME_VARIABLE,
13
+ US_INEQUALITY_INCOME_VARIABLE,
14
+ Inequality,
15
+ calculate_uk_inequality,
16
+ calculate_us_inequality,
17
+ )
18
+ from policyengine.outputs.poverty import (
19
+ UK_POVERTY_VARIABLES,
20
+ US_POVERTY_VARIABLES,
21
+ Poverty,
22
+ UKPovertyType,
23
+ USPovertyType,
24
+ calculate_uk_poverty_rates,
25
+ calculate_us_poverty_rates,
26
+ )
27
+
28
+ __all__ = [
29
+ "Output",
30
+ "OutputCollection",
31
+ "Aggregate",
32
+ "AggregateType",
33
+ "ChangeAggregate",
34
+ "ChangeAggregateType",
35
+ "DecileImpact",
36
+ "calculate_decile_impacts",
37
+ "Poverty",
38
+ "UKPovertyType",
39
+ "USPovertyType",
40
+ "UK_POVERTY_VARIABLES",
41
+ "US_POVERTY_VARIABLES",
42
+ "calculate_uk_poverty_rates",
43
+ "calculate_us_poverty_rates",
44
+ "Inequality",
45
+ "UK_INEQUALITY_INCOME_VARIABLE",
46
+ "US_INEQUALITY_INCOME_VARIABLE",
47
+ "calculate_uk_inequality",
48
+ "calculate_us_inequality",
49
+ ]
@@ -2,6 +2,10 @@ import pandas as pd
2
2
  from pydantic import ConfigDict
3
3
 
4
4
  from policyengine.core import Output, OutputCollection, Simulation
5
+ from policyengine.core.dataset import Dataset
6
+ from policyengine.core.dynamic import Dynamic
7
+ from policyengine.core.policy import Policy
8
+ from policyengine.core.tax_benefit_model_version import TaxBenefitModelVersion
5
9
 
6
10
 
7
11
  class DecileImpact(Output):
@@ -93,8 +97,11 @@ class DecileImpact(Output):
93
97
 
94
98
 
95
99
  def calculate_decile_impacts(
96
- baseline_simulation: Simulation,
97
- reform_simulation: Simulation,
100
+ dataset: Dataset,
101
+ tax_benefit_model_version: TaxBenefitModelVersion,
102
+ baseline_policy: Policy | None = None,
103
+ reform_policy: Policy | None = None,
104
+ dynamic: Dynamic | None = None,
98
105
  income_variable: str = "equiv_hbai_household_net_income",
99
106
  entity: str | None = None,
100
107
  quantiles: int = 10,
@@ -104,6 +111,19 @@ def calculate_decile_impacts(
104
111
  Returns:
105
112
  OutputCollection containing list of DecileImpact objects and DataFrame
106
113
  """
114
+ baseline_simulation = Simulation(
115
+ dataset=dataset,
116
+ tax_benefit_model_version=tax_benefit_model_version,
117
+ policy=baseline_policy,
118
+ dynamic=dynamic,
119
+ )
120
+ reform_simulation = Simulation(
121
+ dataset=dataset,
122
+ tax_benefit_model_version=tax_benefit_model_version,
123
+ policy=reform_policy,
124
+ dynamic=dynamic,
125
+ )
126
+
107
127
  results = []
108
128
  for decile in range(1, quantiles + 1):
109
129
  impact = DecileImpact(
@@ -0,0 +1,276 @@
1
+ """Inequality analysis output types."""
2
+
3
+ from typing import Any
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ from pydantic import ConfigDict
8
+
9
+ from policyengine.core import Output, Simulation
10
+
11
+
12
+ def _gini(values: np.ndarray, weights: np.ndarray) -> float:
13
+ """Calculate weighted Gini coefficient.
14
+
15
+ Args:
16
+ values: Array of income values
17
+ weights: Array of weights
18
+
19
+ Returns:
20
+ Gini coefficient between 0 (perfect equality) and 1 (perfect inequality)
21
+ """
22
+ # Handle edge cases
23
+ if len(values) == 0 or weights.sum() == 0:
24
+ return 0.0
25
+
26
+ # Sort by values
27
+ sorted_indices = np.argsort(values)
28
+ sorted_values = values[sorted_indices]
29
+ sorted_weights = weights[sorted_indices]
30
+
31
+ # Cumulative weights and weighted values
32
+ cumulative_weights = np.cumsum(sorted_weights)
33
+ total_weight = cumulative_weights[-1]
34
+ cumulative_weighted_values = np.cumsum(sorted_values * sorted_weights)
35
+ total_weighted_value = cumulative_weighted_values[-1]
36
+
37
+ if total_weighted_value == 0:
38
+ return 0.0
39
+
40
+ # Calculate Gini using the area formula
41
+ # Gini = 1 - 2 * (area under Lorenz curve)
42
+ lorenz_curve = cumulative_weighted_values / total_weighted_value
43
+ weight_fractions = sorted_weights / total_weight
44
+
45
+ # Area under Lorenz curve using trapezoidal rule
46
+ area = np.sum(weight_fractions * (lorenz_curve - weight_fractions / 2))
47
+
48
+ return float(1 - 2 * area)
49
+
50
+
51
+ class Inequality(Output):
52
+ """Single inequality measure result - represents one database row.
53
+
54
+ This is a single-simulation output type that calculates inequality
55
+ metrics for a given income variable, optionally filtered by
56
+ demographic variables.
57
+ """
58
+
59
+ model_config = ConfigDict(arbitrary_types_allowed=True)
60
+
61
+ simulation: Simulation
62
+ income_variable: str
63
+ entity: str = "household"
64
+
65
+ # Optional demographic filters
66
+ filter_variable: str | None = None
67
+ filter_variable_eq: Any | None = None
68
+ filter_variable_leq: Any | None = None
69
+ filter_variable_geq: Any | None = None
70
+
71
+ # Results populated by run()
72
+ gini: float | None = None
73
+ top_10_share: float | None = None
74
+ top_1_share: float | None = None
75
+ bottom_50_share: float | None = None
76
+
77
+ def run(self):
78
+ """Calculate inequality metrics."""
79
+ # Get income variable info
80
+ income_var_obj = (
81
+ self.simulation.tax_benefit_model_version.get_variable(
82
+ self.income_variable
83
+ )
84
+ )
85
+
86
+ # Get target entity data
87
+ target_entity = self.entity
88
+ data = getattr(self.simulation.output_dataset.data, target_entity)
89
+
90
+ # Map income variable to target entity if needed
91
+ if income_var_obj.entity != target_entity:
92
+ mapped = self.simulation.output_dataset.data.map_to_entity(
93
+ income_var_obj.entity,
94
+ target_entity,
95
+ columns=[self.income_variable],
96
+ )
97
+ income_series = mapped[self.income_variable]
98
+ else:
99
+ income_series = data[self.income_variable]
100
+
101
+ # Get weights
102
+ weight_col = f"{target_entity}_weight"
103
+ if weight_col in data.columns:
104
+ weights = data[weight_col]
105
+ else:
106
+ weights = pd.Series(np.ones(len(income_series)))
107
+
108
+ # Apply demographic filter if specified
109
+ if self.filter_variable is not None:
110
+ filter_var_obj = (
111
+ self.simulation.tax_benefit_model_version.get_variable(
112
+ self.filter_variable
113
+ )
114
+ )
115
+
116
+ if filter_var_obj.entity != target_entity:
117
+ filter_mapped = (
118
+ self.simulation.output_dataset.data.map_to_entity(
119
+ filter_var_obj.entity,
120
+ target_entity,
121
+ columns=[self.filter_variable],
122
+ )
123
+ )
124
+ filter_series = filter_mapped[self.filter_variable]
125
+ else:
126
+ filter_series = data[self.filter_variable]
127
+
128
+ # Build filter mask
129
+ mask = filter_series.notna()
130
+ if self.filter_variable_eq is not None:
131
+ mask &= filter_series == self.filter_variable_eq
132
+ if self.filter_variable_leq is not None:
133
+ mask &= filter_series <= self.filter_variable_leq
134
+ if self.filter_variable_geq is not None:
135
+ mask &= filter_series >= self.filter_variable_geq
136
+
137
+ # Apply mask
138
+ income_series = income_series[mask]
139
+ weights = weights[mask]
140
+
141
+ # Convert to numpy arrays
142
+ values = np.array(income_series)
143
+ weights_arr = np.array(weights)
144
+
145
+ # Remove NaN values
146
+ valid_mask = ~np.isnan(values) & ~np.isnan(weights_arr)
147
+ values = values[valid_mask]
148
+ weights_arr = weights_arr[valid_mask]
149
+
150
+ # Calculate Gini coefficient
151
+ self.gini = _gini(values, weights_arr)
152
+
153
+ # Calculate income shares
154
+ if len(values) > 0 and weights_arr.sum() > 0:
155
+ total_income = np.sum(values * weights_arr)
156
+
157
+ if total_income > 0:
158
+ # Sort by income
159
+ sorted_indices = np.argsort(values)
160
+ sorted_values = values[sorted_indices]
161
+ sorted_weights = weights_arr[sorted_indices]
162
+
163
+ # Cumulative weight fractions
164
+ cumulative_weights = np.cumsum(sorted_weights)
165
+ total_weight = cumulative_weights[-1]
166
+ weight_fractions = cumulative_weights / total_weight
167
+
168
+ # Top 10% share
169
+ top_10_mask = weight_fractions > 0.9
170
+ self.top_10_share = float(
171
+ np.sum(
172
+ sorted_values[top_10_mask]
173
+ * sorted_weights[top_10_mask]
174
+ )
175
+ / total_income
176
+ )
177
+
178
+ # Top 1% share
179
+ top_1_mask = weight_fractions > 0.99
180
+ self.top_1_share = float(
181
+ np.sum(
182
+ sorted_values[top_1_mask] * sorted_weights[top_1_mask]
183
+ )
184
+ / total_income
185
+ )
186
+
187
+ # Bottom 50% share
188
+ bottom_50_mask = weight_fractions <= 0.5
189
+ self.bottom_50_share = float(
190
+ np.sum(
191
+ sorted_values[bottom_50_mask]
192
+ * sorted_weights[bottom_50_mask]
193
+ )
194
+ / total_income
195
+ )
196
+ else:
197
+ self.top_10_share = 0.0
198
+ self.top_1_share = 0.0
199
+ self.bottom_50_share = 0.0
200
+ else:
201
+ self.top_10_share = 0.0
202
+ self.top_1_share = 0.0
203
+ self.bottom_50_share = 0.0
204
+
205
+
206
+ # Default income variables for each country
207
+ UK_INEQUALITY_INCOME_VARIABLE = "equiv_hbai_household_net_income"
208
+ US_INEQUALITY_INCOME_VARIABLE = "household_net_income"
209
+
210
+
211
+ def calculate_uk_inequality(
212
+ simulation: Simulation,
213
+ income_variable: str = UK_INEQUALITY_INCOME_VARIABLE,
214
+ filter_variable: str | None = None,
215
+ filter_variable_eq: Any | None = None,
216
+ filter_variable_leq: Any | None = None,
217
+ filter_variable_geq: Any | None = None,
218
+ ) -> Inequality:
219
+ """Calculate inequality metrics for a UK simulation.
220
+
221
+ Args:
222
+ simulation: The simulation to analyse
223
+ income_variable: Income variable to use (default: equiv_hbai_household_net_income)
224
+ filter_variable: Optional variable to filter by
225
+ filter_variable_eq: Filter for exact match
226
+ filter_variable_leq: Filter for less than or equal
227
+ filter_variable_geq: Filter for greater than or equal
228
+
229
+ Returns:
230
+ Inequality object with Gini and income share metrics
231
+ """
232
+ inequality = Inequality(
233
+ simulation=simulation,
234
+ income_variable=income_variable,
235
+ entity="household",
236
+ filter_variable=filter_variable,
237
+ filter_variable_eq=filter_variable_eq,
238
+ filter_variable_leq=filter_variable_leq,
239
+ filter_variable_geq=filter_variable_geq,
240
+ )
241
+ inequality.run()
242
+ return inequality
243
+
244
+
245
+ def calculate_us_inequality(
246
+ simulation: Simulation,
247
+ income_variable: str = US_INEQUALITY_INCOME_VARIABLE,
248
+ filter_variable: str | None = None,
249
+ filter_variable_eq: Any | None = None,
250
+ filter_variable_leq: Any | None = None,
251
+ filter_variable_geq: Any | None = None,
252
+ ) -> Inequality:
253
+ """Calculate inequality metrics for a US simulation.
254
+
255
+ Args:
256
+ simulation: The simulation to analyse
257
+ income_variable: Income variable to use (default: household_net_income)
258
+ filter_variable: Optional variable to filter by
259
+ filter_variable_eq: Filter for exact match
260
+ filter_variable_leq: Filter for less than or equal
261
+ filter_variable_geq: Filter for greater than or equal
262
+
263
+ Returns:
264
+ Inequality object with Gini and income share metrics
265
+ """
266
+ inequality = Inequality(
267
+ simulation=simulation,
268
+ income_variable=income_variable,
269
+ entity="household",
270
+ filter_variable=filter_variable,
271
+ filter_variable_eq=filter_variable_eq,
272
+ filter_variable_leq=filter_variable_leq,
273
+ filter_variable_geq=filter_variable_geq,
274
+ )
275
+ inequality.run()
276
+ return inequality