Skip to content

Comments

Modernise OG-UK: UK calibration, GS tax functions, real-world mapping#62

Open
nikhilwoodruff wants to merge 12 commits intomainfrom
modernize-policyengine-api
Open

Modernise OG-UK: UK calibration, GS tax functions, real-world mapping#62
nikhilwoodruff wants to merge 12 commits intomainfrom
modernize-policyengine-api

Conversation

@nikhilwoodruff
Copy link
Collaborator

@nikhilwoodruff nikhilwoodruff commented Feb 24, 2026

Summary

Overhauls OG-UK to use properly sourced UK parameters and a clean functional API. Supersedes #61 (moved from fork to fix CI secrets).

UK macro calibration: replaces US defaults with ONS/OBR/GOV.UK data (debt ratio, government spending, tax rates, state pension age, etc.). Fiscal parameters calibrated to OBR November 2025 EFO: steady-state debt at 95% of GDP, revenue/GDP ~39%, fiscal adjustment from period 4 matching UK fiscal rules. Tax instruments include wealth tax proxy (council tax, stamp duty, CGT), effective indirect taxes (VAT + excise), and corporation tax inclusive of business rates. Includes oguk/sources.py for live ONS/BoE fetching with hardcoded fallbacks.

GS tax functions: switches from DEP to Gouveia-Strauss, estimates ETR only and reuses params for MTR (the GS MTR is the analytical derivative of ETR). Fixes two critical bugs: PolicyEngine Policy.__add__ silently dropping parameter_values when simulation_modifier present, and GS MTR estimation instability from separate optimisation.

Real-world mapping: map_to_real_world(baseline, reform) anchors model-unit SS changes to actual UK aggregates from ONS using GDP-scaled £bn changes.

CI modernisation: replaces five legacy workflows (conda, Python 3.9, black, 3-OS matrix) with a single uv-based workflow on Python 3.13 using ruff for linting and formatting.

Steady-state results for 1pp basic rate rise (20% to 21%):

Variable Baseline (£bn) Reform (£bn) Change (£bn) %
GDP 2,891 2,887 -3.3 -0.11%
Consumption 1,477 1,468 -8.6 -0.58%
Investment 540 537 -2.5 -0.47%
Government 604 611 +7.0 +1.16%
Tax revenue 859 866 +7.4 +0.87%
Debt 2,685 2,682 -3.1 -0.12%

Interest rate moves from 5.09% to 5.13%.

Test plan

  • Baseline and reform SS both converge cleanly (resource constraint ~1e-14)
  • uv run python examples/run_oguk.py produces baseline + reform SS with real-world £bn table
  • from oguk import calibrate, solve_steady_state, map_to_real_world imports cleanly
  • CI passes (lint + tests)

nwoodruff-co and others added 11 commits February 23, 2026 23:16
…erface

- Replace setup.py/environment.yml with pyproject.toml (uv/hatch)
- Create clean pydantic-based API in oguk/api.py:
  - CalibrationResult and SteadyStateResult models
  - calibrate(start_year, years, policy) function
  - solve_steady_state(start_year, policy, max_iter) function
- Support Policy objects for reform scenarios
- Remove pandas_datareader dependency (hardcode UK infant mortality)
- Update example to use new API
- Simplify tests to use new calibrate() function
rho from calibrate() was 1D (S,) but OG-Core expects 2D (T+S, S).
Tile at source in calibrate() rather than in each consumer. Also
handle SS outputs that are 1D arrays rather than scalars.

Co-Authored-By: Claude <noreply@anthropic.com>
Add scripts/refresh_calibration.py which scrapes ONS, Bank of England,
and GOV.UK for UK-specific fiscal and economic parameters. Key changes:

- Debt ratio from ONS (0.93 vs old 0.78), gov spending shares from ONS
- Corporation tax 25%, employer NICs 15%, effective VAT 10%, IHT 8%
- Retirement age 66, productivity growth 1% (OBR), real rate 1.75% (BoE)
- Remove 9 US-specific Social Security parameters (AIME/PIA)
- Fix rho, r_gov_scale, r_gov_shift, replacement_rate_adjust formats
- solve_steady_state() and run_oguk.py now load UK defaults JSON
- Steady state converges with positive government spending

Co-Authored-By: Claude <noreply@anthropic.com>
…finitions

The DEP (12-parameter) tax function was producing wildly unstable results
for reform comparisons — a 1p basic rate change produced ~20% output
swings due to the optimizer landing in different local minima.

Root causes:
- OG-Core's tax_data_sample drops observations with capital income < £5,
  removing ~75% of UK microdata (most people have no capital income)
- The DEP functional form is over-parameterised and poorly identified
  for UK data, producing flat ~6% ETRs vs actual 8-36% progressive rates
- Only savings_interest_income was used as capital income (mean £148),
  missing dividends (£1,057), pensions (£2,915), and property (£403)

Fixes:
- Switch from DEP to GS (Gouveia-Strauss) tax function type, which has
  3 parameters and correctly captures UK progressive tax rates
- Use global optimisation (differential evolution) for deterministic,
  stable parameter estimation
- Broaden capital income to include dividends, private pensions, property,
  and savings interest
- Add self-employment income to labour income
- Preserve zero-capital-income observations with small random floor
  instead of dropping them
- Custom data cleaning that retains ~57k of 92k obs (vs 21k with OG-Core)

GS ETR comparison for 1p basic rate reform now shows correct direction
(+0.4-0.6pp) and magnitude across the income distribution.

Co-Authored-By: Claude <noreply@anthropic.com>
1. PolicyEngine Policy combination bug: when combining a reform Policy
   (with parameter_values) and a perturbation Policy (with
   simulation_modifier), the parameter_values were silently dropped.
   This meant reform MTRs were computed under baseline tax rates,
   producing near-zero MTR data. Fix: apply reform parameters directly
   inside the simulation_modifier using the TBS parameter tree.

2. GS MTR estimation instability: separately estimating MTRx/MTRy
   parameters with differential_evolution produced wildly different
   results for nearly identical data (phi0 jumping from 0.65 to 0.04
   for a 1pp tax change). Fix: use ETR parameters for all three
   (ETR, MTRx, MTRy). This works because the GS MTR formula is the
   analytical derivative of the GS ETR — same 3 params give
   mathematically consistent rates.

Before: 1pp basic rate rise showed ~11.5% output change.
After: 1pp basic rate rise shows -0.12% output, +0.93% revenue.

Co-Authored-By: Claude <noreply@anthropic.com>
New map_to_real_world() function scales model-unit SS changes to actual
UK aggregates from ONS. Uses GDP-anchored scaling (single scale factor
from real GDP / model GDP) rather than per-variable percentage changes,
which avoids blow-up when model variables like G are near zero.

Co-Authored-By: Claude <noreply@anthropic.com>
The model was using US Social Security replacement rates (90%/32%/15%
PIA brackets), producing pension outlays of 15.9% of GDP. Combined with
alpha_T=0.15 (total social protection), mandatory spending exceeded
revenue and G was forced negative.

Fix: set replacement_rate_adjust=0.35 to match UK state pension spending
(~5.5% of GDP) and alpha_T=0.04 for non-pension transfers only (UC,
housing benefit, disability). G is now +18.5% of GDP, consistent with
the alpha_G=0.209 calibration from ONS.

Co-Authored-By: Claude <noreply@anthropic.com>
Recalibrate tax and fiscal parameters so model revenue/GDP matches OBR
forecasts (~40% of GDP). Add wealth tax proxy for council tax/SDLT/CGT,
align capital depreciation with economic depreciation, set steady-state
debt ratio to 95%. Replace five legacy CI workflows with single uv-based
Python 3.13 workflow using ruff for linting and formatting.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@nikhilwoodruff
Copy link
Collaborator Author

Hey @jdebacker - I think this moves the repo to a good place for solving the steady state. Think a bit more work is needed on TPI. I've also added the HUGGING_FACE_TOKEN to the repo secrets

…up instructions

Co-Authored-By: Claude <noreply@anthropic.com>
@nikhilwoodruff
Copy link
Collaborator Author

Also just putting here a plot of the microdata/GS fit
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants