UK bonus sacrifice: meet OBR's base at the final weights, not the stage's
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 48/100
Research direction
Read the salary_sacrifice stage and convert_bonuses_to_bonus_sacrifice, then trace how calibration produces the final weights and check whether annual allowance tests read the pension contributions this step changes. Re-run the selection after calibration with the same identity-keyed order and £13.0bn anchor; preserve the stage selection where the post-calibration step does not run. Done means the final-weight projection meets OBR's base without moving calibrated income or violating any bound row.
Written by the indexing model from the issue text.
Description
Problem
The salary_sacrifice stage imputes pension_contributions_via_bonus_sacrifice (#1340, #1383). It picks bonus sacrificers in the order of identity-keyed draws, until the engine's projection of the sacrificed bonuses above the £2,000 threshold at 2027-28 is nearest OBR's base. That base is £13.0bn (OBR supplementary forecast information, 5 February 2026, Table 1.1).
The stage measures the projection at its own weights, before calibration. On #1383's review arm, the base at the final weights falls short of OBR's line by more than a quarter. The column is the same in both files; only the weights move. Vahid flagged the risk in his review of #1383 (item 7).
Why
Calibration to the HMRC SPI income-band rows lowers the weights of the highest earners, and the bonus sacrificers sit mostly there:
- a candidate must be paid at least the upper earnings limit;
- the larger bonuses belong to the higher earners;
- most of the sacrificers are rows of the SPI channel, and calibration lowers those the most.
No calibration row reads the bonus base, so nothing holds it. The salary route does not drift this way, because the HMRC salary-sacrifice rows bind it. The figures behind this diagnosis rest in part on the SPI channel's households, so they stay with the licensed data.
Proposal
Re-pick the bonus sacrificers after calibration, at the final weights. This is a post-calibration step that reruns convert_bonuses_to_bonus_sacrifice on the calibrated weights, with the same identity-keyed order and the same anchor.
- The sacrifice sits beside pay, so no calibrated income moves.
- The £2,000 NICs cap reads it only from April 2029.
- First, check that no bound row reads the pension contributions it adds to, for example through the annual allowance tests.
- Where the step does not run, keep the stage's selection.
The alternative is to bind the base during calibration, as a row deflated to 2025 by the anchor's growth factors. That needs a ruling, since the build refuses facts dated after the calibration year (_refuse_fact_after_target).
Until then
#1383's Phase B and C receipts record that the base holds at the stage's weights only. A costing of the bonus-sacrifice cap on a release built before this lands would understate the base.
- Dominant language
- Python
- Stars
- 0
- Forks
- 6
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 132
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from PolicyEngine/microcosm
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
PolicyEngine/microcosm#1220 ·
Maintainers usually reply within 1 day
-
Update two stale comments about the export-mass reviewed-exclusion registerPossibly taken A pull request linked to this issue is open or already merged. Open
Difficulty 1/5 Under an hour Newbie friendliness 88/100
PolicyEngine/microcosm#1108 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
PolicyEngine/microcosm#475 ·
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
PolicyEngine/microcosm#1406 ·
Maintainers usually reply within 1 day
-
Orrery publication doc still names the closed calibration-diagnostics#208 as a prerequisitePossibly taken @anth-volk claimed this today. Open
Difficulty 1/5 1-3 hours Newbie friendliness 25/100
PolicyEngine/microcosm#1402 ·
Maintainers usually reply within 1 day
All issues in PolicyEngine/microcosm
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
scikit-learn-contrib/category_encoders#553 ·
Maintainers usually reply within 1 day
-
tests/strategies_test.py:174 goes uncovered on about 1 run in 100, failing the coverage ratchetOpenflaky-watch
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
btclib-org/btclib#2839 ·
Maintainers usually reply within 1 day
-
BarrelList constructor drops false-valued iterable contentsPossibly taken @Gambit-Checkmate claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Maintainers usually reply within 19 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 77/100
vllm-project/vllm#63863 ·
Maintainers usually reply within 1 day