Agent Skills: PolicyEngine UK

|

UncategorizedID: PolicyEngine/policyengine-claude/policyengine-uk

Install this agent skill to your local

pnpm dlx add-skill https://github.com/PolicyEngine/policyengine-claude/tree/HEAD/skills/policyengine-uk

Skill Files

Browse the full folder contents for policyengine-uk.

Download Skill

Loading file tree…

skills/policyengine-uk/SKILL.md

Skill Metadata

Name
policyengine-uk
Description
|

PolicyEngine UK

UK-specific facts for the PolicyEngine Python stack. Read the policyengine skill first for the canonical import policyengine as pe interface, the population flow (ensure_datasets / Simulation / economic_impact_analysis), the MicroSeries discipline, 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 <!-- verify --> marker; each was run manually in a policyengine[uk] environment.

Setup

The UK model is an extra — bare policyengine installs no country model:

uv pip install "policyengine[uk]"   # UK model + certified UK data bundle metadata

Household calculations need no data download. Population runs need the UK dataset, which is a private Hugging Face repo — see "Population data" below.

Entity model — and the benunit trap

UK entities are personbenunithousehold. There is no tax_unit and no spm_unit / marital_unit / family — those are US entities. The single most common UK mistake is reaching for result.tax_unit out of US habit; on a UK result it raises AttributeError (verified: a UK result has person, benunit, household and no tax_unit).

| Concept | US entity | UK entity | |---|---|---| | Means-tested assessment unit | tax_unit (+ spm_unit) | benunit (benefit unit) | | Statistical unit for poverty | spm_unit / household | household | | Individual | person | person |

A benefit unit is a single adult or a couple plus their dependent children — the DWP assessment unit for means-tested benefits (Universal Credit, Pension Credit, etc.). One household can contain several benunits (e.g. adult children living with parents). Read means-tested benefits at the benunit level, income tax and NI at the person level, and net income and poverty at the household level.

Household calculations

pe.uk.calculate_household mirrors the US call (keyword-only, year defaults to 2026), but results are read on UK entities. Access is dot-attribute on singular entities; only result.person is a list.

import policyengine as pe

r = pe.uk.calculate_household(
    people=[{"age": 35, "employment_income": 50_000}],
    year=2026,
)
assert round(r.person[0].income_tax) == 7_486
assert round(r.person[0].national_insurance) == 2_994
assert round(r.household.hbai_household_net_income) == 39_520

Net income comes in two conventions: hbai_household_net_income follows the DWP Households Below Average Income definition (the one PolicyEngine reports, and what the poverty measures below use); household_net_income is the plain accounting definition. Prefer the HBAI variable for anything compared against official UK statistics.

Benefit-unit variables read off benunit (a family with two children on £30k gross):

r = pe.uk.calculate_household(
    people=[
        {"age": 35, "employment_income": 30_000},
        {"age": 33},
        {"age": 8},
        {"age": 5},
    ],
    household={"region": "NORTH_WEST"},
    year=2026,
)
r.benunit.universal_credit   # 6029.82
r.benunit.child_benefit      # 2337.40

Reforms are the same flat {parameter_path: value} dict as the US package surface:

reformed = pe.uk.calculate_household(
    people=[{"age": 35, "employment_income": 50_000}],
    year=2026,
    reform={"gov.hmrc.income_tax.allowances.personal_allowance.amount": 15_000},
)
assert round(reformed.person[0].income_tax) == 7_000   # 2,430 more allowance x 20%

Poverty — always state the measure

UK poverty is reported at the household level under two measures, and they can disagree for the same household:

  • in_poverty_bhc — Before Housing Costs.
  • in_poverty_ahc — After Housing Costs.
fam = pe.uk.calculate_household(
    people=[{"age": 35, "employment_income": 16_000}, {"age": 33}, {"age": 4}, {"age": 2}],
    household={"region": "WALES"},
    year=2026,
)
fam.household.in_poverty_bhc   # 1.0  (in poverty before housing costs)
fam.household.in_poverty_ahc   # 0.0  (NOT in poverty after housing costs)

That verified split — same household, BHC in poverty, AHC not — is why a UK poverty figure is meaningless without its measure. Name BHC or AHC in every result, and match whichever the source you compare against used (DWP HBAI headline child-poverty figures are AHC). Deep-poverty variants (in_deep_poverty_bhc / in_deep_poverty_ahc) follow the same convention. At population scale these are the poverty variables behind compute_uk_* breakdowns and the Poverty outputs described in the policyengine skill.

Parameter lookup

For "what is the rate/threshold?" questions, read the parameter tree directly (no simulation). Print the live value in your working year rather than trusting a hardcoded rate — UK rates and thresholds move at every fiscal event.

from policyengine_uk import CountryTaxBenefitSystem

p = CountryTaxBenefitSystem().parameters
p.gov.hmrc.income_tax.allowances.personal_allowance.amount("2026-01-01")   # 12570

Bracket/scale parameters index the scale node with [i] (no .brackets in the reform-dict path, same convention as US):

"gov.hmrc.income_tax.rates.uk[0].rate"   # basic-rate node in a reform dict

When browsing the tree object (not a reform path), a ParameterScale exposes .brackets as a Python list of ParameterScaleBracket, each with .rate(date) / .threshold(date).

Devolved income tax — Scotland and Wales

Income tax rates live under gov.hmrc.income_tax.rates, whose children are dividends, property, savings, savings_starter_rate, scotland, uk. Verify band counts and values live (below is the verified 2026 structure, but treat the parameter tree as the source of truth — do not hardcode rates into analysis):

  • Rest of UK (England & Northern Ireland): gov.hmrc.income_tax.rates.uk is a ParameterScale. Verified 4 brackets / 3 distinct statutory rates — basic 20%, higher 40%, additional 45% (the 4th bracket is a technical top node repeating the additional rate).
  • Scotland: gov.hmrc.income_tax.rates.scotland.rates is a separate ParameterScale with 6 bands (verified 2026: starter, basic, intermediate, higher, advanced, top). Scotland's extra bands are why a UK income-tax analysis must branch on nation rather than assume the rest-of-UK schedule.
  • Wales: there is no separate Welsh income-tax rate parameter (the only WALES parameter anywhere under gov.hmrc is in business_rates). The Welsh Rate of Income Tax is set at parity, so Wales is computed on the rest-of-UK rates.uk schedule. Model Wales via the WALES region, not a separate rate table.
p.gov.hmrc.income_tax.rates.uk.brackets              # verified len 4 (3 distinct rates)
p.gov.hmrc.income_tax.rates.scotland.rates.brackets  # verified len 6

Regions (ITL1)

region is a household-level enum. The Region enum (policyengine_uk/variables/household/demographic/geography.py) is the 12 ITL1 regions plus UNKNOWN:

NORTH_EAST, NORTH_WEST, YORKSHIRE, EAST_MIDLANDS, WEST_MIDLANDS, EAST_OF_ENGLAND,
LONDON, SOUTH_EAST, SOUTH_WEST, WALES, SCOTLAND, NORTHERN_IRELAND   (+ UNKNOWN)

Pass the enum member name as a string: household={"region": "SCOTLAND"}. YORKSHIRE is the key for "Yorkshire and the Humber". UK constituency-level distributional analysis uses the dataset's constituency_code_oa column via compute_uk_constituency_impacts / compute_uk_local_authority_impacts — see the policyengine skill.

Population data

The UK default dataset is populace_uk_2023 (a Populace build), pinned by the certified bundle in policyengine 4.21.0. It lives in a private Hugging Face repo, so population runs require a HUGGING_FACE_TOKEN with access:

export HUGGING_FACE_TOKEN=hf_...   # required for UK population data; household calcs do NOT need it

Do not hardcode a raw hf:// dataset URI — the certified default resolves by name through the bundle. The pre-Populace UK datasets are superseded. See the policyengine-data skill for how Populace UK is built (FRS + WAS imputation) and calibrated, and the policyengine skill for the ensure_datasets / Simulation population flow, which is identical across countries.

Country-model development notes (policyengine_uk directly)

Everything above uses the managed pe.uk surface. When you work inside the UK country model (new variables, structural reforms, debugging formulas), you use policyengine_uk directly, and the reform surface there is the Scenario object, verified importable:

from policyengine_uk.model_api import Scenario   # -> policyengine_uk.utils.scenario.Scenario

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=...) 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 date-ranged dicts — there is no UK-specific from_dict failure. (Note: the UK baseline bakes in the July-2025 Universal Credit reform, so a bare Microsimulation() is already post-July-2025 UC law.)

Reform patterns for country-model work (from the analysis skill's MICROSIMULATION_REFORM_GUIDE.md):

Parameter-change scenario, with period syntax "year:START:COUNT" or an ISO date:

reform = Scenario(parameter_changes={
    "gov.hmrc.income_tax.rates.uk[0].rate": {"year:2026:10": 0.21},   # 21% for 2026..2035
})

Structural or multi-year edits go through a simulation_modifier. The safe parameter-freeze recipe avoids corrupting shared parameter caches — clone, edit the clone, process, copy the values_list back, then reset caches:

def freeze_higher_threshold(sim):
    clone = sim.clone()
    clone.tax_benefit_system.reset_parameters()
    node = clone.tax_benefit_system.parameters.gov.hmrc.income_tax.rates.uk[1].threshold
    current = node("2026-01-01")
    for year in range(2026, 2031):
        node.update(period=str(year), value=current, remove_after=True)
    clone.tax_benefit_system.process_parameters()
    sim.tax_benefit_system.parameters.gov.hmrc.income_tax.rates.uk[1].threshold.values_list = (
        node.values_list
    )
    sim.tax_benefit_system.reset_parameter_caches()

reform = Scenario(simulation_modifier=freeze_higher_threshold)

Two gotchas from that recipe:

  • remove_after=True on .update(...) is mandatory when you mean to change only one period — without it the edit propagates to every later year.
  • Parameter paths are indexed in reform/scenario dicts: ...rates.uk[0].rate, never ...rates.uk.rate.

UK legislation citation convention

Every UK parameter YAML carries legislation.gov.uk references with exact section links. Distinguish primary legislation (Acts) from secondary legislation (Statutory Instruments, cited by SI year/number):

  • Acts (primary): Welfare Reform Act 2012 (Universal Credit), Social Security Contributions and Benefits Act 1992, Income Tax Act 2007, Income Tax (Earnings and Pensions) Act 2003.
  • Statutory Instruments (secondary): cited as SI YYYY/NNNN, e.g. the Universal Credit Regulations 2013 (SI 2013/376). SI URLs use the /uksi/ path; Acts use /ukpga/.
metadata:
  reference:
    - title: Universal Credit Regulations 2013, Schedule 4
      href: https://www.legislation.gov.uk/uksi/2013/376/schedule/4
    - title: Income Tax Act 2007, Section 10
      href: https://www.legislation.gov.uk/ukpga/2007/3/section/10

Uprating usually tracks a statutory index (CPI/RPI or a benefit-specific order) rather than a fixed value — cite the uprating order, not just the base Act, when a value changes annually.

Related skills

  • policyengine — the canonical pe.* interface, population flow, MicroSeries rules, and regional impacts. Read it first.
  • policyengine-data — how Populace 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).
PolicyEngine UK Skill | Agent Skills