Add opt-in flags to estimate MTRs against engine data and to carry NICs as payroll - #82
vahid-ahmadi wants to merge 1 commit into
Conversation
Two flags on calibrate() / solve_steady_state() / run_transition_path(), both defaulting to today's behaviour so no existing result moves: * estimate_mtrs: estimate mtrx against mtr_labinc and mtry against mtr_capinc via ogcore.txfunc.txfunc_est (GS, same cleaning pipeline, same global optimiser), instead of reusing the ETR fit verbatim for both. The engine's finite-difference MTR columns now enter the objective, and labour and capital can diverge. * separate_payroll: NICs fill payroll_tax_liab and frac_tax_payroll is computed from the data instead of being hard-zero, so NICs are visible to OG-Core's revenue accounting as a distinct instrument. NICs stay inside the estimated ETR in both modes: OG-Core's ETR function is a combined income-tax-and-payroll schedule (tax.income_tax_liab adds only the separate flat tau_payroll on top) and frac_tax_payroll is an accounting split of the combined liability (aggregates.py:416, 436). Stripping NICs from the ETR numerator would delete them from the household budget constraint. Extracts _payroll_split(), _fit_gs(), _fit_gs_triple() and _liability_columns() so the pieces are testable without a live calibration; production paths use them. Threads estimate_mtrs through the bracket estimator too, so age_specific="brackets"/"each" honour it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@jdebacker This one is also ready for review whenever you have time. It adds two opt-in flags, both defaulting to today's behaviour, so nothing existing moves:
One thing worth stating plainly, because my own issue #80 had it wrong: this does not remove NICs from the ETR numerator. OG-Core's estimated ETR is a combined income-tax-and-payroll schedule by design ( Flag-off equivalence is measured, not just argued: bit-identical to main through both the pooled and bracket estimators, including the bracket-to-age mapping refactor. The caveat I would want a maintainer's eye on: the flag-on paths are exercised only against synthetic data, and there is a prior warning in the code that separate MTR estimation can land the optimiser in distant basins. Anyone publishing numbers from
|
Addresses #80. Both changes are behind flags defaulting to today's behaviour, so no existing result moves.
Why
The engine's MTRs are computed and then discarded.
calibrate()estimates a Gouveia-Strauss function on the ETR only, then reuses those parameters verbatim asmtrx_paramsandmtry_params. Themtr_labinc/mtr_capinccolumns cost an extraSimulationper income type plus the_person_mtrincidence correction, and never enter the objective — they affect the fit only through the cleaning quantile filters. Labour and capital therefore share an identical marginal schedule, which is wrong for the UK: dividend rates, the savings allowance and the PSA all differ from earned-income treatment.Payroll revenue is mis-attributed.
frac_tax_payroll = 0.0, so the revenue decomposition assigns all NICs revenue to income tax.What changes after merging
Nothing, by default. Two opt-in flags become available:
estimate_mtrs=True—mtrx_paramsfitted againstmtr_labinc,mtry_paramsagainstmtr_capinc, so labour and capital can diverge. Also threaded into_estimate_bracket_tax_functions, soage_specific="brackets"/"each"honour it rather than silently ignoring it.separate_payroll=True—payroll_tax_liabcarries person-level NICs,total_tax_liabcomes from the engine columns rather than the clipped-ETR imputation, andfrac_tax_payrollis computed as Σw·NICs / Σw·(IT+NICs).Two booleans rather than one enum: the changes are independent and each needs to be attributable on its own.
Important: NICs are NOT removed from the ETR numerator
#80 originally proposed stripping NICs out of the ETR base. That would be wrong, and I've corrected the issue.
OG-Core's estimated ETR is a combined income-tax-and-payroll schedule by design (
aggregates.py:497-498: "Payroll taxes are embedded in the income and payroll tax functions, so revenue is a fraction of the combined revenue").frac_tax_payrollis used only inaggregates.revenueto split combined revenue for reporting, and OG-Core's own pipeline computes the same ratio with payroll inside the denominator (txfunc.py:1083). Removing NICs from the numerator would delete them from the household budget constraint unlesstau_payrollwere separately calibrated — and the UK NIC schedule is not flat, so one rate cannot represent it.So
separate_payrollchanges the split, not the base._payroll_split's docstring states what the fraction is of, over which sample, for which year.Change
oguk/api.py(+286/−69)._MicroDatagains adult-maskedincome_taxandnational_insurance. New helpers, all on the production path:_payroll_split,_fit_gs,_fit_gs_triple,_liability_columns, plusTXFUNC_COLS/GS_NUMPARAMS. These also remove the duplicated frac / column /txfunc_estblocks between the pooled and bracket estimators. CHANGELOG entry included.Evidence
oguk/tests/test_tax_function_flags.py— 14 tests in 8.2s, nocalibrate()or_build_specs()call. Flag presence and defaults on all four entry points;_liability_columnsin both modes;_payroll_splitcomputed-not-hardcoded, zero under default columns, zero-total guard; both estimators end-to-end on a 1,200-row synthetic frame with deliberately steeper labour than capital MTRs.The divergence, which is the point of the first change:
ruff format/ruff checkclean. Full suite: 25 passed, 3 skipped, plus 2 pre-existingtest_get_micro_data.pyfailures (DatasetMaterializationError, missingHUGGING_FACE_TOKEN) — confirmed present on unmodifiedmain.What is not verified
seed=1is deterministic, but I cannot show that the realmtr_capincdistribution — heavy zero mass, clipped to [0,1] by_person_mtr— gives a sensible GS fit. Anyone publishing numbers fromestimate_mtrs=Trueshould eyeball the fitted schedules first.frac_tax_payrollis uncomputed (no HF access here). Expect roughly NI/(IT+NI) ≈ 0.2–0.25; the definition cannot exceed OG-Core's validator cap of 1.0._liability_columns(md, False)returns exactlyetr*market_incomeand zeros, and_fit_gs_triple(..., False)returns the same ETR array three times, matching the previous triple-return.