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.
- policyengine-3.2.1/.github/bump_version.py +77 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/.github/fetch_version.py +2 -2
- {policyengine-3.2.0 → policyengine-3.2.1}/.github/workflows/code_changes.yaml +6 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/.github/workflows/pr_code_changes.yaml +17 -3
- {policyengine-3.2.0 → policyengine-3.2.1}/.github/workflows/versioning.yaml +9 -2
- {policyengine-3.2.0 → policyengine-3.2.1}/CHANGELOG.md +8 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/Makefile +3 -7
- {policyengine-3.2.0 → policyengine-3.2.1}/PKG-INFO +89 -3
- {policyengine-3.2.0 → policyengine-3.2.1}/README.md +84 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/examples/employment_income_variation_uk.py +6 -19
- {policyengine-3.2.0 → policyengine-3.2.1}/examples/employment_income_variation_us.py +7 -23
- {policyengine-3.2.0 → policyengine-3.2.1}/examples/household_impact_example.py +4 -12
- {policyengine-3.2.0 → policyengine-3.2.1}/examples/income_distribution_us.py +5 -15
- {policyengine-3.2.0 → policyengine-3.2.1}/examples/policy_change_uk.py +4 -12
- {policyengine-3.2.0 → policyengine-3.2.1}/examples/speedtest_us_simulation.py +23 -52
- {policyengine-3.2.0 → policyengine-3.2.1}/pyproject.toml +45 -22
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/core/__init__.py +3 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/core/dataset.py +17 -53
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/core/parameter.py +2 -6
- policyengine-3.2.1/src/policyengine/core/region.py +204 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/core/simulation.py +21 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/core/tax_benefit_model_version.py +21 -6
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/core/variable.py +2 -0
- policyengine-3.2.1/src/policyengine/countries/__init__.py +9 -0
- policyengine-3.2.1/src/policyengine/countries/uk/__init__.py +5 -0
- policyengine-3.2.1/src/policyengine/countries/uk/regions.py +184 -0
- policyengine-3.2.1/src/policyengine/countries/us/__init__.py +5 -0
- policyengine-3.2.1/src/policyengine/countries/us/data/__init__.py +18 -0
- policyengine-3.2.1/src/policyengine/countries/us/data/districts.py +64 -0
- policyengine-3.2.1/src/policyengine/countries/us/data/places.py +1815 -0
- policyengine-3.2.1/src/policyengine/countries/us/data/states.py +59 -0
- policyengine-3.2.1/src/policyengine/countries/us/regions.py +106 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/outputs/__init__.py +40 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/outputs/aggregate.py +10 -24
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/outputs/change_aggregate.py +9 -25
- policyengine-3.2.1/src/policyengine/outputs/congressional_district_impact.py +100 -0
- policyengine-3.2.1/src/policyengine/outputs/constituency_impact.py +126 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/outputs/decile_impact.py +16 -16
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/outputs/inequality.py +11 -23
- policyengine-3.2.1/src/policyengine/outputs/intra_decile_impact.py +178 -0
- policyengine-3.2.1/src/policyengine/outputs/local_authority_impact.py +125 -0
- policyengine-3.2.1/src/policyengine/outputs/poverty.py +462 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/tax_benefit_models/uk/analysis.py +4 -12
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/tax_benefit_models/uk/datasets.py +8 -24
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/tax_benefit_models/uk/model.py +98 -28
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/tax_benefit_models/us/analysis.py +5 -15
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/tax_benefit_models/us/datasets.py +11 -33
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/tax_benefit_models/us/model.py +95 -53
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/utils/dates.py +1 -3
- policyengine-3.2.1/src/policyengine/utils/entity_utils.py +140 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/utils/parameter_labels.py +7 -4
- policyengine-3.2.1/src/policyengine/utils/parametric_reforms.py +129 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/utils/plotting.py +1 -3
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine.egg-info/PKG-INFO +89 -3
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine.egg-info/SOURCES.txt +37 -4
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine.egg-info/requires.txt +4 -2
- policyengine-3.2.1/tests/conftest.py +24 -0
- policyengine-3.2.1/tests/fixtures/filtering_fixtures.py +161 -0
- policyengine-3.2.1/tests/fixtures/parametric_reforms_fixtures.py +136 -0
- policyengine-3.2.1/tests/fixtures/poverty_by_demographics_fixtures.py +110 -0
- policyengine-3.2.1/tests/fixtures/region_fixtures.py +127 -0
- policyengine-3.2.1/tests/fixtures/us_reform_fixtures.py +124 -0
- policyengine-3.2.1/tests/test_congressional_district_impact.py +131 -0
- policyengine-3.2.1/tests/test_constituency_impact.py +150 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/tests/test_entity_mapping.py +11 -33
- policyengine-3.2.1/tests/test_entity_utils.py +306 -0
- policyengine-3.2.1/tests/test_filtering.py +435 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/tests/test_household_impact.py +2 -6
- policyengine-3.2.1/tests/test_intra_decile_impact.py +271 -0
- policyengine-3.2.1/tests/test_local_authority_impact.py +108 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/tests/test_models.py +68 -6
- {policyengine-3.2.0 → policyengine-3.2.1}/tests/test_pandas3_compatibility.py +1 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/tests/test_parameter_labels.py +25 -6
- policyengine-3.2.1/tests/test_parametric_reforms.py +259 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/tests/test_poverty.py +3 -11
- policyengine-3.2.1/tests/test_poverty_by_demographics.py +282 -0
- policyengine-3.2.1/tests/test_poverty_run.py +265 -0
- policyengine-3.2.1/tests/test_region.py +256 -0
- policyengine-3.2.1/tests/test_uk_regions.py +229 -0
- policyengine-3.2.1/tests/test_us_reform_application.py +148 -0
- policyengine-3.2.1/tests/test_us_regions.py +259 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/uv.lock +27 -52
- policyengine-3.2.0/changelog.yaml +0 -247
- policyengine-3.2.0/src/policyengine/__pycache__/__init__.cpython-313.pyc +0 -0
- policyengine-3.2.0/src/policyengine/outputs/poverty.py +0 -238
- policyengine-3.2.0/src/policyengine/utils/parametric_reforms.py +0 -39
- {policyengine-3.2.0 → policyengine-3.2.1}/.claude/policyengine-guide.md +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/.claude/quick-reference.md +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/.github/CONTRIBUTING.md +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/.github/changelog_template.md +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/.github/get-changelog-diff.sh +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/.github/has-functional-changes.sh +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/.github/is-version-number-acceptable.sh +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/.github/publish-git-tag.sh +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/.github/workflows/docs.yml +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/.github/workflows/pr_docs_changes.yaml +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/.gitignore +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/.python-version +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/LICENSE +0 -0
- /policyengine-3.2.0/changelog_entry.yaml → /policyengine-3.2.1/changelog.d/.gitkeep +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/docs/.gitignore +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/docs/core-concepts.md +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/docs/country-models-uk.md +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/docs/country-models-us.md +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/docs/dev.md +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/docs/index.md +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/docs/myst.yml +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/docs/visualisation.md +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/examples/income_bands_uk.py +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/setup.cfg +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/__init__.py +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/core/cache.py +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/core/dataset_version.py +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/core/dynamic.py +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/core/output.py +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/core/parameter_value.py +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/core/policy.py +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/core/tax_benefit_model.py +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/tax_benefit_models/uk/__init__.py +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/tax_benefit_models/uk/outputs.py +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/tax_benefit_models/uk.py +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/tax_benefit_models/us/__init__.py +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/tax_benefit_models/us/outputs.py +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/tax_benefit_models/us.py +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine/utils/__init__.py +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine.egg-info/dependency_links.txt +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/src/policyengine.egg-info/top_level.txt +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/tests/__init__.py +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/tests/fixtures/__init__.py +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/tests/fixtures/parameter_labels_fixtures.py +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/tests/test_aggregate.py +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/tests/test_cache.py +0 -0
- {policyengine-3.2.0 → policyengine-3.2.1}/tests/test_change_aggregate.py +0 -0
- {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
|
|
3
|
+
from importlib.metadata import version
|
|
4
4
|
|
|
5
|
-
return
|
|
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
|
-
-
|
|
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
|
-
|
|
25
|
-
build
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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"]):
|