PolicyEngine / PolicyEngine/microcosm

UK scorecard: populace_uk_2023 vs enhanced_frs_2024_25 vs admin benchmarks (2026)

Open
#731 1 comment 0 reactions 1 assignee View on GitHub

@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_datasetsSimulation 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:

  1. 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.
  2. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.