diff options
| -rw-r--r-- | cli/cli.go | 13 | ||||
| -rw-r--r-- | cli/cli_test.go | 14 | ||||
| -rw-r--r-- | ride/moving_time.go | 4 |
3 files changed, 24 insertions, 7 deletions
@@ -32,7 +32,10 @@ var ( backtest = flag.Bool("backtest", false, "run data benchmarks comparing computed values against stored reference values") ) -const backtestMinimumDistanceM = 10_000 +const ( + backtestMinimumDistanceM = 10_000 + backtestMinimumMovingRatio = 0.8 +) func Run(db *sql.DB, configPath string, appConfig config.Config) { switch { @@ -134,12 +137,16 @@ func runBacktest(db *sql.DB) error { fmt.Fprintf(table, "p%g\t%.1f\n", p, distribution[p]) } table.Flush() - slog.Info("Moving time backtest", "rides", len(samples), "min_distance_m", backtestMinimumDistanceM, "mean_err_pct", math.Round(meanAbsErr(absErrors)*10)/10, "stderr_pct", math.Round(stdErrAbsErr(absErrors)*10)/10) + slog.Info("Moving time backtest", "rides", len(samples), "min_distance_m", backtestMinimumDistanceM, "min_moving_ratio", backtestMinimumMovingRatio, "mean_err_pct", math.Round(meanAbsErr(absErrors)*10)/10, "stderr_pct", math.Round(stdErrAbsErr(absErrors)*10)/10) return nil } func isBacktestEligible(item rides.Ride) bool { - return item.GPXPath != "" && item.DistanceM >= backtestMinimumDistanceM && item.MovingTimeS > 0 + return item.GPXPath != "" && + item.DistanceM >= backtestMinimumDistanceM && + item.MovingTimeS > 0 && + item.ElapsedTimeS > 0 && + float64(item.MovingTimeS)/float64(item.ElapsedTimeS) >= backtestMinimumMovingRatio } func meanAbsErr(absErrors []float64) float64 { diff --git a/cli/cli_test.go b/cli/cli_test.go index 6a3e148..59dec79 100644 --- a/cli/cli_test.go +++ b/cli/cli_test.go @@ -8,13 +8,23 @@ import ( ) func TestBacktestEligibilityExcludesShortDistances(t *testing.T) { - short := rides.Ride{GPXPath: "ride.gpx", DistanceM: 10_000 - 1, MovingTimeS: 1} - minimum := rides.Ride{GPXPath: "ride.gpx", DistanceM: 10_000, MovingTimeS: 1} + short := rides.Ride{GPXPath: "ride.gpx", DistanceM: 10_000 - 1, MovingTimeS: 8_000, ElapsedTimeS: 10_000} + minimum := rides.Ride{GPXPath: "ride.gpx", DistanceM: 10_000, MovingTimeS: 8_000, ElapsedTimeS: 10_000} assert.False(t, isBacktestEligible(short)) assert.True(t, isBacktestEligible(minimum)) } +func TestBacktestEligibilityExcludesLowMovingRatio(t *testing.T) { + low := rides.Ride{GPXPath: "ride.gpx", DistanceM: 10_000, MovingTimeS: 7_999, ElapsedTimeS: 10_000} + boundary := rides.Ride{GPXPath: "ride.gpx", DistanceM: 10_000, MovingTimeS: 8_000, ElapsedTimeS: 10_000} + noElapsed := rides.Ride{GPXPath: "ride.gpx", DistanceM: 10_000, MovingTimeS: 1} + + assert.False(t, isBacktestEligible(low)) + assert.True(t, isBacktestEligible(boundary)) + assert.False(t, isBacktestEligible(noElapsed)) +} + func TestBacktestStatsUseUnroundedSamples(t *testing.T) { absErrors := []float64{0, 0.1, 0.1} diff --git a/ride/moving_time.go b/ride/moving_time.go index 7d7f5fe..84cc28e 100644 --- a/ride/moving_time.go +++ b/ride/moving_time.go @@ -16,8 +16,8 @@ const movingTimeMinPowerW = 20 // that can count as moving. Longer gaps are pauses (stops, lost signal). const movingTimeMaxGap = 120 * time.Second -// Bench (117 rides, `-backtest`, distance >= 10 km): -// absMean 0.6%, stderr 0.2, p50 0.2, p75 0.4, p90 1.2, p95 2.2, p99 4.8. +// Bench (104 rides, `-backtest`, distance >= 10 km, moving/elapsed ratio >= 0.8): +// absMean 0.4%, stderr 0.1, p50 0.2, p75 0.4, p90 0.8, p95 1.4, p99 2.9. func MovingTime(r Ride) time.Duration { return movingTime(r, movingTimeMinSpeedMps, movingTimeMaxGap) } |
