The Build Log — 3.6 years of a cycling-results model
Roadbook · 2022 → 2026

How a cycling model got built, one climb at a time

A 3.6-year build log for a Grand Tour results predictor — read as a stage profile. Elevation is commit effort; the climbs are the three eras the neural architecture passed through, plus the April 2026 debugging sprint. Two big pushes, a long maturation between, and one model that still starts on the first turn of the pedals.

◄ 2022·09 — départ2024 · the big push2025 · autoencoders2026·04 — the sprint ►

profile = monthly commits, linear history (428 through Apr 2026; the public repo is a squashed snapshot, that history is private). peaks: Feb–Mar 2024 (45/43) and Apr 2026 (85). flat spells are real — the model sat while the sport ran.

428commits
3architecture eras
~15model "flows" tried
8.3Mparams · 2024 flagship
The route, col by col

Three eras, one debugging sprint, and the iterations inside each

CAT 300
2022·09 → 2023·08 · 149 commits
Foundations

The problem, framed for the first time: scrape ProcyclingStats, build per-rider sequences, and ask a small LSTM encoder–decoder whether a rider lands top-10. No images yet — just learning the shape of the data and getting a pipeline to run end to end.

149 commits
model.pymodel_comb.pysequences_np.pyscraper
Best of the era
cycling_np_model — val-precision 0.41, the strongest of the early binary runs
What it taught
the encoder–decoder skeleton stuck — every later era kept it
HORS CAT01
2024·02 → 2024·06 · 118 commits · the flagship
The big push — the winning model

The most productive stretch of the whole project, and the one that paid off in the real world. Rapid iteration you can see in the filenames: attention-LSTM v1 → v5, three combo variants, a CNN image branch — converging on dense_multi, a four-class head that predicts a rider's finishing band (1–25 / 26–75 / 76–125 / 126+) for each of 21 stages, fed by a deliberately hand-rolled image tower that ran stage profiles through keras-OCR into flat 1499-dim vectors.

This drove the best fantasy seasons the project ever had. The receipts are committed in git — and the checkpoint still loads.

dense_multi · encoder → decoder · 8,296,316 params✓ still loads · 2026·07·11
enc · numeric20 × 28season history
enc · images20 × 2 × 1499keras-OCR tower
→
LSTM 40·30·15+ Repeat(21)latent per stage
→
dec · stages21 × 21 (+img)TD dense stack
→
output21 × softmax(4)finishing band
attention_lstm_v1…v5model_combo_2 / _3model_combo_multi ★1d_cnn image branch
Giro 2024 · predicted eve-of-race
12/15 of the final GC top-15 caught. Called 1·2·5 = Pogačar (won), O'Connor (4th), D.Martínez (2nd). No favorite compression — Pogačar scored 1.9× the runner-up.
Tour 2024 · grid saved Jun 27, pre-race
per-stage format handled sprinters ahead of time — Philipsen ranks 4–13, Girmay present. Miss: Vingegaard buried 29–42 (finished 2nd).
Honest caveat
the logged multi-class precision (0.658) was broken — pinned 0/1, no test set ever scored. This era's case rests on real results, not offline metrics.
Provenance
checkpoint 20240627-155334 loads today on CPU (TF 2.10); predictions live in git history.
CAT 102
2025·04 → 2025·12 · 65 commits
Autoencoders & the image encoder

A quieter, more deliberate phase. Instead of flattening stage images with keras-OCR, pre-train autoencoders to compress them into latent vectors, then feed those to a focal-loss binary model (image_encoder v1 → v2). This is the first era with an honest, logged test set — and the best offline number the neural nets ever produced.

65 commits
autoencoder (pretrained)image_encoder_modelimage_encoder_modelv2two-stage training
Best run · autoencoder_binary_v2
test AUC 0.742 · recall 0.58 — the neural-net high-water mark by a real metric
Honest caveat
the Dec-2025 two-stage retrain regressed (val-loss 0.33 → 4.6) — a masking-value bug, fixed for good in the April 2026 sprint
CAT 103
2026·04 · 85 commits · sprints S1 → S23
The debugging sprint — finding out why the net was flat

A second big push, this time on the existing v2 architecture rather than a new one. Twenty-three short sprints: fix the mask mismatch for real (padding wrote -1000, the layers masked -9999), throw every class-imbalance trick at the collapse (focal loss, class weights, stratified batches, warm-up LR, a 4-class head) and watch each one lose to heavy L1 + plain BCE, then instrument the network layer by layer to see where the signal died.

Result: three loss configurations landed on the same AUC (0.63–0.64) with the same top riders and a prediction spread of ~0.007. The loss was never the bottleneck; the 495 → 32-dim encoder squeeze and the RepeatVector copy were.

85 commits
train_baseline.pydiagnose_model.pyfocal · class-weights · stratifiedheavy L1 + BCE
Best run of the era · Jul 2025 config
AUC 0.742 with heavy regularisation and plain BCE — every clever imbalance fix made it worse
What it taught
cosine similarity between random-init rider embeddings was already 0.93: the architecture could not tell riders apart before training started
The bake-off · scored on Giro 2026

Two generations, one race, one ruler

Every model graded on the same held-out race (Giro 2026, never in its training), by the same metric — NDCG@5, how well it ranks the real top finishers per stage. Higher = better.

v2 image-encoder
2025 · neural, retrained
0.31
v1 flagship
2024 · the actual winner
0.21

The actual 2024 winning checkpoint, recovered and run on Giro 2026 — its exact 28/21 feature recipe was hidden behind a one-word typo in the 2024 code ('one_day' → 'one_day_race'). It still picks the right riders (top GC calls: Gall, Vingegaard, Ciccone, Hindley, Bernal) — it just ranks them less sharply than its successor. Progress over time, made literal.

honest · out-of-sample · identical eval unit (per-stage groups, top-25 relevance) · v1 images masked (2026 not in the 2024 OCR store) so 0.21 is a floor · v1 on 183/184 riders, v2 on 123/184 — not the identical rider set

Where the road goes

The winner and its successor disagree — usefully.

The 2025 image-encoder wins on the honest metric. The 2024 neural net won the fantasy season and, uniquely, separated the favorite instead of flattening the field. That gap is the next climb — and the 2024 checkpoint still loads on the first pedal stroke, ready to be raced against whatever comes next.