fix(grafana): time-weighted Ø Speed in Drive Details (avoids standstill sampling bias) - #5499
fix(grafana): time-weighted Ø Speed in Drive Details (avoids standstill sampling bias)#5499wjsall wants to merge 3 commits into
Conversation
The stream inserts positions ~0.3s apart while moving but ~1.6-2s apart at standstill, so AVG(speed) underweights stopped time and overstates average speed by 13-43% in stop-and-go driving (measured on two installations; see issue for second-level data). Replace with a time-weighted average (equivalent to distance / elapsed time), capping inter-sample gaps at 60s so data holes don't skew the weighting. Fixes teslamate-org#5497
✅ Deploy Preview for teslamate ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Thanks for the thorough analysis in #5497 and for the PR — the measurements clearly show the current point average is misleading.
As said, as a user I'd expect Drive Details to show two figures (like most GPS/sports apps do):
- Ø Speed — distance ÷ total duration, including stops
- Ø Moving Speed — average while actually moving
Your time-weighted query effectively converges to the first one. But since this dashboard already has $drive_id, and distance and duration are stored on drives, we can compute it exactly and consistently with the Drives dashboard (which already shows distance / duration as avg speed):
SELECT convert_km((distance / NULLIF(EXTRACT(EPOCH FROM end_date - start_date), 0) * 3600)::numeric, '$length_unit') AS speed_${length_unit}h
FROM drives
WHERE id = $drive_idThis also makes the stat consistent with the other per-drive stats in this dashboard (duration, distance, consumption), which don't react to time-range zooming either.
For the second figure, your time-weighted CTE is exactly the right tool: with standstill excluded (AND speed > 0) it yields a proper moving average — which the current point average only approximates by accident.
Would you be up for reworking the PR in that direction: "Ø Speed" from drives, plus a new "Ø Moving Speed" stat panel next to it?
🤖 Review drafted with Claude Code (Fable 5 high) — sponsored by Claude for Open Source
Per JakobLichterfeld's review on teslamate-org#5499, replace the single time-weighted average speed stat with two separate panels: - "Ø Speed" (existing panel, id 37): now computes distance/duration directly from the drives table instead of the time-weighted CTE. Matches the Drives dashboard and the rest of this page's per-drive stats, and is unaffected by dashboard time-range zoom. - "Ø Moving Speed" (new panel, id 41): keeps the previous time-weighted CTE over positions, now filtered to speed > 0 to exclude standstill, giving a true moving-average speed. Placed directly below Ø Speed (gridPos x=9, y=34) since the y=31 row is fully occupied on the right by the Speed Histogram panel (x=12..24, y=25..34). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Per-drive stats on this dashboard must not react to the dashboard's time range (the very issue this PR fixes for Ø Speed). Ø Moving Speed was still scoped by car_id + $__timeFilter, so widening the range or opening the page outside a drive's window pulled in neighbouring drives' points while the adjacent Ø Speed stayed fixed — inconsistent. positions has drive_id, so scope by it like every other per-drive stat here.
|
Thanks for the great review — you're right,
Verified on a real 23.8 km / 48.1 min drive (~3.8 min stopped):
One note: I initially scoped Ø Moving Speed by |
JakobLichterfeld
left a comment
There was a problem hiding this comment.
Thanks, this is exactly the direction I had in mind — Ø Speed from drives looks good.
One correctness issue in the new Ø Moving Speed panel: speed 0 sits in the CTE's WHERE, and Postgres evaluates WHERE before window functions. So LEAD(date) runs over the already-filtered set, and the last moving sample before a stop gets the whole standstill period as its dt (capped at 60 s) instead of its real sample spacing. A 45 s traffic light assigns 45 s of weight to a ~3 km/h coasting sample — same class of weighting bias as the original bug, just in the opposite direction.
Moving the filter to the outer query fixes it:
WITH pts AS (
SELECT speed, EXTRACT(EPOCH FROM (LEAD(date) OVER (ORDER BY date) - date)) AS dt
FROM positions
WHERE drive_id = $drive_id
)
SELECT
convert_km((SUM(speed * dt) / NULLIF(SUM(dt), 0))::numeric, '$length_unit') AS speed_${length_unit}h
FROM pts
WHERE speed 0 AND dt BETWEEN 0 AND 60(speed 0 also filters out NULLs, so the separate speed IS NOT NULL isn't needed.)
Two nits:
- The trailing newline at the end of the file got dropped — please restore it.
- The new panel sits at
x: 9, y: 34, so it renders alone below Ø Speed with empty space to its left. Worth a look in Grafana;x: 0might read better.
With that fixed I'm happy to merge.
🤖 Review drafted with Claude Code (Opus 5 high) — sponsored by Claude for Open Source
Fixes #5497
Problem
The Drive Details Ø Speed stat computes
AVG(positions.speed)— a plain point average. The stream inserts positions much more densely while moving (~0.29 s/point) than at standstill (~1.6–2 s/point), so stopped time is underweighted and the panel overstates average speed, especially in stop-and-go city driving.Measured on real data (details in #5497):
The bias also numerically approaches "distance ÷ moving time", which misleads users into thinking stopped time is intentionally excluded.
Fix
Time-weighted average (equivalent to distance ÷ elapsed time within the window), with inter-sample gaps capped at 60 s so data holes don't skew the weighting. On the datasets above it returns 18.9 and 30.0 km/h respectively, matching distance ÷ duration.
Single-panel change in
grafana/dashboards/internal/drive-details.json;$__timeFilter,$length_unitconversion and the field alias are unchanged.🤖 Generated with Claude Code