Skip to content

Rail fares freeze: the engine ships both legs of the counterfactual (#105) - #107

Open
vahid-ahmadi wants to merge 1 commit into
mainfrom
uk/rail-fares-lane
Open

Rail fares freeze: the engine ships both legs of the counterfactual (#105)#107
vahid-ahmadi wants to merge 1 commit into
mainfrom
uk/rail-fares-lane

Conversation

@vahid-ahmadi

Copy link
Copy Markdown
Contributor

Closes #105. Branches from main — independent of the other UK PRs.

Same shape as #102: a reform specification with every path resolved against a real policyengine-uk 2.89.2, and no value claims staged.

Why this lane is cheap

The 2026 regulated fares freeze is already in the certified baseline, so the scored object is the counterfactual — what fares would have done under prior law. Unusually, the engine carries that leg too:

year gov.dft.rail.fare_index gov.dft.rail.prior_law_fare_index gap
2026 1.217 1.288 +5.83%
2027 1.268 1.342 +5.84%
2028 1.318 1.394 +5.77%

#13 and #67 both had to construct an indexed counterfactual by hand — the axis #67 found undecomposed in OBR's own re-estimates. Here it is a parameter. The reform is one line: fare_index := prior_law_fare_index.

It reaches an outcome, which is what makes it a lane rather than a curiosity:

rail_usage × fare_index → rail_subsidy_spending → dft_subsidy_spending

That factorisation is deliberate. rail_subsidy_spending's own docstring says it exists "allowing reforms to modify fare prices independently of usage quantity."

What the validator refuses

Each of these is a way the spec could be wrong while still looking right:

Scope, named up front

Regulated fares are a minority of tickets sold in England, and DfT reports savings per passenger where the engine's quantities are per household. Both are recorded as named divergence axes (#59) before any comparison runs — unnamed, either gap gets attributed to the engine.

Not computed here

This PR specifies and resolves; it computes no counterpart. UK counterpart compute runs through #51, which isn't merged. Publishing a number before that lands would bypass the compute contract.

Gates

275 tests (12 new), ruff format --check clean, two build_db runs agree on content_hash, no drift.

#106 is the other half of the pair — bus, which is not expressible.

…105)

The 2026 regulated rail fares freeze is already in the certified
baseline, so the scored object is the counterfactual — what fares would
have done under prior law. Unusually, the engine carries that leg too:

  year   fare_index   prior_law_fare_index   gap
  2026   1.217        1.288                  +5.83%
  2027   1.268        1.342                  +5.84%
  2028   1.318        1.394                  +5.77%

#13 and #67 both had to construct an indexed counterfactual by hand.
Here it is a parameter, which is what makes this lane cheap.

It reaches an outcome: rail_usage x fare_index -> rail_subsidy_spending
-> dft_subsidy_spending. That factorisation is deliberate — the engine's
own docstring says it exists "allowing reforms to modify fare prices
independently of usage quantity".

The validator refuses four things, each a way this could be wrong while
still looking right:

  - a counterfactual that is not a change (scores nothing, reads as
    agreement), checked PER YEAR so a freeze that lapses mid-horizon
    cannot pass on 2026 alone
  - swapped legs, which would score a fare CUT and still look plausible
  - staged value claims: DfT's saving figures are DfT's (#86/#91), and
    belong in their own harvest lane from primary documents
  - unresolved paths — --resolve reads every path and head variable out
    of a real 2.89.2, because this repo has published two false engine
    gaps from paths that were never resolved (#100, #101)

The scope mismatch is named up front rather than discovered later:
regulated fares are a minority of tickets sold, and DfT counts
passengers where the engine counts households. Unnamed, either gap gets
attributed to the engine (#59).

No counterpart is computed here — UK compute runs through #51.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HuXJFVme8HRbnke2Ey3Me
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.

Rail fares (mode 2): PE-UK already encodes the 2026 freeze as a counterfactual pair, and no lane touches it

1 participant