Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/2640.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Corrected England outside London's bus fare yield to exclude only older and disabled concessionary journeys, and refitted the cap-savings table using the same priced boardings.
77 changes: 48 additions & 29 deletions docs/engineering/bus-fares.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,17 @@ household output explicitly.

## Published price benchmark

Microcosm prices journeys using FY2024–25 published facts. The model uses the
same inputs. For each area, receipts `R`, boardings `B`, concessionary
boardings `C`, annual trips per resident `T` and population `P` give:
The model uses FY2024–25 published facts. For each area, receipts `R`,
boardings `B`, concessionary boardings `C`, annual trips per resident `T` and
population `P` give:

- Fare-paying yield per boarding: `y = R / (B - C)`.
- Yield per priced boarding: `y = R / (B - C)`.
- Boardings per resident trip: `k = B / (T * P)`.

| Area | R (£) | B | C | T | P | y (£) | k |
|---|---:|---:|---:|---:|---:|---:|---:|
| London | 1,347,434,943.01459 | 1,821,485,000 | 497,523,765.08908 | 13.1155774642333 | 58,620,101 | 1.017729905895 | 2.369144880275 |
| England outside London | 2,069,953,713.42079 | 1,850,509,887.26131 | 519,107,608.59444 | 28.0685543170643 | 58,620,101 | 1.554716967657 | 1.124669202640 |
| England outside London | 2,069,953,713.42079 | 1,850,509,887.26131 | 402,846,350.14539 | 28.0685543170643 | 58,620,101 | 1.429858292587 | 1.124669202640 |
| Scotland | 391,000,000 | 334,000,000 | 183,599,374.01433 | 28.0685543170643 | 5,546,900 | 2.599723222144 | 2.145240985363 |
| Northern Ireland | 150,082,817.49 | 67,794,551.35 | 8,960,000 | 28.0685543170643 | 1,927,855 | 2.550929922065 | 1.252853758464 |

Expand All @@ -69,6 +69,26 @@ These translations include interchanges and differences between operator
boardings and resident trips; they are not counts of legs for a known trip.
`gov.dft.bus.benchmark_year` (2024) names the fiscal year these facts describe.

Outside London, `C` uses BUS01c's older-and-disabled concessionary journeys.
The other concessions remain in the priced denominator, so the yield absorbs
their discounts. BUS01c's total would also remove 116.3 million youth-scheme
journeys (BUS01c note 4) that the dataset's `bus_pass_eligible` flag does not
generally make free. The older-and-disabled count approximates that flag:
Microcosm flags older-person eligibility and under-5s outside London, but does
not flag disabled pass holders. London uses total concessionary journeys;
Scotland and Northern Ireland retain their own counts.

Charging disabled pass holders raises modelled spending; making under-5s free
lowers it. [Microcosm issue 1385](https://github.com/PolicyEngine/microcosm/issues/1385)
reports that this denominator change moves outside-London fares from 6.8%
above BUS05ai to about 1.8% below it on the certified build
`microcosm-uk-2024-25-national-20261019T083323Z-6cba5d71`, using its existing
weights and model year 2024.

Microcosm's fact selectors and model lock must use the same concessionary
definition as this benchmark. The companion update follows the model release
and is tracked in [Microcosm issue 1385](https://github.com/PolicyEngine/microcosm/issues/1385).

## Fares by year

In other years each area's yield moves with its published fares index:
Expand Down Expand Up @@ -97,7 +117,7 @@ The CPI projection also raises the fares in the index that the £3 cap holds at
£3, while the cap term below measures changes from a nominal £3. From January
2027 a ticket held at a cap therefore costs about £3 times the CPI growth since
March 2026 more than the cap allows. That overstates the outside-London yield
by 0.4% in 2027–28 and 0.8% in 2028–29, about 0.3 points more each year after.
by 0.4% in 2027–28 and 0.7% in 2028–29, about 0.3 points more each year after.
A cap reform is measured against the same reference as the baseline, so its
cost does not move.

Expand Down Expand Up @@ -180,9 +200,9 @@ share of single tickets and its mean uncapped fare at FY2024–25 prices:

| Band | Share | Mean fare |
|---|---:|---:|
| `LOW`, at or below £2 | 0.55 | £1.65 |
| `MIDDLE`, above £2 and up to £3 | 0.30 | £2.21 |
| `HIGH`, above £3 | 0.15 | £3.87 |
| `LOW`, at or below £2 | 0.56 | £1.70 |
| `MIDDLE`, above £2 and up to £3 | 0.31 | £2.21 |
| `HIGH`, above £3 | 0.13 | £3.96 |

The fares rise with the OBR CPI after FY2024–25 and the shares stay, so a fixed
cap saves more as fares rise. The model caps each band's mean fare under both
Expand All @@ -198,44 +218,43 @@ lower edge, and sets the low band's fare so all singles average £2.15:

| Fact | Source | Target | Table gives |
|---|---|---:|---:|
| A full year at £2, 2023–24 | Fare cap support, DfT BUS05bi | £278.95m ± 10% | £280.9m |
| A full year at £2, 2023–24 | Fare cap support, DfT BUS05bi | £278.95m ± 10% | £282.8m |
| Part of the December 2024 to March 2025 index step outside London due to the £3 cap | BUS0415a: +11.0%; neighbouring quarters 0.1% to 2.3% | 10% ± 1 point of the yield | 9.8% |
| The £3 cap in 2025 | ["over £150 million"](https://www.gov.uk/government/speeches/national-bus-fare-cap) | £150m ± 20% | £140.0m |
| The £2 cap in 2027, against no cap | ["£400 million"](https://www.gov.uk/government/speeches/2-bus-fares-from-january-2027) | £400m ± 20% | £411.6m |
| The £3 cap in 2025 | ["over £150 million"](https://www.gov.uk/government/speeches/national-bus-fare-cap) | £150m ± 20% | £144.2m |
| The £2 cap in 2027, against no cap | ["£400 million"](https://www.gov.uk/government/speeches/2-bus-fares-from-january-2027) | £400m ± 20% | £420.2m |
| Mean pre-cap price of single tickets | [DfT evaluation](https://www.gov.uk/government/publications/evaluation-of-the-2-bus-fare-cap), Table 6 | £2.15 ± £0.10 | £2.15 |
| Share of single tickets above £2 | Table 6: £0.32 / (£2.73 − £2.00) | 0.44 ± 0.05 | 0.45 |
| Their mean pre-cap price | Table 6 | £2.73 ± £0.10 | £2.76 |
| 2024–25: £2 for 270 days, then £3 | Fare cap support, DfT BUS05bi | £515.69m, not fitted | £268.5m |
| Share of single tickets above £2 | Table 6: £0.32 / (£2.73 − £2.00) | 0.44 ± 0.05 | 0.44 |
| Their mean pre-cap price | Table 6 | £2.73 ± £0.10 | £2.73 |
| 2024–25: £2 for 270 days, then £3 | Fare cap support, DfT BUS05bi | £515.69m, not fitted | £272.7m |

The £400 million is read as £2 against no cap: the £3 scheme is funded only to
December 2026, and the £2 scheme replaces it. Read as £2 against a continued
£3 cap, the table gives £247.3m, 38% below. The script prints both.
£3 cap, the table gives £252.5m, 37% below. The script prints both.

The 2024–25 grant, for the benchmark year itself, is reported and not fitted;
the table puts it at about half. Grant per fare-paying boarding rose 78% on
2023–24, while the outside-London index rose 6.7%, receipts per fare-paying
boarding rose 2.8%, and the cap rose only for the last quarter. The figure may
the table puts it at about half. Grant per priced boarding rose 77.5% on
2023–24, while the outside-London index rose 6.7%, receipts per priced
boarding rose 2.6%, and the cap rose only for the last quarter. The figure may
therefore hold more than that year's forgone fares (timing or coverage of
payments, not checked). The 2023–24 grant and the two announced figures all
put a full year at £2 near £0.3bn, not £0.6bn. The single £2.748 fare this
table replaced was calibrated to the 2024–25 grant.

The costs use the 0.70 default single share and fare-paying boardings outside
London (BUS01 journeys less BUS01c concessionary journeys: 1,281m in 2023–24
and 1,331m in 2024–25, which also stands for 2025 and 2027), with fares moved
to each year by CPI. Table 6 compares January to October 2022 with the same
months of 2023 and 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.
The costs use the 0.70 default single share and priced boardings outside
London (BUS01a journeys less BUS01c older-and-disabled concessionary journeys:
1,390m in 2023–24 and 1,448m in 2024–25, which also stands for 2025 and 2027),
with fares moved to each year by CPI. Table 6 compares January to October 2022
with the same months of 2023 and is read at face value, for the table's shape.
Deflated to 2022 prices it would put the 2023–24 saving about 40% above the grant.
Possible reasons, not checked, are that the grant pays operators for revenue
lost net of the journeys the cap added, and that Table 6 covers the larger
participating operators only. The two announced figures are budgets; the £2
cap's first ten months paid £210m against £245m planned. The table is a fitted
central estimate, not an observed fare distribution.

At FY2025–26 prices the table saves £0.39 per single ticket for a full year at
£2 against no cap, and £0.15 at £3: about £0.36bn and £0.14bn a year at
2024–25 boardings. The single £2.748 fare this replaced saved £0.75 at £2
(about £0.70bn) and nothing at £3.
At FY2025–26 prices the table saves £0.36 per single ticket for a full year at
£2 against no cap, and £0.14 at £3: about £0.37bn and £0.14bn a year at
2024–25 boardings.

A dataset or calculator can supply `local_bus_uncapped_single_fare` for each
person. Where it is supplied, that person's single tickets cost that one fare
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ metadata:
- title: DfT, GBP 2 bus fares from January 2027 (GBP 400 million), 22 July 2026
href: https://www.gov.uk/government/speeches/2-bus-fares-from-january-2027
LOW:
'2024-01-01': 1.65
'2024-01-01': 1.7
MIDDLE:
'2024-01-01': 2.21
HIGH:
'2024-01-01': 3.87
'2024-01-01': 3.96
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ metadata:
- title: DfT, GBP 2 bus fares from January 2027 (GBP 400 million), 22 July 2026
href: https://www.gov.uk/government/speeches/2-bus-fares-from-january-2027
LOW:
'2024-01-01': 0.55
'2024-01-01': 0.56
MIDDLE:
'2024-01-01': 0.3
'2024-01-01': 0.31
HIGH:
'2024-01-01': 0.15
'2024-01-01': 0.13
16 changes: 11 additions & 5 deletions policyengine_uk/parameters/gov/dft/bus/yield_per_boarding.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
description: Passenger receipts divided by fare-paying boardings in 2024-25, including
single and season tickets. This is a benchmark yield, before subsequent cap changes and
explicit fare-index reforms; held nominally constant unless reformed.
description: Passenger receipts divided by priced boardings in 2024-25, including
single and season tickets. England outside London excludes only older and disabled
concessionary boardings, leaving other concessions in the priced denominator to match
the dataset's bus_pass_eligible flag approximately. This is a benchmark yield before
subsequent fare-index and cap changes.
metadata:
unit: currency-GBP
period: year
label: bus yield per boarding
reference:
- title: DfT bus statistics, BUS01 and BUS05i, 2024-25
- title: DfT bus statistics, BUS01a journeys, BUS01c older and disabled concessions, and BUS05ai receipts, 2024-25
href: https://www.gov.uk/government/statistical-data-sets/bus-statistics-data-tables
- title: DfT BUS01a journeys and BUS01c concessions, March 2026 revised release
href: https://assets.publishing.service.gov.uk/media/69c11f97cfa346b9d4704a50/bus01.ods
- title: DfT BUS05ai passenger fare receipts, March 2026 revised release
href: https://assets.publishing.service.gov.uk/media/69c11f97b920af63be1c7631/bus05i.ods
- title: DfT National Travel Survey, NTS0705a, 2024
href: https://www.gov.uk/government/statistical-data-sets/nts07-car-ownership-and-access
- title: Scottish Transport Statistics 2025, tables 2.2a and 2.8
Expand All @@ -19,7 +25,7 @@ metadata:
LONDON:
'2024-01-01': 1.017729905895
ENGLAND_OUTSIDE_LONDON:
'2024-01-01': 1.554716967657
'2024-01-01': 1.429858292587
SCOTLAND:
'2024-01-01': 2.599723222144
NORTHERN_IRELAND:
Expand Down
41 changes: 29 additions & 12 deletions policyengine_uk/tests/policy/baseline/gov/dft/bus_journeys.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,8 @@
bus_pass_eligible: false
local_bus_single_fare_share: 0.7
output:
# 100 trips * 1.12466920264 boardings * GBP 1.554716967657.
person_bus_fare_spending: 174.85
# 100 trips * 1.12466920264 boardings * GBP 1.429858292587.
person_bus_fare_spending: 160.81

- name: Case 10, the 2025-26 yield follows the fares index, not a cap term.
period: 2025
Expand All @@ -154,9 +154,9 @@
bus_pass_eligible: false
local_bus_single_fare_share: 0.7
output:
# 174.8542 * 214.1797 / 190.2282 (+12.6%). The cap is GBP 3 all year
# 160.8118 * 214.1797 / 190.2282 (+12.6%). The cap is GBP 3 all year
# both in the index and in the cap schedule, so nothing is added.
person_bus_fare_spending: 196.87
person_bus_fare_spending: 181.06

- name: Case 11, London fares follow the flat London index.
period: 2025
Expand Down Expand Up @@ -196,11 +196,11 @@
person_bus_fare_spending: 330.46

# Cap changes use the table of uncapped single fares in
# gov.dft.bus.uncapped_single_fares: 55% of singles at GBP 1.65, 30% at
# GBP 2.21 and 15% at GBP 3.87 at FY2024-25 prices, uprated with OBR CPI
# gov.dft.bus.uncapped_single_fares: 56% of singles at GBP 1.70, 31% at
# GBP 2.21 and 13% at GBP 3.96 at FY2024-25 prices, uprated with OBR CPI
# (FY2025-26 x 1.61059 / 1.55763 = 1.0340). Case 10's passenger has 100 trips
# * 1.12466920264 boardings * 0.7 single share = 78.7268 single boardings, at
# a GBP 196.87 baseline.
# a GBP 181.06 baseline.
- name: Case 14, without a cap in 2025-26 the singles above GBP 3 pay in full.
period: 2025
absolute_error_margin: 0.01
Expand All @@ -211,9 +211,9 @@
local_bus_single_fare_share: 0.7
gov.dft.bus.fares.cap: .inf
output:
# Only the high band (3.87 * 1.0340 = 4.0016) is above GBP 3:
# 196.87 + 78.7268 * 0.15 * 1.0016.
person_bus_fare_spending: 208.70
# Only the high band (3.96 * 1.0340 = 4.0946) is above GBP 3:
# 181.0594 + 78.7268 * 0.13 * 1.0946.
person_bus_fare_spending: 192.26

- name: Case 15, a GBP 2 cap in 2025-26 saves the middle and high bands.
period: 2025
Expand All @@ -226,5 +226,22 @@
gov.dft.bus.fares.cap: 2
output:
# Middle band 2.21 * 1.0340 = 2.2851 falls to 2; the high band falls
# from 3 to 2: 196.87 - 78.7268 * (0.30 * 0.2851 + 0.15 * 1).
person_bus_fare_spending: 178.33
# from 3 to 2: 181.0594 - 78.7268 * (0.31 * 0.2851 + 0.13 * 1).
person_bus_fare_spending: 163.87

- name: Case 16, paying youth journeys remain priced and free-pass journeys cost nothing.
period: 2024
absolute_error_margin: 0.01
input:
people:
person1: {age: 12, other_local_bus_trips: 100, bus_pass_eligible: false}
person2: {age: 30, other_local_bus_trips: 100, bus_pass_eligible: false}
person3: {age: 70, other_local_bus_trips: 100, bus_pass_eligible: true}
households:
household: {members: [person1, person2, person3], region: SOUTH_EAST}
output:
# BUS05ai receipts / (BUS01a total - BUS01c older/disabled boardings)
# = GBP 1.429858292587. Each paying passenger: 100 * 1.12466920264
# boardings per trip * 1.429858292587 = GBP 160.8118.
person_bus_fare_spending: [160.81, 160.81, 0]
bus_fare_spending: 321.62
59 changes: 49 additions & 10 deletions policyengine_uk/tests/test_bus_fare_parameters.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Bus fare parameters agree with each other and with the facts they encode."""

import datetime
import runpy
from pathlib import Path

import pytest
Expand Down Expand Up @@ -52,9 +53,35 @@ def test_reference_cap_is_the_cap_the_published_index_reflects():

BANDS = ("LOW", "MIDDLE", "HIGH")
SINGLE_SHARE = 0.70
# DfT BUS01a journeys less BUS01c concessionary journeys, England outside London.
FARE_PAYING_2023_24 = 1_780_912_900.016 - 499_730_082.115
FARE_PAYING_2024_25 = 1_850_509_887.261 - 519_107_608.594
# DfT BUS01a journeys less BUS01c older/disabled journeys, England outside
# London. Other concessions remain in priced boardings, with their discounts
# reflected in the average yield.
# https://assets.publishing.service.gov.uk/media/69c11f97cfa346b9d4704a50/bus01.ods
PRICED_BOARDINGS_2023_24 = 1_780_912_900.01636 - 391_017_149.37439
PRICED_BOARDINGS_2024_25 = 1_850_509_887.26131 - 402_846_350.14539


def test_outside_london_yield_uses_older_disabled_concessions():
# BUS05ai FY2024-25 receipts divided by BUS01a boardings less only the
# older/disabled BUS01c journeys. Subtracting every concessionary journey
# also excludes other concessions and inflates this yield.
yield_per_boarding = system.parameters.gov.dft.bus.yield_per_boarding
assert yield_per_boarding.ENGLAND_OUTSIDE_LONDON("2024-01-01") == pytest.approx(
2_069_953_713.42079 / PRICED_BOARDINGS_2024_25, abs=1e-12
)


@pytest.mark.parametrize(
"area, expected",
[
("LONDON", 1.017729905895),
("SCOTLAND", 2.599723222144),
("NORTHERN_IRELAND", 2.550929922065),
],
)
def test_other_area_yields_are_unchanged(area, expected):
yield_per_boarding = system.parameters.gov.dft.bus.yield_per_boarding
assert yield_per_boarding.children[area]("2024-01-01") == expected


def cpi(year):
Expand Down Expand Up @@ -109,18 +136,18 @@ def test_table_matches_january_2025_index_step():
# BUS0415a outside London rose 11.0% from December 2024 to March 2025,
# when the cap rose from GBP 2 to GBP 3; neighbouring quarters moved 0.1%
# to 2.3%. The cap's part, 9% to 11% of the yield at the December 2024
# index level (1.554716967657 * 185.9 / 190.2282).
december_yield = 1.554716967657 * 185.9 / 190.2282
# index level (1.429858292587 * 185.9 / 190.2282).
december_yield = 1.429858292587 * 185.9 / 190.2282
step = SINGLE_SHARE * (saving(2) - saving(3)) / december_yield
assert 0.09 <= step <= 0.11


def test_table_matches_2023_24_fare_cap_grant():
# BUS05bi fare cap support for 2023-24, a full year at GBP 2: GBP 278.95m,
# over 1,281m fare-paying boardings. Fares at 2023-24 prices.
# over 1,390m priced boardings. Fares at 2023-24 prices.
grant = (
SINGLE_SHARE
* FARE_PAYING_2023_24
* PRICED_BOARDINGS_2023_24
* saving(2, price_ratio=cpi(2023) / cpi(2024))
)
assert grant == pytest.approx(278_951_028, rel=0.15)
Expand All @@ -129,9 +156,9 @@ def test_table_matches_2023_24_fare_cap_grant():
def test_table_matches_announced_cap_funding():
# Over GBP 150m for the GBP 3 cap in 2025 (DfT written statement, 29
# October 2024) and GBP 400m for the GBP 2 cap in 2027 (DfT, 22 July 2026),
# at the 2024-25 number of fare-paying boardings.
cap_3 = SINGLE_SHARE * FARE_PAYING_2024_25 * saving(3, year=2025)
cap_2 = SINGLE_SHARE * FARE_PAYING_2024_25 * saving(2, year=2027)
# at the 2024-25 number of priced boardings.
cap_3 = SINGLE_SHARE * PRICED_BOARDINGS_2024_25 * saving(3, year=2025)
cap_2 = SINGLE_SHARE * PRICED_BOARDINGS_2024_25 * saving(2, year=2027)
assert cap_3 == pytest.approx(150e6, rel=0.20)
assert cap_2 == pytest.approx(400e6, rel=0.20)

Expand Down Expand Up @@ -162,3 +189,15 @@ def test_removed_uncapped_single_fare_gives_directions():
assert "gov.dft.bus.uncapped_single_fare has been removed" in message
assert "gov.dft.bus.uncapped_single_fares" in message
assert "local_bus_uncapped_single_fare" in message


def test_uncapped_table_matches_fit(pytestconfig):
# The fit uses published DfT facts and the current OBR CPI path. Changes
# to those inputs require refitting both shares and fares.
# CI tests installed wheels from the checkout's pyproject.toml too.
script = pytestconfig.inipath.parent / "scripts" / "fit_bus_cap_savings.py"
fit = runpy.run_path(str(script))["fit"]
_, (shares, fares) = fit()
for (share, fare), fitted_share, fitted_fare in zip(bands(), shares, fares):
assert share == pytest.approx(fitted_share, abs=1e-12)
assert fare == pytest.approx(fitted_fare, abs=1e-12)
Loading
Loading