Repository navigation
Project savings interest by interest received per person - #2638
vahid-ahmadi wants to merge 5 commits into
Conversation
ONS HAXV outturn to 2025, then OBR household financial assets growth times the Table 1.9 deposit rate ratio, net of ons.population growth. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Aggregate impact on certified data: passBase Checks: 0 broken by this change, 1 failing on both sides, 0 fixed. Moves flagged for review: 159 (thresholds in Notes
Moves flagged for review
119 more flags are in the report artifact. Receipts by tax head
Spending by benefit and service
Caseloads
Government balance, each component counted once
Model totals
Household income totals
Poverty rates
People in poverty
Inequality
Mean household net income in each side's own deciles
Standard reform costings (change in gov_balance)
Standard reforms: change in each reform's target
Household net income by decileMean change in household net income per household, and as a share of the decile's base income; households stay in the base run's deciles.
No change in any year: population. Checks failing on both base and head (1)
Counted-once checkEach of 91 components was raised by £1 per unit in 2026-27, and every head of its totals was read household by household: its own head must rise by exactly £1, no other tax head may move, no other benefit may rise, and the model's own total must move by what its heads moved. A head that breaks a rule is measured again twice: with every other head held at its baseline value, and with every other head held at its value after the first £1 while the component rises again. It fails where either still shows the response. 0 failed; 0 were skipped.
Provenance
Dataset: Receipts by tax head: head levels
Spending by benefit and service: head levels
Caseloads: head levels
Government balance, each component counted once: head levels
Model totals: head levels
Household income totals: head levels
Population: head levels
Poverty rates: head levels
People in poverty: head levels
Inequality: head levels
Mean household net income in each side's own deciles: head levels
Standard reform costings (change in gov_balance): head levels
Standard reforms: change in each reform's target: head levels
|
|
@MaxGhenis this fixes two real errors in savings interest uprating, so I think it should go in. Two calls for you:
Merge now, or hold until after the Budget on 28 Oct so Budget scores don't shift under people?
The two errors it fixes: 2025 used OBR's +3.5% forecast where ONS outturn (HAXV, 30 Sep 2026) is -2.8%, and the aggregate series was applied on top of population growth, so population was counted twice (as #2067 fixed for council tax). 🤖 Generated with Claude Code |
Resolve the conflict in yoy_growth.yaml: keep main's March 2026 population references (#2611) and this branch's household interest block. Main's population vintage changed ons.population, so re-derive ons.per_capita.household_interest_income from it as (1 + aggregate) / (1 + population) - 1, which the branch's own test requires, and update the 2025 test case to match (per-person factor 0.9695, was 0.9654). The aggregate HAXV series is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged main into this branch ( One knock-on: #2611 changed Locally: @MaxGhenis the two questions above (merge before or after the 28 Oct Budget, and the financial assets proxy) are still open. 🤖 Generated with Claude Code |
Brings in #2589 (ISA inputs), #2639 (rail per person) and #2650. uprating_indices.yaml keeps both per-person blocks (interest and rail journeys). non_cash_isa_cash_interest moves with savings_interest_income to the per-person interest index, so KNOWN_INDEX_OVERRIDES stays empty. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
All savings interest now uses ons.per_capita.household_interest_income: non_cash_isa_cash_interest (declaration and dataset YAML), the additive ISA-interest total (dataset projection and Core fallback), and gross interest under the legacy contract. #2589 had left these on GDP per capita or the aggregate ONS index pending this PR. Tests that controlled interest through GDP growth now set the interest index; the column test uses different GDP and interest rates so a wrong index fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged main into this branch ( Conflicts. ISA alignment (the follow-up owed after #2589). All savings interest now grows by
The last one matters most. #2589 grew legacy-contract gross interest by GDP per capita in code, which overrode this PR's YAML entry. The certified enhanced_frs_2024_25 uses the legacy contract, so after the merge this PR would have done almost nothing on it ( Tests run locally: CI: all green at Not approved yet, so not queued. The two questions for @MaxGhenis above (timing against the 28 Oct Budget, and the financial assets proxy) are unchanged. 🤖 Generated with Claude Code |
juaristi22
left a comment
There was a problem hiding this comment.
Program review
Base repository: PolicyEngine/policyengine-uk
PR number: 2638
Reviewed head SHA: caba141
Merge base SHA: 49a7590
Mode: full
Scope: 13changedpaths: savings-interest per-person projection/index generation, affected ISA/direct/dataset paths, source values and tests/docs. Changed behavior+affecteddependencies; no privatepopulation/numerical or combined-main audit.
Source manifest: /private/tmp/policyengine-command-runs/881387a1b745/pr2638-external-review-20261026-review-sources.json
Review status: PARTIAL
Source Documents
Critical
C1 — P2: give the cumulative interest index an anchor before its first growth year (OPEN)
Location: policyengine_uk/parameters/gov/economic_assumptions/yoy_growth.yaml:1155 (new first per-person observation); affected declaration policyengine_uk/variables/input/savings_interest_income.py:20.
The new YoY series starts in2021 with21.94% growth, but create_economic_assumption_indices.py:40–45 initializes the first listed year to1.0 and only multiplies growth starting the following year. Its cumulative twin therefore has I[2021]=1, and the2021 growth is never incorporated. Parameter processing generates that index before backdating (tax_benefit_system.py:207,228); backdate_parameters extends its earliest value1 back to2015 (utils/parameters.py:31–46, with utils/dated_reforms.py:208). Thus I[2020]=I[2021]=1.
A concrete affected route is a legacy-origin single-observation multi-year dataset with annual2020 gross savings interest100 and zero exempt interest, queried for2021 without materializing intermediate projection years. UK Simulation._run_formula reconstructs gross, reads variable.uprating, and multiplies by the target/source index ratio (simulation.py:639–687). With this new declaration, that source path gives100×1/1=100. The same2020→2021 transition in ordinary dataset projection reads the new YoY0.2194 and gives121.94 (data/economic_assumptions.py:358–386). The new index is intended to align these two surfaces, but they diverge at the first supported growth interval. This is a static source trace, not an executed model result.
Provide a2020 cumulative anchor so the first2021 YoY change is represented, or explicitly handle/reject that boundary if it is intentionally unsupported. Add a2020→2021 regression comparing direct/fallback index carry with the YoY dataset route; do not fabricate an unqualified2020 population-growth observation to make the generator start earlier. The new tests inspect the2021 YoY scalar, but their weighted example starts2024 and the direct-input tests start2026, so neither detects the dropped first interval.
Coordinator assessment: Named CODE clarification confirmed a supported UKMultiYearDataset single2020observation route, with no autoextension and no2020exclusion. The diff moves savings from the old2020-anchored aggregate index (122.50) to the new2021-anchored per-person index (100), instead of the intended121.94. With zero exemptions/dividends capital_income is100 rather than121.94, and the error feeds household_market_income. This is a confirmed source-traced material income defect, classified CRITICAL under the workflow; conventional priorityP2 identifies the historical boundary. The original should-address category is superseded by the preserved named reachability/severity addendum. No exact tax/net total or executed reproduction is claimed. POLICY independently corroborates the2020/2021ONSlevels supporting the intended2021step.
Should Address
None.
Suggestions
S1 — refresh the stale population example and vintage text in the PR description (OPEN)
The captured body line7 still uses0.72% population growth and−3.46% per-person interest in2025, while the changed parameter and test use0.30% and−3.05% (yoy_growth.yaml:1051,1159). Body line42 calls the population series a March2025 vintage; the current parameter describes the March2026 OBR/ONS2024-based source. Updating these paragraphs would make the prose match the committed inputs. The author has disclosed older population figures in feedback, so this is a prose consistency suggestion, not a separate model-defect inference.
S2 — update the agent-facing dataset guide's old ISA growth rule (OPEN)
docs/engineering/skills/dataset-sources.md:71–74 still says additive ISA interest follows the dataset GDP-per-capita rule. The changed implementation and book now use the per-person interest series. Update that sentence so future maintenance does not restore the old mapping. The legacy/additive representation distinction remains valid.
Evidence Gaps
- Full primary March2026 OBR economy tables1.9/1.11 were unavailable to corroborate exact2025–2030 deposit-rate/financial-assets cells, calendar headers, units and series scope. The literal distinct economy download returned NoAccess HTML, not an XLSX; arithmetic over quoted constants is conditional, not cell/source proof.
- Role checks incomplete: /private/tmp/policyengine-command-runs/881387a1b745/pr2638-external-review-20261026-review-policy.md; see recovered report.
Notes
- One confirmed implementation finding is counted once; POLICY source evidence complements CODE C1. Its source-traced supported2020→2021 case changes capital income. OriginalCODE P2should classification and the named69.856s correction are both retained; P2priority is distinct from the canonical CRITICAL category. No general second review or runtime was performed.
- Two minor consistency suggestions concern stale PR-body population/vintage prose and the agent-facing dataset guide. Existing author comments acknowledge the changed population inputs; no current-model old-population defect is inferred.
- ONS HAXV full official original independently supports S.14/D.41/current-price£m/NSA and2019–2025levels. Per-person conversion and ISA alignment were traced. The broad-financial-assets-as-deposits proxy and Budget landing timing remain Max decisions; this review selects neither.
- Two usable originals: a previously qualified historical OBR landing page reused only for literal link discovery and a fresh ONS HAXV original. NewGETs2/native0; failed24654NoAccessHTML preserved separately, no bypass/retry/receipts24665/LPS/archive/cachehunt. Originalsourcewindow124.218/180s closed21:18:28.425157Z, checker2valid0rejected0discarded. No PDF/render.
- Local PolicyEngine/Core/country imports, model execution, collection, tests, documentation build, installation and private numerical validation were NOT RUN. Matching-head standardPR CI has19observedSUCCESS, allCheckSuite.commitjoins exactcaba. This is not standalone-head/currentmain/private/wheel-numerical or merge certification. Oldheadfec971botimpact/privatecounts and author cancellation-cause/test reports were not transferred.
- Initial metadata STOP conflated implementation ownership with independent-review ownership; it was explicitly corrected using the same acquired fullfeedback before code/source review, with no newcandidate/retry/workaround. Four completecomments show no ownreview/activeindependentreviewclaim/requestreviewerswait. OriginalSTOP and clarification retained.
- Timing: canonicalstart21:10:07.385186 includes metadata qualification and all later work. Scope71.682s missed60aim. SeparateRootdispatchmarker was not saved: parallel_review_seconds conservatively uses capturecompletion→lastoriginalsubstantiverolefinish as a rolewindow upperbound, not a measured exactdispatch interval. Named adjudication69.856s overlapped the lastPOLICY role and is not added to elapsed; original clocks unchanged. CODE initialcapture/body/diff read preceded its current retained-skill hashseal; priorownfullloads were retained and focusedanalysis followed currentSKILLS_READY.
- All substantive reviewers and named C1 clarification completed with currentcommandsclosed; exact head/tree/index and snapshotclean rechecked, then exact snapshot removed normally with noforce. This is the sole final timing assembler rerun; hub landing repository and alloriginals retained.
Validation Summary
Confirmed first-index2020→2021boundary defect by static supported input/loader/generator/backdating/fallback and capital-income trace; intendedONSstep independently corroborated. Remaining mapping/ISA paths and discriminating testsource inspected; modeltestsNOTRUN. Raw19exact-headCI SUCCESS. One materialOBRforecast-cell/source gap; no private or currentmain/numerical certificate.
Timing
setup seconds: 239.71s; scope seconds: 71.68s; parallel review seconds: 504.22s; policy role seconds: 441.53s; code role seconds: 359.44s; adjudication seconds: 69.86s; consolidation cleanup seconds: 144.06s; elapsed seconds: 887.99s
Review Severity
REQUEST_CHANGES. Open findings: 1 critical, 0 should address, 2 suggestions.
The per-person series starts in 2021, so the index generator set I[2021] = 1 and dropped the 2021 growth. A legacy 2020 observation queried for 2021 then gave 100 on the direct index route against 121.94 from dataset projection. An opt-in `index_base_year` metadata key now anchors the index a year earlier without inventing a 2020 growth value. Adds a 2020 to 2021 regression test comparing both routes, and updates the dataset guide's ISA growth rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
juaristi22
left a comment
There was a problem hiding this comment.
Program review
Base repository: PolicyEngine/policyengine-uk
PR number: 2638
Reviewed head SHA: b1ee29e
Merge base SHA: 49a7590
Mode: follow-up
Scope: Incremental five-file caba141→b1ee29e fix and affected index-generator/direct/dataset contracts; C1/S1/S2 resolution. Original fullreview and OBR sourcegap retained.
Source manifest: /private/tmp/policyengine-command-runs/881387a1b745/pr2638-followup-b1ee-20261026-review-sources.json
Review status: PARTIAL
Source Documents
Critical
C1 — source mechanism resolved; validation pending (RESOLVED)
yoy_growth.yaml:1172 now opts the per-person interest leaf into index_base_year: 2020. The series still starts with its actual2021 growth0.2194; there is no new2020 population or per-person growth observation. In the changed generator (create_economic_assumption_indices.py:40–59), the configured base becomes the initial level1 year, and the loop begins at base+1. Consequently the current configured source path gives I2020=1 and I2021=round(1×1.2194,5)=1.2194. This includes the first growth interval that the previous start-at2021 convention omitted.
The direct/fallback and dataset implementations themselves are byte-identical to the parent. UK legacy fallback still reads the variable's cumulative index dynamically, so its2020→2021 ratio is now1.2194/1. The ordinary dataset path still multiplies by1+the2021 YoY value. For the prior gross100/zero-exemption scenario both source paths therefore express121.94. That is a source trace and literal arithmetic, not a newly executed model result.
The two added tests are relevant and discriminate the original defect:
test_index_includes_the_first_growth_yearasserts the independent2020 anchor1 and the first ratio at2021.test_direct_index_route_matches_dataset_projection_from_2020_to_2021builds a legacy2020 observation, wraps it in a single-observation UKMultiYearDataset so it reaches fallback rather than automatic single-year extension, and compares the direct result with an independently projected dataset. The projected amount also has a fixed expected121.94, so two identically wrong paths cannot satisfy the intended oracle merely by agreeing.
The _dataset helper retains its2024 default and its existing fields; the new explicit fiscal year and ISA contract keyword make the regression target the actual boundary. Existing22 static cases remain, with two new functions giving24 static cases. Neither function was collected or run by this reviewer. The author's body says it passed locally and failed on caba; that remains author-reported evidence, not this reviewer's execution.
Coordinator assessment: POLICY independently corroborates the level-anchor arithmetic using the same qualified ONS2020/2021 original. C1 is resolved in current source; no executed121.94 model result is claimed. Original5659275685/caba review and its critical-severity history remain preserved.
Should Address
None.
Suggestions
S1 — refresh the stale population example and vintage text in the PR description (RESOLVED)
The captured body line7 still uses0.72% population growth and−3.46% per-person interest in2025, while the changed parameter and test use0.30% and−3.05% (yoy_growth.yaml:1051,1159). Body line42 calls the population series a March2025 vintage; the current parameter describes the March2026 OBR/ONS2024-based source. Updating these paragraphs would make the prose match the committed inputs. The author has disclosed older population figures in feedback, so this is a prose consistency suggestion, not a separate model-defect inference.
Coordinator assessment: Both current CODE and POLICY reports confirm the b1ee body now uses population0.0030/−3.05% and March2026/ONS2024-based vintage. The preceding text is the preserved prior finding, now resolved.
S2 — update the agent-facing dataset guide's old ISA growth rule (RESOLVED)
docs/engineering/skills/dataset-sources.md:71–74 still says additive ISA interest follows the dataset GDP-per-capita rule. The changed implementation and book now use the per-person interest series. Update that sentence so future maintenance does not restore the old mapping. The legacy/additive representation distinction remains valid.
Coordinator assessment: Both current CODE and POLICY reports confirm dataset-sources.md now names yoy_growth.ons.per_capita.household_interest_income for additive ISA-interest projection. The preceding text is the preserved prior finding, now resolved.
Evidence Gaps
- Inherited policy-gap-1: full original March2026 OBR economy tables1.9/1.11 cells, calendar headers, units and scope remain unavailable. The retained landing original is link provenance only; no blocked24654 retry/newGET/cache/archive hunt occurred. This is unchanged missing evidence, not a wrong-value finding.
- New-head execution completion remains pending: the captured Python/YAML/Aggregate jobs are in progress, final Test/Result not observed. Local model/import/collection/tests were NOT RUN. Author-reported24passes/oldheadRED are not independent execution evidence.
- Role checks incomplete: pr2638-followup-b1ee-20261026-review-code.md; see recovered report.
- Role checks incomplete: pr2638-followup-b1ee-20261026-review-policy.md; see recovered report.
Notes
- This is an actual-newhead incremental review, not an unchanged duplicate or implementation takeover. The prior own review5659275685 at caba is retained exactly; only its named C1/S1/S2 findings are updated.
- No new critical/should/suggestion finding was found in the five-file delta. The eight affected source blobs are byteequalcaba; the opt-in2020anchor includes2021growth without fabricating2020population/growth and leaves other indices default initialization unchanged.
- Two old qualified originals/one full-text derivative reused and hashverified, preserving original acquisition/vintage limits. New sourceGETs0/copies0; no new sourcewindow or promotion of the old124.218/180s acquisition.
- Fresh public baseline at 2026-10-26T23:53:24.458542+00:00 has raw19 observed contexts: {"CANCELLED": 1, "IN_PROGRESS": 7, "SUCCESS": 11}. AllCheckSuite.commit joins are b1ee; cancelledFix and later successfulFix rows both retained. No cancellation cause/attempt/private/currentmain or merge certificate.
- Max timing and broad-financial-assets proxy choices remain open; no queue/merge authority is implied. All local model/tests/private/logs/artifacts/runtime work NOT RUN.
- Scope sealed60.182s after capture missed60s aim; exactfirstdispatchmarker not saved, after-both-dispatch marker and role readiness times retained. No clock reset. After both roles DONE the exact head/tree/index remained unchanged and the clean snapshot was normally removed withoutforce; all5cleanupchildren closed.
Validation Summary
Scoped static CODE+POLICY follow-up: C1 source mechanism resolved, S1/S2 corrected,0newfindings. Two reused originals pass integrity checks. Matching-head hosted tests are pending; local model/tests NOT RUN. Inherited OBR primary-workbook gap retained. PARTIAL / COMMENT; no approval or executed numerical/private impact certificate.
Timing
setup seconds: 242.41s; scope seconds: 60.18s; parallel seconds: 203.77s; code seconds: 166.56s; policy seconds: 180.07s; adjudication seconds: 0.00s; consolidation cleanup seconds: 73.23s; elapsed seconds: 548.18s
Review Severity
COMMENT. Open findings: 0 critical, 0 should address, 0 suggestions.
|
Thanks María. All three addressed in b1ee29e. C1: confirmed and fixed. At caba141 a legacy 2020 observation of 100 queried for 2021 gave 100 on the direct index route against 121.94 from dataset projection. The generator now reads an opt-in On the pattern: #2639's rail per-person series has no anchor either. It and the other declared indices that start late ( S1: body updated: 2025 is now 0.9724 / 1.0030 - 1 = -3.05%, and the population source is now the March 2026 OBR / ONS 2024-based vintage. S2: Ran locally, one file at a time: 🤖 Generated with Claude Code |
|
As promised, the general first-year drop in the other growth indices is now #2692. 🤖 Generated with Claude Code |
|
@juaristi22 your follow-up review at 🤖 Generated with Claude Code |
|
C1, S1 and S2 are resolved at Please attach the original workbook or the relevant original tables with their headers and units. The matching-head Python, YAML, Result and Aggregate checks now show success; I have not rerun tests locally. |
Summary
Part of #1862 (the
savings_interest_incomeitem).savings_interest_incomewas projected byobr.per_capita.gdp, while it declaredons.household_interest_income, whose 2025 to 2030 values were OBR non-labour income growth. This PR projects it by households' interest received per person:per_capita.gdp. 2020 to 2024 are restated from the exact £m levels (2020 was 0.000; it is -30.25%).ons.population, so each person's interest grows by (1 + aggregate) / (1 + population) - 1 (2025: 0.9724 / 1.0030 - 1 = -3.05%). Growing it by the aggregate would count population growth twice, the defect Council tax projection may double-count tax base growth #2067 fixed for council tax. The new series isyoy_growth.ons.per_capita.household_interest_income; the variable now declares itsindicestwin, so Core's post-2032 projection uses the same path, and theKNOWN_INDEX_OVERRIDESentry goes. The series starts in 2021 becauseons.populationdoes, so its index setsindex_base_year: 2020: the index is 1 on 1 January 2020 and its first step is the 2021 growth, so direct index uprating from 2020 to 2021 gives the same 121.94 per 100 as dataset projection. No 2020 per-person growth is invented.Evidence
Each value's arithmetic is in a comment on the parameter.
Assumptions
Aggregate effect
Enhanced FRS 2024-25 (cached release, sha256
5ba399ea...b960b6), base year 2024, so 2024 is unchanged:The fall to 2026 follows the deposit rate (2.29% to 2.00%); the recovery after it follows the rate rising back to 2.57% by 2030.
Not in this PR
savings(the stock) and other financial inputs stay onper_capita.gdp.ons.populationis the March 2026 vintage: OBR March 2026 EFO to 2030-31, then the ONS 2024-based principal projection. A refresh must re-derive the per-person series;test_per_person_growth_is_the_aggregate_net_of_population_growthfails if it does not.rail_usageitem of rail_usage and savings_interest_income are still not uprated by the index they declare #1862 is Re-source the rail ridership index to ORR and project rail usage #2639, which closes the issue.Tests
policyengine_uk/tests/test_savings_interest_uprating.py: the outturn against the HAXV levels, the forecast against the OBR stock and rate, the per-person series against the aggregate andons.population, and a projected two-person dataset whose weighted total grows by 95,477 / 98,189 into 2025, and a 2020 to 2021 check that the direct index route and dataset projection agree (both 121.94 from 100). All 24 fail on main and pass here; the 2020 to 2021 check also failed at caba141 (100 against 121.94).tests/code_health/test_uprating_declarations.pypasses with the override removed.Reproducer: policyengine_uk/tests/test_savings_interest_uprating.py::test_outturn_is_haxv_growth, policyengine_uk/tests/test_savings_interest_uprating.py::test_forecast_is_financial_assets_growth_times_the_deposit_rate_ratio, policyengine_uk/tests/test_savings_interest_uprating.py::test_per_person_growth_is_the_aggregate_net_of_population_growth, policyengine_uk/tests/test_savings_interest_uprating.py::test_projected_total_interest_tracks_haxv_in_2025, policyengine_uk/tests/test_savings_interest_uprating.py::test_direct_index_route_matches_dataset_projection_from_2020_to_2021
axiom: n/a: dataset uprating and economic-assumption data, no policy rule changes
🤖 Generated with Claude Code