Add age-based bus fare allocation and young-person fare reform#1781
Add age-based bus fare allocation and young-person fare reform#1781vahid-ahmadi wants to merge 5 commits into
Conversation
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>
MaxGhenis
left a comment
There was a problem hiding this comment.
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:
-
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.
-
The weights in
fare_allocation_weight_by_ageare 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.
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_spendinginput.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 setsage_limit = 22for 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 throughdft_subsidy_spending, which already feeds bothhousehold_benefits(→ net income) andgov_spending(→ Exchequer cost), so both sides of the policy are captured with no core surgery.Why
bus_fare_spendingis 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
Stacking
Stacked on #1780 (
add-bus-fare-spending-variable) — it referencesbus_fare_spending. Merge #1780 first; this PR's base will retarget tomainautomatically. Also relies on the imputation in policyengine-uk-data#428.🤖 Generated with Claude Code