Cycling Race Predictor Β· System overview

How the model prices a bike race

Seven LightGBM heads, one pricing layer, and a decade of result sheets β€” a tour of every number the prediction app shows, what each one actually measures, and how they fit together. New since the Tour: a day-type retrieval channel that re-picks who's in the conversation without touching a single price.

668K rider-race training rows 5,905 competitions, 2016–2026 181 features per rider 7 pricing heads + 1 retrieval ranker retrained 2026-08-09
Direct links: #plain (friendly version), #tdf (Tour predictions), #books (model vs bookmakers), #heads (how the heads disagree).
Tour diary: Stage-by-stage predictions & reflections β†’ Β· GC tracker Β· Post-race review Β· Stage 2 βœ“ Β· Stage 3 βœ“ Β· Stage 4 βœ“ Β· Stage 5 βœ“ Β· Stage 6 βœ“ Β· Stage 7 βœ“ Β· Stage 8 βœ“ Β· Stage 9 βœ“ Β· Stage 10 βœ“ Β· Stage 11 βœ“ Β· Stage 12 βœ“ Β· Stage 13 βœ“ Β· Stage 14 βœ“ Β· Stage 15 βœ“ Β· Stage 16 βœ“ Β· Stage 17 βœ“ Β· Stage 18 βœ“ Β· Stage 19 βœ“ Β· Stage 20 βœ“ Β· Stage 21 βœ“
Vuelta diary: Stage-by-stage predictions & reflections β†’ Β· GC tracker Β· Post-race review Β· Stage 1 βœ“ Β· Stage 2 βœ“ Β· Stage 3 Β· Stage 4 βœ“ Β· Stage 5 βœ“ Β· Stage 6 βœ“ Β· Stage 7 βœ“ Β· Stage 8 βœ“ Β· Stage 9 βœ“ Β· Stage 10 βœ“ Β· Stage 11 βœ“ Β· Stage 12 βœ“ Β· Stage 13 βœ“ Β· Stage 14 βœ“ Β· Stage 15 βœ“ Β· Stage 16 βœ“ Β· Stage 17 βœ“ Β· Stage 18 βœ“ Β· Stage 19 βœ“ Β· Stage 20 βœ“ Β· Stage 21 βœ“
Race diary: Stage-by-stage predictions & reflections β†’ Β· GC tracker Β· Stage 1 βœ“
Race diary: Stage-by-stage predictions & reflections β†’ Β· GC tracker Β· Stage 1 βœ“
Race diary: Stage-by-stage predictions & reflections β†’ Β· GC tracker Β· Stage 1 βœ“ Β· Stage 2 βœ“ Β· Stage 3 βœ“ Β· Stage 4 βœ“
Race diary: Stage-by-stage predictions & reflections β†’ Β· GC tracker Β· Stage 1 βœ“ Β· Stage 2 βœ“ Β· Stage 3 βœ“ Β· Stage 4 βœ“ Β· Stage 5 βœ“
Race diary: Stage-by-stage predictions & reflections β†’ Β· GC tracker Β· Stage 2 βœ“ Β· Stage 3 βœ“ Β· Stage 4 βœ“
Race diary: Stage-by-stage predictions & reflections β†’ Β· GC tracker Β· Stage 1 βœ“ Β· Stage 2 βœ“ Β· Stage 3 βœ“ Β· Stage 4 βœ“ Β· Stage 5 βœ“
Race diary: Stage-by-stage predictions & reflections β†’ Β· GC tracker Β· Stage 1 βœ“ Β· Stage 2 βœ“ Β· Stage 3 βœ“
Race diary: Stage-by-stage predictions & reflections β†’ Β· GC tracker Β· Stage 1 βœ“ Β· Stage 2

The 30-second version

One race, one pass through the stack

Paste a race URL into the app and it scrapes the startlist, attaches each rider's history as-of today, and runs the field through a stack of models. Each model answers a different question β€” who's strongest, by how much, will they finish at all β€” and a final pricing layer turns those answers into a coherent set of sportsbook-style probabilities.

MAIN LINE CALIBRATION LINE FINISHING LINE Startlist + 164 features/rider Bucket head ranks the whole field (+ time-gap head) Top-15 gate contenders only Contender cascade Win Β· Podium Β· Top 5 Β· Top 15 sharp, discriminative probabilities Pricing layer 1 Β· Win corrector logit shift, fixes bias 2 Β· Blend 0.2 mc + 0.8 LR 3 Β· DNF adjust honest prices each step book-preserving What you see prices, odds, every layer visible Stack features fed into the cascade time-gap + LambdaRank out-of-fold picks, contested Elo, one-day contest signatures LambdaRank head full-field, calibrated win book LR Win % β†’ blend (step 2) DNF head will they finish at all? DNF % β†’ honest prices (step 3) GC outlook head (separate panel) mid-race only: final GC given standings after stage k β€” runs beside the stack, not through it Day-menu retrieval channel (keyed stage days Β· new Aug 2026) a day-type-featured ranker re-picks the top-15 gate for the keyed day β€” every price above is unchanged

The main line ranks, gates, and prices. Two side lines β€” a calibration head and a finishing head β€” join at the pricing layer rather than being averaged in earlier. The GC outlook head runs beside the stack, not through it; the new day-menu channel can re-decide who passes the top-15 gate on a keyed stage day, but never touches a price.

A one-line mental model: the bucket head ranks the field β†’ the cascade prices the top β†’ the corrector fixes its win bias β†’ the blend borrows LambdaRank's calibration β†’ the DNF adjustment makes the prices honest β€” and every intermediate number stays visible in the table rather than being swallowed by the final one.

The riders on this team

Seven heads, seven different questions

Every head is a LightGBM model, but each differs on three axes: what it predicts, who it compares (the whole field? only contenders? riders who didn't finish?), and what loss it optimizes. Those choices determine what its number means β€” and why no single head is enough.

Results are scored on a tier scale: winner β†’ podium β†’ top 5 β†’ top 15 β†’ top 30 β†’ top 50 β†’ finisher, referred to below as buckets.

Bucket head

"Roughly where in the result sheet does this rider land?"

Full field Ordinal regression

The ranking backbone. Predicts a continuous finishing tier for every starter and sorts the field. It is also the gatekeeper: its top 15 defines who counts as a contender everywhere downstream.

Time-gap head

"How far behind the winner, in seconds?"

Full field Regression

Same ordering intuition as the bucket head, but in physical units β€” a 2-second gap and a 12-minute gap can be the same tier, and this head knows the difference. That magnitude signal is why its prediction is also fed back into the cascade as a feature.

Contender cascade

"Among the genuine contenders, how do the top placings split?"

Top-15 finishers 5-class softmax

The headline pricing head. Trained only on gated contenders, with the bucket head's own opinion as an input, it is sharp at separating winner from podium from top 5. Its probabilities are conditional β€” on being a contender, and on finishing. Drives the displayed Win / Podium / Top 5 / Top 15 % and the odds.

LambdaRank head

"Over the entire startlist, what's one coherent win book?"

Full field Ranking loss

Trained with a learning-to-rank objective and converted to probabilities by a per-race softmax with a fitted temperature. It never beat the cascade at picking winners β€” but its probabilities are almost perfectly calibrated, which is exactly what you want when comparing against a bookmaker's line.

DNF head

"Will they finish at all?"

All starters Binary classifier

The only head whose training population includes riders who abandoned. Every other probability is implicitly "given that they finish" β€” this head supplies the missing factor (AUC β‰ˆ 0.89). Applied only at the pricing layer; mixing it into the finish-conditioned heads was tested and made things worse.

GC outlook head

"Given the standings after stage k, who wins the overall?"

Mid-race state Bucket + cascade

A state-aware re-run of the main architecture for stage races: it sees each rider's current GC time, rank, and gap. Mid-race it beats simply carrying pre-race predictions forward by ~18 points of winner accuracy. Deliberately shown as its own panel, never merged into the pre-race table.

Contested Elo

"How strong is this rider when a result was actually disputed?"

All riders Rating system

Not a model head but a measurement: an Elo rating built only from informative comparisons β€” contested sprints within the winner's group, splits between adjacent groups. It enters the models as a feature; the app also shows it raw, with the number of comparisons behind it.

Win corrector

"Is the cascade systematically shy about winners?"

Contender rows Logit shift

A single fitted parameter, not a model: a logit-scale shift that fixes the cascade's measured win-market under-confidence (validation calibration 0.97 β†’ 1.00 at the last training run). Applied before anything else in the pricing layer.

Team tactics

How the heads combine β€” two distinct mechanisms

Inside the models: feature stacking

The cascade doesn't work alone. At training time, out-of-fold predictions from the time-gap and LambdaRank heads are added to its feature set, alongside the contested Elo rating and β€” for one-day races β€” contest signatures: a forecast of which kind of rider (sprinter, climber, puncheur…) will actually contest the finish, built from the startlist's composition and the race's history. At prediction time the same 17 columns are attached from the live heads, so training and serving see the same world.

After the models: the pricing layer

Three post-processing steps, each book-preserving β€” they reshape who gets which share of probability without inflating the total implied over the field.

Win corrector

The fitted logit shift is applied to contender win probabilities and the odds are recomputed. Deliberately not renormalized β€” pinning the book here was the root cause of an earlier calibration bug.

Blend win market

The sharp cascade and the calibrated LambdaRank book are combined into a third number, then rescaled so the field's total win probability still matches the cascade's book:

blend = 0.2 Γ— cascade + 0.8 Γ— LambdaRank  β†’  renormalized to the cascade book

An overlay that borrows LambdaRank's calibration without changing the total vig.

DNF adjustment β€” honest prices

Every market is multiplied by the probability of finishing:

P(podium) = P(podium | finishes) Γ— P(finishes)

Podium / top-5 / top-15 shrink directly (you can't podium if you don't finish); the win market is renormalized within the race so the total win book is preserved. The result is the price you'd actually want to compare against a bookmaker, whose "did not finish" is just a losing ticket.

Where heads deliberately stay separate

The cascade's Win % and LambdaRank's Win % are shown side by side, and any rider where they disagree by 2+ percentage points is highlighted. That disagreement is a feature, not a bug β€” the two heads carry genuinely different signal, and a large gap is the app telling you to look closer.

Win % β€” the cascade

Use it to pick winners. Sharpest at separating the very best riders from each other; conditioned on contending and finishing.

LR Win % β€” LambdaRank

Use it to compare against a book. Near-perfectly calibrated across the whole field; the honest baseline for value hunting.

Likewise, the GC outlook stays a separate panel, the Elo rating stays a raw column, and every run β€” both heads' probabilities, the blend, DNF, adjusted prices β€” is logged to a prediction database for long-term comparison against real results and real odds.

New β€” August 2026

The day-type retrieval channel

The Tour exposed the stack's one structural hole: transition stages decided by a surviving break or a hard late move. The boards priced sprints and summit finishes well, but on those in-between days the winner repeatedly came from a rider class the pre-race top 15 had buried. A month of gated experiments produced a fix β€” and, just as importantly, established what the fix is not allowed to be.

The menu

Eleven day types, defined in rider-composition space β€” who contests the front ~15 (sprinters, climbers, puncheurs, rouleurs, break specialists…), not what the profile looks like. Three are routed mechanically (individual time trial; full bunch sprint; GC day, where the overall contenders fight for the win). The other eight are analog day types learned from a decade of stages, organized in two sharply distinct families: escape/climb (mountain escape β†’ hard puncheur β†’ rouleur power group) and power-sprint (survivor's gallop β†’ split sprint β†’ fast-finisher group). Within a family the items are graded neighbors; across families they barely overlap. The menu is a frozen artifact β€” assigning a day is a stored-parameter lookup, never a refit.

The ranker

A retrained variant of the bucket head with 14 extra columns: the keyed day type's canonical composition, plus per-rider kernel affinities β€” how often the rider shows up, and how they finish, on historically similar days (exposure, attendance, top-3 rate, mean finish), computed strictly as-of race morning from 642K historical rider-rides with a two-year half-life. It engages only when a stage is keyed.

What it may and may not do

Every stronger use of this signal was tested and failed honestly, which is why the channel is retrieval-only:

DesignVerdictEvidence
Day-type features inside the pricing headsRejected+0.011 log loss vs the market benchmark, worse on 5 of 5 folds β€” a global tax paid even by races the channel never touches
Conditional boards served as pricesRejected+0.016 even with perfect keys. The signal is top-15-grade: day-type affinity says who shows up at the front, not who wins the kick
Retrieval only β€” pool membership and board orderShippedPricing neutral (+0.0005); with judgment-grade keys, top-15 pool coverage +2.45pp and mean winner rank βˆ’1.84 on the hard residual days, 5 of 5 folds

The structural guarantee follows from the wiring, not from good intentions: the keyed ranker can only change who passes the top-15 gate and in what order. Riders it promotes are priced by the same production contender cascade, corrector, blend, and DNF adjustment as anyone else. A wrong key costs pool composition for a day; it cannot corrupt a price.

Serving it at the Vuelta

Each race morning, a human keys the stage to a menu item or a weighted mix ("mountain escape 0.6 / hard puncheur 0.4" β€” the mix happens at the ranking-score level, never by averaging prices). Stage pages show the key as a keyed: chip; unkeyed stages serve the plain production board; one-day races key themselves from their own historical signature. In the dress rehearsal β€” Tour stage 4 replayed under a mountain-escape key β€” the keyed board put Mads Pedersen first (he won), promoted the eventual break riders from as deep as 88th into the pool, and dropped Pogačar down the order with his price untouched.

The Vuelta a EspaΓ±a 2026 is the channel's prospective evaluation: every morning's key is archived before the stage, every evening it's scored against an oracle labeler that keys the day from the actual result. That scorecard β€” not the Tour replay β€” will decide whether human day-reading is close enough to the ceiling for the channel to stay on by default.

Reading the app

Every column in a prediction run

One table per reference race (plus an averaged tab). Each column traces back to exactly one place in the stack:

ColumnComes fromHow to read it
Rank Β· Predicted BucketRankingThe field sorted by the bucket head (optionally ensembled with the time-gap head)
Expected BucketRankingContinuous finishing-tier estimate; lower is better
Time Gap (s)RankingPredicted seconds behind the winner
ContenderRankingInside the bucket head's top 15 β€” eligible for cascade pricing
Win / Podium / Top 5 / Top 15 %CascadeDiscriminative probabilities, win corrector already applied
LR Win / LR Podium %LambdaRankCalibrated full-field book; the sportsbook-comparison line
Ξ” Win ppBothCascade vs LambdaRank disagreement; highlighted at β‰₯ 2pp
Blend Win %Pricing0.2 Γ— cascade + 0.8 Γ— LambdaRank, book-preserving
DNF %DNF headProbability of not finishing, given a start
Win…Top 15 % (adj)PricingHonest prices: markets Γ— probability of finishing
Elo Β· Elo nRatingContested-comparison rating and how many duels back it
Odds (win…top 30)CascadeDecimal / American / fractional, from corrected probabilities
GC Outlook panelGC headMid-race only: final-GC probabilities given current standings
Keyed board (stage pages)RetrievalBoard order under the morning's day-type key; pool changes, prices don't

Everything degrades gracefully. Each head is optional β€” if an artifact is missing, its columns disappear and a provenance panel says exactly which layers engaged: which references were used, whether live form re-aging ran, whether signatures applied (one-day races only), whether the corrector, DNF head, and blend were active.

The engine room

What it's all built on

A weekly pipeline scrapes results from FirstCycling, migrates them into SQLite, and precomputes ~1.1M point-in-time rider snapshots back to 2010 β€” every feature strictly "as of the day before the race," so the models never peek at the future. Training covers 2016–2026: about 668K rider-race rows across 5,905 competitions at the WorldTour, ProSeries, and .1 levels. The full suite was last retrained on 2026-08-09, on a corpus current through that day β€” including the complete Tour de France 2026 and a repaired 2025 late-season block (a source-site markup change had silently un-dated 35 races, Vuelta 2025 among them; they were caught, re-scraped, and re-migrated before this training run).

Each rider carries 164 features into a prediction β€” long-run quality percentiles, recent-form trajectories (recomputed live as of prediction day), team context, race-profile fit against the course's gradients β€” plus 17 stacked columns (head outputs, contested Elo, contest signatures), for 181 into the flagship ranking head; the day-menu variant adds 14 more. Every prediction run is persisted, so the system continuously builds the dataset needed to judge itself against real books.