This is a submission for the Hacktoberfest Weekend Challenge: Build for a Friend
My mate Sam wants to go under five hours at an IRONMAN 70.3. He hasn't decided which one yet, which is quite an important detail.
A 70.3 is a 1.9 km swim, a 90 km ride and a half marathon. The distances stay the same; the day out does not. Weymouth's bike course climbs about 1,043 metres. Westfriesland's climbs 55. You can see how picking the right one might help.
I've raced Weymouth three times, so I wasn't short of opinions. Before passing those on as advice, though, I thought it might be worth getting some numbers.
What I Built
Race Card uses your own training to work out which 70.3 gives you the best shot at your goal time.
Give it a Strava or Garmin export and it predicts a range for each leg, then runs 6,000 simulated race days on eight real courses. You get a chance of going under your goal at each one, plus a look at which leg would make the biggest difference if you improved it.
It all runs on your laptop. Prior Labs' TabPFN handles the predictions, and Google's Gemma 4, through Ollama, reads the log and writes the plan. Both are open models. Nothing you've logged leaves the machine.
Sam's training is his, so I tested it on mine first. If I raced tomorrow, this is what it reckons:
- Erkner, La Quinta, Türkiye, Westfriesland (all flat): about 4:48, with an 85 to 86% chance of sub 5.
- Weymouth: 5:06, with a 31% chance.
- Swansea, the hilliest: 5:08, with a 24% chance.
Almost all of that gap is on the bike. At Weymouth, it reckons sub 5 becomes a coin flip if my bike gets 3.4% faster, or my run gets 6.5% faster. That's a much more useful conversation to have with Sam than just telling him Weymouth is hard. He's since run it on his own training. More on that below.
Demo
Live: iamrobertmoore.github.io/race-card
Here's the 80-second tour, with me talking you through it:
A few things to have a play with:
- Pick a course from the dropdown, or click any row in "Every course, ranked". The ring and finish time animate to that course.
- Drag the bike slider on Weymouth. At 3% faster, the chance goes from 31% to 47% and the bike time drops by 5 minutes.
- Scroll to "Checked before trusted" to see how TabPFN did against four simpler guesses on the same races.
- Open "See all 79 legs" to look through every past race leg it predicted, including how far off it was.
The live page uses my training. There's deliberately no upload box: if you want to use your own data, you run it on your own machine using the code below.
Code
iamrobertmoore
/
race-card
Which IRONMAN 70.3 gives you the best shot at your goal time? TabPFN + Gemma 4 on your own laptop, from your Strava export.
Race Card
Which IRONMAN 70.3 gives you the best shot at your goal time? Race Card reads your own training export, predicts every leg of race day as a range, and ranks eight real 70.3 courses by your chance of going under the goal. Then it shows which leg would move that chance most.
It runs on your laptop with two open models, and nothing you've logged is uploaded anywhere.
Live demo, built from my own Strava export: https://iamrobertmoore.github.io/race-card/
I built it for my friend Sam, who wants to go under five hours at a 70.3 and hasn't picked which one His training is his, so I tested it on mine first. He has since run it on his own export, on his own laptop, and is working on his bike.
What it found on my data
- If I raced tomorrow: about 4:47 on the flat courses (around 86% chance…
MIT licence, and you can run the tests without downloading a model.
How I Built It
TabPFN does the predicting
TabPFN is a tabular foundation model. You give it a table and ask it to predict a column, without setting up a training loop yourself. For each discipline, I give it one row per session: distance, climbing per km, how many days ago it was, my training load over the preceding 7 and 42 days, and whether it was a race. The thing it's trying to predict is speed.
What makes it useful here is that it gives me a spread rather than a single number. I take the 5th to 95th percentile of likely speed for each leg on each course, and the page draws 6,000 race days from those spreads. The proportion that finish under five hours becomes the chance in the ring. Drag a slider and your browser runs that simulation again.
It's quick, too. A full card from 4,484 sessions takes about 20 seconds on a CPU, with no GPU needed. The backtest takes a few minutes more.
Checking it before trusting it
Before handing Sam a prediction, I wanted to know whether it was any good. So I made it predict my past races, using only the training I'd logged before each race day. No peeking at what happened afterwards.
62 of 79 race legs landed inside its 80% range. That's 78%, which is about what you'd hope an honest 80% range would catch. The typical miss was 6.9%, with no consistent tendency to predict me too fast or too slow.
Then I got round to the question I should probably have asked first: was TabPFN actually better than something simple? I tried four other ways of predicting the same 79 legs, all with the same cut-off:
| Same 79 legs | Typical miss |
|---|---|
| TabPFN | 6.9% |
| Gradient-boosted trees, trained on exactly the same rows | 7.9% |
| Same as my last race at that distance | 8.3% |
| A straight-line fit | 11.1% |
| My recent training pace | 12.1% |
A win, but not a thrashing. TabPFN was closer than the trees on 48 of 79 legs. The other useful difference is that the trees here give me one number, whereas TabPFN gives me the range the whole page depends on.
There was one mistake it made rather reliably. I've raced Weymouth in 2018, 2019 and 2022, and it predicted my bike too fast every time, by 7 to 11%. It knows how much the course climbs, but not what Weymouth's hills and wind actually do to you. I've put that warning on the page: treat the bike predictions for hilly courses as optimistic. I certainly do.
Gemma reads the log
For the model to learn what race effort looks like, it needs to know which sessions were races. Strava doesn't record that, and tagging 4,574 activities by hand wasn't something I wanted to make part of the job. Sam certainly wouldn't thank me for it.
That's Gemma 4's job. It reads the activity titles through Ollama on the laptop and picks out the races. I'd already been through my own list by hand for my card, so I had something to mark its homework against. It took three goes.
The first run took 47 minutes and got about two-thirds right. Gemma 4 thinks before answering by default. Useful for a hard question; less useful when it spends 45 seconds deliberating over "Morning Run". It also called old uploads with nothing but a date for a title races, while missing whole actual race days.
To be fair, it found races I'd missed as well: five Canterbury 10s, a swimrun, a DNF, an Oysterman swim and a Hampstead Heath aquathlon I won in 2015. Those went into the answer key, along with another DNF I spotted while checking. So the human wasn't getting full marks either.
By the third run, I'd turned thinking off, skipped titles written by the app rather than me, and given it the other sessions from the same day as context. I also spelt out the things it kept getting wrong, including Sufferfest sessions, bricks and "Open Water Swim".
That version read 1,005 titles in under three minutes and found 166 of my 181 races, with 6 false alarms. I tuned the prompt on the same list I scored it against, so that's a best-case result, not an independent test. It still missed all three legs of Challenge Roth, which is quite a race to overlook.
Gemma also writes the note on the card. It only gets the numbers Race Card has already calculated. If it puts a number in the note that wasn't in those facts, the note gets binned and it has another go. This is what it wrote in 13 seconds on my laptop:
Hey Robert, remember the 70.3 Weymouth? Your chance of hitting sub 5 is 31%, which is lower than the 86% at the 70.3 Erkner. If you can shave 3% off your bike leg, your chance jumps to 47%. Let's nail that bike leg!
The courses are real
The climbing figures come from IRONMAN's own course routes or finishers.com. I checked them on Friday. If a run's climbing isn't published, the page says so and treats it as flat rather than making something up.
Things I got wrong first
- Strava's export has two columns both called "Distance". The first is in kilometres, except for swims, where it's metres with a comma in. My first parser turned a 2,192 metre swim into 2,192 km. Quite a morning in the water.
- One race swim came out at 6.4 km/h. That's faster than the 1,500 metre world record pace. Sadly, it was a GPS glitch. Anything outside a sensible speed range now gets dropped before the model sees it.
- The newest TabPFN wants an account and an accepted licence before it'll download. Fine for me, but not what I wanted to mean by "just run it, Sam". TabPFN v2's weights download from Hugging Face without an account, so that's the version it uses.
- Gemma's first note said "Hey Sam" and then gave him my numbers. Exactly the thing I'd said I wouldn't do. It's now explicitly told whose training the numbers come from, and the note on the page is addressed to me.
- The note said 48% while the slider said 47%. Two simulations, different random draws, and a result hovering around 47.5. They now use the same draws and agree to the percent.
- My first design was beige and green and looked like a pension statement. It's dark now, with rings.
Handing it over
I sent Sam the link on Sunday morning. He had a look through my numbers, then ran it on his own export, on his own laptop. That's the version I actually built it for. His numbers stayed on his machine, but he told me what it had said and that he'd found it "very insightful".
Weymouth looks even less promising for him: an 11% chance of sub 5, against my 31%. He's not changing races yet, though. He wants to put the work into his bike and see whether he can close the gap. If it doesn't come together, he'll aim for a course where his odds are better.
That's the decision I wanted him to be able to make. I wasn't trying to talk him out of Weymouth. I wanted him to know what choosing it costs him, and where the work needs to go.
Why Does Open Innovation Matter?
Because this is someone's training log, not just a handy table of numbers.
A Strava export contains heart rate, GPS tracks starting at your front door, and fifteen years of your mornings. Asking Sam to send all of that to a service just to help him pick a race didn't sit right with me. With open models, I don't have to. TabPFN and Gemma both run on the laptop, so the data stays there. I never saw Sam's file. I only know the number he chose to tell me.
He doesn't need an account, an API key or a subscription, either. TabPFN v2 downloads without signing up, and Gemma runs offline through Ollama. It costs nothing to run, and there's nobody who can put the price up or switch it off before his race.
Being able to see inside it also meant I could test it properly. I backtested it on my own races, found the places it got things wrong, and put those on the page. That's much harder with a closed API.
There is a trade-off. A frontier model would probably have found more of my races, faster, than Gemma 4 running on a laptop. Reading the log is where a closed model would likely have won. But I'd have had to send fifteen years of activity titles to somebody else's server, and Gemma found 166 of 181 without any of them leaving the machine.
My Agent Session
I built this with Claude in a cloud sandbox linked to my laptop, so there's no DevRelay transcript to embed. The commit history is the record, starting with an empty repo on Friday morning.
Claude wrote the code, ran the backtests and drafted the page. I made the calls, including a few worth mentioning:
- Whose data it shows. I asked whether I could just use my training and call it Sam's. Claude pushed back. It was right, and the first screen now says whose training you're looking at.
- Gemma ran on my Mac, not in the sandbox. Whenever the race-finding prompt changed, I ran it locally and pasted the score back. Three runs: 47 minutes, then 4, then under 3.
- The baselines. On Sunday morning, I went through the other TabPFN entries with Claude. The good ones all compared their model against something simple. Mine didn't. That's how the table above came about.
Prize Categories
Best Use of TabPFN. TabPFN does the predicting, rather than being an extra bolted on for the challenge. It takes my own sessions and predicts a full spread of speeds for each leg. Those spreads drive the 6,000 simulated race days behind every chance on the page. On the backtest of 79 of my race legs, 62 landed inside its 80% range. Its typical miss was smaller than the gradient-boosted trees, a straight-line fit, my recent pace or "same as last time".
Best Use of Gemma. Gemma 4 runs locally through Ollama and has two jobs. First, it finds the races in the activity log so the prediction model can learn what race effort looks like: 166 of my 181 races from 1,005 titles in under three minutes, with 6 false alarms. Then it writes the note on the card. Any number it invents gets rejected.
What this is not
A few limits worth being clear about before anyone plans a season around it:
- It's based on one person's training. It isn't coaching or medical advice.
- It knows distance and climbing, but nothing about heat, wind, sea state or whether the swim is non-wetsuit.
- It simulates each leg separately, which makes the range a little narrower than real life. If you're having a bad day, it tends to follow you through all three.
- Transitions are a flat 7 minutes.
Why I built it
I picked Weymouth three times. Nobody showed me this first.

Top comments (0)