Matt Priselac

II.Workshop 02 · Modeling · Agents · Calibration

Calling the peloton

I publish predictions for men’s WorldTour and ProSeries road races before they happen, then settle every one of them against the result afterwards, misses included.

Live and publishing, in active development

The short version

The point isn’t to beat the bookmakers. The point is to find out what a model can and can’t know about a bike race, and to be honest about it in public.

What it does

Under the hood is a small stack of LightGBM models trained on FirstCycling results from 2016 to 2026: about 668,000 rider-race rows across 5,905 competitions, and about 1.1 million point-in-time snapshots going back to 2010. Every feature is computed as of the day before the race, so the model never gets to peek. Rather than one model, there are seven “heads,” each answering a different question.

  • Who are the contenders?A bucket head ranks the whole startlist and picks a top 15.
  • Who wins, podiums, or makes the top five?A contender cascade, trained only on those top-15 riders.
  • What’s a fair price for everyone?A LambdaRank head over the full field, for a calibrated win book.
  • How far back?A time-gap head, in seconds behind the winner.
  • Who doesn’t finish?A DNF head.
  • How’s the GC shaping up?A mid-race outlook head that reads the current standings.
  • Who has won the contested fights?A “contested Elo” built only from disputed head-to-heads, used as a feature.

A pricing layer turns those into probabilities: one fitted correction to the cascade, a blend of 20% cascade and 80% LambdaRank, and a DNF adjustment. Since August 2026 there is also a day-type channel: I key each stage to one of eleven kinds of day (flat sprint, summit finish, and so on), and a ranker re-picks the shortlist. It is deliberately not allowed to touch anyone’s price.

Around the models sits a crew of agents that runs every night: one fetches results, one reads what kind of day a stage will be, one writes the previews and retrospectives, and a coordinator keeps them on schedule. Anything that needs a human lands in my inbox, and every draft waits for my verdict before it goes out. Every race ends with a written review of what the model got right and wrong.

Why calibration, not tips

For a one-day race the model names the winner about one time in four. That isn’t the interesting number. The interesting question is whether, over a season, things the model calls 30% happen about 30% of the time. Bookmaker odds sit on every page as a benchmark for how much information the model has, not as betting guidance.

What it has taught me so far

  • The model reads a race about as well as the market, but it’s shy at the top.At the 2026 Tour, it found the right contenders as well as the book did, but when it said 63.5% for a top five, it happened 82.1% of the time.
  • Know what kind of day it is.Most of the Tour deficit came from a handful of hard-man and breakaway days. That finding became the day-type channel.
  • Let the day type change the shortlist, never the prices.Every version that let it move prices made the predictions worse.
  • One Tour is a single sample.By the Vuelta the model had closed a 0.46-nat-per-stage gap to finish level with the market on stage winners, and clearly ahead on days the bunch or the GC group settled.
  • Escapes are still the hard problem.In 12 of 13 hindsight runs it couldn’t find the breakaway winner. Neither, mostly, could the market.

Next

From the Vuelta review: put the GC outlook head on the GC page, teach the day key to ask “who has a reason to chase,” and calibrate against the current race rather than a fixed July constant.

Development diary

406 commits over three seasons, from a first scrape in January 2025 to a nightly crew of agents publishing on their own.

Jan 2025Jul 2025 · 37five quiet monthsJul 2026 · the TourSep 2026 · 177

Season one: learning what a race is

January – August 2025
Dev diary
29 January 2025

First commit, first scrape

The repo starts the same week as the first big scrape of FirstCycling: about 3,200 race files from 2000 to 2024, sorted into WorldTour, ProSeries and the tier below. Within three days there is a module structure, a rider “career” object with a dated race history, and date cleaning so that history is cut off at a day rather than a year. That last change is the seed of the rule the whole project still lives by: nothing the model sees can postdate the race.

Dev diary
February 2025

The first predictors

The first models are deliberately simple: predict from a rider’s average past finishing place, then from bib numbers (teams number their leaders first), then population-wide and per-rider regressions with better evaluation metrics. Alongside them, a “race characteristics” database tries to describe each race by the shape of its finish, ending in a five-dimension race feature set on 19 February.

Dev diary
March – April 2025

What kind of race was that?

Most of the spring goes into characterizing races from their results alone: how shattered the field was, how clumped, how big the biggest group. Then a full suite of statistical measures and clustering tools as an alternative. The question “what kind of day was it?” turns out to be the thread that runs through the whole project.

Dev diary
16 May 2025

The webscrape refactor

The scraper is rebuilt with logging and tests. It is also the first commit that mentions Claude: guidance notes for the scraping module, so an AI pair programmer can work on it safely.

Dev diary
July 2025

Embeddings, interactions, and a database too big for the assistant

The busiest month of the first season (37 commits). The first real training infrastructure lands (a 2019 model “fully functional” on 10 July), followed by rider embeddings, interaction-based embeddings, correlation tracking by race type, and clustering of “race kinds.” GPU optimization is tried and abandoned. On 18 July the database grows so large it crashes Claude Code, so I add an MCP server to give the assistant query access instead.

Dev diary
August 2025

Going quiet

A last round of race-kind clustering and stability analysis on 9 to 11 August, then nothing for five months.

Season two: a product

January – June 2026
Dev diary
4–5 January 2026

Back at it

The whole 2025 season is re-scraped in one go, about 150 races. The code follows six weeks later.

Dev diary
16–28 February 2026

A Streamlit app, and 1,368 files thrown away

A pre-frontend round of model work, then a local Streamlit app for picking a race and seeing predictions. On 28 February comes the great clear-out: 1,368 dead files removed, taking the repo from 1,447 tracked files to 79. Abandoned experiments, the old average-based and per-rider models, the race-kind and embedding systems: all gone, with the LightGBM pipeline as the survivor. The same day brings a weekly update pipeline, a LightGBM probability predictor, and odds displayed in American and fractional formats. From here on most commits are co-authored with Claude.

Experiment log
8–9 March 2026

Experiments get a framework

An experiment harness and the “contender” model arrive: train the win and podium model only on the top 15. Folds switch to full-year splits to avoid seasonal bias, and specialist models that didn’t help are removed.

Experiment log
April 2026

Testing a thesis, and losing

The experiment log opens with a hypothesis: combine the shape of a race with the makeup of its field. Raw gradient features ship on 12 April. The joint thesis is then “empirically falsified” under two field formulations and four architectures. The reason is structural: in stage races, which are 72% of the data, the same ~170 riders start every stage, so the field looks identical whatever the terrain.

The ranking experiments that follow teach two more lessons: the loss function isn’t the bottleneck, and neural scorers can’t match LightGBM on tabular features. A Set Transformer, meant to let riders “see” each other, underperforms because the features were built for scoring riders one at a time. The keeper is a LambdaRank head, which never beats the cascade at picking winners but is almost perfectly calibrated. It ships on 18 April, with the training window moved back to 2016.

Experiment log
4 May 2026

Two good ideas that didn’t replicate

A blend of the multiclass and LambdaRank heads, then a stacked meta-learner. The meta-learner looked promising on three folds and evaporated on five. It was shelved, with the note: n=3 paired CV is too small to settle directional questions.

Dev diary
9–12 June 2026

The race as it unfolds, and a Cloudflare wall

Within-race state arrives: what has happened in the stages so far. It improves probability quality in 5 of 5 folds, more the further a race goes. A mid-race GC head beats “final GC = current GC” by 18 points on early-race winner capture. FirstCycling puts up a Cloudflare challenge, so scraping moves to a headed Chrome browser. Then a new stack (contested Elo, one-day contest signatures, a DNF head and a pricing layer) is gated, retrained and served over three commits in two days.

Season three: in public

July 2026 – now
Experiment log
3 July 2026

A bias found, a fix shelved

A backtest shows riders who finished a grand tour within 45 days are systematically underpriced. A form-feature fix works on its own but adds nothing on top of the full stack, because contested Elo already carries the signal. It’s shelved before it ships. The lesson: test features on top of the full stack before wiring them in. The same day, the Tour de France 2026 predictions site gets built.

Live
4–25 July 2026

The Tour, stage by stage

One commit per day: odds captured each morning, a preview published, yesterday settled, a reflection written. Mid-race additions include break scenarios (odds if the break wins), a GC tracker page, and a “punchy” rider filter. The 27 July review is blunt: the market beat the model on picking winners, the GC board “barely moved,” and most of the damage came from hard-man and breakaway days.

Experiment log
8–9 August 2026

Knowing what kind of day it is

The Tour’s lesson becomes a program: an eleven-item menu of day types. It survives each gate until step 4, where wiring day-type features into the pricing heads fails on every check, 0 of 5 folds. Top-1 picks improved while probability quality cratered, “the exact failure mode” the gating policy exists to catch. Letting day-specific boards serve prices fails too. What survives is retrieval-only: the day key may change who makes the shortlist, never anyone’s price. On the same day, FirstCycling’s new markup is found to have silently stripped dates from 35 races. They’re re-scraped and the full suite is retrained.

Dev diary
5–7 September 2026

Building the nightly crew

The project gets an agent architecture. Managed agents run each night: a coordinator, a results acquirer, a day-key reader, an editorial writer and an ops triager, with a Supabase control plane and an operator inbox for me. The work is queued as lettered “chips” and built in parallel branches, mostly by Claude, one by Codex. ProCyclingStats blocks the agents’ fetcher, so the UCI’s own results become the primary source. A day-key agent, replayed over the Vuelta, keys 9 of 11 stages exactly, against 5 of 11 for the human key. September has 177 commits, more than the whole of 2025.

Live
13 September 2026

Level with the market

The Vuelta closes. The model finishes level with the market on stage winners, after trailing by 0.46 nats a stage at the Tour. It is well ahead on days the bunch or the GC group settled, and still blind to the escapes.

Dev diary
16–22 September 2026

No human setup

The “no-human-setup wave”: a stage race no longer needs a hand-written config, a capture step on my own machine hands the cloud what it can’t fetch itself, and the nightly run reaches a published board on its own. The unattended nightly now covers WorldTour and ProSeries; the tier below stays attended. The lessons ledger keeps pace, with entries like A key nobody uses is not a key and The cloud cannot learn about a new race by itself.

Dev diary
2–6 October 2026

Races with no market

Every agent moves to Opus 5.5, with the results acquirer on Sonnet 5.5. A day-key reader is taught to work without odds. Route research reads the organiser’s road book once per race. An editorial review loop means every draft waits for my verdict. The nightly goes to two ticks a day.

Live
13 October 2026

Where it stands

38 races, 102 prediction pages and 110 settled units, with the Tour of Guangxi live.