Repository navigation
Bus fares: follow published fares by area and year, and price cap changes from a fitted fare table - #2546
Conversation
The preserve_calendar_dates flag sat on the fares and reference_fares nodes, and leaves only see node metadata that is propagated. The caps were therefore annualised like any other parameter, which happened to be harmless while every change fell on 1 January. A cap change dated part-way through a year waited for the next calendar year. Propagate the flag to the cap leaves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The engine held the FY2024-25 yield per fare-paying boarding in every later year and moved it only by a cap term measured against a fixed 2024 cap schedule. Outside London that lifted the 2025-26 yield 24.9%, while DfT's BUS0415a index for the same area rose 12.6% on the same April-to-March basis, cap rise included. Each area's yield now moves with its BUS0415a index (London, England outside London, Scotland), stored as published quarter-end values dated at the start of their quarter and day-weighted over 6 April to 5 April. After the last published quarter (March 2026) the index grows with the OBR CPI. Northern Ireland has no index, so its yield follows CPI. reference_fares.cap becomes the cap the index already reflects: the cap in force up to the last published quarter, held after it. Both schedules are read in the same year, so the cap term is zero while the index is published and adds the scheduled 2027 GBP 2 cap and its 2028 end after it. reference_year is removed with directions; benchmark_year names the benchmark year. Each area following its own index means the England total rises 7.6% in 2025-26, more than DfT's England index (6.4%), which weights outside London at about one half where receipts weight it at 61%. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One uncapped fare of GBP 2.748 for every single ticket made a GBP 3 cap worth nothing and priced a full year at GBP 2 at about GBP 0.70bn. DfT's evaluation of the GBP 2 cap puts that fare near the average of the tickets above GBP 2, which were less than half of single tickets. gov.dft.bus.uncapped_single_fares replaces it with three bands, each with its share of single tickets and mean uncapped fare at FY2024-25 prices, uprated with CPI: 55% at GBP 1.65, 30% at GBP 2.21 and 15% at GBP 3.87. The model caps each band under both schedules and weights by share. A supplied local_bus_uncapped_single_fare (now registered as a supplied input and CPI-uprated when datasets are extended) replaces the table for that person. scripts/fit_bus_cap_savings.py fits the table by least squares to public facts: the 2023-24 fare cap grant (GBP 280.9m against 278.95m), the January 2025 BUS0415a step (9.8% of the yield against 9 to 11%), the funding announced for the GBP 3 cap in 2025 (GBP 140.0m against "over GBP 150 million") and the GBP 2 cap in 2027 (GBP 411.6m against 400m), and the shape of DfT's Table 6. A full year at GBP 2 now saves about GBP 0.36bn a year at 2024-25 boardings and GBP 3 about GBP 0.14bn. uncapped_single_fare is removed with directions. 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: 0 (thresholds in Notes
Moves flagged for reviewNone. Spending by benefit and service
Caseloads
Household net income by decileNo household's net income changes in any year. No change in any year: receipts by tax head; government balance, each component counted once; model totals; components the model counts twice; household income totals; population; 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. Checks failing on both base and head (1)
Counted-once checkEach of 84 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
Components the model counts twice: 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
|
vahid-ahmadi
left a comment
There was a problem hiding this comment.
@juaristi22, thanks. This is a clear improvement on the £2.748 single fare: the baseline now follows published fares, a £3 cap binds, and the fit is reproducible. I found no must-fix items. Three shoulds below are about stating what the fit leaves out, plus three nits.
Reviewed at head b0968773, base 76cfc9e7.
What I checked
- BUS0415a. All 39 values in
local_bus_fares_index.yamlmatch DfT's June 2026 release ("Last updated: June 2026", archived 19 June 2026) exactly, including the unrounded March 2026 cells. The base is March 2005 = 100, at current prices. - BUS05bi fare cap support. £278.951m in 2023-24 (England outside London, the same as England). Matches
fit_bus_cap_savings.py:27. - BUS01a and BUS01c. England outside London journeys of 1,780.913m (2023-24) and 1,850.510m (2024-25), and concessionary journeys of 499.730m and 519.108m. These match
fit_bus_cap_savings.py:18-19. - Evaluation Table 6 (Frontier Economics and SYSTRA, p. 66). £1.49 to £1.40, £2.15 to £1.83, and £2.73 to £2.00. The "less than half of total pre-£2BFC bus trips" wording and the £210m spent against £245m planned are both in the report. Note that Table 6 covers "Big and LSME participating operators" only, which the docs already say.
- GOV.UK. The written statement of 29 October 2024 says "funding of over £150 million" for the £3 cap through December 2025. The statement of 22 July 2026 says £2 in England outside London from 1 January to 31 December 2027, "backed by £400 million of extra government funding". The £3 guidance (updated 19 August 2026) says £3 continues to the end of 2026.
fares/cap.yamlmatches all three. - Fit.
scripts/fit_bus_cap_savings.pyruns in about 14 seconds and returns the committed table (0.55/0.30/0.15 at £1.65/£2.21/£3.87, loss 0.322). Each fact matches the PR body. - Tests. The
gov/dftYAML tests pass (42). The bus, fiscal-year, dated-reform, parameter-description, parameter-metadata, uprating, scenario-baseline and removed-parameter Python tests pass (339 passed, 1 skipped). - Base against head, one South East fare-paying passenger, 100 trips, 0.70 singles:
- Baseline 2024-25 to 2028-29: £174.85, £218.42, £203.09, £174.97, £218.42 on base, and £174.85, £196.87, £196.23, £193.65, £224.39 on head. Both match the PR table.
- In 2025-26, with no cap, £3 and £2 for the whole year: £218.42, £218.42 and £174.85 on base, and £208.70, £196.87 and £178.33 on head. A £3 cap now binds.
- Calculating
local_bus_uncapped_single_farefirst does not make the formula skip the table (the #2307 concern), so a cached default is not taken for a supplied fare.
Findings
-
should: the BUS0415a link now serves a revised release.
local_bus_fares_index.yaml:16,uncapped_single_fares/fare.yaml:17anduncapped_single_fares/share.yaml:16cite.../6a310ac715f2a70fac7e6006/bus0415.odsas the release to March 2026. That URL now returns a 301 to.../6aaa829244ec1aa417346b51/bus0415.ods, the 16 September 2026 release. That release revises the last two quarters and adds June 2026:Quarter Area In this PR (June release) Live file (September release) Dec 2025 England outside London 214.3 214.1848 [r] Mar 2026 England outside London 217.9557 216.9745 [r] Mar 2026 Scotland 234.6833 233.6138 [r] So anyone who follows the reference gets numbers that do not match the YAML. Keeping the June vintage for Microcosm parity is fine (the PR body says why). Please name the vintage in the reference title ("June 2026 release") and point the href at a copy that will not change, either the Wayback snapshot (
web.archive.org/web/20260619124752/https://assets.publishing.service.gov.uk/media/6a310ac715f2a70fac7e6006/bus0415.ods) or the copy Microcosm vendors. -
should: report the 2024-25 fare cap grant check. BUS05bi puts 2024-25 fare cap support at £515.693m. That is the figure #2140 calibrated the £2.748 fare to. Under the new table, 2024-25 (270 days at £2 and 95 at £3, 2024-25 prices, 0.70 singles, 1,331m fare-paying boardings) implies about £268m, roughly half. Issue #2542 calls this figure "a check, not an anchor". Neither the docs fit table (
docs/engineering/bus-fares.md:182-190) nor the PR body reports what the check gives. Leaving it out is defensible: the 2023-24 grant, the £150m and the £400m all point to a full year at £2 costing near £0.3bn, not £0.6bn. But it is the benchmark year's own published figure, and part of why the headline moves from £0.70bn to £0.36bn. Please add it as a row, marked as not fitted, with the implied £268m and the reason. -
should: say how the £400m is read.
fit_bus_cap_savings.py:72-75treats the £400m as £2 against no cap (the table gives £411.6m). The statement calls it "£400 million of extra government funding" for a scheme that "will replace the existing £3 fare cap". The £3 scheme is only funded to December 2026, so £2 against no cap is a fair reading. If the £400m is instead on top of a continued £3 cap, the matching figure is £2 against £3, which the table puts at about £247m, 38% below target. Please state the reading in the script comment at line 34 and in the docs, so the next fit does not flip it by accident. -
nit: the aggregate impact check does not test this change. The CI comment reports "No change in any year" on
enhanced_frs_2024_25. On main,report_spec.yamllists no bus measure, and no variable readsbus_fare_spendingorperson_bus_fare_spendingapart from each other. So the job would show no change whatever this PR did to bus fares. The real check is the parity run in microcosm#1331. One sentence in the PR body would stop anyone reading "0 moves" as evidence. -
nit: the national table and areas with their own lower caps.
docs/engineering/bus-fares.md:122-123already says local lower caps need caller-supplied fares. Because the table is fitted to national grants and applied the same way in every English region outside London, a national cap reform will overstate savings where a combined authority already runs its own lower single fare, and understate them elsewhere. National totals are fine. Regional and distributional outputs move the wrong way. Please add a sentence there saying so, as dashboards split these results by region. -
nit: CPI applies to capped fares after the last published quarter. After March 2026 the index grows with CPI. That includes fares held at £3 by the cap, while the cap term (
person_bus_fare_spending.py:101-112) is measured against a nominal £3. In 2028-29, with no cap, the high band is priced at about its CPI-uprated fare plus £3 × (CPI growth since 2026). That is roughly 1% of the outside-London yield, so it is small. A line in the docs is enough.
Smaller points, no action needed:
local_bus_uncapped_single_fareno longer has a formula, socalculatereturns 0 where nothing is supplied (variables/gov/dft/local_bus_uncapped_single_fare.py). The docstring says it is optional. Anyone reading the variable directly may still take the 0 for a fare.- Three bus fare indices now sit side by side:
fare_indexandprior_law_fare_index(neutral reform levers, both 1) andlocal_bus_fares_index(published). #1869 asked for the current-law published series underfare_index. The new name and the "do not reuse" note in the issue settle that. A cross-reference in thefare_index.yamldescription would help.
Overlap and merge order
- No other open PR touches
gov/dft/busor the bus variables. - #2308 (
supplied_inputs.py), #2536 (uprating_indices.yaml) and #2490 (utils/parameters.py) touch files this PR adds one entry to. Any conflicts should be one-line conflicts. - The removed parameters (
gov.dft.bus.reference_year,gov.dft.bus.uncapped_single_fare) are not used by thebus-fare-caporfree-childcare-reformanalyses.bus-fare-capapplies its own all-ticket reduction to reported household spending, so its results do not change.
PR body: the Reproducer: and axiom: n/a lines, both changelog fragments and green CI at head are all present.
🤖 Generated with Claude Code
…es out - Name the June 2026 release in the BUS0415a references and point them at the archived copy. The DfT address now serves the September 2026 release, which revises the last two quarters. Record the file's sha256. - Report the 2024-25 fare cap grant (GBP 515.69m) as a check the fit does not target. The table puts it at GBP 268.5m; the docs give the reason, and the script prints it. - State that the GBP 400m for 2027 is read as GBP 2 against no cap. Against a continued GBP 3 cap the table gives GBP 247.3m, which the script also prints. - Document that the CPI projection also raises fares held at the GBP 3 cap (0.4% of the outside-London yield in 2027-28, 0.8% in 2028-29). Reform costs do not move, since both runs share the reference. - Document that the national table misplaces cap savings between regions where combined authorities run their own lower fares. - Say that local_bus_uncapped_single_fare reads 0 where nothing is supplied, and point fare_index at local_bus_fares_index. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@vahid-ahmadi thanks. All six are addressed in 6a4192d, and the PR body is updated.
Also, 🤖 Generated with Claude Code |
vahid-ahmadi
left a comment
There was a problem hiding this comment.
@juaristi22, thanks. All three shoulds and all three nits are fixed in 6a4192db8, and I found nothing new at must or should level, so I'm approving.
Re-reviewed the delta b0968773..6a4192db8 (7 files: the docs, the three BUS0415a references, fare_index.yaml, the variable docstring and the fit script).
Earlier findings
- should, BUS0415a vintage: Fixed. The three references now say "June 2026 release (archived copy)" and point at the Wayback snapshot. I downloaded it: 23,434 bytes, sha256
04721e9a…c7adf, the same as the comment inlocal_bus_fares_index.yaml. Its cover sheet says "Last updated: June 2026, Next update: October 2026". All 39 YAML values match its BUS0415a sheet, including the unrounded March 2026 cells. The old DfT address still redirects to the September file (24,111 bytes, "Last updated: September 2026"), as the new note says. The PR body's figures for that release (England +2%, London 0%, Scotland +5% in the year to June 2026) match the file, to rounding. - should, 2024-25 grant: Fixed. It is now a docs table row marked not fitted (£515.69m published, £268.5m from the table), and the script prints it. The new supporting numbers match DfT's tables:
- Grant per fare-paying boarding rose 78%: £515.693m / 1,331.4m against £278.951m / 1,281.2m (BUS05bi, BUS01).
- Receipts per fare-paying boarding rose 2.8%: £2,069.95m / 1,331.4m against £1,936.89m / 1,281.2m (BUS05ai passenger fare receipts, England outside London).
- The index rose 6.7% on the mean of the four quarter ends (178.0 to 189.9). On the model's day-weighted blend it is 6.8% (178.11 to 190.23). Either works.
- should, reading of the £400m: Fixed. The script comment, the docs and the PR body now read it as £2 against no cap and give the other reading too (£247.3m, 38% below). The script prints both.
- nit, aggregate impact check: Fixed. The PR body now says the report has no measure that reads bus fare spending, and points to microcosm#1331.
- nit, combined authorities' own lower caps: Fixed.
docs/engineering/bus-fares.md:136-140. - nit, CPI on capped fares: Fixed.
docs/engineering/bus-fares.md:96-102. I recomputed the sizes as 0.70 singles × 0.15 high-band share × £3 × CPI growth since March 2026, over the outside-London yield. That gives 0.44% in 2027-28, 0.78% in 2028-29 and 1.1% in 2029-30, in line with the docs. The point that cap reform costs don't move is right too: both runs sharereference_faresand the index.
The two smaller points are also done: the docstring for local_bus_uncapped_single_fare now explains the 0, and fare_index.yaml points to local_bus_fares_index.
Re-run at 6a4192db8
scripts/fit_bus_cap_savings.pystill returns the committed table (0.55/0.30/0.15 at £1.65/£2.21/£3.87, loss 0.322) with the same fitted facts. The two new lines print £268.47m and £247.29m, which match the docs and the PR body. The share and fare values are unchanged in the delta.- The
gov/dftYAML tests pass (42). The bus fare, parameter description and parameter metadata Python tests pass (49). The delta changes no values or formulas, so I didn't re-run the wider set from last round. CI covers it. - The head merges cleanly with main (
7cbdc2b9). No other open PR touches the bus parameters, variables or docs, and #2308, #2536 and #2490 are still open, with the one-line overlaps noted last round. - CI at head is green, including all four Python shards and the aggregate impact job.
🤖 Generated with Claude Code
María's ruling of 2026-10-21 replaces the calendar-year basis of 2026-09-10: the 2025 England and London fare-receipt rows move from 2024-25 as the engine's model year does. Each area's BUS0415a quarter-end index is in force from the start of the quarter it closes and is day-weighted over 6 April to 5 April, as policyengine-uk reads gov.dft.bus.local_bus_fares_index (fiscal_year_blend, PolicyEngine/policyengine-uk#2546). London and England outside London move by their own series and England's receipts move as their sum, as the engine prices each area by its own index. On the June 2026 release England's factor is 1.0763 (1.0570 on the calendar basis) and London's 1.0000086. The bus parity receipt also ties each area's factor to the engine's index ratio (its measurement year over its benchmark year), and the engine test holds the tie within 0.1% at the locked engine. The target references are unchanged; the membership and the national compile-parity receipts re-derive from the pinned feed, the two fare rows only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
María's ruling of 2026-10-21 replaces the calendar-year basis of 2026-09-10: the 2025 England and London fare-receipt rows move from 2024-25 as the engine's model year does. Each area's BUS0415a quarter-end index is in force from the start of the quarter it closes and is day-weighted over 6 April to 5 April, as policyengine-uk reads gov.dft.bus.local_bus_fares_index (fiscal_year_blend, PolicyEngine/policyengine-uk#2546). London and England outside London move by their own series and England's receipts move as their sum, as the engine prices each area by its own index. On the June 2026 release England's factor is 1.0763 (1.0570 on the calendar basis) and London's 1.0000086. The bus parity receipt also ties each area's factor to the engine's index ratio (its measurement year over its benchmark year), and the engine test holds the tie within 0.1% at the locked engine. The target references are unchanged; the membership and the national compile-parity receipts re-derive from the pinned feed, the two fare rows only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes #2542
What and why
From 2025-26 the engine held the FY2024-25 yield per fare-paying boarding and moved it only by a cap term against a fixed 2024 cap schedule, with one uncapped fare of £2.748 for every single ticket. Outside London the 2025-26 yield rose 24.9%, while DfT's BUS0415a index rose 12.6% on the same April-to-March basis. A £3 cap was worth nothing, and a full year at £2 was priced at about £0.70bn.
Three ordered commits:
preserve_calendar_datessat on thefaresandreference_faresnodes and never reached the cap leaves, so the caps were annualised like other parameters. That was harmless while every change fell on 1 January. A bare-year reform of the cap ({2025: 2}) covered April to December only. A dated change part-way through a year waited for the next calendar year. The index files now propagate the flag.gov.dft.bus.local_bus_fares_indexholds BUS0415a quarter-end values for London, England outside London and Scotland, from DfT's June 2026 release (quarters to March 2026). The references point at an archived copy, because DfT's address for that file now serves the September 2026 release. Each value is dated at the start of its quarter, andfiscal_year_blendday-weights the values over 6 April to 5 April. After March 2026 the index grows with the OBR CPI.reference_fares.capbecomes the cap the index already reflects: the cap in force up to the last published quarter, held after it. Both schedules are read in the same year. The cap term is therefore zero while the index is published. After that it adds the scheduled £2 cap from January 2027 and its end in 2028.benchmark_yearreplacesreference_year, which is removed with directions.yield_per_boardingandboardings_per_tripkeep their names, areas and dates.gov.dft.bus.uncapped_single_faresreplacesuncapped_single_fare, which is removed with directions. At FY2024-25 prices, CPI-uprated: 55% of single tickets at £1.65, 30% at £2.21 and 15% at £3.87.scripts/fit_bus_cap_savings.pyfits the table by least squares to the public facts below. Shares move in steps of one point and fares in steps of one penny, and each band's mean stays at least 10% above its lower edge.local_bus_uncapped_single_farereplaces the table for that person. It is now inSUPPLIED_INPUT_VARIABLESand is CPI-uprated when datasets are extended.Rulings (20 Oct): the April-to-March window; after the last published quarter, CPI plus the scheduled caps; the table fitted to all the public facts.
The costs use the 0.70 default single share and fare-paying boardings outside London (BUS01 less BUS01c). Table 6 is read at face value, for the table's shape. Deflated to 2022 prices it would put the 2023-24 saving about a third above the grant (reasons not checked). The two announced figures are budgets: the £2 cap's first ten months paid £210m against £245m planned. In 2025-26 a full year at £2 against no cap saves about £0.36bn at 2024-25 boardings, and £3 saves about £0.14bn.
One fare-paying passenger outside London, 100 trips, 0.70 singles, default parameters:
In 2025-26 on main, no cap and the £3 cap both give £218.42. Here: no cap £208.70, £3 cap £196.87, £2 cap £178.33.
For consumers
Not in this PR
Two limits are documented in
docs/engineering/bus-fares.md:Test plan
policyengine_uk/tests/policy/baseline/gov/dft/bus_journeys.yaml): cases 1–8 adapted (benchmark_yearreplacesreference_year). Cases 9–15 are new, with expected values worked by hand from DfT tables and the arithmetic in comments. They cover each area in 2024-25 and 2025-26, no cap and a £2 cap in 2025-26.test_bus_fare_reforms.py:test_bus_fare_parameters.py: the reference cap equals the cap up to the index's last published quarter and is held after it; the table matches each public fact; the removed parameters give directions.gov/dftYAML tests (42), the bus Python tests, and the parameter-description, fiscal-year, dated-reform, removed-parameter, uprating and supplied-input tests. The full suite is left to CI.ruff formatandruff checkon the repository.changelog.d/2542.fixed.mdandchangelog.d/2542.removed.md.The aggregate impact check on certified data cannot see this change. No measure in its report reads bus fare spending, so its "no change in any year" is not evidence either way. The test of the totals is the parity run in PolicyEngine/microcosm#1331.
Reproducer:
policyengine_uk/tests/policy/baseline/gov/dft/bus_journeys.yaml::Case 10, the 2025-26 yield follows the fares index, not a cap term.;policyengine_uk/tests/test_bus_fare_reforms.py::test_a_three_pound_cap_binds_and_cap_reforms_follow_the_table;policyengine_uk/tests/test_bus_fare_reforms.py::test_a_cap_change_part_way_through_a_year_keeps_its_dateaxiom: n/a: bus fares are priced from DfT statistics, and the national fare cap is a grant-funded voluntary scheme with no statutory provision to encode.
🤖 Generated with Claude Code