wattif

Ask what-if of your own ride data.

A local command-line tool that reads the FIT files from your Garmin and answers two questions most cycling tools skip, both without a power meter.

Faster, but mostly by working harder

Over two years speed rose 15% and heart rate 11%, while speed per heartbeat rose only 3%. 100 105 110 115 2024Q3 2025Q1 2025Q3 2026Q1 2026Q3 speed +15% heart rate +11% efficiency +3%
Two years of rides, indexed to 2024Q3 = 100. Efficiency is wind- and terrain-corrected speed per heartbeat. Sample output on synthetic rides.

Am I actually getting fitter?

Not raw average speed, which wind and terrain drown out, but speed per heartbeat, corrected for wind, gradient and equipment. A fast day into a tailwind stops looking like progress, and a hard slog into a headwind stops looking like decline.

What would make me faster?

Tyres, riding position, a different bike, fewer stops. Measured against your own rides rather than a generic calculator.

Within-ride A/B tests work today. The ranked what-if table is still to come.

How it works

Both answers come out of the same physics: estimate power from speed, gradient and air density, correct every ride for the wind you actually rode through, then compare like with like.

The same 26 km/h, three very different efforts

At the same 26 km/h: about 141 W in still air, 278 W into a 15 km/h headwind, 262 W up a 2% climb. Still air, flat 141 W 15 km/h headwind 278 W 2% climb 262 W air tyres climbing
Watts at the wheel from wattif's model, for the example profile: 85.5 kg rider and bike, on the hoods. Average speed alone cannot tell these rides apart.
$ wattif analyse --min-km 20
Analysing 49 ride(s)...

49 ride(s) analysed, 49 with weather.

quarter   n    raw   equiv     hr   headwind   efficiency
2024Q3    5   22.7    23.3    147    +0.06     0.1583
2024Q4    5   23.0    23.6    149    +0.01     0.1591
2025Q1    5   23.2    23.8    154    -0.01     0.1546
2025Q2    5   23.9    24.7    153    +0.04     0.1620
2025Q3    5   24.3    24.8    156    -0.04     0.1591
2025Q4    5   25.3    26.0    161    -0.05     0.1614
2026Q1    5   25.3    26.3    161    +0.01     0.1631
2026Q2    5   26.3    26.9    165    +0.04     0.1632
2026Q3    9   26.2    26.7    163    +0.04     0.1638
Quarter by quarter: raw speed, the wind- and terrain-equivalent speed, heart rate and efficiency (speed per heartbeat). Sample output on synthetic rides.

Test a change inside one ride

Whole-ride comparisons are a poor way to judge equipment. Speed per heartbeat scatters around 5% from ride to ride, so a 4% change needs roughly twenty-five rides per arm before it clears the noise, by which time the seasons have moved underneath the experiment.

wattif ab puts both arms inside the same ride instead. Set the watch to auto-lap every 5 km, switch setup at every beep and change nothing else. Weather, fitness and route are then identical on both sides of the comparison.

A 30 km ride cut into six 5 km blocks, alternating hoods and drops. The start of each block is discarded, and each block is compared with the mean of its two neighbours. hoods 0 km drops hoods 10 km drops hoods 20 km drops 30 km each block vs the mean of its neighbours dropped: heart rate still settling after the switch
to resolverides needed
5%4
3%10
2%23
$ wattif ab --since 2026-09-01 \
    --arms hoods,drops --no-weather
  2026-09-03 08:00  8 block(s)  40.5 km
  2026-09-07 09:00  8 block(s)  40.6 km
  2026-09-12 10:00  8 block(s)  40.5 km
  2026-09-16 11:00  8 block(s)  40.7 km

32 block(s) over 4 ride(s), 24 drift-corrected pair(s).

arm            equiv km/h     hr   efficiency
  hoods            25.89    161     0.1613
  drops            26.90    162     0.1662

drops vs hoods: +3.88%  (95% CI +1.48% to +6.27%)
uncorrected difference: +3.03%

per-ride scatter of the paired difference: 1.50%
  to resolve 2%: 5 ride(s)
  to resolve 3%: 2 ride(s)
  to resolve 5%: 1 ride(s)

The difference is significant at 95%.
Hoods against drops over four rides. Sample output on synthetic rides, which are less noisy than real ones, so they resolve faster than the table above.

Sanity-check the rig before testing anything real: do a few rides with the same setup throughout, still auto-lapping every 5 km, then run an arm against itself with wattif ab --since <date of the first of those rides> --arms null_a,null_b. It should report roughly nothing, with an interval spanning zero. If it does not, no equipment answer from those blocks will be trustworthy either.

Private by design

No server, no account, no telemetry. Your rides stay on your machine. The only thing that leaves it is a coarsened coordinate for a historical weather lookup, and that is cacheable and optional. Heart rate is health data, which is why this stays a local tool rather than a hosted one.

Your Garmin password is used once to obtain tokens and is never written to disk. Garmin has no personal API, so wattif sync speaks to the same private endpoints their phone app uses. That has broken before, so the official Garmin export ZIP always works as a fallback.

Install

Python 3.11 or newer.

$ git clone https://github.com/jakubwaller/wattif.git && cd wattif
$ python3 -m venv .venv && source .venv/bin/activate
$ pip install -e .
$ cp config/profile.example.yaml config/profile.yaml   # then edit it

$ wattif sync                       # pull recent rides from Garmin Connect
$ wattif import ~/Downloads/x.zip   # or import a Garmin bulk export
$ wattif analyse --min-km 20        # corrected trend, quarter by quarter

More in the README.