跳到正文
DEV Community· Dominique Siacci·· 2 小時前AI 評分44

wasitthewind:真的是風的問題嗎?我用十次騎行與無風幽靈競賽

Was it really the wind? I raced my windless ghost on ten rides

AI 導讀

作者開發 wasitthewind,從騎行 FIT 檔重建沿途風況,並以相同功率模擬無風騎行,讓用戶在地圖上與「幽靈」比賽。工具以開放權重的 TabPFN v2 預測騎行速度;在十次騎行的測試中,其速度誤差比線性回歸低 40%。

正文

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine

What I Built

The first time my ghost raced me, it finished 34 minutes 53 seconds behind.

The ghost is me on the same ride with the wind taken out: same road, same power second by second, same stops. On 19 August I spent six hours riding one way across the north of Corsica, mostly with the wind at my back, so a ghost without that wind should lose. Not by 35 minutes, though. And it lost on my nine other rides too, the calmest mornings included. My ghost was telling me the wind had helped me on every single ride. It was lying, and finding out why became most of this project.

Last week I published z2analyzer, a tool that splits my minutes above Zone 2 between the road, the heat and me. Its last section began with three words: "Wind isn't counted." After a slow ride, the wind is the easy culprit, and this week I wanted a tool that checks the excuse.

wasitthewind is a small web app I host myself. After a ride, I drop the FIT file from my bike computer, and a few minutes later the page races me against my ghost on a dark map of the ride: the ghost pulls ahead where the wind was in my face and drops back where it pushed me. In racing games, the ghost is a past lap replayed next to you; mine is the ride I would have had without the wind. The gap at the finish is what the wind cost me, or gave me, with a range around it. The page also points to the stretch where the wind weighed most, and can set the ride against an earlier one on the same road.

For the 58 km loop I rode on 27 September:

It wasn't the wind.
Your ghost, same legs and no wind, finishes 0:35 ahead
of you, and every other way of counting within 0:35:
the wind barely touched this ride.

Where the wind weighed most: km 42.1 → 47.2,
0:29 lost over 5.1 km, headwind 4 km/h at 10 m.

It looks back at a ride already ridden. It doesn't plan routes, pick a start time or give training advice.

Demo

The video runs on my own rides, from a new analysis to the race and its verdict. It shows times, percentages and wind speeds only, and no map of the first and last 2 km of a ride.

Code

wasitthewind

Was it really the wind?

After a slow ride, the wind is the easy culprit. wasitthewind rebuilds the wind along your track, then replays the same ride with the wind taken out, at the same power: your ghost. You race it on a map. The gap at the finish is what the wind cost you, or gave you, with a range. It also points to the stretches where the wind weighed most, and compares the ride with an earlier one on the same road.

It is a small web app you run on your own machine: open it, start a new analysis, drop the FIT file of your bike computer, and the race comes a few minutes later. Every analysis stays in the list. A command line does the same for scripts.

Demo video (88 s, on the author's own rides): https://youtu.be/Yp9h0NMVNS4

It looks back at a ride…

Python, Apache 2.0, 42 tests. wasitthewind serve is the web app: it runs on your machine, keeps every analysis, and its pages load nothing from the internet, not even map tiles. ghost runs the same race from the command line. The tables below come out of the scripts in tools/, and docs/results.md has them all, with what failed.

How I Built It

The wind along the ride

No weather service ever gets my track. wasitthewind asks Open-Meteo's archive of past forecasts for a fixed lattice of points around the ride, from Météo-France's AROME (every 15 minutes, 1.3 km apart) and ECMWF's IFS (hourly), and interpolates on my machine: each second of the ride gets a headwind and a crosswind, 10 m above the ground.

A rebuilt wind is a guess, so I checked it against what the Météo-France stations within 15 km and Ajaccio airport measured at the same hours:

At the hours of my rides, 210 station-hours Measured AROME IFS
Mean wind speed 2.5 m/s 1.7 m/s 1.3 m/s
Direction error, wind above 2 m/s 25° 48°

AROME follows the direction but is a third too weak; IFS is weaker still and often wrong about where the wind comes from. AROME is the headline, IFS a second opinion. And a rider isn't on a 10 m mast: a textbook wind profile gives 0.54 of the 10 m wind to someone 1.2 m above open country, 0.29 among trees and houses.

The ghost's legs

The ghost needs my legs: my speed for a given power, grade, headwind, crosswind, temperature and time already in the saddle. I gave that to TabPFN v2, a tabular foundation model with open weights: it takes my other rides as context and predicts each 30-second piece of a new ride in one forward pass, with no training run, on a CPU. My data: ten outdoor rides with power-meter pedals since mid-August, 24.7 hours. I scored it on each ride with models that never saw it:

Speed error on rides it never saw With AROME's wind With IFS's Without the wind
TabPFN v2 0.69 m/s 0.71 m/s 0.72 m/s
Linear regression 1.21 m/s 1.19 m/s 1.18 m/s
Constant 2.29 m/s

TabPFN makes 40 % less error than a straight line, and it's the only model the wind helps. The ghost then rides each piece in my time × (my speed with the wind ÷ my speed without), both as TabPFN predicts them, and stops where I stopped.

Why the first ghost lied

My rides come in two kinds: windier mornings on the flat coast road west of Ajaccio, and calm days inland, with climbs. TabPFN noticed. A strong crosswind meant "coast, fast"; no wind at all meant "inland, slow". Asked for my speed with the wind set to zero, it answered for an inland day, and my ghost slowed down on every ride. The wind told it what kind of day it was, more than what the air was doing.

So I took the wind out in several ways and compared each ghost with plain physics given the same wind. A point is one percent of the ride's time:

How the wind is taken out Distance from the physics, mean over 10 rides
All of it set to zero (the first ghost) 4.2 points
Crosswind set to zero 3.5 points
Headwind set to zero 1.0 point
A model that never saw the crosswind, headwind set to zero 0.7 point
Headwind turned around 0.3 point

What worked is to never ask for a day without wind. The ghost keeps the crosswind and asks TabPFN twice, with the headwind as it was and turned around into a tailwind of the same strength, then takes the mean. What the model learned from how strong the wind was, the kind of day, is the same in both answers and cancels out; what's left is what the wind did by blowing one way rather than the other.

A test the ghost failed

Physics gives a check that needs no TabPFN: flat roads I rode both ways.

38 flat stretches ridden both ways, 61 km Way into the wind, slower by
At the same power and grade 3.6 %
Physics, with 0.54 of the wind taken out (fixed before looking) 0.1 %
TabPFN's ghost, before and after taking the wind out 1.7 % → 1.8 %

The physics makes the gap melt; TabPFN's ghost leaves it where it was. From ten rides in light wind, with a model wind a third too weak, it learned a headwind effect too faint to show on these roads. So the verdict never rests on TabPFN alone: the range holds the other weather model and the physics, with my drag and rolling resistance fitted on my other rides, at 0.54 and at 0.29 of the wind.

So, was it the wind?

Each of my ten rides, raced against a ghost that learned my legs from the nine others. A plus means the wind cost me time; a minus, that it gave me some.

Ride Time, stops included Wind at 10 m Wind cost me (TabPFN, AROME) Range
14 Aug, inland 98 min 0.6 m/s +0:20 −0:17 to +0:20
16 Aug, inland 124 min 0.4 m/s −0:01 −0:09 to −0:01
19 Aug, the north, one way 369 min 2.7 m/s −3:34 −6:00 to −1:04
24 Aug, west coast 63 min 0.9 m/s +0:00 −0:59 to +0:12
29 Aug, west coast 92 min 2.0 m/s +0:02 −0:11 to +0:35
31 Aug, west coast 63 min 0.9 m/s +0:01 −0:21 to +0:12
13 Sep, the loop 194 min 1.1 m/s +0:10 −1:12 to +0:12
20 Sep, the loop 185 min 0.8 m/s −0:11 −1:10 to +0:14
27 Sep, the loop 177 min 1.4 m/s +0:35 −0:34 to +0:35
4 Oct, west coast 207 min 1.6 m/s −0:08 −1:40 to +0:24

On my nine round trips, 20 hours on the road, the wind cost me 48 seconds in all: mostly dawn rides in light wind, and on a loop, what the wind takes on the way out it mostly hands back on the way home. Only the long one-way day in the north moved by minutes, and even there TabPFN's −3:34 sits with the physics at 0.29 of the wind (−3:20) rather than at 0.54 (−6:00), which says again that its headwind is on the faint side.

The comparison says the same from another side. Against 13 September on the same loop, 27 September comes out 16 minutes 34 seconds faster, and the wind cost me 25 seconds more on the faster day. Whatever changed between those two Sundays, the wind wasn't it.

Why Does Open Innovation Matter?

A FIT file says where I ride, second by second, and when I leave home. wasitthewind looks like a web service, but it's one I run myself: the files I drop stay on my machines, the pages load no map tiles, and the only things about a ride that go out are the 0.1° blocks around it, about 11 km, and its date. TabPFN's weights are downloaded once and run on a small CPU, with no account and no cost per analysis, so the ghost keeps racing even if a service closes. The wind comes from open data: Open-Meteo's archive of the Météo-France and ECMWF models, Météo-France's station records, and the airport reports kept by the Iowa Environmental Mesonet.

The open weights also made the lie findable. To catch the first ghost, I ran TabPFN on the same ten rides in six different ways, then on the flat roads, then for every line of the range. Behind an API, every one of those runs would have sent my rides out, and I would have stopped asking sooner.

And nothing in it is mine except the numbers: your FIT files, your bike and your mass replace mine.

Prize Categories

Best Use of TabPFN. TabPFN v2 is the ghost's legs: on rides it never saw, it predicts my speed with 40 % less error than a linear regression, and it's the only model the wind made better. The post shows where its first ghost lied, how turning the headwind around fixed it, and where it still falls short. Built with PriorLabs-TabPFN.

What it doesn't know

The wind is rebuilt from weather models, not measured on the bike, and on my rides AROME was a third weaker than the stations. A road between walls, trees or cliffs feels less of it than the grid says, a gap between two hills more. Drafting is invisible: on 4 October I rode part of the way on a friend's wheel, and no file says so. The ghost knows me only through my rides, mostly in light wind; on a stormy day it would be guessing. It's a finding about a ride already ridden, for one rider and one bike, and not training advice.

Where you ride, does the wind ever cost you more than a minute on a loop? I'd like to know how strong it has to blow before it really counts.

來源:DEV Community · dev.to