From 13c830112f2d402200527be803149071bd6dccd1 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 29 Sep 2026 12:20:27 -0400 Subject: [PATCH 1/5] Require every reported number to come from a model run Adds a top-level rule to the policyengine skill: fiscal, distributional, poverty and household figures come from running the microsimulation for each scenario, never from scaling, interpolating or extrapolating other runs' outputs or from a side model. When there are too many scenarios, run fewer and say how many runs stand behind each figure. The policyengine-uk skill gains a verified "Macro scenarios" section: a CPI/earnings path must be a Scenario applied before the data load. The same changes passed as a reform= dict are a silent no-op (verified: no income, benefit rate, State Pension or income tax moved). policyengine-tools says data files hold model outputs only, and the review-program rubric makes a scaled or extrapolated figure critical. Releases the plugin as 4.0.3. Co-Authored-By: Claude Opus 5.5 --- bundles/analysis-tools.json | 2 +- bundles/api-development.json | 2 +- bundles/app-development.json | 2 +- bundles/complete.json | 2 +- bundles/country-models.json | 2 +- bundles/dashboard-builder.json | 2 +- bundles/data-science.json | 2 +- bundles/essential.json | 2 +- skills/policyengine-tools/SKILL.md | 5 +- skills/policyengine-uk/SKILL.md | 53 ++++++++++++++++++++ skills/policyengine/SKILL.md | 26 +++++++++- skills/review-program/references/workflow.md | 6 ++- targets/claude/marketplace.template.json | 2 +- 13 files changed, 96 insertions(+), 12 deletions(-) diff --git a/bundles/analysis-tools.json b/bundles/analysis-tools.json index 8cb9b3b..6ce0e13 100644 --- a/bundles/analysis-tools.json +++ b/bundles/analysis-tools.json @@ -2,7 +2,7 @@ "name": "analysis-tools", "description": "Policy analysis and research - impact studies, dashboards, notebooks, and visualizations", "category": "analysis", - "version": "4.0.2", + "version": "4.0.3", "keywords": [ "analysis", "research", diff --git a/bundles/api-development.json b/bundles/api-development.json index 0320eb7..666d510 100644 --- a/bundles/api-development.json +++ b/bundles/api-development.json @@ -2,7 +2,7 @@ "name": "api-development", "description": "API development - Flask endpoints, caching, services, and integration patterns", "category": "development", - "version": "4.0.2", + "version": "4.0.3", "keywords": [ "api", "flask", diff --git a/bundles/app-development.json b/bundles/app-development.json index 269406e..160d545 100644 --- a/bundles/app-development.json +++ b/bundles/app-development.json @@ -2,7 +2,7 @@ "name": "app-development", "description": "React app development - app-v2, standalone tools, Vercel deployment, and PolicyEngine-specific patterns", "category": "development", - "version": "4.0.2", + "version": "4.0.3", "keywords": [ "react", "frontend", diff --git a/bundles/complete.json b/bundles/complete.json index ed4b8b1..4a55aa4 100644 --- a/bundles/complete.json +++ b/bundles/complete.json @@ -2,7 +2,7 @@ "name": "complete", "description": "Complete PolicyEngine knowledge base - all agents, commands, and skills for users, analysts, and contributors", "category": "complete", - "version": "4.0.2", + "version": "4.0.3", "keywords": [ "complete", "all", diff --git a/bundles/country-models.json b/bundles/country-models.json index 9e4912a..0e1f95e 100644 --- a/bundles/country-models.json +++ b/bundles/country-models.json @@ -2,7 +2,7 @@ "name": "country-models", "description": "Complete multi-agent workflow for implementing government benefit programs in PolicyEngine country packages", "category": "development", - "version": "4.0.2", + "version": "4.0.3", "keywords": [ "tax", "benefits", diff --git a/bundles/dashboard-builder.json b/bundles/dashboard-builder.json index 00d418c..51da779 100644 --- a/bundles/dashboard-builder.json +++ b/bundles/dashboard-builder.json @@ -2,7 +2,7 @@ "name": "dashboard-builder", "description": "Orchestrated AI workflow for creating PolicyEngine dashboards from natural-language descriptions", "category": "development", - "version": "4.0.2", + "version": "4.0.3", "keywords": [ "dashboard", "calculator", diff --git a/bundles/data-science.json b/bundles/data-science.json index 6d6f99d..ab99d69 100644 --- a/bundles/data-science.json +++ b/bundles/data-science.json @@ -2,7 +2,7 @@ "name": "data-science", "description": "Data analysis, survey enhancement, imputation, calibration, and microdata utilities", "category": "data", - "version": "4.0.2", + "version": "4.0.3", "keywords": [ "data", "microdata", diff --git a/bundles/essential.json b/bundles/essential.json index 12f75ab..a031766 100644 --- a/bundles/essential.json +++ b/bundles/essential.json @@ -2,7 +2,7 @@ "name": "essential", "description": "Essential PolicyEngine knowledge for all users - understanding the platform, using the web app, and basic concepts", "category": "getting-started", - "version": "4.0.2", + "version": "4.0.3", "keywords": [ "beginner", "users", diff --git a/skills/policyengine-tools/SKILL.md b/skills/policyengine-tools/SKILL.md index e9802ef..13fcefd 100644 --- a/skills/policyengine-tools/SKILL.md +++ b/skills/policyengine-tools/SKILL.md @@ -127,7 +127,10 @@ multizone. Register in `policyengine-app-v2/website/src/data/apps.json` (`type`, ## Data and backend patterns Never paste ad-hoc computed numbers into source. Always: Python script → data file -(JSON/CSV) → frontend imports it. Pick by need: +(JSON/CSV) → frontend imports it. The data file holds model outputs only, one full +microsimulation per scenario shown; the pipeline and the frontend never scale, interpolate +or extrapolate results to cover scenarios that were not run (see "Every reported number is a +model run" in the `policyengine` skill). Pick by need: - **A — Precomputed JSON/CSV**: finite scenarios (legislative trackers, static analyses). A `scripts/*.py` microsimulation writes to `public/data/`; the frontend imports it. Zero diff --git a/skills/policyengine-uk/SKILL.md b/skills/policyengine-uk/SKILL.md index c706fbb..30d09e4 100644 --- a/skills/policyengine-uk/SKILL.md +++ b/skills/policyengine-uk/SKILL.md @@ -212,6 +212,59 @@ bundle. The pre-Microcosm UK datasets are superseded. See the `policyengine-data Microcosm UK is built (FRS + WAS imputation) and calibrated, and the `policyengine` skill for the `ensure_datasets` / `Simulation` population flow, which is identical across countries. +## Macro scenarios (a different CPI or earnings path) + +A macro path goes in through the growth series in `gov.economic_assumptions.yoy_growth.obr` +(`consumer_price_index`, `average_earnings`; move `rpi` and `cpih` with CPI if prices move), +passed as a `Scenario` applied **before the data load**: + + +```python +from policyengine.tax_benefit_models.uk import managed_microsimulation +from policyengine_uk.utils.scenario import Scenario + +OBR = "gov.economic_assumptions.yoy_growth.obr" +base = managed_microsimulation() +p = base.tax_benefit_system.parameters +changes = { + f"{OBR}.{series}": { + f"year:{y}-01-01:1": float(p.get_child(f"{OBR}.{series}")(f"{y}-01-01")) + 0.01 + for y in range(2027, 2034) + } + for series in ("consumer_price_index", "average_earnings") +} +macro = managed_microsimulation( + scenario=Scenario(parameter_changes=changes, applied_before_data_load=True) +) +for year in (2029, 2034): # inside and beyond the dataset's own years + assert macro.calculate("employment_income", year).sum() > base.calculate("employment_income", year).sum() +``` + +Verified in policyengine 5.3.0 / policyengine-uk 2.90.2 (2026-09), by reading the code and +running it with +1pp CPI and earnings in 2027-2033: + +- Before the data load, `Simulation.apply_parameter_changes` reloads the parameters, applies + the edits and reruns `process_parameters()`, which rebuilds the triple lock, the uprating + indices and every index-uprated parameter; the dataset is then extended with those + parameters. Employment income, child benefit rates, the State Pension and income tax all + moved (2034 employment income £1,662.6bn → £1,779.4bn), including years past the + dataset's last year. +- **The same changes as a `reform=` dict do nothing.** `Scenario.from_reform` applies after + the data load and after `process_parameters()`, so the growth series change but nothing + derived from them is rebuilt: incomes, benefit rates, the State Pension and income tax were + identical to baseline. No error is raised. +- Period keys: in `apply_parameter_changes` a bare year (`"2030"`) means the fiscal year from + 6 April. The `yoy_growth` series are calendar-year values keyed 1 January, so use + `"year:2030-01-01:1"`. The `"2030-01-01.2030-12-31"` form used in `reform=` dicts raises + `ValueError` here. +- One process can build several such simulations, but `policyengine` materialises the + dataset under `./data/`: run parallel processes from separate working directories or the + HDF5 file lock fails. + +To combine a macro path with a policy reform, build the simulation with the macro `Scenario`, +then apply the policy dict with `Scenario.from_reform(policy).simulation_modifier(sim)` and +`sim.tax_benefit_system.reset_parameter_caches()` before calculating. + ## Country-model development notes (policyengine_uk directly) Everything above uses the managed `pe.uk` surface. When you work *inside* the UK country model diff --git a/skills/policyengine/SKILL.md b/skills/policyengine/SKILL.md index dbb89ce..1440149 100644 --- a/skills/policyengine/SKILL.md +++ b/skills/policyengine/SKILL.md @@ -8,7 +8,7 @@ description: | cost of a policy, revenue estimate, poverty rate, child poverty, winners and losers, decile, Gini, inequality, CTC, EITC, SNAP, income tax, universal credit, state-level analysis, congressional district, constituency, Microcosm (formerly Populace) dataset, MicroSeries, ensure_datasets, - economic_impact_analysis, managed_microsimulation. + economic_impact_analysis, managed_microsimulation, scenarios, uncertainty, Monte Carlo. NOT for: implementing new variables/parameters inside country models (use policyengine-model-development) or calling the REST API from JS (use policyengine-api). metadata: @@ -25,6 +25,30 @@ Originally verified against policyengine 4.21.0 (2026-07); the marked examples r against the latest release (5.0.1 at 2026-08). Re-verify the bundle when precision matters (see "Checking what you're running" below). +## Every reported number is a model run + +PolicyEngine is a microsimulation model, and its outputs are the product. Every fiscal, +distributional, poverty or household figure an analysis reports must come from running the +model for that scenario and year, never from arithmetic on the output of other runs. None of +these is allowed, however well it is validated: + +- scaling one run's totals by a ratio of indices, levels or shares (for example + `cost = spend * (index_a - index_b) / index_central` applied to thousands of Monte Carlo + draws, with a handful of full runs as a "check"); +- interpolating or extrapolating between runs, years or parameter values, or applying an + elasticity or share taken from one run to another scenario; +- a side model (closed form, spreadsheet, regression on model output) that stands in for the + model on some scenarios; +- changing one parameter in a scenario that should move the rest of the system. A macro or + price path goes in as model inputs, so every uprated parameter and income follows it (UK: + see "Macro scenarios" in the `policyengine-uk` skill). + +When the design needs more scenarios than you can run, run fewer: choose a small set you can +explain, run each one fully, and state how many full runs stand behind each figure. A +distribution comes from full runs over sampled scenarios, with the sampling error stated. +Arithmetic on model *inputs* is fine (building a parameter path from an uprating rule, say); +arithmetic on model *outputs* is not. If the model cannot produce a figure, do not publish it. + ## Setup Country models are extras — bare `policyengine` installs neither: diff --git a/skills/review-program/references/workflow.md b/skills/review-program/references/workflow.md index 6fade63..6fb224c 100644 --- a/skills/review-program/references/workflow.md +++ b/skills/review-program/references/workflow.md @@ -493,7 +493,11 @@ Classify confirmed findings: an explicit repository completion requirement (such as source corroboration or functional tests). Identify that requirement and the concrete failure. A plausible output difference, missing optional test, unknown source or preferred implementation pattern alone is not - a confirmed critical. + a confirmed critical. A reported fiscal, distributional, poverty or household figure + that comes from arithmetic on model output instead of a model run (scaling by an index + or share ratio, interpolation or extrapolation between runs, a side model standing in + for the model) is always critical, whatever validation accompanies it; see "Every + reported number is a model run" in the `policyengine` skill. - **SHOULD ADDRESS**: missing boundary coverage on tested behavior, maintainability or repo-standard issues. Missing rounding/flooring/capping is normally here unless a demonstrated case changes eligibility/category or materially changes results. diff --git a/targets/claude/marketplace.template.json b/targets/claude/marketplace.template.json index 1b97c6b..ffb772b 100644 --- a/targets/claude/marketplace.template.json +++ b/targets/claude/marketplace.template.json @@ -1,7 +1,7 @@ { "$schema": "https://claude.ai/schemas/plugin-marketplace.json", "name": "policyengine-claude", - "version": "4.0.2", + "version": "4.0.3", "description": "Complete PolicyEngine knowledge base - for users, analysts, and contributors across the entire PolicyEngine ecosystem", "owner": { "name": "PolicyEngine", From a429cb16dafd891a0a9069ca7370ce66fe0189f9 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 29 Sep 2026 12:42:01 -0400 Subject: [PATCH 2/5] Address review: scope the rule to other runs, safe macro+policy recipe - The rule bans figures derived from other runs' output, not arithmetic on the runs being reported (differences, sums, ratios, sampling error). - Macro + policy: two macro simulations; policies that process_parameters() reads go into the macro scenario; `+` drops applied_before_data_load; don't compare against sim.baseline. - Note the triple-lock (2034) and index (2039) horizons, that pe.uk Policy/Dynamic can't carry a macro path, and why reform= is a no-op. - reform-comparator no longer allows labelled linear scaling; the review-program rubric covers published estimates, not test values. Co-Authored-By: Claude Opus 5.5 --- skills/policyengine-uk/SKILL.md | 38 ++++++++++++++------ skills/policyengine/SKILL.md | 17 +++++---- skills/review-program/references/workflow.md | 12 ++++--- targets/claude/agents/reform-comparator.md | 2 +- 4 files changed, 46 insertions(+), 23 deletions(-) diff --git a/skills/policyengine-uk/SKILL.md b/skills/policyengine-uk/SKILL.md index 30d09e4..0dc830c 100644 --- a/skills/policyengine-uk/SKILL.md +++ b/skills/policyengine-uk/SKILL.md @@ -25,8 +25,9 @@ the canonical `import policyengine as pe` interface, the population flow and dataset handling — this skill only carries what is *different* about the UK. Verified against policyengine 4.21.0 + policyengine-uk 2.89.2 + policyengine-core 3.30.0 -(2026-07). UK examples are not in the `[us]`-only CI verify set, so they carry no +(2026-07). UK examples are not in the `[us]`-only CI verify set, so they carry no fast `` marker; each was run manually in a `policyengine[uk]` environment. +Population examples may carry ``. ## Setup @@ -216,7 +217,10 @@ Microcosm UK is built (FRS + WAS imputation) and calibrated, and the `policyengi A macro path goes in through the growth series in `gov.economic_assumptions.yoy_growth.obr` (`consumer_price_index`, `average_earnings`; move `rpi` and `cpih` with CPI if prices move), -passed as a `Scenario` applied **before the data load**: +passed as a `Scenario` applied **before the data load**. This needs +`managed_microsimulation(scenario=...)`: the `pe.uk` `Simulation`, `Policy`/`Dynamic` and +`economic_impact_analysis` interfaces apply parameter values after the data load, so growth +edits made through them are silent no-ops too. ```python @@ -249,21 +253,35 @@ running it with +1pp CPI and earnings in 2027-2033: parameters. Employment income, child benefit rates, the State Pension and income tax all moved (2034 employment income £1,662.6bn → £1,779.4bn), including years past the dataset's last year. -- **The same changes as a `reform=` dict do nothing.** `Scenario.from_reform` applies after - the data load and after `process_parameters()`, so the growth series change but nothing - derived from them is rebuilt: incomes, benefit rates, the State Pension and income tax were - identical to baseline. No error is raised. +- **The same changes as a `reform=` dict do nothing.** `Scenario.from_reform` turns the dict + into a plain `.update()` modifier that runs after the data load and never calls + `process_parameters()`, so the growth series change but nothing derived from them is + rebuilt: incomes, benefit rates, the State Pension and income tax were identical to + baseline. No error is raised. (An after-load `Scenario(parameter_changes=...)` rebuilds the + parameters but not the microdata, which was already uprated.) - Period keys: in `apply_parameter_changes` a bare year (`"2030"`) means the fiscal year from 6 April. The `yoy_growth` series are calendar-year values keyed 1 January, so use `"year:2030-01-01:1"`. The `"2030-01-01.2030-12-31"` form used in `reform=` dicts raises `ValueError` here. +- The rebuilt series have fixed horizons in policyengine-uk 2.90.2: the triple lock covers + 2022-2034 (`create_triple_lock.py`) and the uprating indices run to 2039. Growth edits after + those years do not move the State Pension or index-uprated parameters; check the parameter + you care about in the final year before relying on a long-horizon path. - One process can build several such simulations, but `policyengine` materialises the dataset under `./data/`: run parallel processes from separate working directories or the HDF5 file lock fails. -To combine a macro path with a policy reform, build the simulation with the macro `Scenario`, -then apply the policy dict with `Scenario.from_reform(policy).simulation_modifier(sim)` and -`sim.tax_benefit_system.reset_parameter_caches()` before calculating. +To combine a macro path with a policy, build two simulations with the same macro `Scenario`: a +macro baseline and a macro reform. For a policy that only sets rates, thresholds or amounts for +every year you report, apply it to the reform simulation with +`Scenario.from_reform(policy).simulation_modifier(sim)`, then +`sim.tax_benefit_system.reset_parameter_caches()`, before calculating. If the policy changes +anything `process_parameters()` reads (`gov.dwp.state_pension.triple_lock.*`, +`gov.economic_assumptions.*`, or an uprated parameter whose later years should follow it), +merge it into the macro scenario instead: +`Scenario(parameter_changes={**macro_changes, **policy_changes}, applied_before_data_load=True)`. +Do not combine them with `+` (`Scenario.__add__` drops `applied_before_data_load`), and do not +compare against `sim.baseline`, which is built on the default macro path. ## Country-model development notes (policyengine_uk directly) @@ -277,7 +295,7 @@ from policyengine_uk.model_api import Scenario # -> policyengine_uk.utils.scen `Scenario` is a Pydantic model with two fields — `parameter_changes` (`{path: {period: value}}`) and `simulation_modifier` (a `Callable[[Simulation], None]`) — and scenarios compose with `+` -(parameter dicts merge, modifiers chain). The country-package `Microsimulation(reform=...)` +(parameter dicts merge, modifiers chain; the sum drops `applied_before_data_load`). The country-package `Microsimulation(reform=...)` accepts **either a plain dict or a `Reform` class** (its verified signature is `reform: Union[Dict, Type[Reform]]`) and internally calls `Scenario.from_reform(reform)`. `Reform.from_dict(dict, country_id="uk")` also works and applies correctly for both flat and diff --git a/skills/policyengine/SKILL.md b/skills/policyengine/SKILL.md index 1440149..835621b 100644 --- a/skills/policyengine/SKILL.md +++ b/skills/policyengine/SKILL.md @@ -28,9 +28,9 @@ against the latest release (5.0.1 at 2026-08). Re-verify the bundle when precisi ## Every reported number is a model run PolicyEngine is a microsimulation model, and its outputs are the product. Every fiscal, -distributional, poverty or household figure an analysis reports must come from running the -model for that scenario and year, never from arithmetic on the output of other runs. None of -these is allowed, however well it is validated: +distributional, poverty or household figure an analysis presents as a PolicyEngine estimate +must come from running the model for that scenario and year, never derived from the output of +runs for other scenarios. None of these is allowed, however well it is validated: - scaling one run's totals by a ratio of indices, levels or shares (for example `cost = spend * (index_a - index_b) / index_central` applied to thousands of Monte Carlo @@ -40,14 +40,17 @@ these is allowed, however well it is validated: - a side model (closed form, spreadsheet, regression on model output) that stands in for the model on some scenarios; - changing one parameter in a scenario that should move the rest of the system. A macro or - price path goes in as model inputs, so every uprated parameter and income follows it (UK: - see "Macro scenarios" in the `policyengine-uk` skill). + price path goes in as model inputs, so the model's uprating carries it through (UK: see + "Macro scenarios" in the `policyengine-uk` skill). When the design needs more scenarios than you can run, run fewer: choose a small set you can explain, run each one fully, and state how many full runs stand behind each figure. A distribution comes from full runs over sampled scenarios, with the sampling error stated. -Arithmetic on model *inputs* is fine (building a parameter path from an uprating rule, say); -arithmetic on model *outputs* is not. If the model cannot produce a figure, do not publish it. +Arithmetic on model *inputs* is fine (building a parameter path from an uprating rule, say), +and so is arithmetic that combines outputs of the runs for the scenario being reported: reform +minus baseline, sums over years, ratios, percentage changes, averages, deflation, and sampling +error across full runs. Arithmetic that produces a figure for a scenario nobody ran is not. If +the model cannot produce a figure, do not publish it. ## Setup diff --git a/skills/review-program/references/workflow.md b/skills/review-program/references/workflow.md index 6fb224c..4e9a68e 100644 --- a/skills/review-program/references/workflow.md +++ b/skills/review-program/references/workflow.md @@ -493,11 +493,13 @@ Classify confirmed findings: an explicit repository completion requirement (such as source corroboration or functional tests). Identify that requirement and the concrete failure. A plausible output difference, missing optional test, unknown source or preferred implementation pattern alone is not - a confirmed critical. A reported fiscal, distributional, poverty or household figure - that comes from arithmetic on model output instead of a model run (scaling by an index - or share ratio, interpolation or extrapolation between runs, a side model standing in - for the model) is always critical, whatever validation accompanies it; see "Every - reported number is a model run" in the `policyengine` skill. + a confirmed critical. A figure published as a PolicyEngine estimate (analysis, report, + dashboard data or blog) that was derived from other runs' output instead of a run of that + scenario (scaling by an index or share ratio, interpolation or extrapolation between + runs, a side model standing in for the model) is always critical, whatever validation + accompanies it: this is an explicit repository requirement; see "Every reported number + is a model run" in the `policyengine` skill. Hand-calculated expected values in tests are + not covered. - **SHOULD ADDRESS**: missing boundary coverage on tested behavior, maintainability or repo-standard issues. Missing rounding/flooring/capping is normally here unless a demonstrated case changes eligibility/category or materially changes results. diff --git a/targets/claude/agents/reform-comparator.md b/targets/claude/agents/reform-comparator.md index 254b905..a45e1b5 100644 --- a/targets/claude/agents/reform-comparator.md +++ b/targets/claude/agents/reform-comparator.md @@ -157,7 +157,7 @@ Build a `benchmark_agreement` block in the output: The stage does NOT fire only when the reform has a full external cluster within ±25% (Step 2b already CORROBORATED). -**Rule of thumb — replace verbal reasoning with model runs.** If the natural comparator section is written as "TCJA scored X, our reform is 1/6 the size, so linear scaling suggests Y" — that's a mirror candidate. Run TCJA's shape through our model and check we reproduce X. Verbal linear-scaling should not appear in a final report; it should either be corroborated by a mirror run or explicitly labeled as an unvalidated sanity check. +**Rule of thumb — replace verbal reasoning with model runs.** If the natural comparator section is written as "TCJA scored X, our reform is 1/6 the size, so linear scaling suggests Y" — that's a mirror candidate. Run TCJA's shape through our model and check we reproduce X. Linear scaling must not appear in a report, labeled or not: either run the comparator's shape through the model or leave the comparison out (see "Every reported number is a model run" in the `policyengine` skill). When the trigger fires, invoke `model-corroborator` with the original reform context and the benchmark cluster. The corroborator picks 1-2 closest-shape candidates, builds mirror reform-dicts (and a baseline policy if the source uses a different baseline schedule like TCJA-extension vs OBBBA-current-law), submits them to the PE API, polls for completion, and computes per-candidate corroboration: From 5029e7c16e7a55e42b251de87f59eb664727b257 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 29 Sep 2026 13:12:56 -0400 Subject: [PATCH 3/5] Address review round 2: drop an unverified uprating claim, align scope - Macro + policy: remove the claim that merging makes later years of an uprated parameter follow a one-year change; set every reported year instead. State the before-load period keys and that a repeated path replaces the macro one. - policyengine skill: the macro Scenario is the one analysis use of policyengine_uk directly. - policyengine-uk skill: the 5.3.0 bundle's UK default is enhanced_frs_2024_25 (policyengine.py#491, 21 Aug 2026). - policyengine-tools: data files may hold arithmetic on the runs shown and labelled external comparators. Co-Authored-By: Claude Opus 5.5 --- skills/policyengine-tools/SKILL.md | 7 ++++--- skills/policyengine-uk/SKILL.md | 24 +++++++++++++----------- skills/policyengine/SKILL.md | 3 ++- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/skills/policyengine-tools/SKILL.md b/skills/policyengine-tools/SKILL.md index 13fcefd..054fdc6 100644 --- a/skills/policyengine-tools/SKILL.md +++ b/skills/policyengine-tools/SKILL.md @@ -127,9 +127,10 @@ multizone. Register in `policyengine-app-v2/website/src/data/apps.json` (`type`, ## Data and backend patterns Never paste ad-hoc computed numbers into source. Always: Python script → data file -(JSON/CSV) → frontend imports it. The data file holds model outputs only, one full -microsimulation per scenario shown; the pipeline and the frontend never scale, interpolate -or extrapolate results to cover scenarios that were not run (see "Every reported number is a +(JSON/CSV) → frontend imports it. The data file holds model outputs for the scenarios +shown (and arithmetic on those runs, such as reform minus baseline), plus clearly labelled +external comparators; the pipeline and the frontend never scale, interpolate or extrapolate +results to cover scenarios that were not run (see "Every reported number is a model run" in the `policyengine` skill). Pick by need: - **A — Precomputed JSON/CSV**: finite scenarios (legislative trackers, static analyses). A diff --git a/skills/policyengine-uk/SKILL.md b/skills/policyengine-uk/SKILL.md index 0dc830c..8af8822 100644 --- a/skills/policyengine-uk/SKILL.md +++ b/skills/policyengine-uk/SKILL.md @@ -200,8 +200,9 @@ dataset's `constituency_code_oa` column via `compute_uk_constituency_impacts` / ## Population data -The UK default dataset is **`populace_uk_2023`** (a Microcosm build), pinned by the certified -bundle in policyengine 4.21.0. It lives in a **private** Hugging Face repo, so population runs +The UK default dataset in policyengine 5.3.0's certified bundle is **`enhanced_frs_2024_25`** +(policyengine-uk-data 1.56.16; it replaced the Microcosm `populace_uk_2023` default in August +2026). It lives in a **private** Hugging Face repo, so population runs require a `HUGGING_FACE_TOKEN` with access: ```bash @@ -272,15 +273,16 @@ running it with +1pp CPI and earnings in 2027-2033: HDF5 file lock fails. To combine a macro path with a policy, build two simulations with the same macro `Scenario`: a -macro baseline and a macro reform. For a policy that only sets rates, thresholds or amounts for -every year you report, apply it to the reform simulation with -`Scenario.from_reform(policy).simulation_modifier(sim)`, then -`sim.tax_benefit_system.reset_parameter_caches()`, before calculating. If the policy changes -anything `process_parameters()` reads (`gov.dwp.state_pension.triple_lock.*`, -`gov.economic_assumptions.*`, or an uprated parameter whose later years should follow it), -merge it into the macro scenario instead: -`Scenario(parameter_changes={**macro_changes, **policy_changes}, applied_before_data_load=True)`. -Do not combine them with `+` (`Scenario.__add__` drops `applied_before_data_load`), and do not +macro baseline and a macro reform. Apply a policy that sets rates, thresholds or amounts to the +reform simulation with `Scenario.from_reform(policy).simulation_modifier(sim)`, then +`sim.tax_benefit_system.reset_parameter_caches()`, before calculating, and set its value for +every year you report: don't rely on later years of an uprated parameter following a one-year +change. Settings that `process_parameters()` reads (`gov.dwp.state_pension.triple_lock.*`, +`gov.economic_assumptions.*`) go into the macro scenario instead: +`Scenario(parameter_changes={**macro_changes, **policy_changes}, applied_before_data_load=True)`, +with `apply_parameter_changes` period keys (a bare year is a fiscal year; the `"a.b"` range form +raises `ValueError`). A policy path that repeats a macro path replaces it entirely. Do not +combine scenarios with `+` (`Scenario.__add__` drops `applied_before_data_load`), and do not compare against `sim.baseline`, which is built on the default macro path. ## Country-model development notes (policyengine_uk directly) diff --git a/skills/policyengine/SKILL.md b/skills/policyengine/SKILL.md index 835621b..2760ed9 100644 --- a/skills/policyengine/SKILL.md +++ b/skills/policyengine/SKILL.md @@ -66,7 +66,8 @@ Analysis always runs on the **latest released** `policyengine` (`>=5.0.1`; resol from PyPI as described in "Checking what you're running"). Each release pins exactly-matched country-model versions and the certified data bundle, which is what makes results reproducible. Directly-imported country packages (`policyengine_us` / `policyengine_uk`) are -for model development and tests, not for analysis compute. +for model development and tests, not for analysis compute (the exception: a UK macro path needs +`policyengine_uk`'s `Scenario`; see "Macro scenarios" in the `policyengine-uk` skill). ## Household calculations (fast, ~2 GB RAM) From b4d40f3576e7d6e90fa57916f08c0630013655a2 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 29 Sep 2026 18:06:40 -0400 Subject: [PATCH 4/5] Name the current UK default dataset consistently across skills The certified bundle in policyengine 5.3.0 through 6.1.2 serves enhanced_frs_2024_25 (policyengine-uk-data 1.56.16) as the UK default, checked in both manifests on 2026-09-29. The policyengine and policyengine-data skills and the UK skill's frontmatter still called populace_uk_2023 the default; the round-3 review flagged the mismatch. Co-Authored-By: Claude Opus 5.5 --- skills/policyengine-data/SKILL.md | 6 ++++-- skills/policyengine-uk/SKILL.md | 12 ++++++------ skills/policyengine/SKILL.md | 4 ++-- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/skills/policyengine-data/SKILL.md b/skills/policyengine-data/SKILL.md index ccbeedd..f8fb6db 100644 --- a/skills/policyengine-data/SKILL.md +++ b/skills/policyengine-data/SKILL.md @@ -5,7 +5,7 @@ description: | built, calibrated, versioned, and named. Covers the Microcosm stack (Frame kernel, microcosm-fit conditional models, microcosm-calibrate weights with L0 sparsity, build/release gates), the certified datasets that flow into policyengine bundles (populace_us_2024 sparse - ~57k default, populace_us_2024_acs_local ~1.6M local-area, populace_uk_2023 private), the + ~57k default, populace_us_2024_acs_local ~1.6M local-area, enhanced_frs_2024_25 UK), the "one national dataset filtered by geography" local-area philosophy, the calibration diagnostics dashboard, and where data work goes now that policyengine-us-data is archived. Triggers: Microcosm (formerly Populace), Frame, microcosm-fit, microcosm-calibrate, calibration target, survey @@ -92,12 +92,14 @@ bundle/manifest.json`): |---|---|---| | `populace_us_2024` | US default. Build J, sparse, ~57k households calibrated to tens of thousands of admin targets | resolves automatically; do not pass a raw URI | | `populace_us_2024_acs_local` | US local-area build. Build L, ~1.6M households, ACS multispine, PUMA-assigned to CD-119 / county / state | load **by name**, never implicit | -| `populace_uk_2023` | UK default (Microcosm, FRS+WAS) | private HF repo — set `HUGGING_FACE_TOKEN` | +| `enhanced_frs_2024_25` | UK default since August 2026 (policyengine-uk-data Enhanced FRS; `populace_uk_2023`, Microcosm FRS+WAS, before) | private HF repo — set `HUGGING_FACE_TOKEN` | Verified manifest build ids (2026-07): US `populace_us_2024` @ `populace-us-2024-buildj-sparse-rmloss100-75d5add-20260710`; UK `populace_uk_2023` @ `populace-uk-2023-dd68c73-...`. The manifest also carries a `dataset_overlays` section (where the `acs_local` overlay lives) and per-region `region_datasets`. +As of policyengine 6.1.2 (checked 2026-09-29): UK `enhanced_frs_2024_25` @ +`policyengine-uk-data-1.56.16`; US `populace_us_2024` @ `populace-us-2024-spm-20260915`. ### The two defaults, precisely diff --git a/skills/policyengine-uk/SKILL.md b/skills/policyengine-uk/SKILL.md index 8af8822..b46a7a6 100644 --- a/skills/policyengine-uk/SKILL.md +++ b/skills/policyengine-uk/SKILL.md @@ -4,7 +4,7 @@ description: | Load before writing PolicyEngine-UK code — UK household calculations, income tax (incl. Scottish and Welsh variations), National Insurance, Universal Credit and other benefits, poverty and reform analysis. Covers the person/benunit/household entity model (UK has NO - tax_unit), pe.uk.calculate_household, BHC vs AHC poverty, ITL1 regions, populace_uk_2023 + tax_unit), pe.uk.calculate_household, BHC vs AHC poverty, ITL1 regions, enhanced_frs_2024_25 data, and the policyengine_uk Scenario surface for country-model development. Triggers: Microcosm (formerly Populace), policyengine uk, pe.uk, Universal Credit, child benefit, pension credit, housing benefit, council tax, income tax, national insurance, personal allowance, Scottish income @@ -200,8 +200,8 @@ dataset's `constituency_code_oa` column via `compute_uk_constituency_impacts` / ## Population data -The UK default dataset in policyengine 5.3.0's certified bundle is **`enhanced_frs_2024_25`** -(policyengine-uk-data 1.56.16; it replaced the Microcosm `populace_uk_2023` default in August +The UK default dataset in the certified bundle (policyengine 5.3.0 through 6.1.2) is +**`enhanced_frs_2024_25`** (policyengine-uk-data 1.56.16; it replaced the Microcosm `populace_uk_2023` default in August 2026). It lives in a **private** Hugging Face repo, so population runs require a `HUGGING_FACE_TOKEN` with access: @@ -210,8 +210,8 @@ export HUGGING_FACE_TOKEN=hf_... # required for UK population data; household ``` Do not hardcode a raw `hf://` dataset URI — the certified default resolves by name through the -bundle. The pre-Microcosm UK datasets are superseded. See the `policyengine-data` skill for how -Microcosm UK is built (FRS + WAS imputation) and calibrated, and the `policyengine` skill for the +bundle. See the `policyengine-data` skill for the dataset stack and calibration, and the +`policyengine` skill for the `ensure_datasets` / `Simulation` population flow, which is identical across countries. ## Macro scenarios (a different CPI or earnings path) @@ -371,6 +371,6 @@ fixed value — cite the uprating order, not just the base Act, when a value cha - `policyengine` — the canonical `pe.*` interface, population flow, MicroSeries rules, and regional impacts. Read it first. -- `policyengine-data` — how Microcosm UK (FRS + WAS) is built and calibrated. +- `policyengine-data` — the dataset stack (the Enhanced FRS default, Microcosm) and calibration. - `policyengine-model-development` — implementing new UK variables and parameters. - `policyengine-us` — the US counterpart (and the source of the `tax_unit` habit to unlearn). diff --git a/skills/policyengine/SKILL.md b/skills/policyengine/SKILL.md index 2760ed9..61e9894 100644 --- a/skills/policyengine/SKILL.md +++ b/skills/policyengine/SKILL.md @@ -273,11 +273,11 @@ Certified defaults resolve automatically — **do not pass raw `hf://` URIs**: |---|---|---| | `populace_us_2024` | US default (Microcosm, ~57k households calibrated to ~30k+ admin targets) | public | | `populace_us_2024_acs_local` | US local-area build (~1.6M households, ACS multispine, PUMA-assigned CD-119/county/state) | load **by name** for state/district work; never selected implicitly | -| `populace_uk_2023` | UK default (Microcosm) | private HF repo — set `HUGGING_FACE_TOKEN` | +| `enhanced_frs_2024_25` | UK default (policyengine-uk-data Enhanced FRS 2024-25; certified in policyengine 5.3.0 to 6.1.2) | private HF repo — set `HUGGING_FACE_TOKEN` | The pre-2026 datasets are gone: -`enhanced_cps_2024` and `enhanced_frs_2023_24` are superseded by Microcosm, and the per-area +`enhanced_cps_2024` is superseded by Microcosm and `enhanced_frs_2023_24` by `enhanced_frs_2024_25`, and the per-area files (`hf://policyengine/policyengine-us-data/states/*.h5`, `districts/*.h5`) no longer exist — policyengine-us-data is archived. **Local-area analysis = filter one national dataset From c706b6f310725b331689f4c755ba6f21606101ee Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 11 Nov 2026 00:36:07 -0500 Subject: [PATCH 5/5] Address review: restore dataset lines, drop false parenthetical, rewrite UK macro-scenario section to current code Co-Authored-By: Claude Opus 5.5 --- skills/policyengine-uk/SKILL.md | 68 +++++++++++---------------------- skills/policyengine/SKILL.md | 2 +- 2 files changed, 23 insertions(+), 47 deletions(-) diff --git a/skills/policyengine-uk/SKILL.md b/skills/policyengine-uk/SKILL.md index 01f5790..3e04cdd 100644 --- a/skills/policyengine-uk/SKILL.md +++ b/skills/policyengine-uk/SKILL.md @@ -217,11 +217,13 @@ Microcosm UK is built (FRS + WAS imputation) and calibrated, and the `policyengi ## Macro scenarios (a different CPI or earnings path) A macro path goes in through the growth series in `gov.economic_assumptions.yoy_growth.obr` -(`consumer_price_index`, `average_earnings`; move `rpi` and `cpih` with CPI if prices move), -passed as a `Scenario` applied **before the data load**. This needs -`managed_microsimulation(scenario=...)`: the `pe.uk` `Simulation`, `Policy`/`Dynamic` and -`economic_impact_analysis` interfaces apply parameter values after the data load, so growth -edits made through them are silent no-ops too. +(`consumer_price_index`, `average_earnings`; move `rpi` and `cpih` with CPI if prices move). +Pass it to `managed_microsimulation` as a `Scenario` applied **before the data load**, as +below, so the dataset is projected on the new path along with the parameters derived from +it. Don't send a macro path through the `pe.uk` `Simulation`, `Policy`/`Dynamic` or +`economic_impact_analysis` interfaces: they hand parameter values to policyengine-uk as a +reform when it builds the simulation, on each year's data as the dataset was already +projected. ```python @@ -245,45 +247,19 @@ for year in (2029, 2034): # inside and beyond the dataset's own years assert macro.calculate("employment_income", year).sum() > base.calculate("employment_income", year).sum() ``` -Verified in policyengine 5.3.0 / policyengine-uk 2.90.2 (2026-09), by reading the code and -running it with +1pp CPI and earnings in 2027-2033: - -- Before the data load, `Simulation.apply_parameter_changes` reloads the parameters, applies - the edits and reruns `process_parameters()`, which rebuilds the triple lock, the uprating - indices and every index-uprated parameter; the dataset is then extended with those - parameters. Employment income, child benefit rates, the State Pension and income tax all - moved (2034 employment income £1,662.6bn → £1,779.4bn), including years past the - dataset's last year. -- **The same changes as a `reform=` dict do nothing.** `Scenario.from_reform` turns the dict - into a plain `.update()` modifier that runs after the data load and never calls - `process_parameters()`, so the growth series change but nothing derived from them is - rebuilt: incomes, benefit rates, the State Pension and income tax were identical to - baseline. No error is raised. (An after-load `Scenario(parameter_changes=...)` rebuilds the - parameters but not the microdata, which was already uprated.) -- Period keys: in `apply_parameter_changes` a bare year (`"2030"`) means the fiscal year from - 6 April. The `yoy_growth` series are calendar-year values keyed 1 January, so use - `"year:2030-01-01:1"`. The `"2030-01-01.2030-12-31"` form used in `reform=` dicts raises - `ValueError` here. -- The rebuilt series have fixed horizons in policyengine-uk 2.90.2: the triple lock covers - 2022-2034 (`create_triple_lock.py`) and the uprating indices run to 2039. Growth edits after - those years do not move the State Pension or index-uprated parameters; check the parameter - you care about in the final year before relying on a long-horizon path. -- One process can build several such simulations, but `policyengine` materialises the - dataset under `./data/`: run parallel processes from separate working directories or the - HDF5 file lock fails. - -To combine a macro path with a policy, build two simulations with the same macro `Scenario`: a -macro baseline and a macro reform. Apply a policy that sets rates, thresholds or amounts to the -reform simulation with `Scenario.from_reform(policy).simulation_modifier(sim)`, then -`sim.tax_benefit_system.reset_parameter_caches()`, before calculating, and set its value for -every year you report: don't rely on later years of an uprated parameter following a one-year -change. Settings that `process_parameters()` reads (`gov.dwp.state_pension.triple_lock.*`, -`gov.economic_assumptions.*`) go into the macro scenario instead: -`Scenario(parameter_changes={**macro_changes, **policy_changes}, applied_before_data_load=True)`, -with `apply_parameter_changes` period keys (a bare year is a fiscal year; the `"a.b"` range form -raises `ValueError`). A policy path that repeats a macro path replaces it entirely. Do not -combine scenarios with `+` (`Scenario.__add__` drops `applied_before_data_load`), and do not -compare against `sim.baseline`, which is built on the default macro path. +This example last ran on policyengine 5.3.0 / policyengine-uk 2.90.2 (2026-09), on the +then-default `enhanced_frs_2024_25`: +1pp CPI and earnings in 2027-2033 raised 2034 employment +income from £1,662.6bn to £1,779.4bn. policyengine-uk has since changed how reforms and +scenarios reach the parameters (`policyengine_uk.utils.dated_reforms`), so check any other +route, period key or series horizon against the installed version before relying on it, and +measure again on the current bundle before you quote a figure. `policyengine` downloads the +dataset to `./data/` under the working directory, so run parallel processes from separate +working directories. + +To combine a macro path with a policy, build a macro baseline and a macro reform on the same +macro `Scenario`, check on the installed version that the policy reaches the reform's +parameters for every year you report, and compare the two. Don't compare against +`sim.baseline`, which policyengine-uk builds on the default macro path. ## Country-model development notes (policyengine_uk directly) @@ -297,7 +273,7 @@ from policyengine_uk.model_api import Scenario # -> policyengine_uk.utils.scen `Scenario` is a Pydantic model with two fields — `parameter_changes` (`{path: {period: value}}`) and `simulation_modifier` (a `Callable[[Simulation], None]`) — and scenarios compose with `+` -(parameter dicts merge, modifiers chain; the sum drops `applied_before_data_load`). The country-package `Microsimulation(reform=...)` +(parameter dicts merge, modifiers chain). The country-package `Microsimulation(reform=...)` accepts **either a plain dict or a `Reform` class** (its verified signature is `reform: Union[Dict, Type[Reform]]`) and internally calls `Scenario.from_reform(reform)`. `Reform.from_dict(dict, country_id="uk")` also works and applies correctly for both flat and @@ -371,6 +347,6 @@ fixed value — cite the uprating order, not just the base Act, when a value cha - `policyengine` — the canonical `pe.*` interface, population flow, MicroSeries rules, and regional impacts. Read it first. -- `policyengine-data` — the dataset stack (the Enhanced FRS default, Microcosm) and calibration. +- `policyengine-data` — how Microcosm UK (FRS + WAS) is built and calibrated. - `policyengine-model-development` — implementing new UK variables and parameters. - `policyengine-us` — the US counterpart (and the source of the `tax_unit` habit to unlearn). diff --git a/skills/policyengine/SKILL.md b/skills/policyengine/SKILL.md index 8f80bdf..76e7c42 100644 --- a/skills/policyengine/SKILL.md +++ b/skills/policyengine/SKILL.md @@ -277,7 +277,7 @@ Certified defaults resolve automatically — **do not pass raw `hf://` URIs**: The pre-2026 datasets are gone: -`enhanced_cps_2024` is superseded by Microcosm and `enhanced_frs_2023_24` by `enhanced_frs_2024_25`, and the per-area +`enhanced_cps_2024` and `enhanced_frs_2023_24` are superseded by Microcosm, and the per-area files (`hf://policyengine/policyengine-us-data/states/*.h5`, `districts/*.h5`) no longer exist — policyengine-us-data is archived. **Local-area analysis = filter one national dataset