Skip to content

Add age-based bus fare allocation and young-person fare reform#1781

Open
vahid-ahmadi wants to merge 5 commits into
mainfrom
bus-fare-age-allocation
Open

Add age-based bus fare allocation and young-person fare reform#1781
vahid-ahmadi wants to merge 5 commits into
mainfrom
bus-fare-age-allocation

Conversation

@vahid-ahmadi

Copy link
Copy Markdown
Collaborator

What

Adds the person/age allocation layer that lets PolicyEngine model age-targeted bus fare policies (e.g. Scotland-style free travel for under-22s), on top of the household bus_fare_spending input.

  • gov.dft.bus.fare_allocation_weight_by_age — provisional per-age weight for splitting household bus fares across members. NTS (England) bus-trip-by-age profile, adjusted for concessionary (free-pass) travel so it tracks fares paid, not trips. Clearly documented as a modelling allocation, not a fare-incidence estimate.
  • gov.dft.bus.young_person_fare.{age_limit, rate} — policy levers. Inert by default (age_limit = 0), so baseline is unchanged. A reform sets age_limit = 22 for free under-22 travel; rate (default 0 = free) supports discounts/flat fares too.
  • household_bus_fare_weight / person_bus_fare_spending — the allocation (household_fare × age_weight / household_total_weight), conserving the household total.
  • bus_fare_relief — government-funded fares for eligible young people. Routed through dft_subsidy_spending, which already feeds both household_benefits (→ net income) and gov_spending (→ Exchequer cost), so both sides of the policy are captured with no core surgery.

Why

bus_fare_spending is household-level (LCFS has no person-level fares), and rail is modelled at household level too. An age-targeted reform needs the fare attributed to people, which this provides — following existing PE-UK household→person allocation patterns (housing_benefit_assessable_capital, employer_ni_response_consumer_incidence).

Tests

test_bus_fare_age_allocation.py (3 passing): allocation splits by age weight and conserves the total; baseline relief is zero; an under-22 reform relieves the eligible member's allocated fare.

Caveats / scope

  • The age weights are provisional (two verified NTS anchors — 17–20 peak, ~25 mean — plus modelled mid-bands and assumed concessionary paid-shares; England used as a UK proxy). Fine for a directional prototype; publication-grade costing should use historical NTS0601 ODS or NTS microdata.
  • Static — no behavioural response (lower fares → more trips) yet.

Stacking

Stacked on #1780 (add-bus-fare-spending-variable) — it references bus_fare_spending. Merge #1780 first; this PR's base will retarget to main automatically. Also relies on the imputation in policyengine-uk-data#428.

🤖 Generated with Claude Code

vahid-ahmadi and others added 5 commits June 16, 2026 14:25
Companion to the LCFS bus fare imputation in policyengine-uk-data (#428).
Household-level COICOP 7.3.2 (bus & coach fares), mirroring petrol_spending/
diesel_spending. CPI-uprated; not added into the consumption total, which
already counts bus fares via transport_consumption. Provides the passenger
fare households pay (vs the ETB-based bus_subsidy_spending) for modelling
bus fare reforms.

Resolves #1779.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Allocate household bus_fare_spending across members by an NTS-derived,
concessionary-adjusted age weight (provisional), enabling age-targeted bus
fare reforms such as Scotland-style free travel for under-22s.

- gov.dft.bus.fare_allocation_weight_by_age: provisional per-age allocation weight
- gov.dft.bus.young_person_fare.{age_limit,rate}: policy levers (inert by default)
- household_bus_fare_weight, person_bus_fare_spending: the allocation
- bus_fare_relief: government-funded fares for eligible young people, routed
  through dft_subsidy_spending so it counts as both an in-kind household
  benefit and government spending

Depends on the bus_fare_spending input variable (#1780).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Base automatically changed from add-bus-fare-spending-variable to main June 17, 2026 11:13
@vahid-ahmadi
vahid-ahmadi requested a review from MaxGhenis June 17, 2026 12:55
@vahid-ahmadi vahid-ahmadi self-assigned this Jun 17, 2026

@MaxGhenis MaxGhenis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The modelling is sound and the reform is correctly inert by default (agent-assisted review): age_limit defaults to 0, so bus_fare_relief is zero for everyone and dft_subsidy_spending is unchanged at baseline. The household-to-person allocation conserves the household total, and routing relief through dft_subsidy_spending captures both the in-kind household benefit and the Exchequer cost without touching core. No double-count: bus_fare_spending has no other consumer on main, and bus fares are VAT zero-rated.

Two things before this can merge:

  1. CI has never run on this branch (zero check runs), and it's 72 commits behind main. #1780 and uk-data#428 have both merged since, so rebase onto main and push to trigger CI — the diff still shows #1780's files because the branch predates that squash-merge, and those drop out on rebase. Not mergeable until CI is green.

  2. The weights in fare_allocation_weight_by_age are provisional modelling assumptions, not measured values — only the 17-20 peak is anchored to NTS; the mid-bands and concessionary paid-shares are assumed, with England as a UK proxy. Fine for a directional prototype and documented as such, and it only affects the age-incidence of a reform (the household total is conserved). Flagging so we don't reuse these for a published costing without NTS0601 / NTS microdata.

Minor: test_bus_fare_spending_uses_cpi_uprating is sitting in test_road_fuel_volume_uprating.py — give it its own file. And consider whether young_person_fare belongs under gov.contrib rather than the baseline gov.dft.bus tree, since it models a non-UK-wide scheme that only switches on via reform; either works, but contrib signals "contributed lever" more clearly.

No blocking code changes — I'll approve once it's rebased and CI is green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants