policyengine 3.4.0__tar.gz → 3.4.2__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 (144) hide show
  1. {policyengine-3.4.0 → policyengine-3.4.2}/CHANGELOG.md +14 -0
  2. {policyengine-3.4.0 → policyengine-3.4.2}/PKG-INFO +16 -5
  3. {policyengine-3.4.0 → policyengine-3.4.2}/README.md +11 -0
  4. policyengine-3.4.2/examples/paper_repro_uk.py +76 -0
  5. {policyengine-3.4.0 → policyengine-3.4.2}/pyproject.toml +5 -5
  6. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/core/__init__.py +7 -0
  7. policyengine-3.4.2/src/policyengine/core/release_manifest.py +178 -0
  8. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/core/simulation.py +14 -0
  9. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/core/tax_benefit_model_version.py +30 -0
  10. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/countries/uk/regions.py +2 -1
  11. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/countries/us/regions.py +12 -3
  12. policyengine-3.4.2/src/policyengine/data/release_manifests/uk.json +27 -0
  13. policyengine-3.4.2/src/policyengine/data/release_manifests/us.json +30 -0
  14. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/outputs/decile_impact.py +30 -14
  15. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/tax_benefit_models/uk/__init__.py +1 -0
  16. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/tax_benefit_models/uk/analysis.py +15 -16
  17. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/tax_benefit_models/uk/datasets.py +27 -17
  18. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/tax_benefit_models/uk/model.py +15 -1
  19. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/tax_benefit_models/uk/outputs.py +3 -8
  20. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/tax_benefit_models/us/__init__.py +1 -0
  21. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/tax_benefit_models/us/analysis.py +2 -5
  22. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/tax_benefit_models/us/datasets.py +26 -16
  23. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/tax_benefit_models/us/model.py +13 -2
  24. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/tax_benefit_models/us/outputs.py +3 -8
  25. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine.egg-info/PKG-INFO +16 -5
  26. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine.egg-info/SOURCES.txt +5 -0
  27. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine.egg-info/requires.txt +4 -4
  28. {policyengine-3.4.0 → policyengine-3.4.2}/tests/test_intra_decile_impact.py +131 -1
  29. {policyengine-3.4.0 → policyengine-3.4.2}/tests/test_models.py +26 -0
  30. policyengine-3.4.2/tests/test_release_manifests.py +114 -0
  31. {policyengine-3.4.0 → policyengine-3.4.2}/tests/test_uk_regions.py +4 -2
  32. {policyengine-3.4.0 → policyengine-3.4.2}/tests/test_us_regions.py +12 -4
  33. {policyengine-3.4.0 → policyengine-3.4.2}/uv.lock +279 -43
  34. {policyengine-3.4.0 → policyengine-3.4.2}/.claude/policyengine-guide.md +0 -0
  35. {policyengine-3.4.0 → policyengine-3.4.2}/.claude/quick-reference.md +0 -0
  36. {policyengine-3.4.0 → policyengine-3.4.2}/.github/CONTRIBUTING.md +0 -0
  37. {policyengine-3.4.0 → policyengine-3.4.2}/.github/bump_version.py +0 -0
  38. {policyengine-3.4.0 → policyengine-3.4.2}/.github/changelog_template.md +0 -0
  39. {policyengine-3.4.0 → policyengine-3.4.2}/.github/check-changelog.sh +0 -0
  40. {policyengine-3.4.0 → policyengine-3.4.2}/.github/fetch_version.py +0 -0
  41. {policyengine-3.4.0 → policyengine-3.4.2}/.github/get-changelog-diff.sh +0 -0
  42. {policyengine-3.4.0 → policyengine-3.4.2}/.github/has-functional-changes.sh +0 -0
  43. {policyengine-3.4.0 → policyengine-3.4.2}/.github/is-version-number-acceptable.sh +0 -0
  44. {policyengine-3.4.0 → policyengine-3.4.2}/.github/publish-git-tag.sh +0 -0
  45. {policyengine-3.4.0 → policyengine-3.4.2}/.github/workflows/pr_code_changes.yaml +0 -0
  46. {policyengine-3.4.0 → policyengine-3.4.2}/.github/workflows/pr_docs_changes.yaml +0 -0
  47. {policyengine-3.4.0 → policyengine-3.4.2}/.github/workflows/push.yaml +0 -0
  48. {policyengine-3.4.0 → policyengine-3.4.2}/.gitignore +0 -0
  49. {policyengine-3.4.0 → policyengine-3.4.2}/.python-version +0 -0
  50. {policyengine-3.4.0 → policyengine-3.4.2}/LICENSE +0 -0
  51. {policyengine-3.4.0 → policyengine-3.4.2}/Makefile +0 -0
  52. {policyengine-3.4.0 → policyengine-3.4.2}/changelog.d/.gitkeep +0 -0
  53. {policyengine-3.4.0 → policyengine-3.4.2}/docs/.gitignore +0 -0
  54. {policyengine-3.4.0 → policyengine-3.4.2}/docs/advanced-outputs.md +0 -0
  55. {policyengine-3.4.0 → policyengine-3.4.2}/docs/core-concepts.md +0 -0
  56. {policyengine-3.4.0 → policyengine-3.4.2}/docs/country-models-uk.md +0 -0
  57. {policyengine-3.4.0 → policyengine-3.4.2}/docs/country-models-us.md +0 -0
  58. {policyengine-3.4.0 → policyengine-3.4.2}/docs/dev.md +0 -0
  59. {policyengine-3.4.0 → policyengine-3.4.2}/docs/economic-impact-analysis.md +0 -0
  60. {policyengine-3.4.0 → policyengine-3.4.2}/docs/examples.md +0 -0
  61. {policyengine-3.4.0 → policyengine-3.4.2}/docs/index.md +0 -0
  62. {policyengine-3.4.0 → policyengine-3.4.2}/docs/myst.yml +0 -0
  63. {policyengine-3.4.0 → policyengine-3.4.2}/docs/regions-and-scoping.md +0 -0
  64. {policyengine-3.4.0 → policyengine-3.4.2}/docs/visualisation.md +0 -0
  65. {policyengine-3.4.0 → policyengine-3.4.2}/examples/employment_income_variation_uk.py +0 -0
  66. {policyengine-3.4.0 → policyengine-3.4.2}/examples/employment_income_variation_us.py +0 -0
  67. {policyengine-3.4.0 → policyengine-3.4.2}/examples/household_impact_example.py +0 -0
  68. {policyengine-3.4.0 → policyengine-3.4.2}/examples/income_bands_uk.py +0 -0
  69. {policyengine-3.4.0 → policyengine-3.4.2}/examples/income_distribution_us.py +0 -0
  70. {policyengine-3.4.0 → policyengine-3.4.2}/examples/policy_change_uk.py +0 -0
  71. {policyengine-3.4.0 → policyengine-3.4.2}/examples/speedtest_us_simulation.py +0 -0
  72. {policyengine-3.4.0 → policyengine-3.4.2}/examples/us_budgetary_impact.py +0 -0
  73. {policyengine-3.4.0 → policyengine-3.4.2}/setup.cfg +0 -0
  74. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/__init__.py +0 -0
  75. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/core/cache.py +0 -0
  76. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/core/dataset.py +0 -0
  77. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/core/dataset_version.py +0 -0
  78. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/core/dynamic.py +0 -0
  79. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/core/output.py +0 -0
  80. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/core/parameter.py +0 -0
  81. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/core/parameter_node.py +0 -0
  82. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/core/parameter_value.py +0 -0
  83. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/core/policy.py +0 -0
  84. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/core/region.py +0 -0
  85. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/core/scoping_strategy.py +0 -0
  86. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/core/tax_benefit_model.py +0 -0
  87. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/core/variable.py +0 -0
  88. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/countries/__init__.py +0 -0
  89. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/countries/uk/__init__.py +0 -0
  90. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/countries/us/__init__.py +0 -0
  91. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/countries/us/data/__init__.py +0 -0
  92. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/countries/us/data/districts.py +0 -0
  93. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/countries/us/data/places.py +0 -0
  94. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/countries/us/data/states.py +0 -0
  95. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/outputs/__init__.py +0 -0
  96. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/outputs/aggregate.py +0 -0
  97. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/outputs/change_aggregate.py +0 -0
  98. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/outputs/congressional_district_impact.py +0 -0
  99. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/outputs/constituency_impact.py +0 -0
  100. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/outputs/inequality.py +0 -0
  101. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/outputs/intra_decile_impact.py +0 -0
  102. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/outputs/local_authority_impact.py +0 -0
  103. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/outputs/poverty.py +0 -0
  104. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/tax_benefit_models/uk.py +0 -0
  105. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/tax_benefit_models/us.py +0 -0
  106. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/utils/__init__.py +0 -0
  107. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/utils/dates.py +0 -0
  108. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/utils/entity_utils.py +0 -0
  109. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/utils/parameter_labels.py +0 -0
  110. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/utils/parametric_reforms.py +0 -0
  111. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine/utils/plotting.py +0 -0
  112. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine.egg-info/dependency_links.txt +0 -0
  113. {policyengine-3.4.0 → policyengine-3.4.2}/src/policyengine.egg-info/top_level.txt +0 -0
  114. {policyengine-3.4.0 → policyengine-3.4.2}/tests/__init__.py +0 -0
  115. {policyengine-3.4.0 → policyengine-3.4.2}/tests/conftest.py +0 -0
  116. {policyengine-3.4.0 → policyengine-3.4.2}/tests/fixtures/__init__.py +0 -0
  117. {policyengine-3.4.0 → policyengine-3.4.2}/tests/fixtures/filtering_fixtures.py +0 -0
  118. {policyengine-3.4.0 → policyengine-3.4.2}/tests/fixtures/parameter_labels_fixtures.py +0 -0
  119. {policyengine-3.4.0 → policyengine-3.4.2}/tests/fixtures/parametric_reforms_fixtures.py +0 -0
  120. {policyengine-3.4.0 → policyengine-3.4.2}/tests/fixtures/poverty_by_demographics_fixtures.py +0 -0
  121. {policyengine-3.4.0 → policyengine-3.4.2}/tests/fixtures/region_fixtures.py +0 -0
  122. {policyengine-3.4.0 → policyengine-3.4.2}/tests/fixtures/us_reform_fixtures.py +0 -0
  123. {policyengine-3.4.0 → policyengine-3.4.2}/tests/fixtures/variable_label_fixtures.py +0 -0
  124. {policyengine-3.4.0 → policyengine-3.4.2}/tests/test_aggregate.py +0 -0
  125. {policyengine-3.4.0 → policyengine-3.4.2}/tests/test_cache.py +0 -0
  126. {policyengine-3.4.0 → policyengine-3.4.2}/tests/test_change_aggregate.py +0 -0
  127. {policyengine-3.4.0 → policyengine-3.4.2}/tests/test_congressional_district_impact.py +0 -0
  128. {policyengine-3.4.0 → policyengine-3.4.2}/tests/test_constituency_impact.py +0 -0
  129. {policyengine-3.4.0 → policyengine-3.4.2}/tests/test_entity_mapping.py +0 -0
  130. {policyengine-3.4.0 → policyengine-3.4.2}/tests/test_entity_utils.py +0 -0
  131. {policyengine-3.4.0 → policyengine-3.4.2}/tests/test_filtering.py +0 -0
  132. {policyengine-3.4.0 → policyengine-3.4.2}/tests/test_household_impact.py +0 -0
  133. {policyengine-3.4.0 → policyengine-3.4.2}/tests/test_inequality.py +0 -0
  134. {policyengine-3.4.0 → policyengine-3.4.2}/tests/test_local_authority_impact.py +0 -0
  135. {policyengine-3.4.0 → policyengine-3.4.2}/tests/test_pandas3_compatibility.py +0 -0
  136. {policyengine-3.4.0 → policyengine-3.4.2}/tests/test_parameter_labels.py +0 -0
  137. {policyengine-3.4.0 → policyengine-3.4.2}/tests/test_parametric_reforms.py +0 -0
  138. {policyengine-3.4.0 → policyengine-3.4.2}/tests/test_poverty.py +0 -0
  139. {policyengine-3.4.0 → policyengine-3.4.2}/tests/test_poverty_by_demographics.py +0 -0
  140. {policyengine-3.4.0 → policyengine-3.4.2}/tests/test_poverty_run.py +0 -0
  141. {policyengine-3.4.0 → policyengine-3.4.2}/tests/test_region.py +0 -0
  142. {policyengine-3.4.0 → policyengine-3.4.2}/tests/test_scoping_strategy.py +0 -0
  143. {policyengine-3.4.0 → policyengine-3.4.2}/tests/test_us_reform_application.py +0 -0
  144. {policyengine-3.4.0 → policyengine-3.4.2}/tests/test_variable_labels.py +0 -0
@@ -1,3 +1,17 @@
1
+ ## [3.4.2] - 2026-04-12
2
+
3
+ ### Changed
4
+
5
+ - Align the bundled UK release manifest with the pinned `policyengine-uk` package version and updated data package revisions.
6
+
7
+
8
+ ## [3.4.1] - 2026-04-09
9
+
10
+ ### Fixed
11
+
12
+ - Fixed the UK paper reproduction workflow so the checked-in example runs on Python 3.14 and the associated analysis helpers handle that path cleanly.
13
+
14
+
1
15
  ## [3.4.0] - 2026-04-08
2
16
 
3
17
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: policyengine
3
- Version: 3.4.0
3
+ Version: 3.4.2
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
@@ -678,10 +678,10 @@ Requires-Dist: requests>=2.31.0
678
678
  Requires-Dist: psutil>=5.9.0
679
679
  Provides-Extra: uk
680
680
  Requires-Dist: policyengine_core>=3.23.6; extra == "uk"
681
- Requires-Dist: policyengine-uk>=2.51.0; extra == "uk"
681
+ Requires-Dist: policyengine-uk==2.78.0; extra == "uk"
682
682
  Provides-Extra: us
683
683
  Requires-Dist: policyengine_core>=3.23.6; extra == "us"
684
- Requires-Dist: policyengine-us>=1.213.1; extra == "us"
684
+ Requires-Dist: policyengine-us==1.602.0; extra == "us"
685
685
  Provides-Extra: dev
686
686
  Requires-Dist: pytest; extra == "dev"
687
687
  Requires-Dist: furo; extra == "dev"
@@ -693,8 +693,8 @@ Requires-Dist: build; extra == "dev"
693
693
  Requires-Dist: pytest-asyncio>=0.26.0; extra == "dev"
694
694
  Requires-Dist: ruff>=0.9.0; extra == "dev"
695
695
  Requires-Dist: policyengine_core>=3.23.6; extra == "dev"
696
- Requires-Dist: policyengine-uk>=2.51.0; extra == "dev"
697
- Requires-Dist: policyengine-us>=1.213.1; extra == "dev"
696
+ Requires-Dist: policyengine-uk==2.78.0; extra == "dev"
697
+ Requires-Dist: policyengine-us==1.602.0; extra == "dev"
698
698
  Requires-Dist: towncrier>=24.8.0; extra == "dev"
699
699
  Requires-Dist: mypy>=1.11.0; extra == "dev"
700
700
  Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
@@ -747,6 +747,7 @@ print(f"Total UC spending: £{agg.result / 1e9:.1f}bn")
747
747
  - `examples/income_distribution_us.py`: Analyse benefit distribution by decile
748
748
  - `examples/employment_income_variation_uk.py`: Model employment income phase-outs
749
749
  - `examples/policy_change_uk.py`: Analyse policy reform impacts
750
+ - `examples/paper_repro_uk.py`: Reproduce the UK reform analysis used in the JOSS paper draft
750
751
 
751
752
  ## Installation
752
753
 
@@ -838,6 +839,16 @@ echo "Description of change" > changelog.d/my-change.added
838
839
 
839
840
  On merge, the versioning workflow bumps the version, builds the changelog, and creates a GitHub Release.
840
841
 
842
+ ## Paper reproduction
843
+
844
+ Use the pinned interpreter and the UK extra to run the checked-in paper repro:
845
+
846
+ ```bash
847
+ uv run --python 3.14 --extra uk python examples/paper_repro_uk.py
848
+ ```
849
+
850
+ On first run this will create `./data/enhanced_frs_2023_24_year_2026.h5`.
851
+
841
852
  ## Features
842
853
 
843
854
  - **Multi-country support**: UK and US tax-benefit systems
@@ -45,6 +45,7 @@ print(f"Total UC spending: £{agg.result / 1e9:.1f}bn")
45
45
  - `examples/income_distribution_us.py`: Analyse benefit distribution by decile
46
46
  - `examples/employment_income_variation_uk.py`: Model employment income phase-outs
47
47
  - `examples/policy_change_uk.py`: Analyse policy reform impacts
48
+ - `examples/paper_repro_uk.py`: Reproduce the UK reform analysis used in the JOSS paper draft
48
49
 
49
50
  ## Installation
50
51
 
@@ -136,6 +137,16 @@ echo "Description of change" > changelog.d/my-change.added
136
137
 
137
138
  On merge, the versioning workflow bumps the version, builds the changelog, and creates a GitHub Release.
138
139
 
140
+ ## Paper reproduction
141
+
142
+ Use the pinned interpreter and the UK extra to run the checked-in paper repro:
143
+
144
+ ```bash
145
+ uv run --python 3.14 --extra uk python examples/paper_repro_uk.py
146
+ ```
147
+
148
+ On first run this will create `./data/enhanced_frs_2023_24_year_2026.h5`.
149
+
139
150
  ## Features
140
151
 
141
152
  - **Multi-country support**: UK and US tax-benefit systems
@@ -0,0 +1,76 @@
1
+ """Reproduce the UK policy-reform analysis used in the JOSS paper draft.
2
+
3
+ This script uses the same reform shown in `paper.md`, but adds the missing
4
+ dataset setup so it can run end-to-end from a fresh checkout.
5
+
6
+ Run:
7
+ uv run --python 3.14 --extra uk python examples/paper_repro_uk.py
8
+ """
9
+
10
+ from datetime import date
11
+
12
+ from policyengine.core import Parameter, ParameterValue, Policy, Simulation
13
+ from policyengine.tax_benefit_models.uk import (
14
+ economic_impact_analysis,
15
+ ensure_datasets,
16
+ uk_latest,
17
+ )
18
+
19
+
20
+ def load_dataset(year: int = 2026):
21
+ """Load or create the enhanced UK dataset used for population analysis."""
22
+ datasets = ensure_datasets(
23
+ datasets=["hf://policyengine/policyengine-uk-data/enhanced_frs_2023_24.h5"],
24
+ years=[year],
25
+ )
26
+ return datasets[f"enhanced_frs_2023_24_{year}"]
27
+
28
+
29
+ def create_reform(year: int = 2026) -> Policy:
30
+ """Set the UK personal allowance to zero for one tax year."""
31
+ parameter = Parameter(
32
+ name="gov.hmrc.income_tax.allowances.personal_allowance.amount",
33
+ tax_benefit_model_version=uk_latest,
34
+ )
35
+
36
+ return Policy(
37
+ name="Zero personal allowance",
38
+ parameter_values=[
39
+ ParameterValue(
40
+ parameter=parameter,
41
+ start_date=date(year, 1, 1),
42
+ end_date=date(year, 12, 31),
43
+ value=0,
44
+ )
45
+ ],
46
+ )
47
+
48
+
49
+ def main():
50
+ dataset = load_dataset()
51
+ reform = create_reform()
52
+
53
+ baseline = Simulation(
54
+ dataset=dataset,
55
+ tax_benefit_model_version=uk_latest,
56
+ )
57
+ reformed = Simulation(
58
+ dataset=dataset,
59
+ tax_benefit_model_version=uk_latest,
60
+ policy=reform,
61
+ )
62
+
63
+ analysis = economic_impact_analysis(baseline, reformed)
64
+
65
+ first_decile = analysis.decile_impacts.outputs[0]
66
+
67
+ print(f"Dataset: {dataset.filepath}")
68
+ print(f"Households: {len(dataset.data.household):,}")
69
+ print(f"Baseline Gini: {analysis.baseline_inequality.gini:.4f}")
70
+ print(f"Reform Gini: {analysis.reform_inequality.gini:.4f}")
71
+ print(f"Decile 1 mean change: {first_decile.absolute_change:,.2f}")
72
+ print(f"Programmes analysed: {len(analysis.programme_statistics.outputs)}")
73
+
74
+
75
+ if __name__ == "__main__":
76
+ main()
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "policyengine"
7
- version = "3.4.0"
7
+ version = "3.4.2"
8
8
  description = "A package to conduct policy analysis using PolicyEngine tax-benefit models."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -28,11 +28,11 @@ dependencies = [
28
28
  [project.optional-dependencies]
29
29
  uk = [
30
30
  "policyengine_core>=3.23.6",
31
- "policyengine-uk>=2.51.0",
31
+ "policyengine-uk==2.78.0",
32
32
  ]
33
33
  us = [
34
34
  "policyengine_core>=3.23.6",
35
- "policyengine-us>=1.213.1",
35
+ "policyengine-us==1.602.0",
36
36
  ]
37
37
  dev = [
38
38
  "pytest",
@@ -45,8 +45,8 @@ dev = [
45
45
  "pytest-asyncio>=0.26.0",
46
46
  "ruff>=0.9.0",
47
47
  "policyengine_core>=3.23.6",
48
- "policyengine-uk>=2.51.0",
49
- "policyengine-us>=1.213.1",
48
+ "policyengine-uk==2.78.0",
49
+ "policyengine-us==1.602.0",
50
50
  "towncrier>=24.8.0",
51
51
  "mypy>=1.11.0",
52
52
  "pytest-cov>=5.0.0",
@@ -12,6 +12,13 @@ from .policy import Policy as Policy
12
12
  from .region import Region as Region
13
13
  from .region import RegionRegistry as RegionRegistry
14
14
  from .region import RegionType as RegionType
15
+ from .release_manifest import CountryReleaseManifest as CountryReleaseManifest
16
+ from .release_manifest import DataPackageVersion as DataPackageVersion
17
+ from .release_manifest import DataReleaseArtifact as DataReleaseArtifact
18
+ from .release_manifest import DataReleaseManifest as DataReleaseManifest
19
+ from .release_manifest import PackageVersion as PackageVersion
20
+ from .release_manifest import get_data_release_manifest as get_data_release_manifest
21
+ from .release_manifest import get_release_manifest as get_release_manifest
15
22
  from .scoping_strategy import RegionScopingStrategy as RegionScopingStrategy
16
23
  from .scoping_strategy import RowFilterStrategy as RowFilterStrategy
17
24
  from .scoping_strategy import ScopingStrategy as ScopingStrategy
@@ -0,0 +1,178 @@
1
+ import os
2
+ from functools import lru_cache
3
+ from importlib.resources import files
4
+ from pathlib import Path
5
+
6
+ import requests
7
+ from pydantic import BaseModel, Field
8
+
9
+ HF_REQUEST_TIMEOUT_SECONDS = 30
10
+
11
+
12
+ class PackageVersion(BaseModel):
13
+ name: str
14
+ version: str
15
+
16
+
17
+ class DataPackageVersion(PackageVersion):
18
+ repo_id: str
19
+ repo_type: str = "model"
20
+ release_manifest_path: str = "release_manifest.json"
21
+
22
+
23
+ class CompatibleModelPackage(BaseModel):
24
+ name: str
25
+ specifier: str
26
+
27
+
28
+ class ArtifactPathReference(BaseModel):
29
+ path: str
30
+
31
+
32
+ class ArtifactPathTemplate(BaseModel):
33
+ path_template: str
34
+
35
+ def resolve(self, **kwargs: str) -> str:
36
+ return self.path_template.format(**kwargs)
37
+
38
+
39
+ class DataReleaseArtifact(BaseModel):
40
+ kind: str
41
+ path: str
42
+ repo_id: str
43
+ revision: str
44
+ sha256: str | None = None
45
+ size_bytes: int | None = None
46
+
47
+ @property
48
+ def uri(self) -> str:
49
+ return build_hf_uri(
50
+ repo_id=self.repo_id,
51
+ path_in_repo=self.path,
52
+ revision=self.revision,
53
+ )
54
+
55
+
56
+ class DataReleaseManifest(BaseModel):
57
+ schema_version: int
58
+ data_package: PackageVersion
59
+ compatible_model_packages: list[CompatibleModelPackage] = Field(
60
+ default_factory=list
61
+ )
62
+ default_datasets: dict[str, str] = Field(default_factory=dict)
63
+ artifacts: dict[str, DataReleaseArtifact] = Field(default_factory=dict)
64
+
65
+
66
+ class CountryReleaseManifest(BaseModel):
67
+ country_id: str
68
+ policyengine_version: str
69
+ model_package: PackageVersion
70
+ data_package: DataPackageVersion
71
+ default_dataset: str
72
+ datasets: dict[str, ArtifactPathReference] = Field(default_factory=dict)
73
+ region_datasets: dict[str, ArtifactPathTemplate] = Field(default_factory=dict)
74
+
75
+ @property
76
+ def default_dataset_uri(self) -> str:
77
+ return resolve_dataset_reference(self.country_id, self.default_dataset)
78
+
79
+
80
+ def build_hf_uri(repo_id: str, path_in_repo: str, revision: str) -> str:
81
+ return f"hf://{repo_id}/{path_in_repo}@{revision}"
82
+
83
+
84
+ @lru_cache
85
+ def get_release_manifest(country_id: str) -> CountryReleaseManifest:
86
+ manifest_path = files("policyengine").joinpath(
87
+ "data", "release_manifests", f"{country_id}.json"
88
+ )
89
+ if not manifest_path.is_file():
90
+ raise ValueError(f"No bundled release manifest for country '{country_id}'")
91
+
92
+ return CountryReleaseManifest.model_validate_json(manifest_path.read_text())
93
+
94
+
95
+ def _data_release_manifest_url(data_package: DataPackageVersion) -> str:
96
+ return (
97
+ "https://huggingface.co/"
98
+ f"{data_package.repo_id}/resolve/{data_package.version}/"
99
+ f"{data_package.release_manifest_path}"
100
+ )
101
+
102
+
103
+ @lru_cache
104
+ def get_data_release_manifest(country_id: str) -> DataReleaseManifest:
105
+ country_manifest = get_release_manifest(country_id)
106
+ data_package = country_manifest.data_package
107
+
108
+ headers = {}
109
+ token = os.environ.get("HUGGING_FACE_TOKEN")
110
+ if token:
111
+ headers["Authorization"] = f"Bearer {token}"
112
+
113
+ response = requests.get(
114
+ _data_release_manifest_url(data_package),
115
+ headers=headers,
116
+ timeout=HF_REQUEST_TIMEOUT_SECONDS,
117
+ )
118
+ if response.status_code in (401, 403):
119
+ raise ValueError(
120
+ "Could not fetch the data release manifest from Hugging Face. "
121
+ "If this country uses a private data repo, set HUGGING_FACE_TOKEN."
122
+ )
123
+ response.raise_for_status()
124
+ return DataReleaseManifest.model_validate_json(response.text)
125
+
126
+
127
+ def resolve_dataset_reference(country_id: str, dataset: str) -> str:
128
+ if "://" in dataset:
129
+ return dataset
130
+
131
+ manifest = get_release_manifest(country_id)
132
+ path_reference = manifest.datasets.get(dataset)
133
+ if path_reference is not None:
134
+ return build_hf_uri(
135
+ repo_id=manifest.data_package.repo_id,
136
+ path_in_repo=path_reference.path,
137
+ revision=manifest.data_package.version,
138
+ )
139
+
140
+ data_release_manifest = get_data_release_manifest(country_id)
141
+ artifact = data_release_manifest.artifacts.get(dataset)
142
+ if artifact is None:
143
+ raise ValueError(
144
+ f"Unknown dataset '{dataset}' for country '{country_id}'. "
145
+ f"Known datasets: {sorted(manifest.datasets)}"
146
+ )
147
+
148
+ return artifact.uri
149
+
150
+
151
+ def dataset_logical_name(dataset: str) -> str:
152
+ return Path(dataset.rsplit("@", 1)[0]).stem
153
+
154
+
155
+ def resolve_default_datasets(country_id: str) -> list[str]:
156
+ manifest = get_release_manifest(country_id)
157
+ return list(manifest.datasets.keys())
158
+
159
+
160
+ def resolve_region_dataset_path(
161
+ country_id: str,
162
+ region_type: str,
163
+ **kwargs: str,
164
+ ) -> str | None:
165
+ manifest = get_release_manifest(country_id)
166
+ template = manifest.region_datasets.get(region_type)
167
+ if template is None:
168
+ return None
169
+
170
+ resolved_path = template.resolve(**kwargs)
171
+ if "://" in resolved_path:
172
+ return resolved_path
173
+
174
+ return build_hf_uri(
175
+ repo_id=manifest.data_package.repo_id,
176
+ path_in_repo=resolved_path,
177
+ revision=manifest.data_package.version,
178
+ )
@@ -94,3 +94,17 @@ class Simulation(BaseModel):
94
94
  def load(self):
95
95
  """Load the simulation's output dataset."""
96
96
  self.tax_benefit_model_version.load(self)
97
+
98
+ @property
99
+ def release_bundle(self) -> dict[str, str | None]:
100
+ bundle = (
101
+ self.tax_benefit_model_version.release_bundle
102
+ if self.tax_benefit_model_version is not None
103
+ else {}
104
+ )
105
+ return {
106
+ **bundle,
107
+ "dataset_filepath": self.dataset.filepath
108
+ if self.dataset is not None
109
+ else None,
110
+ }
@@ -4,6 +4,7 @@ from uuid import uuid4
4
4
 
5
5
  from pydantic import BaseModel, Field
6
6
 
7
+ from .release_manifest import CountryReleaseManifest, PackageVersion
7
8
  from .tax_benefit_model import TaxBenefitModel
8
9
 
9
10
  if TYPE_CHECKING:
@@ -32,6 +33,13 @@ class TaxBenefitModelVersion(BaseModel):
32
33
  region_registry: "RegionRegistry | None" = Field(
33
34
  default=None, description="Registry of supported geographic regions"
34
35
  )
36
+ release_manifest: CountryReleaseManifest | None = Field(
37
+ default=None,
38
+ exclude=True,
39
+ )
40
+ model_package: PackageVersion | None = Field(default=None)
41
+ data_package: PackageVersion | None = Field(default=None)
42
+ default_dataset_uri: str | None = Field(default=None)
35
43
 
36
44
  @property
37
45
  def parameter_values(self) -> list["ParameterValue"]:
@@ -116,6 +124,28 @@ class TaxBenefitModelVersion(BaseModel):
116
124
  return None
117
125
  return self.region_registry.get(code)
118
126
 
127
+ @property
128
+ def release_bundle(self) -> dict[str, str | None]:
129
+ return {
130
+ "country_id": self.release_manifest.country_id
131
+ if self.release_manifest is not None
132
+ else None,
133
+ "policyengine_version": self.release_manifest.policyengine_version
134
+ if self.release_manifest is not None
135
+ else None,
136
+ "model_package": self.model_package.name
137
+ if self.model_package is not None
138
+ else None,
139
+ "model_version": self.version,
140
+ "data_package": self.data_package.name
141
+ if self.data_package is not None
142
+ else None,
143
+ "data_version": self.data_package.version
144
+ if self.data_package is not None
145
+ else None,
146
+ "default_dataset_uri": self.default_dataset_uri,
147
+ }
148
+
119
149
  def __repr__(self) -> str:
120
150
  # Give the id and version, and the number of variables, parameters, parameter nodes, parameter values
121
151
  return f"<TaxBenefitModelVersion id={self.id} variables={len(self.variables)} parameters={len(self.parameters)} parameter_nodes={len(self.parameter_nodes)} parameter_values={len(self.parameter_values)}>"
@@ -15,6 +15,7 @@ import logging
15
15
  from typing import TYPE_CHECKING
16
16
 
17
17
  from policyengine.core.region import Region, RegionRegistry
18
+ from policyengine.core.release_manifest import resolve_region_dataset_path
18
19
  from policyengine.core.scoping_strategy import (
19
20
  RowFilterStrategy,
20
21
  WeightReplacementStrategy,
@@ -127,7 +128,7 @@ def build_uk_region_registry(
127
128
  code="uk",
128
129
  label="United Kingdom",
129
130
  region_type="national",
130
- dataset_path=f"{UK_DATA_BUCKET}/enhanced_frs_2023_24.h5",
131
+ dataset_path=resolve_region_dataset_path("uk", "national"),
131
132
  )
132
133
  )
133
134
 
@@ -8,6 +8,7 @@ in the data/ subdirectory:
8
8
  """
9
9
 
10
10
  from policyengine.core.region import Region, RegionRegistry
11
+ from policyengine.core.release_manifest import resolve_region_dataset_path
11
12
  from policyengine.core.scoping_strategy import RowFilterStrategy
12
13
 
13
14
  from .data import AT_LARGE_STATES, DISTRICT_COUNTS, US_PLACES, US_STATES
@@ -40,7 +41,7 @@ def build_us_region_registry() -> RegionRegistry:
40
41
  code="us",
41
42
  label="United States",
42
43
  region_type="national",
43
- dataset_path=f"{US_DATA_BUCKET}/enhanced_cps_2024.h5",
44
+ dataset_path=resolve_region_dataset_path("us", "national"),
44
45
  )
45
46
  )
46
47
 
@@ -52,7 +53,11 @@ def build_us_region_registry() -> RegionRegistry:
52
53
  label=name,
53
54
  region_type="state",
54
55
  parent_code="us",
55
- dataset_path=f"{US_DATA_BUCKET}/states/{abbrev}.h5",
56
+ dataset_path=resolve_region_dataset_path(
57
+ "us",
58
+ "state",
59
+ state_code=abbrev,
60
+ ),
56
61
  state_code=abbrev,
57
62
  state_name=name,
58
63
  )
@@ -76,7 +81,11 @@ def build_us_region_registry() -> RegionRegistry:
76
81
  label=label,
77
82
  region_type="congressional_district",
78
83
  parent_code=f"state/{state_abbrev.lower()}",
79
- dataset_path=f"{US_DATA_BUCKET}/districts/{district_code}.h5",
84
+ dataset_path=resolve_region_dataset_path(
85
+ "us",
86
+ "congressional_district",
87
+ district_code=district_code,
88
+ ),
80
89
  state_code=state_abbrev,
81
90
  state_name=state_name,
82
91
  )
@@ -0,0 +1,27 @@
1
+ {
2
+ "country_id": "uk",
3
+ "policyengine_version": "3.4.1",
4
+ "model_package": {
5
+ "name": "policyengine-uk",
6
+ "version": "2.78.0"
7
+ },
8
+ "data_package": {
9
+ "name": "policyengine-uk-data",
10
+ "version": "1.40.3",
11
+ "repo_id": "policyengine/policyengine-uk-data-private"
12
+ },
13
+ "default_dataset": "enhanced_frs_2023_24",
14
+ "datasets": {
15
+ "frs_2023_24": {
16
+ "path": "frs_2023_24.h5"
17
+ },
18
+ "enhanced_frs_2023_24": {
19
+ "path": "enhanced_frs_2023_24.h5"
20
+ }
21
+ },
22
+ "region_datasets": {
23
+ "national": {
24
+ "path_template": "enhanced_frs_2023_24.h5"
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "country_id": "us",
3
+ "policyengine_version": "3.4.1",
4
+ "model_package": {
5
+ "name": "policyengine-us",
6
+ "version": "1.602.0"
7
+ },
8
+ "data_package": {
9
+ "name": "policyengine-us-data",
10
+ "version": "1.77.0",
11
+ "repo_id": "policyengine/policyengine-us-data"
12
+ },
13
+ "default_dataset": "enhanced_cps_2024",
14
+ "datasets": {
15
+ "enhanced_cps_2024": {
16
+ "path": "enhanced_cps_2024.h5"
17
+ }
18
+ },
19
+ "region_datasets": {
20
+ "national": {
21
+ "path_template": "enhanced_cps_2024.h5"
22
+ },
23
+ "state": {
24
+ "path_template": "states/{state_code}.h5"
25
+ },
26
+ "congressional_district": {
27
+ "path_template": "districts/{district_code}.h5"
28
+ }
29
+ }
30
+ }
@@ -97,32 +97,48 @@ class DecileImpact(Output):
97
97
 
98
98
 
99
99
  def calculate_decile_impacts(
100
- dataset: Dataset,
101
- tax_benefit_model_version: TaxBenefitModelVersion,
100
+ dataset: Dataset | None = None,
101
+ tax_benefit_model_version: TaxBenefitModelVersion | None = None,
102
102
  baseline_policy: Policy | None = None,
103
103
  reform_policy: Policy | None = None,
104
104
  dynamic: Dynamic | None = None,
105
105
  income_variable: str = "equiv_hbai_household_net_income",
106
106
  entity: str | None = None,
107
107
  quantiles: int = 10,
108
+ baseline_simulation: Simulation | None = None,
109
+ reform_simulation: Simulation | None = None,
108
110
  ) -> OutputCollection[DecileImpact]:
109
111
  """Calculate decile-by-decile impact of a reform.
110
112
 
111
113
  Returns:
112
114
  OutputCollection containing list of DecileImpact objects and DataFrame
113
115
  """
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
- )
116
+ if (baseline_simulation is None) != (reform_simulation is None):
117
+ raise ValueError(
118
+ "baseline_simulation and reform_simulation must be provided together"
119
+ )
120
+
121
+ if baseline_simulation is None:
122
+ if dataset is None or tax_benefit_model_version is None:
123
+ raise ValueError(
124
+ "dataset and tax_benefit_model_version are required when simulations are not provided"
125
+ )
126
+
127
+ baseline_simulation = Simulation(
128
+ dataset=dataset,
129
+ tax_benefit_model_version=tax_benefit_model_version,
130
+ policy=baseline_policy,
131
+ dynamic=dynamic,
132
+ )
133
+ reform_simulation = Simulation(
134
+ dataset=dataset,
135
+ tax_benefit_model_version=tax_benefit_model_version,
136
+ policy=reform_policy,
137
+ dynamic=dynamic,
138
+ )
139
+
140
+ baseline_simulation.ensure()
141
+ reform_simulation.ensure()
126
142
 
127
143
  results = []
128
144
  for decile in range(1, quantiles + 1):
@@ -31,6 +31,7 @@ if find_spec("policyengine_uk") is not None:
31
31
  UKYearData.model_rebuild()
32
32
  PolicyEngineUKDataset.model_rebuild()
33
33
  PolicyEngineUKLatest.model_rebuild()
34
+ ProgrammeStatistics.model_rebuild()
34
35
 
35
36
  __all__ = [
36
37
  "UKYearData",