model 01 — overlapping generations · OG-UK
Eighty cohorts. One economy.
OG-UK is an overlapping-generations model of the UK economy for dynamic general-equilibrium analysis of fiscal policy. It builds on OG-Core — which holds the theory and logic of a general OG model — with a UK calibration sourced from ONS, OBR, Bank of England, and GOV.UK data. The model outputs changes in macroeconomic aggregates (GDP, investment, consumption), wages, interest rates, and tax revenues over time.
One command, no clone.
OG-UK is a Python package — pip installs it straight from GitHub.
The microdata is fetched on first run (set
HUGGING_FACE_TOKEN to run non-interactively, e.g. in CI).
pip install git+https://github.com/PSLmodels/OG-UKinstall into a virtual environment (python3 -m venv .venv) to keep the dependencies isolated. Python 3.11+.
Score a reform in four steps.
The whole scoring loop is: build a reform, solve baseline and reform, map to £bn. Here is the complete program for raising the basic rate of income tax from 20% to 21%.
from datetime import datetime
from policyengine.core import ParameterValue, Policy
from policyengine.tax_benefit_models.uk import uk_latest
from oguk import solve_steady_state, map_to_real_world
# 1. build a reform from PolicyEngine parameters
param = uk_latest.get_parameter("gov.hmrc.income_tax.rates.uk[0].rate")
reform = Policy(
name="Basic rate 21%",
parameter_values=[
ParameterValue(parameter=param, value=0.21,
start_date=datetime(2026, 1, 1))
],
)
# 2. solve baseline and reform steady states (~5-15 min each)
baseline = solve_steady_state(start_year=2026) # no policy = baseline
reform_ss = solve_steady_state(start_year=2026, policy=reform)
# 3. map model units to real-world £bn
impact = map_to_real_world(baseline, reform_ss)
# 4. read the results
print(f"GDP change: {impact.gdp_change:+.1f}bn ({impact.gdp_pct:+.3f}%)")
print(f"Tax revenue change: {impact.tax_revenue_change:+.1f}bn")
print(f"Interest rate: {impact.r_baseline:.2%} → {impact.r_reform:.2%}")Reforms are statute, not stylized wedges.
A reform is a PolicyEngine Policy object — actual
parameters of the UK tax and benefit system. OG-UK uses
PolicyEngine-UK, an open-source microsimulation model
built on the Family Resources Survey, HMRC's Survey of Personal Incomes,
the Living Costs and Food Survey, and the Wealth and Assets Survey. It
computes each filer's effective tax rate and marginal tax rates on
labor and capital income under the reform, then OG-UK fits parametric
tax functions to that microdata — so the model sees the reform's true
shape across the income and age distribution, not a single stylized
wedge.
| parameter path | what it is |
|---|---|
gov.hmrc.income_tax.rates.uk[0].rate | basic rate (20%) |
gov.hmrc.income_tax.rates.uk[1].rate | higher rate (40%) |
gov.hmrc.income_tax.rates.uk[2].rate | additional rate (45%) |
gov.hmrc.income_tax.allowances.personal_allowance.amount | personal allowance (£12,570) |
gov.hmrc.national_insurance.class_1.rates.employee.main | NI main rate |
gov.hmrc.national_insurance.class_1.rates.employee.higher | NI upper rate |
Stack several ParameterValues in one Policy
to score a package — a rate rise plus an allowance change scores as one
combined reform.
Following the method of DeBacker et al. (2019), OG-UK fits tax
functions that are monotonically increasing in labor and capital
income — which keeps each household's budget set convex and the lifetime
optimization well-behaved — separately for the effective rate and the
two marginal rates, for each age and each of the first ten years. The
age_specific and tax_func_type options
(section 04) control how finely those functions are estimated.
param_overrides={"cit_rate": [[0.26]]} (see section 05).
Three dials: mode, tax functions, sectors.
| dial | choices | trade-off |
|---|---|---|
| mode | solve_steady_state() · run_transition_path() |
long-run answer in ~5–15 min · full year-by-year path in ~60–90 min |
| age_specific | "pooled" · "brackets" · "each" |
one tax function for all ages (fastest, most stable) · one per age group · one per age (80 functions, slowest) |
| multi_sector | False · True |
one production sector · eight SIC-calibrated industries (energy, manufacturing, …); needed for sector-level questions like energy price shocks |
# first pass: fast and stable
ss = solve_steady_state(start_year=2026, policy=reform)
# more accurate tax functions, 8 industries
ss = solve_steady_state(start_year=2026, policy=reform,
age_specific="brackets", multi_sector=True)
start with pooled + 1-sector; scale up only once the fast run
answers your question. non-converging solve? raise max_iter
(default 250), fall back to pooled, and check whether the
reform is extreme — OLG models can diverge on very large shocks.
Beyond statute: TFP, corporation tax, spending.
Both solvers accept param_overrides — a dict of OG-Core
parameters applied on top of everything else. Use it for shocks that
aren't tax-and-benefit statute.
# corporation tax cut (27% effective → 25%)
ss = solve_steady_state(param_overrides={"cit_rate": [[0.25]]})
# productivity: +0.4% TFP level shock
ss = solve_steady_state(param_overrides={"Z": [[1.004]]})
# combine a statutory reform with a structural shock in one run
ss = solve_steady_state(policy=reform,
param_overrides={"cit_rate": [[0.25]]})Z (TFP) for productivity shocks — never
g_y_annual, which is the balanced-growth normalisation the
model detrends by, not a productivity lever.
The fiscal-year profile a budget needs.
The steady state answers "where does the economy settle?" The transition path answers "what happens each year on the way there" — 60 years of it, solved for baseline and reform in one call. It runs on Dask workers, so pass a client.
from dask.distributed import Client
from oguk import run_transition_path, map_transition_to_real_world
client = Client(n_workers=4, threads_per_worker=1)
base_tp, reform_tp = run_transition_path(start_year=2026, policy=reform,
client=client)
client.close()
impact = map_transition_to_real_world(base_tp, reform_tp)
# impact.years → ["2026-27", "2027-28", ...]
# impact.gdp_change[t] → £bn change from baseline in year t
# same arrays for consumption, investment, revenue, debtEverything maps back to £bn.
map_to_real_world() returns a MacroImpact with,
for each aggregate, the reform level, the change from baseline, and the
percent change — all current-price £bn — plus baseline and reform
interest rates.
.gdp/.gdp_change/.gdp_pct.consumption,.investment,.government— same triple each.tax_revenue,.debt— same triple each.r_baseline,.r_reform— steady-state interest rates