Make the sector TFP residual unit-invariant in K and L - #81
vahid-ahmadi wants to merge 1 commit into
Conversation
_sector_tfp backed out sector TFP as a Solow residual, dividing GVA by a CES aggregator with K in £m (ONS CAPSTK) and L in thousands of jobs (ONS JOBS02). Under CES with epsilon != 1 the two inputs are summed inside the aggregator, so their measurement units set their relative weight and the resulting Z dispersion was a measurement artefact: expressing L in jobs gave a 3.79x spread and K in £ gave 2.17e6x, against 25.2x as shipped. The Cobb-Douglas branch was not scale-free either, since K^gamma_m carries a sector-specific exponent. Normalise K and L to dimensionless model units — each as a share of its own aggregate — before they enter the aggregator, in both branches. Z is now exactly invariant to rescaling either input. The existing GVA-weighted output normalisation is unchanged, so Z still has GVA-weighted mean 1. Spread falls from 25.2x to 4.39x. Also add optional capital/labour arguments so the invariance is testable, and correct the contradictory epsilon-shrinkage comments: no shrinkage is applied to epsilon anywhere. Epsilon and gamma values are unchanged. Fixes PSLmodels#71 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Correcting an overclaim in the description above, flagged by an independent review round. The PR says the fix means multi-sector TFP is "calibrated from the data rather than from the units", and calls the residual 4.39x spread "plausible genuine dispersion". The first half is right; the second overstates what this establishes. Unit-invariance is not the same as convention-independence. Under CES with epsilon != 1 the dispersion depends on the relative level of k to l, and "each input as a share of its own aggregate" pins that at exactly 1.0 — which is a convention, not a measurement. Holding the ONS data fixed and varying only that relative level:
The last row essentially reproduces What the fix does establish, and what still matters: on What it does not establish is that 4.39x is the economically correct dispersion. Note in particular that the shares convention pins the effective k/l at 1.0 while the model solves at an aggregate K/L of about 4.98 (from So the accurate framing is: this replaces an indefensible implicit normalisation with an explicit, unit-invariant one. The remaining dispersion is still convention-dependent and not yet consistent with the model's own K/L. I'd rather state that than leave the stronger claim standing. Two smaller points from the same review, both fair:
|
|
@jdebacker Would you have time to look at this one? It is ready and has been sitting since 31 August. Short version: This changes M=8 model output, which is the reason it deserves a look rather than a rubber stamp. One honest caveat I added in a comment below: unit-invariance is not convention-independence. The shares normalisation pins the effective k/l at 1.0, while the model itself solves at an aggregate K/L of about 4.98 — and varying only that relative level moves the spread across 3.64x / 4.39x / 6.95x / 25.23x. So this replaces an indefensible implicit normalisation with an explicit, unit-invariant one; it does not establish that 4.39x is the economically right answer. A model-consistent normalisation would be a fixed point and is a larger piece of work. The |
Fixes #71.
Why
_sector_tfpbacks out sector TFP as a Solow residual, dividing GVA by a production aggregator withKin £m (ONS CAPSTK) andLin thousands of jobs (ONS JOBS02). Neither branch of that calculation is scale-free:epsilon != 1): K and L are summed inside the aggregator, so the units they happen to be measured in set their relative weight.epsilon == 1, which_EPSILON[2]hits):K^gamma_mcarries a sector-specific exponent, so a common rescaling of K does not cancel in the GVA-weighted normalisation either.The consequence is that the cross-sector TFP dispersion — the entire economic content of the calibration — was an artefact of a measurement choice:
What changes after merging
Multi-sector (
M=8) TFP is calibrated from the data rather than from the units, andZbecomes invariant to rescaling either input:[2.611 0.538 0.416 0.220 4.820 0.191 0.343 0.420][1.302 0.935 1.062 1.045 2.268 0.604 0.517 0.630]Real Estate remains the outlier but falls 4.82 → 2.27, and the six non-outlier sectors collapse into ~0.52–1.30. The residual 4.4x is plausible genuine dispersion — Real Estate's imputed-rent GVA against a dwelling-dominated capital stock is a real feature of the data.
This changes M=8 model output. That is the point of the fix, not a side effect, but any saved multi-sector baselines are now stale.
multi_sector=False(the default) is untouched — this dict only loads under M=8.Change
Each input is converted to a share of its own aggregate before entering the aggregator, applied before the loop so it covers both the CES and Cobb-Douglas branches. Output normalisation is unchanged, so
Zstill has GVA-weighted mean 1 (verified: exactly 1.0). Epsilon and gamma values are untouched.Optional
capital=/labour=arguments were added purely so the invariance is testable — the module globals are otherwise unreachable from a test.Also fixes two contradictory comments in the same file:
industry_params.py:157-158claimed epsilon shrinkage was applied "inget_industry_params()" while theget_industry_paramsdocstring claimed it was "applied in_EPSILONvalues". Neither was true — line 354 is a plainepsilon = list(_EPSILON). Both now state what the code does. The gamma shrinkage is real and documented as before.Evidence
oguk/tests/test_industry_params.py, 8 tests in 0.15s. The key one is parametrised over rescaling K alone, L alone, and both, using both the real unit-conversion factors (1e6, 1e3) and arbitrary non-round ones (137.0, 0.017), assertingrtol=1e-12. Measured after the fix:Verified to fail on the pre-fix code. Also tested: GVA-weighted mean is 1, and
get_industry_params()["Z"][0]matches_sector_tfpwith the shipped arguments.ruff format --checkandruff checkclean. Two failures intest_get_micro_data.pyare pre-existing onmain—DatasetMaterializationErrorfrom a missingHUGGING_FACE_TOKEN, unrelated to this change (confirmed against main).