PolicyEngine / PolicyEngine/microcosm
UK scorecard: populace_uk_2023 vs enhanced_frs_2024_25 vs admin benchmarks (2026)
@juaristi22 is already working on this.
Since Aug 20, 2026.
- Dominant language
- Python
- Stars
- 0
- Forks
- 4
- Avg merge
- 1d 3h
- Merged PRs (30d)
- 94
Description
cc @juaristi22 — María, could you take a look at why we're off on some of these?
I ran a benchmark scorecard of populace_uk_2023 against enhanced_frs_2024_25 (both simulated for 2026 through policyengine.py, pe.uk.ensure_datasets → Simulation with the pinned UK model version) and compared to approximate admin benchmarks.
Scorecard (2026)
| Metric | populace_uk_2023 | enhanced_frs_2024_25 | Benchmark (~2026) |
|---|---|---|---|
| Population | 69.9m | 70.0m | ONS ~68.3–69m |
| Households | 29.5m | 31.5m | ONS ~29m |
| State pension | £130.8bn | £127.5bn | OBR ~£140–146bn |
| Income tax | £419bn | £312bn | OBR ~£330bn |
| Income taxpayers | 36.7m | 42.7m | HMRC ~37–40m |
| Universal credit | £39.9bn / 3.3m families | £81.3bn / 6.35m | ~£80–86bn / ~6.7m hh |
| Child benefit | £17.4bn | £17.7bn | ~£13.5bn |
| Pension credit | £6.8bn | £7.5bn | ~£6bn |
| Council tax | £60.1bn | £52.5bn | ~£50bn |
| Poverty (BHC) | 16.7% | 13.5% | HBAI ~17% |
| Top 1% share (net hh income) | 6.9% | 6.9% | — |
| Gini (net hh income) | 0.385 | 0.370 | ONS ~0.34–0.36 |
Populace does well on demographics, household counts, taxpayer counts, pension credit, and poverty. The two large misses are:
- Income tax +27% vs OBR (£419bn vs ~£330bn) — consistent with the +20.8% vs the SPI-anchored benchmark already diagnosed in UK_COVERAGE_PROGRESS.md as distributional.
- Universal credit at less than half of admin (£39.9bn / 3.3m families vs ~£80–86bn / ~6.7m households) — consistent with #701 (UC not bound in the certified 148-target surface).
Smaller ones worth a look: child benefit ~+30% (though enhanced FRS shares this), council tax ~+20%, state pension ~£10–15bn low (also shared), and Gini a touch high.
Reproduction code
"""Benchmark scorecard for a UK dataset via policyengine.py (year 2026).
Usage: python scorecard.py <dataset-name-or-hf-uri>
e.g. python scorecard.py populace_uk_2023
"""
import sys
import numpy as np
import policyengine as pe
from policyengine import Simulation
DATASET = sys.argv[1]
d = pe.uk.load_datasets(datasets=[DATASET], years=[2026], data_folder="./data")
dataset = list(d.values())[0]
PERSON_VARS = [
"age",
"state_pension",
"income_tax",
"national_insurance",
"employment_income",
"self_employment_income",
"dividend_income",
"pension_income",
"capital_gains",
]
BENUNIT_VARS = ["universal_credit", "child_benefit", "pension_credit"]
HH_VARS = ["household_net_income", "income_tax", "council_tax", "in_poverty_bhc"]
sim = Simulation(
dataset=dataset,
tax_benefit_model_version=pe.uk.model,
extra_variables={
"person": PERSON_VARS,
"benunit": BENUNIT_VARS,
"household": HH_VARS,
},
)
sim.ensure()
out = sim.output_dataset.data
def arr(ent, var):
return np.asarray(getattr(out, ent)[var], float)
pw = arr("person", "person_weight")
bw = arr("benunit", "benunit_weight")
hw = arr("household", "household_weight")
print(f"SCORECARD {DATASET} year 2026")
age = arr("person", "age")
print(f"population_m {pw.sum()/1e6:.2f}")
print(f"children_u16_m {pw[age < 16].sum()/1e6:.2f}")
print(f"age_65plus_m {pw[age >= 65].sum()/1e6:.2f}")
print(f"households_m {hw.sum()/1e6:.2f}")
for v in PERSON_VARS[1:]:
print(f"{v}_bn {(arr('person', v)*pw).sum()/1e9:.1f}")
it = arr("person", "income_tax")
print(f"taxpayers_m {pw[it > 0].sum()/1e6:.2f}")
for v in BENUNIT_VARS:
x = arr("benunit", v)
print(f"{v}_bn {(x*bw).sum()/1e9:.1f}")
print(f"{v}_families_m {bw[x > 0].sum()/1e6:.2f}")
hni = arr("household", "household_net_income")
print(f"council_tax_bn {(arr('household','council_tax')*hw).sum()/1e9:.1f}")
pov = arr("household", "in_poverty_bhc")
print(f"poverty_bhc_pct {100*(pov*hw).sum()/hw.sum():.1f}")
order = np.argsort(hni)
h, w = hni[order], hw[order]
cw = np.cumsum(w) / w.sum()
inc = h * w
print(f"top1_share_pct {100*inc[cw >= 0.99].sum()/inc.sum():.1f}")
print(f"top10_share_pct {100*inc[cw >= 0.90].sum()/inc.sum():.1f}")
p = np.cumsum(w) / w.sum()
L = np.cumsum(np.clip(inc, 0, None)) / np.clip(inc, 0, None).sum()
print(f"gini {1 - 2*np.trapezoid(L, p):.3f}")
Notes: national_insurance here is the personal-side variable only; benchmarks are approximate (OBR EFO / HMRC / DWP / ONS, nearest year) and meant for orders of magnitude, not precise scoring. All figures are weighted aggregates only — no microdata is included.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.