policyengine 3.2.0__tar.gz → 3.2.1__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 (134) hide show
  1. policyengine-3.2.1/.github/bump_version.py +77 -0
  2. {policyengine-3.2.0 → policyengine-3.2.1}/.github/fetch_version.py +2 -2
  3. {policyengine-3.2.0 → policyengine-3.2.1}/.github/workflows/code_changes.yaml +6 -0
  4. {policyengine-3.2.0 → policyengine-3.2.1}/.github/workflows/pr_code_changes.yaml +17 -3
  5. {policyengine-3.2.0 → policyengine-3.2.1}/.github/workflows/versioning.yaml +9 -2
  6. {policyengine-3.2.0 → policyengine-3.2.1}/CHANGELOG.md +8 -0
  7. {policyengine-3.2.0 → policyengine-3.2.1}/Makefile +3 -7
  8. {policyengine-3.2.0 → policyengine-3.2.1}/PKG-INFO +89 -3
  9. {policyengine-3.2.0 → policyengine-3.2.1}/README.md +84 -0
  10. {policyengine-3.2.0 → policyengine-3.2.1}/examples/employment_income_variation_uk.py +6 -19
  11. {policyengine-3.2.0 → policyengine-3.2.1}/examples/employment_income_variation_us.py +7 -23
  12. {policyengine-3.2.0 → policyengine-3.2.1}/examples/household_impact_example.py +4 -12
  13. {policyengine-3.2.0 → policyengine-3.2.1}/examples/income_distribution_us.py +5 -15
  14. {policyengine-3.2.0 → policyengine-3.2.1}/examples/policy_change_uk.py +4 -12
  15. {policyengine-3.2.0 → policyengine-3.2.1}/examples/speedtest_us_simulation.py +23 -52
  16. {policyengine-3.2.0 → policyengine-3.2.1}/pyproject.toml +45 -22
  17. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/core/__init__.py +3 -0
  18. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/core/dataset.py +17 -53
  19. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/core/parameter.py +2 -6
  20. policyengine-3.2.1/src/policyengine/core/region.py +204 -0
  21. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/core/simulation.py +21 -0
  22. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/core/tax_benefit_model_version.py +21 -6
  23. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/core/variable.py +2 -0
  24. policyengine-3.2.1/src/policyengine/countries/__init__.py +9 -0
  25. policyengine-3.2.1/src/policyengine/countries/uk/__init__.py +5 -0
  26. policyengine-3.2.1/src/policyengine/countries/uk/regions.py +184 -0
  27. policyengine-3.2.1/src/policyengine/countries/us/__init__.py +5 -0
  28. policyengine-3.2.1/src/policyengine/countries/us/data/__init__.py +18 -0
  29. policyengine-3.2.1/src/policyengine/countries/us/data/districts.py +64 -0
  30. policyengine-3.2.1/src/policyengine/countries/us/data/places.py +1815 -0
  31. policyengine-3.2.1/src/policyengine/countries/us/data/states.py +59 -0
  32. policyengine-3.2.1/src/policyengine/countries/us/regions.py +106 -0
  33. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/outputs/__init__.py +40 -0
  34. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/outputs/aggregate.py +10 -24
  35. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/outputs/change_aggregate.py +9 -25
  36. policyengine-3.2.1/src/policyengine/outputs/congressional_district_impact.py +100 -0
  37. policyengine-3.2.1/src/policyengine/outputs/constituency_impact.py +126 -0
  38. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/outputs/decile_impact.py +16 -16
  39. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/outputs/inequality.py +11 -23
  40. policyengine-3.2.1/src/policyengine/outputs/intra_decile_impact.py +178 -0
  41. policyengine-3.2.1/src/policyengine/outputs/local_authority_impact.py +125 -0
  42. policyengine-3.2.1/src/policyengine/outputs/poverty.py +462 -0
  43. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/tax_benefit_models/uk/analysis.py +4 -12
  44. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/tax_benefit_models/uk/datasets.py +8 -24
  45. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/tax_benefit_models/uk/model.py +98 -28
  46. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/tax_benefit_models/us/analysis.py +5 -15
  47. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/tax_benefit_models/us/datasets.py +11 -33
  48. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/tax_benefit_models/us/model.py +95 -53
  49. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/utils/dates.py +1 -3
  50. policyengine-3.2.1/src/policyengine/utils/entity_utils.py +140 -0
  51. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/utils/parameter_labels.py +7 -4
  52. policyengine-3.2.1/src/policyengine/utils/parametric_reforms.py +129 -0
  53. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/utils/plotting.py +1 -3
  54. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine.egg-info/PKG-INFO +89 -3
  55. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine.egg-info/SOURCES.txt +37 -4
  56. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine.egg-info/requires.txt +4 -2
  57. policyengine-3.2.1/tests/conftest.py +24 -0
  58. policyengine-3.2.1/tests/fixtures/filtering_fixtures.py +161 -0
  59. policyengine-3.2.1/tests/fixtures/parametric_reforms_fixtures.py +136 -0
  60. policyengine-3.2.1/tests/fixtures/poverty_by_demographics_fixtures.py +110 -0
  61. policyengine-3.2.1/tests/fixtures/region_fixtures.py +127 -0
  62. policyengine-3.2.1/tests/fixtures/us_reform_fixtures.py +124 -0
  63. policyengine-3.2.1/tests/test_congressional_district_impact.py +131 -0
  64. policyengine-3.2.1/tests/test_constituency_impact.py +150 -0
  65. {policyengine-3.2.0 → policyengine-3.2.1}/tests/test_entity_mapping.py +11 -33
  66. policyengine-3.2.1/tests/test_entity_utils.py +306 -0
  67. policyengine-3.2.1/tests/test_filtering.py +435 -0
  68. {policyengine-3.2.0 → policyengine-3.2.1}/tests/test_household_impact.py +2 -6
  69. policyengine-3.2.1/tests/test_intra_decile_impact.py +271 -0
  70. policyengine-3.2.1/tests/test_local_authority_impact.py +108 -0
  71. {policyengine-3.2.0 → policyengine-3.2.1}/tests/test_models.py +68 -6
  72. {policyengine-3.2.0 → policyengine-3.2.1}/tests/test_pandas3_compatibility.py +1 -0
  73. {policyengine-3.2.0 → policyengine-3.2.1}/tests/test_parameter_labels.py +25 -6
  74. policyengine-3.2.1/tests/test_parametric_reforms.py +259 -0
  75. {policyengine-3.2.0 → policyengine-3.2.1}/tests/test_poverty.py +3 -11
  76. policyengine-3.2.1/tests/test_poverty_by_demographics.py +282 -0
  77. policyengine-3.2.1/tests/test_poverty_run.py +265 -0
  78. policyengine-3.2.1/tests/test_region.py +256 -0
  79. policyengine-3.2.1/tests/test_uk_regions.py +229 -0
  80. policyengine-3.2.1/tests/test_us_reform_application.py +148 -0
  81. policyengine-3.2.1/tests/test_us_regions.py +259 -0
  82. {policyengine-3.2.0 → policyengine-3.2.1}/uv.lock +27 -52
  83. policyengine-3.2.0/changelog.yaml +0 -247
  84. policyengine-3.2.0/src/policyengine/__pycache__/__init__.cpython-313.pyc +0 -0
  85. policyengine-3.2.0/src/policyengine/outputs/poverty.py +0 -238
  86. policyengine-3.2.0/src/policyengine/utils/parametric_reforms.py +0 -39
  87. {policyengine-3.2.0 → policyengine-3.2.1}/.claude/policyengine-guide.md +0 -0
  88. {policyengine-3.2.0 → policyengine-3.2.1}/.claude/quick-reference.md +0 -0
  89. {policyengine-3.2.0 → policyengine-3.2.1}/.github/CONTRIBUTING.md +0 -0
  90. {policyengine-3.2.0 → policyengine-3.2.1}/.github/changelog_template.md +0 -0
  91. {policyengine-3.2.0 → policyengine-3.2.1}/.github/get-changelog-diff.sh +0 -0
  92. {policyengine-3.2.0 → policyengine-3.2.1}/.github/has-functional-changes.sh +0 -0
  93. {policyengine-3.2.0 → policyengine-3.2.1}/.github/is-version-number-acceptable.sh +0 -0
  94. {policyengine-3.2.0 → policyengine-3.2.1}/.github/publish-git-tag.sh +0 -0
  95. {policyengine-3.2.0 → policyengine-3.2.1}/.github/workflows/docs.yml +0 -0
  96. {policyengine-3.2.0 → policyengine-3.2.1}/.github/workflows/pr_docs_changes.yaml +0 -0
  97. {policyengine-3.2.0 → policyengine-3.2.1}/.gitignore +0 -0
  98. {policyengine-3.2.0 → policyengine-3.2.1}/.python-version +0 -0
  99. {policyengine-3.2.0 → policyengine-3.2.1}/LICENSE +0 -0
  100. /policyengine-3.2.0/changelog_entry.yaml → /policyengine-3.2.1/changelog.d/.gitkeep +0 -0
  101. {policyengine-3.2.0 → policyengine-3.2.1}/docs/.gitignore +0 -0
  102. {policyengine-3.2.0 → policyengine-3.2.1}/docs/core-concepts.md +0 -0
  103. {policyengine-3.2.0 → policyengine-3.2.1}/docs/country-models-uk.md +0 -0
  104. {policyengine-3.2.0 → policyengine-3.2.1}/docs/country-models-us.md +0 -0
  105. {policyengine-3.2.0 → policyengine-3.2.1}/docs/dev.md +0 -0
  106. {policyengine-3.2.0 → policyengine-3.2.1}/docs/index.md +0 -0
  107. {policyengine-3.2.0 → policyengine-3.2.1}/docs/myst.yml +0 -0
  108. {policyengine-3.2.0 → policyengine-3.2.1}/docs/visualisation.md +0 -0
  109. {policyengine-3.2.0 → policyengine-3.2.1}/examples/income_bands_uk.py +0 -0
  110. {policyengine-3.2.0 → policyengine-3.2.1}/setup.cfg +0 -0
  111. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/__init__.py +0 -0
  112. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/core/cache.py +0 -0
  113. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/core/dataset_version.py +0 -0
  114. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/core/dynamic.py +0 -0
  115. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/core/output.py +0 -0
  116. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/core/parameter_value.py +0 -0
  117. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/core/policy.py +0 -0
  118. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/core/tax_benefit_model.py +0 -0
  119. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/tax_benefit_models/uk/__init__.py +0 -0
  120. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/tax_benefit_models/uk/outputs.py +0 -0
  121. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/tax_benefit_models/uk.py +0 -0
  122. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/tax_benefit_models/us/__init__.py +0 -0
  123. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/tax_benefit_models/us/outputs.py +0 -0
  124. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/tax_benefit_models/us.py +0 -0
  125. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/utils/__init__.py +0 -0
  126. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine.egg-info/dependency_links.txt +0 -0
  127. {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine.egg-info/top_level.txt +0 -0
  128. {policyengine-3.2.0 → policyengine-3.2.1}/tests/__init__.py +0 -0
  129. {policyengine-3.2.0 → policyengine-3.2.1}/tests/fixtures/__init__.py +0 -0
  130. {policyengine-3.2.0 → policyengine-3.2.1}/tests/fixtures/parameter_labels_fixtures.py +0 -0
  131. {policyengine-3.2.0 → policyengine-3.2.1}/tests/test_aggregate.py +0 -0
  132. {policyengine-3.2.0 → policyengine-3.2.1}/tests/test_cache.py +0 -0
  133. {policyengine-3.2.0 → policyengine-3.2.1}/tests/test_change_aggregate.py +0 -0
  134. {policyengine-3.2.0 → policyengine-3.2.1}/tests/test_inequality.py +0 -0
@@ -0,0 +1,77 @@
1
+ """Infer semver bump from towncrier fragment types and update version."""
2
+
3
+ import re
4
+ import sys
5
+ from pathlib import Path
6
+
7
+
8
+ def get_current_version(pyproject_path: Path) -> str:
9
+ text = pyproject_path.read_text()
10
+ match = re.search(r'^version\s*=\s*"(\d+\.\d+\.\d+)"', text, re.MULTILINE)
11
+ if not match:
12
+ print(
13
+ "Could not find version in pyproject.toml",
14
+ file=sys.stderr,
15
+ )
16
+ sys.exit(1)
17
+ return match.group(1)
18
+
19
+
20
+ def infer_bump(changelog_dir: Path) -> str:
21
+ fragments = [
22
+ f for f in changelog_dir.iterdir() if f.is_file() and f.name != ".gitkeep"
23
+ ]
24
+ if not fragments:
25
+ print("No changelog fragments found", file=sys.stderr)
26
+ sys.exit(1)
27
+
28
+ categories = {f.suffix.lstrip(".") for f in fragments}
29
+ for f in fragments:
30
+ parts = f.stem.split(".")
31
+ if len(parts) >= 2:
32
+ categories.add(parts[-1])
33
+
34
+ if "breaking" in categories:
35
+ return "major"
36
+ if "added" in categories or "removed" in categories:
37
+ return "minor"
38
+ return "patch"
39
+
40
+
41
+ def bump_version(version: str, bump: str) -> str:
42
+ major, minor, patch = (int(x) for x in version.split("."))
43
+ if bump == "major":
44
+ return f"{major + 1}.0.0"
45
+ elif bump == "minor":
46
+ return f"{major}.{minor + 1}.0"
47
+ else:
48
+ return f"{major}.{minor}.{patch + 1}"
49
+
50
+
51
+ def update_file(path: Path, old_version: str, new_version: str):
52
+ text = path.read_text()
53
+ updated = text.replace(
54
+ f'version = "{old_version}"',
55
+ f'version = "{new_version}"',
56
+ )
57
+ if updated != text:
58
+ path.write_text(updated)
59
+ print(f" Updated {path}")
60
+
61
+
62
+ def main():
63
+ root = Path(__file__).resolve().parent.parent
64
+ pyproject = root / "pyproject.toml"
65
+ changelog_dir = root / "changelog.d"
66
+
67
+ current = get_current_version(pyproject)
68
+ bump = infer_bump(changelog_dir)
69
+ new = bump_version(current, bump)
70
+
71
+ print(f"Version: {current} -> {new} ({bump})")
72
+
73
+ update_file(pyproject, current, new)
74
+
75
+
76
+ if __name__ == "__main__":
77
+ main()
@@ -1,8 +1,8 @@
1
1
  def fetch_version():
2
2
  try:
3
- import importlib
3
+ from importlib.metadata import version
4
4
 
5
- return importlib.import_module("policyengine").__version__
5
+ return version("policyengine")
6
6
  except Exception as e:
7
7
  print(f"Error fetching version: {e}")
8
8
  return None
@@ -21,8 +21,14 @@ jobs:
21
21
  - name: Install ruff
22
22
  run: pip install ruff
23
23
 
24
+ - name: Run ruff format check
25
+ run: ruff format --check .
26
+
24
27
  - name: Run ruff check
25
28
  run: ruff check .
29
+
30
+ - name: Run ruff format check
31
+ run: ruff format --check .
26
32
  Test:
27
33
  runs-on: macos-latest
28
34
  permissions:
@@ -3,9 +3,6 @@
3
3
  name: Code changes
4
4
  on:
5
5
  pull_request:
6
- branches:
7
- - main
8
-
9
6
  paths:
10
7
  - src/**
11
8
  - tests/**
@@ -21,8 +18,25 @@ jobs:
21
18
  - name: Install ruff
22
19
  run: pip install ruff
23
20
 
21
+ - name: Run ruff format check
22
+ run: ruff format --check .
23
+
24
24
  - name: Run ruff check
25
25
  run: ruff check .
26
+ Mypy:
27
+ runs-on: ubuntu-latest
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+ - name: Install uv
31
+ uses: astral-sh/setup-uv@v5
32
+ - name: Set up Python
33
+ uses: actions/setup-python@v5
34
+ with:
35
+ python-version: '3.13'
36
+ - name: Install package
37
+ run: uv pip install -e .[dev] --system
38
+ - name: Run mypy (informational)
39
+ run: mypy src/policyengine || echo "::warning::mypy found errors (non-blocking until codebase is clean)"
26
40
  Test:
27
41
  runs-on: macos-latest
28
42
  strategy:
@@ -7,7 +7,7 @@ on:
7
7
  - main
8
8
 
9
9
  paths:
10
- - changelog_entry.yaml
10
+ - changelog.d/**
11
11
  - .github/**
12
12
  workflow_dispatch:
13
13
 
@@ -27,7 +27,7 @@ jobs:
27
27
  with:
28
28
  python-version: 3.13
29
29
  - name: Build changelog
30
- run: pip install yaml-changelog && make changelog
30
+ run: pip install yaml-changelog towncrier && make changelog
31
31
  - name: Preview changelog update
32
32
  run: ".github/get-changelog-diff.sh"
33
33
  - name: Update changelog
@@ -66,3 +66,10 @@ jobs:
66
66
  user: __token__
67
67
  password: ${{ secrets.PYPI }}
68
68
  skip_existing: true
69
+ - name: Create GitHub Release
70
+ run: |
71
+ VERSION=$(python .github/fetch_version.py)
72
+ gh release create "$VERSION" \
73
+ --title "v$VERSION" \
74
+ --notes "See [CHANGELOG.md](https://github.com/PolicyEngine/policyengine.py/blob/main/CHANGELOG.md) for details." \
75
+ --latest
@@ -1,3 +1,11 @@
1
+ ## [3.2.1] - 2026-03-10
2
+
3
+ ### Changed
4
+
5
+ - Migrated from changelog_entry.yaml to towncrier fragments to eliminate merge conflicts.
6
+ - Switched code formatter from black to ruff format.
7
+
8
+
1
9
  # Changelog
2
10
 
3
11
  All notable changes to this project will be documented in this file.
@@ -21,14 +21,10 @@ clean:
21
21
  find . -not -path "./.venv/*" -type f -name "*.h5" -delete
22
22
 
23
23
  changelog:
24
- build-changelog changelog.yaml --output changelog.yaml --update-last-date --start-from 1.0.0 --append-file changelog_entry.yaml
25
- build-changelog changelog.yaml --org PolicyEngine --repo policyengine.py --output CHANGELOG.md --template .github/changelog_template.md
26
- bump-version changelog.yaml pyproject.toml
27
- rm changelog_entry.yaml || true
28
- touch changelog_entry.yaml
29
-
24
+ python .github/bump_version.py
25
+ towncrier build --yes --version $$(python -c "import re; print(re.search(r'version = \"(.+?)\"', open('pyproject.toml').read()).group(1))")
30
26
  build-package:
31
27
  python -m build
32
28
 
33
29
  test:
34
- pytest tests
30
+ pytest tests --cov=policyengine --cov-report=term-missing
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: policyengine
3
- Version: 3.2.0
3
+ Version: 3.2.1
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
@@ -683,7 +683,6 @@ Provides-Extra: us
683
683
  Requires-Dist: policyengine_core>=3.23.6; extra == "us"
684
684
  Requires-Dist: policyengine-us>=1.213.1; extra == "us"
685
685
  Provides-Extra: dev
686
- Requires-Dist: black; extra == "dev"
687
686
  Requires-Dist: pytest; extra == "dev"
688
687
  Requires-Dist: furo; extra == "dev"
689
688
  Requires-Dist: autodoc_pydantic; extra == "dev"
@@ -692,10 +691,13 @@ Requires-Dist: yaml-changelog>=0.1.7; extra == "dev"
692
691
  Requires-Dist: itables; extra == "dev"
693
692
  Requires-Dist: build; extra == "dev"
694
693
  Requires-Dist: pytest-asyncio>=0.26.0; extra == "dev"
695
- Requires-Dist: ruff>=0.5.0; extra == "dev"
694
+ Requires-Dist: ruff>=0.9.0; extra == "dev"
696
695
  Requires-Dist: policyengine_core>=3.23.6; extra == "dev"
697
696
  Requires-Dist: policyengine-uk>=2.51.0; extra == "dev"
698
697
  Requires-Dist: policyengine-us>=1.213.1; extra == "dev"
698
+ Requires-Dist: towncrier>=24.8.0; extra == "dev"
699
+ Requires-Dist: mypy>=1.11.0; extra == "dev"
700
+ Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
699
701
  Dynamic: license-file
700
702
 
701
703
  # PolicyEngine.py
@@ -748,10 +750,94 @@ print(f"Total UC spending: £{agg.result / 1e9:.1f}bn")
748
750
 
749
751
  ## Installation
750
752
 
753
+ ### As a library
754
+
751
755
  ```bash
752
756
  pip install policyengine
753
757
  ```
754
758
 
759
+ This installs both UK and US country models. To install only one:
760
+
761
+ ```bash
762
+ pip install policyengine[uk] # UK model only
763
+ pip install policyengine[us] # US model only
764
+ ```
765
+
766
+ ### For development
767
+
768
+ ```bash
769
+ git clone https://github.com/PolicyEngine/policyengine.py.git
770
+ cd policyengine.py
771
+ uv pip install -e .[dev] # install with dev dependencies (pytest, ruff, mypy, etc.)
772
+ ```
773
+
774
+ ## Development
775
+
776
+ ### Running configurations
777
+
778
+ | Configuration | Install | Use case |
779
+ |---------------|---------|----------|
780
+ | **Library user** | `pip install policyengine` | Using the package in your own code |
781
+ | **UK only** | `pip install policyengine[uk]` | Only need UK simulations |
782
+ | **US only** | `pip install policyengine[us]` | Only need US simulations |
783
+ | **Developer** | `uv pip install -e .[dev]` | Contributing to the package |
784
+
785
+ ### Common commands
786
+
787
+ ```bash
788
+ make format # ruff format
789
+ make test # pytest with coverage
790
+ make docs # build Jupyter Book documentation
791
+ make clean # remove caches, build artifacts, .h5 files
792
+ ```
793
+
794
+ ### Testing
795
+
796
+ Tests require a `HUGGING_FACE_TOKEN` environment variable for downloading datasets:
797
+
798
+ ```bash
799
+ export HUGGING_FACE_TOKEN=hf_...
800
+ make test
801
+ ```
802
+
803
+ To run a specific test:
804
+
805
+ ```bash
806
+ pytest tests/test_models.py -v
807
+ pytest tests/test_parametric_reforms.py -k "test_uk" -v
808
+ ```
809
+
810
+ ### Linting and type checking
811
+
812
+ ```bash
813
+ ruff format . # format code
814
+ ruff check . # lint
815
+ mypy src/policyengine # type check (informational — not yet enforced in CI)
816
+ ```
817
+
818
+ ### CI pipeline
819
+
820
+ PRs trigger the following checks:
821
+
822
+ | Check | Status | Command |
823
+ |-------|--------|---------|
824
+ | Lint + format | Required | `ruff check .` + `ruff format --check .` |
825
+ | Tests (Python 3.13) | Required | `make test` |
826
+ | Tests (Python 3.14) | Required | `make test` |
827
+ | Mypy | Informational | `mypy src/policyengine` |
828
+ | Docs build | Required | Jupyter Book build |
829
+
830
+ ### Versioning and releases
831
+
832
+ This project uses [towncrier](https://towncrier.readthedocs.io/) for changelog management. When making a PR, add a changelog fragment:
833
+
834
+ ```bash
835
+ # Fragment types: breaking, added, changed, fixed, removed
836
+ echo "Description of change" > changelog.d/my-change.added
837
+ ```
838
+
839
+ On merge, the versioning workflow bumps the version, builds the changelog, and creates a GitHub Release.
840
+
755
841
  ## Features
756
842
 
757
843
  - **Multi-country support**: UK and US tax-benefit systems
@@ -48,10 +48,94 @@ print(f"Total UC spending: £{agg.result / 1e9:.1f}bn")
48
48
 
49
49
  ## Installation
50
50
 
51
+ ### As a library
52
+
51
53
  ```bash
52
54
  pip install policyengine
53
55
  ```
54
56
 
57
+ This installs both UK and US country models. To install only one:
58
+
59
+ ```bash
60
+ pip install policyengine[uk] # UK model only
61
+ pip install policyengine[us] # US model only
62
+ ```
63
+
64
+ ### For development
65
+
66
+ ```bash
67
+ git clone https://github.com/PolicyEngine/policyengine.py.git
68
+ cd policyengine.py
69
+ uv pip install -e .[dev] # install with dev dependencies (pytest, ruff, mypy, etc.)
70
+ ```
71
+
72
+ ## Development
73
+
74
+ ### Running configurations
75
+
76
+ | Configuration | Install | Use case |
77
+ |---------------|---------|----------|
78
+ | **Library user** | `pip install policyengine` | Using the package in your own code |
79
+ | **UK only** | `pip install policyengine[uk]` | Only need UK simulations |
80
+ | **US only** | `pip install policyengine[us]` | Only need US simulations |
81
+ | **Developer** | `uv pip install -e .[dev]` | Contributing to the package |
82
+
83
+ ### Common commands
84
+
85
+ ```bash
86
+ make format # ruff format
87
+ make test # pytest with coverage
88
+ make docs # build Jupyter Book documentation
89
+ make clean # remove caches, build artifacts, .h5 files
90
+ ```
91
+
92
+ ### Testing
93
+
94
+ Tests require a `HUGGING_FACE_TOKEN` environment variable for downloading datasets:
95
+
96
+ ```bash
97
+ export HUGGING_FACE_TOKEN=hf_...
98
+ make test
99
+ ```
100
+
101
+ To run a specific test:
102
+
103
+ ```bash
104
+ pytest tests/test_models.py -v
105
+ pytest tests/test_parametric_reforms.py -k "test_uk" -v
106
+ ```
107
+
108
+ ### Linting and type checking
109
+
110
+ ```bash
111
+ ruff format . # format code
112
+ ruff check . # lint
113
+ mypy src/policyengine # type check (informational — not yet enforced in CI)
114
+ ```
115
+
116
+ ### CI pipeline
117
+
118
+ PRs trigger the following checks:
119
+
120
+ | Check | Status | Command |
121
+ |-------|--------|---------|
122
+ | Lint + format | Required | `ruff check .` + `ruff format --check .` |
123
+ | Tests (Python 3.13) | Required | `make test` |
124
+ | Tests (Python 3.14) | Required | `make test` |
125
+ | Mypy | Informational | `mypy src/policyengine` |
126
+ | Docs build | Required | Jupyter Book build |
127
+
128
+ ### Versioning and releases
129
+
130
+ This project uses [towncrier](https://towncrier.readthedocs.io/) for changelog management. When making a PR, add a changelog fragment:
131
+
132
+ ```bash
133
+ # Fragment types: breaking, added, changed, fixed, removed
134
+ echo "Description of change" > changelog.d/my-change.added
135
+ ```
136
+
137
+ On merge, the versioning workflow bumps the version, builds the changelog, and creates a GitHub Release.
138
+
55
139
  ## Features
56
140
 
57
141
  - **Multi-country support**: UK and US tax-benefit systems
@@ -125,17 +125,12 @@ def create_dataset_with_varied_employment_income(
125
125
  "region": ["LONDON"] * n_households, # Required by policyengine-uk
126
126
  "council_tax": [0.0] * n_households, # Simplified - no council tax
127
127
  "rent": [median_annual_rent] * n_households, # Median UK rent
128
- "tenure_type": ["RENT_PRIVATELY"]
129
- * n_households, # Required for uprating
128
+ "tenure_type": ["RENT_PRIVATELY"] * n_households, # Required for uprating
130
129
  }
131
130
 
132
131
  # Create MicroDataFrames
133
- person_df = MicroDataFrame(
134
- pd.DataFrame(person_data), weights="person_weight"
135
- )
136
- benunit_df = MicroDataFrame(
137
- pd.DataFrame(benunit_data), weights="benunit_weight"
138
- )
132
+ person_df = MicroDataFrame(pd.DataFrame(person_data), weights="person_weight")
133
+ benunit_df = MicroDataFrame(pd.DataFrame(benunit_data), weights="benunit_weight")
139
134
  household_df = MicroDataFrame(
140
135
  pd.DataFrame(household_data), weights="household_weight"
141
136
  )
@@ -273,9 +268,7 @@ def visualise_results(results: dict) -> None:
273
268
  # Calculate net employment income (employment income minus tax)
274
269
  net_employment = [
275
270
  emp - tax
276
- for emp, tax in zip(
277
- results["employment_income_hh"], results["household_tax"]
278
- )
271
+ for emp, tax in zip(results["employment_income_hh"], results["household_tax"])
279
272
  ]
280
273
 
281
274
  # Stack benefits and income components using PolicyEngine colors
@@ -339,17 +332,11 @@ def main():
339
332
  simulation = run_simulation(dataset)
340
333
 
341
334
  print("Extracting results using aggregate filters...")
342
- results = extract_results_by_employment_income(
343
- simulation, employment_incomes
344
- )
335
+ results = extract_results_by_employment_income(simulation, employment_incomes)
345
336
 
346
337
  print("\nSample results:")
347
338
  for emp_inc in [0, 25000, 50000, 100000]:
348
- idx = (
349
- employment_incomes.index(emp_inc)
350
- if emp_inc in employment_incomes
351
- else -1
352
- )
339
+ idx = employment_incomes.index(emp_inc) if emp_inc in employment_incomes else -1
353
340
  if idx >= 0:
354
341
  print(
355
342
  f" Employment income £{emp_inc:,}: HBAI net income £{results['hbai_household_net_income'][idx]:,.0f}"
@@ -127,24 +127,16 @@ def create_dataset_with_varied_employment_income(
127
127
  }
128
128
 
129
129
  # Create MicroDataFrames
130
- person_df = MicroDataFrame(
131
- pd.DataFrame(person_data), weights="person_weight"
132
- )
130
+ person_df = MicroDataFrame(pd.DataFrame(person_data), weights="person_weight")
133
131
  household_df = MicroDataFrame(
134
132
  pd.DataFrame(household_data), weights="household_weight"
135
133
  )
136
134
  marital_unit_df = MicroDataFrame(
137
135
  pd.DataFrame(marital_unit_data), weights="marital_unit_weight"
138
136
  )
139
- family_df = MicroDataFrame(
140
- pd.DataFrame(family_data), weights="family_weight"
141
- )
142
- spm_unit_df = MicroDataFrame(
143
- pd.DataFrame(spm_unit_data), weights="spm_unit_weight"
144
- )
145
- tax_unit_df = MicroDataFrame(
146
- pd.DataFrame(tax_unit_data), weights="tax_unit_weight"
147
- )
137
+ family_df = MicroDataFrame(pd.DataFrame(family_data), weights="family_weight")
138
+ spm_unit_df = MicroDataFrame(pd.DataFrame(spm_unit_data), weights="spm_unit_weight")
139
+ tax_unit_df = MicroDataFrame(pd.DataFrame(tax_unit_data), weights="tax_unit_weight")
148
140
 
149
141
  # Create temporary file
150
142
  tmpdir = tempfile.mkdtemp()
@@ -227,9 +219,7 @@ def visualise_results(results: dict) -> None:
227
219
  # Calculate net employment income (employment income minus tax)
228
220
  net_employment = [
229
221
  emp - tax
230
- for emp, tax in zip(
231
- results["employment_income_hh"], results["household_tax"]
232
- )
222
+ for emp, tax in zip(results["employment_income_hh"], results["household_tax"])
233
223
  ]
234
224
 
235
225
  # Stack benefits and income components using PolicyEngine colors
@@ -287,17 +277,11 @@ def main():
287
277
  simulation = run_simulation(dataset)
288
278
 
289
279
  print("Extracting results using aggregate filters...")
290
- results = extract_results_by_employment_income(
291
- simulation, employment_incomes
292
- )
280
+ results = extract_results_by_employment_income(simulation, employment_incomes)
293
281
 
294
282
  print("\nSample results:")
295
283
  for emp_inc in [0, 50000, 100000, 200000]:
296
- idx = (
297
- employment_incomes.index(emp_inc)
298
- if emp_inc in employment_incomes
299
- else -1
300
- )
284
+ idx = employment_incomes.index(emp_inc) if emp_inc in employment_incomes else -1
301
285
  if idx >= 0:
302
286
  print(
303
287
  f" Employment income ${emp_inc:,}: household net income ${results['household_net_income'][idx]:,.0f}"
@@ -34,13 +34,9 @@ def uk_example():
34
34
  result = calculate_uk_impact(household)
35
35
 
36
36
  print("\nSingle adult, £50k income:")
37
- print(
38
- f" Net income: £{result.household['hbai_household_net_income']:,.0f}"
39
- )
37
+ print(f" Net income: £{result.household['hbai_household_net_income']:,.0f}")
40
38
  print(f" Income tax: £{result.person[0]['income_tax']:,.0f}")
41
- print(
42
- f" National Insurance: £{result.person[0]['national_insurance']:,.0f}"
43
- )
39
+ print(f" National Insurance: £{result.person[0]['national_insurance']:,.0f}")
44
40
  print(f" Total tax: £{result.household['household_tax']:,.0f}")
45
41
 
46
42
  # Family with two children, £30k income, renting
@@ -64,9 +60,7 @@ def uk_example():
64
60
  result = calculate_uk_impact(household)
65
61
 
66
62
  print("\nFamily (2 adults, 2 children), £30k income, renting:")
67
- print(
68
- f" Net income: £{result.household['hbai_household_net_income']:,.0f}"
69
- )
63
+ print(f" Net income: £{result.household['hbai_household_net_income']:,.0f}")
70
64
  print(f" Income tax: £{result.person[0]['income_tax']:,.0f}")
71
65
  print(f" Child benefit: £{result.benunit[0]['child_benefit']:,.0f}")
72
66
  print(f" Universal credit: £{result.benunit[0]['universal_credit']:,.0f}")
@@ -81,9 +75,7 @@ def us_example():
81
75
 
82
76
  # Single adult earning $50,000
83
77
  household = USHouseholdInput(
84
- people=[
85
- {"age": 35, "employment_income": 50_000, "is_tax_unit_head": True}
86
- ],
78
+ people=[{"age": 35, "employment_income": 50_000, "is_tax_unit_head": True}],
87
79
  tax_unit={"filing_status": "SINGLE"},
88
80
  household={"state_code_str": "CA"},
89
81
  year=2024,
@@ -26,9 +26,7 @@ from policyengine.utils.plotting import COLORS, format_fig
26
26
 
27
27
  def load_representative_data(year: int = 2024) -> PolicyEngineUSDataset:
28
28
  """Load representative household microdata for a given year."""
29
- dataset_path = (
30
- Path(__file__).parent / "data" / f"enhanced_cps_2024_year_{year}.h5"
31
- )
29
+ dataset_path = Path(__file__).parent / "data" / f"enhanced_cps_2024_year_{year}.h5"
32
30
 
33
31
  if not dataset_path.exists():
34
32
  raise FileNotFoundError(
@@ -83,15 +81,11 @@ def calculate_income_decile_statistics(simulation: Simulation) -> dict:
83
81
  quantile_eq=decile_num,
84
82
  )
85
83
  if decile_num == 1:
86
- print(
87
- f" First Aggregate created ({time.time() - pre_create:.2f}s)"
88
- )
84
+ print(f" First Aggregate created ({time.time() - pre_create:.2f}s)")
89
85
  pre_run = time.time()
90
86
  agg.run()
91
87
  if decile_num == 1:
92
- print(
93
- f" First Aggregate.run() complete ({time.time() - pre_run:.2f}s)"
94
- )
88
+ print(f" First Aggregate.run() complete ({time.time() - pre_run:.2f}s)")
95
89
  market_incomes.append(agg.result / 1e9)
96
90
 
97
91
  agg = Aggregate(
@@ -234,9 +228,7 @@ def calculate_income_decile_statistics(simulation: Simulation) -> dict:
234
228
  print(f" {prog} complete ({time.time() - prog_start:.2f}s)")
235
229
 
236
230
  print(f"Tax benefits complete ({time.time() - tax_benefits_start:.2f}s)")
237
- print(
238
- f"\nTotal statistics calculation time: {time.time() - start_time:.2f}s"
239
- )
231
+ print(f"\nTotal statistics calculation time: {time.time() - start_time:.2f}s")
240
232
 
241
233
  return {
242
234
  "deciles": deciles,
@@ -392,9 +384,7 @@ def main():
392
384
  print(f"Total benefits: ${total_benefits:.1f}bn")
393
385
  print(f"Total net income: ${total_net_income:.1f}bn")
394
386
  print(f"Total households: {total_households:.1f}m")
395
- print(
396
- f"Average effective tax rate: {total_tax / total_market_income * 100:.1f}%"
397
- )
387
+ print(f"Average effective tax rate: {total_tax / total_market_income * 100:.1f}%")
398
388
 
399
389
  print("\nBenefit programs by decile:")
400
390
  benefit_programs = [
@@ -82,9 +82,7 @@ def run_baseline_simulation(dataset: PolicyEngineUKDataset) -> Simulation:
82
82
  return simulation
83
83
 
84
84
 
85
- def run_reform_simulation(
86
- dataset: PolicyEngineUKDataset, policy: Policy
87
- ) -> Simulation:
85
+ def run_reform_simulation(dataset: PolicyEngineUKDataset, policy: Policy) -> Simulation:
88
86
  """Run reform microsimulation with policy changes."""
89
87
  simulation = Simulation(
90
88
  dataset=dataset,
@@ -95,9 +93,7 @@ def run_reform_simulation(
95
93
  return simulation
96
94
 
97
95
 
98
- def analyse_overall_impact(
99
- baseline_sim: Simulation, reform_sim: Simulation
100
- ) -> dict:
96
+ def analyse_overall_impact(baseline_sim: Simulation, reform_sim: Simulation) -> dict:
101
97
  """Analyse overall winners, losers, and financial impact."""
102
98
  winners = ChangeAggregate(
103
99
  baseline_simulation=baseline_sim,
@@ -198,9 +194,7 @@ def analyse_impact_by_income_decile(
198
194
  }
199
195
 
200
196
 
201
- def visualise_results(
202
- overall: dict, by_decile: dict, reform_name: str
203
- ) -> None:
197
+ def visualise_results(overall: dict, by_decile: dict, reform_name: str) -> None:
204
198
  """Create visualisations of policy change impacts."""
205
199
  fig = make_subplots(
206
200
  rows=1,
@@ -270,9 +264,7 @@ def print_summary(overall: dict, decile: dict, reform_name: str) -> None:
270
264
  print(f" Losers: {overall['losers']:.2f}m households")
271
265
  print(f" No change: {overall['no_change']:.2f}m households")
272
266
  print("\nFinancial impact:")
273
- print(
274
- f" Net income change: £{overall['total_change']:.2f}bn (negative = loss)"
275
- )
267
+ print(f" Net income change: £{overall['total_change']:.2f}bn (negative = loss)")
276
268
  print(f" Tax revenue change: £{overall['tax_revenue_change']:.2f}bn")
277
269
  print("\nImpact by income decile:")
278
270
  for i, label in enumerate(decile["labels"]):