From 57e48b23987630936c84300d87ea5be682cb64bd Mon Sep 17 00:00:00 2001 From: Martin Kagamino Lehoux Date: Thu, 20 Aug 2026 15:47:52 +0200 Subject: feat(ride): compute moving time from GPX with backtest Add ride.MovingTime, a speed-threshold algorithm over track timestamps. Add a -backtest CLI command comparing computed values against Strava's reference across all stored rides, with a percentile distribution summary. Baseline: mean err 3.9%, p50 1.6, p75 4.6, p90 8.4, p95 12.1, p99 44.9. --- ride/moving_time.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 ride/moving_time.go (limited to 'ride/moving_time.go') diff --git a/ride/moving_time.go b/ride/moving_time.go new file mode 100644 index 0000000..8403772 --- /dev/null +++ b/ride/moving_time.go @@ -0,0 +1,28 @@ +package ride + +import "time" + +const MovingTimeAlgorithmVersion = "v1" + +// movingTimeMinSpeedMps is the minimum implied speed between two consecutive +// track points for the interval to count as moving (1 km/h). +const movingTimeMinSpeedMps = 1.0 / 3.6 + +// Baseline backtest (356 rides, `-backtest`): mean err 3.9% (stderr 0.4), +// p50 1.6, p75 4.6, p90 8.4, p95 12.1, p99 44.9. +// Worst offenders overcount stationary GPS jitter (lunch stops) and +// undercount trainer rides with little GPS movement. Tuned in future commits. +func MovingTime(r Ride) time.Duration { + var moving time.Duration + for i := 1; i < r.Len(); i++ { + dt := r.Timestamp(i).Sub(r.Timestamp(i - 1)) + if dt <= 0 { + continue + } + dd := r.DistanceM(i) - r.DistanceM(i - 1) + if dd/dt.Seconds() > movingTimeMinSpeedMps { + moving += dt + } + } + return moving +} \ No newline at end of file -- cgit v1.3