Modernise OG-UK: UK calibration, GS tax functions, real-world mapping#62
Open
nikhilwoodruff wants to merge 12 commits intomainfrom
Open
Modernise OG-UK: UK calibration, GS tax functions, real-world mapping#62nikhilwoodruff wants to merge 12 commits intomainfrom
nikhilwoodruff wants to merge 12 commits intomainfrom
Conversation
…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>
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>
Collaborator
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

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.pyfor 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 droppingparameter_valueswhensimulation_modifierpresent, 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%):
Interest rate moves from 5.09% to 5.13%.
Test plan
uv run python examples/run_oguk.pyproduces baseline + reform SS with real-world £bn tablefrom oguk import calibrate, solve_steady_state, map_to_real_worldimports cleanly