DEV Community

Cover image for I Asked 11 AI Models to Predict My Race Splits. Once I Named the Race, 5 Clearly Beat My Model
Robert Moore
Robert Moore

Posted on Edited on

I Asked 11 AI Models to Predict My Race Splits. Once I Named the Race, 5 Clearly Beat My Model

Kaggle Benchmarking Challenge Submission

This is a submission for the Kaggle Benchmarking Challenge

Last weekend I built a model to predict my triathlon splits. It's called Race Card, it runs on TabPFN, and before trusting it I checked it against every race I've done since 2014. I was rather pleased with myself.

Then, slightly later than would have been useful, I had a thought: could I have just pasted my training log into a chatbot and asked?

So, naturally, I built something else to find out: a benchmark.

TL;DR

  • With just the numbers to go on, only one of 11 models clearly beat Race Card: GPT-6 Astra.
  • Give them the race name as well and 10 of 11 improved, with 5 clearly beating Race Card. Once I gave TabPFN the same facts, 6 were still ahead, but none by a clear margin.
  • The name helped for a much more basic reason than I'd expected: it told the models whether a 21 km run was a standalone half marathon or came straight after a 90 km bike. Without it, every model assumed "off the bike" and predicted my standalone halves too slow on average. My own model made exactly the same mistake. A follow-up with made-up race names backed this up: letting the models link repeat visits helped on races I'd done before, but only the real name fixed the half marathons.
  • Most models' "80% ranges" are far too narrow. Blind, 7 of 11 caught fewer of my actual times than an honest range should. I'd put more faith in the middle estimate than the range around it.

What I Benchmarked

I used 79 real race legs (swim, bike and run) from my racing between 2014 and 2026: half Ironmans, sea swims and half marathons. For each leg, the model gets my training history up to that race, with nothing from afterwards, and has to predict how fast I went. It gives a median speed and an 80% range: a lower and upper estimate that should contain the actual result eight times out of ten.

For each leg, it sees:

  • the sport, distance and how much the course climbs
  • my training load on race day: hours in the last 7 and 42 days, and kilometres in that sport
  • a table of previous sessions in the same sport: every earlier race, plus all sessions from the 42 days before race day

The rows come from the same data Race Card's TabPFN was trained on, with the same cut-off. The idea was to give them the same information, ask the same question and see how they got on.

I made two versions of the task, with one small change to the information they get:

  • Blind. No names, no dates. Just numbers.
  • Told which race. The same numbers, plus the event's name and year for both the current race and earlier ones. Something like "Ironman 70.3 Weymouth (2019)."

The second version came from something Race Card kept getting wrong. It predicted my Weymouth bike split too fast every time, by 6 to 11%. It knows how much the course climbs, but Weymouth's particular combination of hills and wind doesn't really come across in that number. I wondered whether a language model might have a better idea of what I was in for.

What the model actually sees (one leg, trimmed)
You are predicting how fast an amateur triathlete will go in one leg of a race, using only their own training log.

The race is Saxon Shore Half Marathon, on 2022-05-22.      <- only in the "told which race" version
Sport: running. Race leg: 21.08 km with 85 m of climbing (4.0 m per km).
On race day the athlete had trained 5.7 hours in the previous 7 days and 43.3 hours in the previous 42 days, including 181.5 km of running.

Below are the athlete's earlier running sessions, oldest first: every race, and every session in the 42 days before race day ...

days_before,km,climb_m_per_km,load_7h,load_42h,sport_km_42,race,kmh,event
0,6.53,1.2,5.6,42.7,174.9,0,11.47,
1,8.57,9.8,5.2,42.1,166.4,0,13.05,
...
307,21.67,28.4,8.1,50.6,150.9,1,12.37,Ironman 70.3 Staffordshire (2021)
972,21.27,0.0,6.0,42.8,138.4,1,12.04,Ironman 70.3 Weymouth (2019)
1434,21.14,0.0,3.8,35.6,139.1,1,13.76,Saxon Shore Half Marathon (2018)
...

Predict the athlete's average moving speed in km/h for this race leg. Give your median estimate, and a low and a high value such that you would expect the true speed to be below low 10% of the time and above high 10% of the time. In basis, say in one short sentence what your estimate rests on.
Enter fullscreen mode Exit fullscreen mode

(basis is the answer field for that one-line explanation.) The blind version is identical, apart from removing the race line and the event column. Those three race rows are worth a look: all about 21 km, two at roughly 12 km/h and one at 13.8. Take away the names and there's nothing to tell you which one I started on fresh legs.

I scored each model in two ways. Typical miss measures how far its median prediction was from my actual time: the average percentage error across all 79 legs, regardless of whether it was too fast or too slow. Range counts how many of my actual times fell inside its own 80% range. A well-calibrated range should catch about 63 of 79, with anything from 56 to 70 within the variation you'd expect by chance.

The comparison includes Race Card itself (TabPFN with all its rows) and the baselines I'd checked it against: TabPFN with exactly the rows the language models see, and the method most of us probably use anyway: "same as my last race".

Models Tested

I tested 11 models from five labs, all through Kaggle's model proxy: GPT-6 Astra, GPT-5.6 Terra, GPT-5.4 mini, Claude Opus 5, Claude Sonnet 5, Claude Haiku 4.5, Gemini 3.1 Pro, Gemini 3.8 Flash, Gemini 3.7 Flash, Grok 4.20 Reasoning and GLM-5. For most labs, I included both a flagship and a smaller, faster model. I wanted to see whether the bigger models actually earned their keep here.

A quick word on what I mean by clearly better. There are only 79 legs here, and one bad race can shift an average quite a bit. I only call a win clear if the gap survives resampling those 79 legs 5,000 times: technically, a 95% bootstrap interval that doesn't cross zero. I also ran 16 model and task pairings at least twice. The biggest change in typical miss between runs was 0.7 percentage points, so I wouldn't get too excited about small gaps in the table.

Findings

Typical miss, blind Typical miss, told the race Inside its 80% range, blind Inside its 80% range, told the race
My model (Race Card: TabPFN, all rows) 6.8% n/a 63 of 79 n/a
TabPFN, same rows as the models 6.3% 6.0% (given the race facts) 67 of 79 67 of 79
GPT-6 Astra 6.0% 5.3% 65 of 79 69 of 79
Claude Sonnet 5 6.5% 6.4% 56 of 79 55 of 79
Gemini 3.1 Pro 6.5% 5.5% 37 of 79 42 of 79
Gemini 3.7 Flash 6.5% 5.3% 49 of 79 56 of 79
GPT-5.6 Terra 6.6% 5.6% 52 of 79 53 of 79
Gemini 3.8 Flash 6.6% 5.4% 48 of 79 54 of 79
Grok 4.20 Reasoning 6.8% 6.1% 56 of 79 55 of 79
Claude Opus 5 6.9% 5.8% 59 of 79 62 of 79
GLM-5 7.1% 6.2% 41 of 79 42 of 79
Claude Haiku 4.5 7.7% 7.5% 42 of 79 44 of 79
GPT-5.4 mini 8.3% 8.3% 38 of 79 38 of 79
"Same as my last race" 8.3% n/a n/a n/a

Blind, all 11 beat "same as my last race", and one beat my model

"Same as last time" missed by 8.3% across the 76 legs where I had a previous race to copy. Every model did better on those same legs, although GPT-5.4 mini only just scraped past at 8.1%.

Race Card was harder to beat. Six models were ahead on average, with Grok also just edging it, but only GPT-6 Astra had a clear win: 6.0% against 6.8%, with both of its runs agreeing. Astra's range held up too, catching 65 of 79 actual times. The other gaps could be explained by chance, apart from GPT-5.4 mini, which was clearly worse.

I hadn't expected TabPFN to get better with fewer rows. Give it previous races and the last six weeks of training and it scored 6.3%, against 6.8% with everything. Apparently, most of my training sessions have very little to say about race day.

Told which race, 5 clearly beat the model I built

Adding the race name improved 10 of 11 models, with a clear improvement for 6. GPT-5.4 mini was the only one that stayed put. Five models now clearly beat Race Card: GPT-6 Astra (5.3%), Gemini 3.7 Flash (5.3%), Gemini 3.8 Flash (5.4%), Gemini 3.1 Pro (5.5%) and GPT-5.6 Terra (5.6%).

At that point, though, the language models knew something my model didn't. To make it a fairer contest, I gave TabPFN the same facts the name gives away in two extra columns: "this run comes straight off the bike" and "this is the same event as today". Its typical miss dropped from 6.3% to 6.0%. Six models were still ahead on average, but none by a clear margin. Against TabPFN using the same rows without those extra facts, only Astra and Gemini 3.7 Flash were clearly better.

That's where I have to be fair to my own model. Give the best language models the race name and they clearly beat Race Card as I shipped it. Give TabPFN the same facts and I can't claim a clear winner.

The off-the-bike assumption

I'd expected the name to help because models know things about courses: Weymouth is windy, Staffordshire is hilly. Once I broke the results down by leg type, I realised I'd been looking past something rather more obvious.

Run legs account for just over a third of the dataset, but two-thirds of the improvement. The biggest single effect was on my six standalone half marathons. Every model made what I've started calling the off-the-bike assumption:

Chart: average error on my six standalone half marathons for each model. Blind, every model predicted them too slow, from 4% to 13%; told the race's name, 10 of 11 moved closer, with GPT-6 Astra going from 6.7% too slow to 0.3%. TabPFN on the same rows was 9.1% too slow.

A 21 km run in my log is usually the final leg of a half Ironman, at about 12 km/h after a swim and 90 km on the bike. On fresh legs, I run a standalone half marathon 40 to 50 seconds a kilometre faster. Without the name, the model has no way of telling which sort of day it's looking at, so it guesses "off the bike". Every single model predicted my standalone halves too slow on average. Here's Claude Opus 5, blind, on my first one in 2014:

Half-marathon off the bike with low recent volume

That prediction was 17.8% too slow. GPT-6 Astra hedged its bets on Saxon Shore in 2022, "allowing for the flatter course and triathlon fatigue" without the name, and came in 5 to 7% too slow across three runs. Give it the name and it went with "a faster half marathon on this relatively flat course", getting within 2%.

It worked the other way round too. Without the name, the Gemini models and GLM-5 predicted my runs after the bike 3.5 to 5% too fast. Once they knew the race, they were within a percentage point.

I can't be too smug about spotting this: my model has exactly the same blind spot. TabPFN on the same rows was 9.1% too slow on those six half marathons, and Race Card itself was 8.7% too slow. Adding the "off the bike" column only brought that down to 7.5%; there simply aren't many standalone halves in my history for it to learn from. The best language models managed it with the name, a handful of examples and a sentence of common sense.

Recognising a course I'd raced before mattered less than I'd expected. For most of the models that improved, the benefit was at least as big on races I was doing for the first time.

Part of the explanation is that repeat races give everyone an easier job. On the 40 legs where I'd done the event before, simply copying my speed from that event last time missed by 5.0%. Using "same as my last race" missed by 8.1% on those legs. TabPFN on the same rows managed 4.7%, while the six models that clearly improved with the name managed 4.2 to 4.5%.

The bigger gaps were on the 39 legs at events I hadn't done before: 7.9% for TabPFN on the same rows, compared with 6.3 to 6.6% for the best four language models given the race name.

Weymouth: the course knowledge is real, but smaller

The Weymouth bike splits still show most clearly how a model can put the name to good use. Race Card was 10.8%, 6.2% and 8.3% too fast in 2018, 2019 and 2022. Here's how Claude Opus 5 did once it knew the race:

Weymouth bike Actual Opus 5, told the race Race Card
2018 2:50:18 2.1% too fast 10.8% too fast
2019 2:44:36 0.3% too slow 6.2% too fast
2022 2:46:42 1.6% too fast 8.3% too fast

Each answer includes a one-line reason. For 2019, Opus wrote:

Mainly his 2018 Weymouth ride (31.95 km/h) on the same course, adjusted slightly upward for his modestly better 2019 form [...] tempered by flat-but-windy coastal conditions

That's pretty much how I'd work it out myself. Without the name, the same model was 5 to 7% too fast on all three, much like everything else.

Some models made less of the clue. GPT-6 Astra mentioned my "slower past Weymouth results", then still predicted a time 7 to 8% too fast. It had spotted the evidence without doing much about it. Gemini 3.8 Flash improved a lot overall, but was still 5 to 9% too fast at Weymouth. Its explanations mention "similar 90 km race performances", without referring to the course.

Made-up names: knowing the course, or finding my last ride?

The obvious objection to all this: maybe the name doesn't tell the model anything about the race at all. Maybe it just lets it find my last go at the same event. So on Sunday evening I ran one more version on three models, with every event name replaced by a label like "Race G". The same event always gets the same label, so a model can still link repeat visits, but the name itself means nothing.

Typical miss Blind Made-up names Real names
Gemini 3.7 Flash 6.5% 6.1% 5.3%
GPT-6 Astra 6.0% 5.7% 5.3%
Claude Opus 5 6.9% 6.1% 5.8%

Linking the repeat visits gets somewhere between a third and two-thirds of the way. It helps on races I'd done before: Opus went from 5.6% to 4.3% on those, as good as with the real names (4.5%). On races I was doing for the first time, the made-up labels barely moved anything.

The off-the-bike assumption survived completely. With made-up names, all three were still 6 to 11% too slow on my standalone half marathons. "Race G" doesn't tell you it's a half marathon. "Saxon Shore Half Marathon" does.

Weymouth sits in between. With made-up labels, Opus was 3.5 to 4.6% too fast at "Race K", against 5 to 7% blind. With the real name it got within about 2%. So the course knowledge is real; it's just the smaller part.

This is one run per model, so I wouldn't lean on the smaller gaps. But all three moved the same way.

Most of them are far too sure of themselves

This is the bit I'd want to know before planning a race around any of these predictions.

Chart: how many of my 79 actual times landed inside each model's 80% range, blind. An honest range lands between 56 and 70. GPT-6 Astra caught 65, Claude Opus 5 59; seven models caught fewer than 56, down to 37 for Gemini 3.1 Pro. Most ranges would need to be 1.2 to 2.2 times wider to catch 8 in 10.

Race Card's 80% range caught 63 of my 79 actual times, pretty much as advertised. The language models caught between 37 and 65, with 7 of the 11 falling below what you'd reasonably expect by chance. GPT-5.4 mini's "80% range" managed 38 out of 79. That's closer to a coin flip, which isn't quite the reassurance the label suggests.

Take Gemini 3.7 Flash on my 2022 Weymouth bike, without the race name. In one run, its 80% range was 2:26 to 2:39. I did 2:46:42. Even the slow end of its range was more than seven minutes too optimistic.

The ranges need more room either side of the middle estimate. Gemini 3.1 Pro's were about 11% of the predicted time wide, compared with 24% for Race Card. To catch 8 in 10, most models would need ranges 1.2 to 2.2 times wider.

GPT-6 Astra was the exception: 65 of 79 blind and 69 when told the race, with ranges nearly as wide as Race Card's. It had both the best middle estimate and the most reliable range in the blind task. It's the one I'd actually use.

What I Got Wrong

  • Why the name helps. I built the second task with Weymouth's hills and wind in mind. The bigger issue was whether I'd already done a bike ride before starting a half marathon. I only spotted it when I broke the results down by leg type afterwards. Race Card, it turns out, had been making the same mistake all along.
  • Thinking more data would help TabPFN. It didn't. Cutting the history down to races and the last six weeks of training made it better.
  • The headline. The first version of this post said 8 models beat my model. What I actually had was 8 ahead of TabPFN on the same rows, based on averages. Once I checked which gaps survived resampling the legs, that became 2 clear wins against that version of TabPFN, 5 against Race Card as I shipped it, and none against TabPFN with the same facts. Rather less tidy as a headline, but more accurate.

What I'd Tell Another Athlete

  • Tell it what you're racing. The event name, or even just "standalone half" or "off the bike", is worth more than switching to a bigger model.
  • Put more faith in the middle estimate. If you want a range, I'd widen whatever it gives you by about half.
  • Look at your last go at the same race. If you've done the event before, your speed there last time is a much better starting point than your most recent race somewhere else.
  • Gemini Flash does a good job. Both Flash models were within a whisker of the flagships blind (6.5% and 6.6%) and right up with the best when told the race (5.3% and 5.4%), for a fraction of the cost. The smaller models from the other labs, Haiku and GPT-5.4 mini, were the weakest in both versions. The name made little or no difference to them.

For Race Card, the fix is straightforward: an "off the bike" flag and a "same event" flag. Those are what took the typical miss from 6.3% to 6.0% above.

What I'd Measure Next

  • Someone else's legs. So far, it's all been me. I'd like to know whether a model that knows Weymouth is as useful for someone who's never raced it.
  • Made-up names on every model. I ran that test on three models. The other eight would show how general it is.
  • Asking for the range differently. I'd try asking for the 10th and 90th percentile as times rather than speeds, or asking for five scenarios, to see whether the ranges become more honest.

How I Built It

  • The dataset comes from Race Card's own pipeline: the same 79 legs and the same rows TabPFN saw. I re-ran TabPFN and all the simple baselines on exactly the published rows so the comparisons use identical data. TabPFN's score is within a tenth of a percent of the one in my Race Card post.
  • Privacy. It's my own training history, so I've stripped it right back: no GPS, coordinates, activity names, IDs, gear, weather or time of day. A check script blocks the upload if anything else slips through.
  • The task uses Kaggle's kaggle-benchmarks library. Each leg gets one prompt and a structured answer: low, median and high speed, plus a one-line reason. Code checks the answer against my actual time, with no LLM judge involved. The data is built into the task, so anyone can re-run it.
  • Every number in this post comes from facts files generated from the downloaded runs. A script checks the post against them before I publish. The whole week's testing used about $38 of Kaggle's free model allowance; the extra TabPFN run with the race facts didn't use any of that.

Limits

  • This is still one athlete and 79 legs. Each model had one to three runs, rather than dozens.
  • Only six legs are standalone half marathons. The finding is consistent across 10 of 11 models, but I wouldn't want that to obscure the fact that we're talking about six races.
  • With the race name, a model could in principle know something about the event from its training. It can't know my result: my name doesn't appear anywhere in the prompt.
  • DeepSeek-R1 never completed the task properly. It wrote out its reasoning for every leg but didn't give an answer in the required format. Gemma 4 31B kept hitting "heavy load" errors and never finished a run either. Neither is in the table.
  • A handful of legs failed because of problems on the provider's side, at most two per run. I've left those out of the average for that run.

My Benchmark

Run it yourself on Kaggle: Triathlon race splits

If you've ever asked a chatbot to predict your race time, I'd love to hear how it went. And whether you told it which race you were doing.

Top comments (1)

Collapse
 
arhancanli profile image
Arhan Canli •

The off-the-bike result is the part I'd keep: a model that can't see which kind of day it is will guess the common one, and TabPFN on the same rows fails the same way (9.1% too slow on the six standalone halves).

One thing on how "clearly" is computed: resampling 79 legs treats them as independent, but the swim, bike and run of one race share weather and form, and the repeat visits to Weymouth share a course. Resampling whole races (a cluster bootstrap) will widen those intervals, so I'd check whether Astra's 6.0% against 6.8% blind still excludes zero. With 11 models and two task versions there are also many comparisons against Race Card, so a few "clear" wins would turn up at 95% even if nothing differed.

The range result needs the same care: 56 to 70 of 79 is the band for independent legs, and clustered legs make it wider, so some of the 7 models that fell short may sit inside it. Gemini 3.1 Pro at 37 of 79 is outside either way.

Did the bootstrap resample by leg or by race?