policyengine 3.2.4__tar.gz → 3.3.0__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 (142) hide show
  1. policyengine-3.3.0/.github/workflows/pr_docs_changes.yaml +27 -0
  2. {policyengine-3.2.4 → policyengine-3.3.0}/CHANGELOG.md +7 -0
  3. {policyengine-3.2.4 → policyengine-3.3.0}/PKG-INFO +1 -1
  4. policyengine-3.3.0/docs/advanced-outputs.md +276 -0
  5. {policyengine-3.2.4 → policyengine-3.3.0}/docs/core-concepts.md +112 -6
  6. {policyengine-3.2.4 → policyengine-3.3.0}/docs/country-models-uk.md +3 -5
  7. {policyengine-3.2.4 → policyengine-3.3.0}/docs/country-models-us.md +4 -5
  8. policyengine-3.3.0/docs/dev.md +101 -0
  9. policyengine-3.3.0/docs/economic-impact-analysis.md +287 -0
  10. policyengine-3.3.0/docs/examples.md +67 -0
  11. policyengine-3.3.0/docs/index.md +19 -0
  12. {policyengine-3.2.4 → policyengine-3.3.0}/docs/myst.yml +6 -3
  13. policyengine-3.3.0/docs/regions-and-scoping.md +251 -0
  14. {policyengine-3.2.4 → policyengine-3.3.0}/docs/visualisation.md +1 -1
  15. policyengine-3.3.0/examples/us_budgetary_impact.py +155 -0
  16. {policyengine-3.2.4 → policyengine-3.3.0}/pyproject.toml +1 -1
  17. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine.egg-info/PKG-INFO +1 -1
  18. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine.egg-info/SOURCES.txt +5 -0
  19. policyengine-3.2.4/.github/workflows/pr_docs_changes.yaml +0 -36
  20. policyengine-3.2.4/docs/dev.md +0 -8
  21. policyengine-3.2.4/docs/index.md +0 -9
  22. {policyengine-3.2.4 → policyengine-3.3.0}/.claude/policyengine-guide.md +0 -0
  23. {policyengine-3.2.4 → policyengine-3.3.0}/.claude/quick-reference.md +0 -0
  24. {policyengine-3.2.4 → policyengine-3.3.0}/.github/CONTRIBUTING.md +0 -0
  25. {policyengine-3.2.4 → policyengine-3.3.0}/.github/bump_version.py +0 -0
  26. {policyengine-3.2.4 → policyengine-3.3.0}/.github/changelog_template.md +0 -0
  27. {policyengine-3.2.4 → policyengine-3.3.0}/.github/check-changelog.sh +0 -0
  28. {policyengine-3.2.4 → policyengine-3.3.0}/.github/fetch_version.py +0 -0
  29. {policyengine-3.2.4 → policyengine-3.3.0}/.github/get-changelog-diff.sh +0 -0
  30. {policyengine-3.2.4 → policyengine-3.3.0}/.github/has-functional-changes.sh +0 -0
  31. {policyengine-3.2.4 → policyengine-3.3.0}/.github/is-version-number-acceptable.sh +0 -0
  32. {policyengine-3.2.4 → policyengine-3.3.0}/.github/publish-git-tag.sh +0 -0
  33. {policyengine-3.2.4 → policyengine-3.3.0}/.github/workflows/pr_code_changes.yaml +0 -0
  34. {policyengine-3.2.4 → policyengine-3.3.0}/.github/workflows/push.yaml +0 -0
  35. {policyengine-3.2.4 → policyengine-3.3.0}/.gitignore +0 -0
  36. {policyengine-3.2.4 → policyengine-3.3.0}/.python-version +0 -0
  37. {policyengine-3.2.4 → policyengine-3.3.0}/LICENSE +0 -0
  38. {policyengine-3.2.4 → policyengine-3.3.0}/Makefile +0 -0
  39. {policyengine-3.2.4 → policyengine-3.3.0}/README.md +0 -0
  40. {policyengine-3.2.4 → policyengine-3.3.0}/changelog.d/.gitkeep +0 -0
  41. {policyengine-3.2.4 → policyengine-3.3.0}/docs/.gitignore +0 -0
  42. {policyengine-3.2.4 → policyengine-3.3.0}/examples/employment_income_variation_uk.py +0 -0
  43. {policyengine-3.2.4 → policyengine-3.3.0}/examples/employment_income_variation_us.py +0 -0
  44. {policyengine-3.2.4 → policyengine-3.3.0}/examples/household_impact_example.py +0 -0
  45. {policyengine-3.2.4 → policyengine-3.3.0}/examples/income_bands_uk.py +0 -0
  46. {policyengine-3.2.4 → policyengine-3.3.0}/examples/income_distribution_us.py +0 -0
  47. {policyengine-3.2.4 → policyengine-3.3.0}/examples/policy_change_uk.py +0 -0
  48. {policyengine-3.2.4 → policyengine-3.3.0}/examples/speedtest_us_simulation.py +0 -0
  49. {policyengine-3.2.4 → policyengine-3.3.0}/setup.cfg +0 -0
  50. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/__init__.py +0 -0
  51. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/core/__init__.py +0 -0
  52. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/core/cache.py +0 -0
  53. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/core/dataset.py +0 -0
  54. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/core/dataset_version.py +0 -0
  55. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/core/dynamic.py +0 -0
  56. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/core/output.py +0 -0
  57. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/core/parameter.py +0 -0
  58. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/core/parameter_node.py +0 -0
  59. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/core/parameter_value.py +0 -0
  60. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/core/policy.py +0 -0
  61. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/core/region.py +0 -0
  62. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/core/scoping_strategy.py +0 -0
  63. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/core/simulation.py +0 -0
  64. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/core/tax_benefit_model.py +0 -0
  65. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/core/tax_benefit_model_version.py +0 -0
  66. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/core/variable.py +0 -0
  67. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/countries/__init__.py +0 -0
  68. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/countries/uk/__init__.py +0 -0
  69. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/countries/uk/regions.py +0 -0
  70. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/countries/us/__init__.py +0 -0
  71. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/countries/us/data/__init__.py +0 -0
  72. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/countries/us/data/districts.py +0 -0
  73. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/countries/us/data/places.py +0 -0
  74. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/countries/us/data/states.py +0 -0
  75. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/countries/us/regions.py +0 -0
  76. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/outputs/__init__.py +0 -0
  77. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/outputs/aggregate.py +0 -0
  78. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/outputs/change_aggregate.py +0 -0
  79. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/outputs/congressional_district_impact.py +0 -0
  80. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/outputs/constituency_impact.py +0 -0
  81. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/outputs/decile_impact.py +0 -0
  82. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/outputs/inequality.py +0 -0
  83. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/outputs/intra_decile_impact.py +0 -0
  84. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/outputs/local_authority_impact.py +0 -0
  85. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/outputs/poverty.py +0 -0
  86. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/tax_benefit_models/uk/__init__.py +0 -0
  87. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/tax_benefit_models/uk/analysis.py +0 -0
  88. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/tax_benefit_models/uk/datasets.py +0 -0
  89. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/tax_benefit_models/uk/model.py +0 -0
  90. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/tax_benefit_models/uk/outputs.py +0 -0
  91. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/tax_benefit_models/uk.py +0 -0
  92. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/tax_benefit_models/us/__init__.py +0 -0
  93. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/tax_benefit_models/us/analysis.py +0 -0
  94. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/tax_benefit_models/us/datasets.py +0 -0
  95. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/tax_benefit_models/us/model.py +0 -0
  96. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/tax_benefit_models/us/outputs.py +0 -0
  97. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/tax_benefit_models/us.py +0 -0
  98. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/utils/__init__.py +0 -0
  99. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/utils/dates.py +0 -0
  100. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/utils/entity_utils.py +0 -0
  101. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/utils/parameter_labels.py +0 -0
  102. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/utils/parametric_reforms.py +0 -0
  103. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine/utils/plotting.py +0 -0
  104. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine.egg-info/dependency_links.txt +0 -0
  105. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine.egg-info/requires.txt +0 -0
  106. {policyengine-3.2.4 → policyengine-3.3.0}/src/policyengine.egg-info/top_level.txt +0 -0
  107. {policyengine-3.2.4 → policyengine-3.3.0}/tests/__init__.py +0 -0
  108. {policyengine-3.2.4 → policyengine-3.3.0}/tests/conftest.py +0 -0
  109. {policyengine-3.2.4 → policyengine-3.3.0}/tests/fixtures/__init__.py +0 -0
  110. {policyengine-3.2.4 → policyengine-3.3.0}/tests/fixtures/filtering_fixtures.py +0 -0
  111. {policyengine-3.2.4 → policyengine-3.3.0}/tests/fixtures/parameter_labels_fixtures.py +0 -0
  112. {policyengine-3.2.4 → policyengine-3.3.0}/tests/fixtures/parametric_reforms_fixtures.py +0 -0
  113. {policyengine-3.2.4 → policyengine-3.3.0}/tests/fixtures/poverty_by_demographics_fixtures.py +0 -0
  114. {policyengine-3.2.4 → policyengine-3.3.0}/tests/fixtures/region_fixtures.py +0 -0
  115. {policyengine-3.2.4 → policyengine-3.3.0}/tests/fixtures/us_reform_fixtures.py +0 -0
  116. {policyengine-3.2.4 → policyengine-3.3.0}/tests/fixtures/variable_label_fixtures.py +0 -0
  117. {policyengine-3.2.4 → policyengine-3.3.0}/tests/test_aggregate.py +0 -0
  118. {policyengine-3.2.4 → policyengine-3.3.0}/tests/test_cache.py +0 -0
  119. {policyengine-3.2.4 → policyengine-3.3.0}/tests/test_change_aggregate.py +0 -0
  120. {policyengine-3.2.4 → policyengine-3.3.0}/tests/test_congressional_district_impact.py +0 -0
  121. {policyengine-3.2.4 → policyengine-3.3.0}/tests/test_constituency_impact.py +0 -0
  122. {policyengine-3.2.4 → policyengine-3.3.0}/tests/test_entity_mapping.py +0 -0
  123. {policyengine-3.2.4 → policyengine-3.3.0}/tests/test_entity_utils.py +0 -0
  124. {policyengine-3.2.4 → policyengine-3.3.0}/tests/test_filtering.py +0 -0
  125. {policyengine-3.2.4 → policyengine-3.3.0}/tests/test_household_impact.py +0 -0
  126. {policyengine-3.2.4 → policyengine-3.3.0}/tests/test_inequality.py +0 -0
  127. {policyengine-3.2.4 → policyengine-3.3.0}/tests/test_intra_decile_impact.py +0 -0
  128. {policyengine-3.2.4 → policyengine-3.3.0}/tests/test_local_authority_impact.py +0 -0
  129. {policyengine-3.2.4 → policyengine-3.3.0}/tests/test_models.py +0 -0
  130. {policyengine-3.2.4 → policyengine-3.3.0}/tests/test_pandas3_compatibility.py +0 -0
  131. {policyengine-3.2.4 → policyengine-3.3.0}/tests/test_parameter_labels.py +0 -0
  132. {policyengine-3.2.4 → policyengine-3.3.0}/tests/test_parametric_reforms.py +0 -0
  133. {policyengine-3.2.4 → policyengine-3.3.0}/tests/test_poverty.py +0 -0
  134. {policyengine-3.2.4 → policyengine-3.3.0}/tests/test_poverty_by_demographics.py +0 -0
  135. {policyengine-3.2.4 → policyengine-3.3.0}/tests/test_poverty_run.py +0 -0
  136. {policyengine-3.2.4 → policyengine-3.3.0}/tests/test_region.py +0 -0
  137. {policyengine-3.2.4 → policyengine-3.3.0}/tests/test_scoping_strategy.py +0 -0
  138. {policyengine-3.2.4 → policyengine-3.3.0}/tests/test_uk_regions.py +0 -0
  139. {policyengine-3.2.4 → policyengine-3.3.0}/tests/test_us_reform_application.py +0 -0
  140. {policyengine-3.2.4 → policyengine-3.3.0}/tests/test_us_regions.py +0 -0
  141. {policyengine-3.2.4 → policyengine-3.3.0}/tests/test_variable_labels.py +0 -0
  142. {policyengine-3.2.4 → policyengine-3.3.0}/uv.lock +0 -0
@@ -0,0 +1,27 @@
1
+ # Workflow that runs on code changes to a pull request.
2
+
3
+ name: Docs changes
4
+ on:
5
+ pull_request:
6
+ branches:
7
+ - main
8
+
9
+ paths:
10
+ - docs/**
11
+ - .github/**
12
+ workflow_dispatch:
13
+
14
+ jobs:
15
+ Test:
16
+ runs-on: ubuntu-latest
17
+ name: Test documentation builds
18
+ steps:
19
+ - name: Checkout repo
20
+ uses: actions/checkout@v4
21
+ - uses: actions/setup-node@v4
22
+ with:
23
+ node-version: 18.x
24
+ - name: Install MyST
25
+ run: npm install -g mystmd
26
+ - name: Test documentation builds
27
+ run: cd docs && myst build --html
@@ -1,3 +1,10 @@
1
+ ## [3.3.0] - 2026-03-20
2
+
3
+ ### Added
4
+
5
+ - Added documentation for economic impact analysis, advanced outputs (DecileImpact, Poverty, Inequality, IntraDecileImpact), regions and scoping strategies, simulation lifecycle (ensure vs run), Dynamic class, data loading, and simulation modifiers. Added US budgetary impact example script. Fixed PR docs CI to use MyST matching production.
6
+
7
+
1
8
  ## [3.2.4] - 2026-03-17
2
9
 
3
10
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: policyengine
3
- Version: 3.2.4
3
+ Version: 3.3.0
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
@@ -0,0 +1,276 @@
1
+ # Advanced outputs
2
+
3
+ Beyond `Aggregate` and `ChangeAggregate` (covered in [Core concepts](core-concepts.md)), the package provides specialised output types for distributional analysis, poverty measurement, and inequality metrics.
4
+
5
+ All output types follow the same pattern: create an instance, call `.run()`, read the result fields. Convenience functions are provided for common use cases.
6
+
7
+ ## OutputCollection
8
+
9
+ Many convenience functions return an `OutputCollection[T]`, a container holding both the individual output objects and a pandas DataFrame:
10
+
11
+ ```python
12
+ from policyengine.core import OutputCollection
13
+
14
+ # Returned by calculate_decile_impacts(), calculate_us_poverty_rates(), etc.
15
+ collection = calculate_us_poverty_rates(simulation)
16
+
17
+ # Access individual objects
18
+ for poverty in collection.outputs:
19
+ print(f"{poverty.poverty_type}: {poverty.rate:.4f}")
20
+
21
+ # Access as DataFrame
22
+ print(collection.dataframe)
23
+ ```
24
+
25
+ ## DecileImpact
26
+
27
+ Calculates the impact of a policy reform on a single income decile: baseline and reform mean income, absolute and relative change, and counts of people better off, worse off, and unchanged.
28
+
29
+ ### Using the convenience function
30
+
31
+ ```python
32
+ from policyengine.outputs.decile_impact import calculate_decile_impacts
33
+
34
+ decile_impacts = calculate_decile_impacts(
35
+ dataset=dataset,
36
+ tax_benefit_model_version=us_latest,
37
+ baseline_policy=None, # Current law
38
+ reform_policy=reform,
39
+ income_variable="household_net_income", # Default for US
40
+ )
41
+
42
+ for d in decile_impacts.outputs:
43
+ print(f"Decile {d.decile}: "
44
+ f"baseline={d.baseline_mean:,.0f}, "
45
+ f"reform={d.reform_mean:,.0f}, "
46
+ f"change={d.absolute_change:+,.0f} "
47
+ f"({d.relative_change:+.2f}%)")
48
+ ```
49
+
50
+ ### Using directly
51
+
52
+ ```python
53
+ from policyengine.outputs.decile_impact import DecileImpact
54
+
55
+ impact = DecileImpact(
56
+ baseline_simulation=baseline_sim,
57
+ reform_simulation=reform_sim,
58
+ income_variable="household_net_income",
59
+ decile=5, # 5th decile
60
+ )
61
+ impact.run()
62
+
63
+ print(f"Count better off: {impact.count_better_off:,.0f}")
64
+ print(f"Count worse off: {impact.count_worse_off:,.0f}")
65
+ ```
66
+
67
+ ### Parameters
68
+
69
+ | Parameter | Default | Description |
70
+ |---|---|---|
71
+ | `income_variable` | `equiv_hbai_household_net_income` | Income variable to group by and measure changes |
72
+ | `decile_variable` | `None` | Use a pre-computed grouping variable instead of `qcut` |
73
+ | `entity` | Auto-detected | Entity level for the income variable |
74
+ | `quantiles` | `10` | Number of quantile groups (10 = deciles, 5 = quintiles) |
75
+
76
+ For US simulations, use `income_variable="household_net_income"`. The UK default (`equiv_hbai_household_net_income`) is the equivalised HBAI measure.
77
+
78
+ ## IntraDecileImpact
79
+
80
+ Classifies people within each decile into five income change categories:
81
+
82
+ | Category | Threshold |
83
+ |---|---|
84
+ | Lose more than 5% | change <= -5% |
85
+ | Lose less than 5% | -5% < change <= -0.1% |
86
+ | No change | -0.1% < change <= 0.1% |
87
+ | Gain less than 5% | 0.1% < change <= 5% |
88
+ | Gain more than 5% | change > 5% |
89
+
90
+ Proportions are people-weighted (using `household_count_people * household_weight`).
91
+
92
+ ### Using the convenience function
93
+
94
+ ```python
95
+ from policyengine.outputs.intra_decile_impact import compute_intra_decile_impacts
96
+
97
+ intra = compute_intra_decile_impacts(
98
+ baseline_simulation=baseline_sim,
99
+ reform_simulation=reform_sim,
100
+ income_variable="household_net_income",
101
+ )
102
+
103
+ for row in intra.outputs:
104
+ if row.decile == 0:
105
+ label = "Overall"
106
+ else:
107
+ label = f"Decile {row.decile}"
108
+ print(f"{label}: "
109
+ f"lose>5%={row.lose_more_than_5pct:.2%}, "
110
+ f"lose<5%={row.lose_less_than_5pct:.2%}, "
111
+ f"no change={row.no_change:.2%}, "
112
+ f"gain<5%={row.gain_less_than_5pct:.2%}, "
113
+ f"gain>5%={row.gain_more_than_5pct:.2%}")
114
+ ```
115
+
116
+ The function returns deciles 1-10 plus an overall average at `decile=0`.
117
+
118
+ ## Poverty
119
+
120
+ Calculates poverty headcount and rates for a single simulation, with optional demographic filtering.
121
+
122
+ ### Poverty types
123
+
124
+ **UK** (4 measures):
125
+ - Absolute before housing costs (BHC)
126
+ - Absolute after housing costs (AHC)
127
+ - Relative before housing costs (BHC)
128
+ - Relative after housing costs (AHC)
129
+
130
+ **US** (2 measures):
131
+ - SPM poverty
132
+ - Deep SPM poverty (below 50% of SPM threshold)
133
+
134
+ ### Calculating all poverty rates
135
+
136
+ ```python
137
+ from policyengine.outputs.poverty import (
138
+ calculate_uk_poverty_rates,
139
+ calculate_us_poverty_rates,
140
+ )
141
+
142
+ # US
143
+ us_poverty = calculate_us_poverty_rates(simulation)
144
+ for p in us_poverty.outputs:
145
+ print(f"{p.poverty_type}: headcount={p.headcount:,.0f}, rate={p.rate:.4f}")
146
+
147
+ # UK
148
+ uk_poverty = calculate_uk_poverty_rates(simulation)
149
+ for p in uk_poverty.outputs:
150
+ print(f"{p.poverty_type}: headcount={p.headcount:,.0f}, rate={p.rate:.4f}")
151
+ ```
152
+
153
+ ### Poverty by demographic group
154
+
155
+ ```python
156
+ from policyengine.outputs.poverty import (
157
+ calculate_us_poverty_by_age,
158
+ calculate_us_poverty_by_gender,
159
+ calculate_us_poverty_by_race,
160
+ calculate_uk_poverty_by_age,
161
+ calculate_uk_poverty_by_gender,
162
+ )
163
+
164
+ # By age group (child <18, adult 18-64, senior 65+)
165
+ by_age = calculate_us_poverty_by_age(simulation)
166
+ for p in by_age.outputs:
167
+ print(f"{p.filter_group} {p.poverty_type}: {p.rate:.4f}")
168
+
169
+ # By gender
170
+ by_gender = calculate_us_poverty_by_gender(simulation)
171
+
172
+ # By race (US only: WHITE, BLACK, HISPANIC, OTHER)
173
+ by_race = calculate_us_poverty_by_race(simulation)
174
+ ```
175
+
176
+ ### Custom filters
177
+
178
+ ```python
179
+ from policyengine.outputs.poverty import Poverty
180
+
181
+ # Child poverty only
182
+ child_poverty = Poverty(
183
+ simulation=simulation,
184
+ poverty_variable="spm_unit_is_in_spm_poverty",
185
+ entity="person",
186
+ filter_variable="age",
187
+ filter_variable_leq=17,
188
+ )
189
+ child_poverty.run()
190
+ print(f"Child SPM poverty rate: {child_poverty.rate:.4f}")
191
+ ```
192
+
193
+ ### Result fields
194
+
195
+ | Field | Description |
196
+ |---|---|
197
+ | `headcount` | Weighted count of people in poverty |
198
+ | `total_population` | Weighted total population (after filters) |
199
+ | `rate` | `headcount / total_population` |
200
+ | `filter_group` | Group label set by demographic convenience functions |
201
+
202
+ ## Inequality
203
+
204
+ Calculates weighted inequality metrics for a single simulation: Gini coefficient and income share measures.
205
+
206
+ ### Using convenience functions
207
+
208
+ ```python
209
+ from policyengine.outputs.inequality import (
210
+ calculate_uk_inequality,
211
+ calculate_us_inequality,
212
+ )
213
+
214
+ # US (uses household_net_income by default)
215
+ ineq = calculate_us_inequality(simulation)
216
+ print(f"Gini: {ineq.gini:.4f}")
217
+ print(f"Top 10% share: {ineq.top_10_share:.4f}")
218
+ print(f"Top 1% share: {ineq.top_1_share:.4f}")
219
+ print(f"Bottom 50% share: {ineq.bottom_50_share:.4f}")
220
+
221
+ # UK (uses equiv_hbai_household_net_income by default)
222
+ ineq = calculate_uk_inequality(simulation)
223
+ ```
224
+
225
+ ### With demographic filters
226
+
227
+ ```python
228
+ # Inequality among working-age adults only
229
+ ineq = calculate_us_inequality(
230
+ simulation,
231
+ filter_variable="age",
232
+ filter_variable_geq=18,
233
+ filter_variable_leq=64,
234
+ )
235
+ ```
236
+
237
+ ### Using directly
238
+
239
+ ```python
240
+ from policyengine.outputs.inequality import Inequality
241
+
242
+ ineq = Inequality(
243
+ simulation=simulation,
244
+ income_variable="household_net_income",
245
+ entity="household",
246
+ )
247
+ ineq.run()
248
+ ```
249
+
250
+ ### Result fields
251
+
252
+ | Field | Description |
253
+ |---|---|
254
+ | `gini` | Weighted Gini coefficient (0 = perfect equality, 1 = perfect inequality) |
255
+ | `top_10_share` | Share of total income held by top 10% |
256
+ | `top_1_share` | Share of total income held by top 1% |
257
+ | `bottom_50_share` | Share of total income held by bottom 50% |
258
+
259
+ ## Comparing baseline and reform
260
+
261
+ Poverty and inequality are single-simulation outputs. To compare baseline and reform, compute both and take the difference:
262
+
263
+ ```python
264
+ baseline_poverty = calculate_us_poverty_rates(baseline_sim)
265
+ reform_poverty = calculate_us_poverty_rates(reform_sim)
266
+
267
+ for bp, rp in zip(baseline_poverty.outputs, reform_poverty.outputs):
268
+ change = rp.rate - bp.rate
269
+ print(f"{bp.poverty_type}: {bp.rate:.4f} -> {rp.rate:.4f} ({change:+.4f})")
270
+
271
+ baseline_ineq = calculate_us_inequality(baseline_sim)
272
+ reform_ineq = calculate_us_inequality(reform_sim)
273
+ print(f"Gini change: {reform_ineq.gini - baseline_ineq.gini:+.4f}")
274
+ ```
275
+
276
+ The `economic_impact_analysis()` function does this automatically and returns both baseline and reform poverty/inequality in the `PolicyReformAnalysis` result. See [Economic impact analysis](economic-impact-analysis.md).
@@ -117,6 +117,40 @@ dataset = PolicyEngineUKDataset(
117
117
  )
118
118
  ```
119
119
 
120
+ ## Data loading
121
+
122
+ Before running simulations, you need representative microdata. The package provides three functions for managing datasets:
123
+
124
+ - **`ensure_datasets()`**: Load from disk if available, otherwise download and compute (recommended)
125
+ - **`create_datasets()`**: Always download from HuggingFace and compute from scratch
126
+ - **`load_datasets()`**: Load previously saved HDF5 files from disk
127
+
128
+ ```python
129
+ from policyengine.tax_benefit_models.us import ensure_datasets
130
+
131
+ # First run: downloads from HuggingFace, computes variables, saves to ./data/
132
+ # Subsequent runs: loads from disk instantly
133
+ datasets = ensure_datasets(
134
+ datasets=["hf://policyengine/policyengine-us-data/enhanced_cps_2024.h5"],
135
+ years=[2026],
136
+ data_folder="./data",
137
+ )
138
+ dataset = datasets["enhanced_cps_2024_2026"]
139
+ ```
140
+
141
+ ```python
142
+ from policyengine.tax_benefit_models.uk import ensure_datasets
143
+
144
+ datasets = ensure_datasets(
145
+ datasets=["hf://policyengine/policyengine-uk-data/enhanced_frs_2023_24.h5"],
146
+ years=[2026],
147
+ data_folder="./data",
148
+ )
149
+ dataset = datasets["enhanced_frs_2023_24_2026"]
150
+ ```
151
+
152
+ All datasets are stored as HDF5 files on disk. No database server is required.
153
+
120
154
  ## Simulations
121
155
 
122
156
  Simulations apply tax-benefit models to datasets, calculating all variables for the specified year.
@@ -141,6 +175,25 @@ output_household = simulation.output_dataset.data.household
141
175
  print(output_household[["household_id", "household_net_income", "household_tax"]])
142
176
  ```
143
177
 
178
+ ### Simulation lifecycle: `run()` vs `ensure()`
179
+
180
+ The `Simulation` class provides two methods for computing results:
181
+
182
+ | Method | Behaviour |
183
+ |---|---|
184
+ | `simulation.run()` | Always recomputes from scratch. No caching. |
185
+ | `simulation.ensure()` | Checks in-memory LRU cache, then tries loading from disk, then falls back to `run()` + `save()`. |
186
+
187
+ ```python
188
+ # One-off computation (no caching)
189
+ simulation.run()
190
+
191
+ # Cache-or-compute (preferred for production use)
192
+ simulation.ensure()
193
+ ```
194
+
195
+ `ensure()` uses a module-level LRU cache (max 100 simulations) and saves output datasets as HDF5 files alongside the input dataset. On repeated calls, it returns cached results instantly. For baseline-vs-reform comparisons, `economic_impact_analysis()` calls `ensure()` internally, so you rarely need to call it yourself.
196
+
144
197
  ### Accessing calculated variables
145
198
 
146
199
  After running a simulation, you can access the calculated variables from the output dataset:
@@ -211,6 +264,56 @@ reform = Simulation(
211
264
  reform.run()
212
265
  ```
213
266
 
267
+ ### Combining policies
268
+
269
+ Policies can be combined using the `+` operator:
270
+
271
+ ```python
272
+ combined = policy_a + policy_b
273
+ # Concatenates parameter_values and chains simulation_modifiers
274
+ ```
275
+
276
+ ### Simulation modifiers
277
+
278
+ For reforms that cannot be expressed as parameter value changes, `Policy` accepts a `simulation_modifier` callable that directly manipulates the underlying `policyengine_core` simulation:
279
+
280
+ ```python
281
+ def my_modifier(sim):
282
+ """Custom reform logic applied to the core simulation object."""
283
+ p = sim.tax_benefit_system.parameters
284
+ # Modify parameters programmatically
285
+ return sim
286
+
287
+ policy = Policy(
288
+ name="Custom reform",
289
+ simulation_modifier=my_modifier,
290
+ )
291
+ ```
292
+
293
+ Note: the UK model supports `simulation_modifier`. The US model currently only uses the `parameter_values` path.
294
+
295
+ ## Dynamic behavioural responses
296
+
297
+ The `Dynamic` class is structurally identical to `Policy` and represents behavioural responses to policy changes (e.g., labour supply elasticities). It is applied after the policy in the simulation pipeline.
298
+
299
+ ```python
300
+ from policyengine.core.dynamic import Dynamic
301
+
302
+ dynamic = Dynamic(
303
+ name="Labour supply response",
304
+ parameter_values=[...], # Same format as Policy
305
+ )
306
+
307
+ simulation = Simulation(
308
+ dataset=dataset,
309
+ tax_benefit_model_version=uk_latest,
310
+ policy=policy,
311
+ dynamic=dynamic,
312
+ )
313
+ ```
314
+
315
+ Dynamic responses can also be combined using the `+` operator and support `simulation_modifier` callables.
316
+
214
317
  ## Outputs
215
318
 
216
319
  Output classes provide structured analysis of simulation results.
@@ -480,7 +583,7 @@ COLORS = {
480
583
 
481
584
  ### 1. Analyse employment income variation
482
585
 
483
- See `examples/employment_income_variation_uk.py` for a complete example of:
586
+ See [UK employment income variation](examples.md#uk-employment-income-variation) for a complete example of:
484
587
  - Creating custom datasets with varied parameters
485
588
  - Running single simulations
486
589
  - Extracting results with filters
@@ -488,7 +591,7 @@ See `examples/employment_income_variation_uk.py` for a complete example of:
488
591
 
489
592
  ### 2. Policy reform analysis
490
593
 
491
- See `examples/policy_change_uk.py` for:
594
+ See [UK policy reform analysis](examples.md#uk-policy-reform-analysis) for:
492
595
  - Applying parametric reforms
493
596
  - Comparing baseline and reform
494
597
  - Analysing winners/losers by decile
@@ -496,7 +599,7 @@ See `examples/policy_change_uk.py` for:
496
599
 
497
600
  ### 3. Distributional analysis
498
601
 
499
- See `examples/income_distribution_us.py` for:
602
+ See [US income distribution](examples.md#us-income-distribution) for:
500
603
  - Loading representative microdata
501
604
  - Calculating statistics by income decile
502
605
  - Mapping variables across entity levels
@@ -549,8 +652,11 @@ See `examples/income_distribution_us.py` for:
549
652
 
550
653
  ## Next steps
551
654
 
552
- - See `examples/` for complete working examples
553
- - Review country-specific documentation:
655
+ - [Economic impact analysis](economic-impact-analysis.md): Full baseline-vs-reform comparison workflow
656
+ - [Advanced outputs](advanced-outputs.md): DecileImpact, Poverty, Inequality, IntraDecileImpact
657
+ - [Regions and scoping](regions-and-scoping.md): Sub-national analysis (states, constituencies, districts)
658
+ - Country-specific documentation:
554
659
  - [UK tax-benefit model](country-models-uk.md)
555
660
  - [US tax-benefit model](country-models-us.md)
556
- - Explore the API reference for detailed class documentation
661
+ - [Visualisation](visualisation.md): Publication-ready charts
662
+ - [Examples](examples.md): Complete working scripts
@@ -363,11 +363,9 @@ When creating custom datasets, validate:
363
363
 
364
364
  ## Examples
365
365
 
366
- See working examples in the `examples/` directory:
367
-
368
- - `employment_income_variation_uk.py`: Vary employment income, analyse benefit phase-outs
369
- - `policy_change_uk.py`: Apply reforms, analyse winners/losers
370
- - `income_bands_uk.py`: Create income band scenarios
366
+ - [UK employment income variation](examples.md#uk-employment-income-variation): Vary employment income, analyse benefit phase-outs
367
+ - [UK policy reform analysis](examples.md#uk-policy-reform-analysis): Apply reforms, analyse winners/losers
368
+ - [UK income bands](examples.md#uk-income-bands): Calculate net income and tax by income decile
371
369
 
372
370
  ## References
373
371
 
@@ -431,11 +431,10 @@ When creating custom datasets, validate:
431
431
 
432
432
  ## Examples
433
433
 
434
- See working examples in the `examples/` directory:
435
-
436
- - `income_distribution_us.py`: Analyse benefit distribution by income decile
437
- - `employment_income_variation_us.py`: Vary employment income, analyse phase-outs
438
- - `speedtest_us_simulation.py`: Performance benchmarking
434
+ - [US income distribution](examples.md#us-income-distribution): Analyse benefit distribution by income decile
435
+ - [US employment income variation](examples.md#us-employment-income-variation): Vary employment income, analyse phase-outs
436
+ - [US budgetary impact](examples.md#us-budgetary-impact): Full baseline-vs-reform comparison
437
+ - [Simulation performance](examples.md#simulation-performance): Performance benchmarking
439
438
 
440
439
  ## References
441
440
 
@@ -0,0 +1,101 @@
1
+ # Development
2
+
3
+ ## Principles
4
+
5
+ 1. **STRONG** preference for simplicity. Let's make this package as simple as it possibly can be.
6
+ 2. Remember the goal of this package: to make it easy to create, run, save and analyse PolicyEngine simulations. When considering further features, always ask: can we instead *make it super easy* for people to do this outside the package?
7
+ 3. Be consistent about property names. `name` = human readable few words you could put as the noun in a sentence without fail. `id` = unique identifier, ideally a UUID. `description` = longer human readable text that describes the object. `created_at` and `updated_at` = timestamps for when the object was created and last updated.
8
+ 4. Constraints can be good. We should set constraints where they help us simplify the codebase and usage, but not where they unnecessarily block useful functionality.
9
+
10
+ ## Setup
11
+
12
+ ```bash
13
+ git clone https://github.com/PolicyEngine/policyengine.py.git
14
+ cd policyengine.py
15
+ uv pip install -e .[dev]
16
+ ```
17
+
18
+ This installs both UK and US country models plus dev dependencies (pytest, ruff, mypy, towncrier).
19
+
20
+ ## Common commands
21
+
22
+ ```bash
23
+ make format # ruff format
24
+ make test # pytest with coverage
25
+ make docs # build documentation site
26
+ make clean # remove caches, build artifacts, .h5 files
27
+ ```
28
+
29
+ ## Testing
30
+
31
+ Tests require a `HUGGING_FACE_TOKEN` environment variable for downloading datasets:
32
+
33
+ ```bash
34
+ export HUGGING_FACE_TOKEN=hf_...
35
+ make test
36
+ ```
37
+
38
+ To run a specific test:
39
+
40
+ ```bash
41
+ pytest tests/test_models.py -v
42
+ pytest tests/test_parametric_reforms.py -k "test_uk" -v
43
+ ```
44
+
45
+ ## Linting and formatting
46
+
47
+ ```bash
48
+ ruff format . # format code
49
+ ruff check . # lint
50
+ mypy src/policyengine # type check (informational)
51
+ ```
52
+
53
+ ## CI pipeline
54
+
55
+ PRs trigger the following checks:
56
+
57
+ | Check | Status | Command |
58
+ |---|---|---|
59
+ | Lint + format | Required | `ruff check .` + `ruff format --check .` |
60
+ | Tests (Python 3.13) | Required | `make test` |
61
+ | Tests (Python 3.14) | Required | `make test` |
62
+ | Mypy | Informational | `mypy src/policyengine` |
63
+ | Docs build | Required | MyST build |
64
+
65
+ ## Versioning and releases
66
+
67
+ This project uses [towncrier](https://towncrier.readthedocs.io/) for changelog management. When making a PR, add a changelog fragment:
68
+
69
+ ```bash
70
+ # Fragment types: breaking, added, changed, fixed, removed
71
+ echo "Description of change" > changelog.d/my-change.added
72
+ ```
73
+
74
+ On merge, the versioning workflow bumps the version, builds the changelog, and creates a GitHub Release.
75
+
76
+ ## Architecture
77
+
78
+ ### Package layout
79
+
80
+ ```
81
+ src/policyengine/
82
+ ├── core/ # Domain models (Simulation, Dataset, Policy, etc.)
83
+ ├── tax_benefit_models/
84
+ │ ├── uk/ # UK model, datasets, analysis, outputs
85
+ │ └── us/ # US model, datasets, analysis, outputs
86
+ ├── outputs/ # Output templates (Aggregate, Poverty, etc.)
87
+ ├── countries/ # Geographic region registries
88
+ └── utils/ # Helpers (reforms, entity mapping, plotting)
89
+ ```
90
+
91
+ ### Key design decisions
92
+
93
+ **Pydantic everywhere**: All domain objects are Pydantic `BaseModel` subclasses. This gives us validation, serialisation, and clear field documentation.
94
+
95
+ **HDF5 for storage**: Datasets and simulation outputs are stored as HDF5 files. No database server is required. The `MicroDataFrame` from the `microdf` package wraps pandas DataFrames with weight-aware `.sum()`, `.mean()`, `.count()`.
96
+
97
+ **Country-specific model classes**: `PolicyEngineUSLatest` and `PolicyEngineUKLatest` each implement `run()`, `save()`, and `load()`. The US model passes reforms as a dict at `Microsimulation(reform=...)` construction time. The UK model supports both parametric reforms and `simulation_modifier` callables applied post-construction.
98
+
99
+ **LRU cache + file caching**: `Simulation.ensure()` checks an in-process LRU cache (max 100 entries), then tries loading from disk, then falls back to `run()` + `save()`.
100
+
101
+ **Output pattern**: All output types inherit from `Output`, implement `.run()`, and populate result fields. Convenience functions (e.g., `calculate_us_poverty_rates()`) create, run, and return collections of output objects.