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.
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.
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.
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.
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.
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.
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.
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
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.