Skip to content

fix(grafana): time-weighted Ø Speed in Drive Details (avoids standstill sampling bias) - #5499

Open
wjsall wants to merge 3 commits into
teslamate-org:mainfrom
wjsall:fix-avg-speed-time-weighted
Open

fix(grafana): time-weighted Ø Speed in Drive Details (avoids standstill sampling bias)#5499
wjsall wants to merge 3 commits into
teslamate-org:mainfrom
wjsall:fix-avg-speed-time-weighted

Conversation

@wjsall

@wjsall wjsall commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

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):

dataset point average (panel) distance ÷ duration (truth) error
24-min city drive, 7.51 km (second-level CSV) 27 km/h 18.8 km/h +43 %
48-min drive, 23.8 km (second installation) 33.7 km/h 29.7 km/h +13 %

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_unit conversion and the field alias are unchanged.

🤖 Generated with Claude Code

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
@netlify

netlify Bot commented Jul 11, 2026

Copy link
Copy Markdown

Deploy Preview for teslamate ready!

Name Link
🔨 Latest commit d6dee61
🔍 Latest deploy log https://app.netlify.com/projects/teslamate/deploys/6a61b2866303590008794618
😎 Deploy Preview https://deploy-preview-5499--teslamate.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@CLAassistant

CLAassistant commented Jul 11, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@JakobLichterfeld JakobLichterfeld left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_id

This 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

@JakobLichterfeld
JakobLichterfeld marked this pull request as draft July 18, 2026 13:24
@JakobLichterfeld JakobLichterfeld added kind:bug Something isn't working and removed note:discussion Details or approval are up for discussion labels Jul 18, 2026
wjsall and others added 2 commits July 23, 2026 14:02
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.
@wjsall

wjsall commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the great review — you're right, distance / duration from drives is the cleaner, exact source. Reworked into two stats as you suggested:

  • Ø Speed (panel 37) — distance / (end_date − start_date) via $drive_id, matching the on-page Drive Duration stat (so the two line up).
  • Ø Moving Speed (new stat, panel 41) — the time-weighted CTE with AND speed > 0, scoped by drive_id like the other per-drive stats so it doesn't react to the time range either.

Verified on a real 23.8 km / 48.1 min drive (~3.8 min stopped):

  • Ø Speed = 29.7 km/h (distance ÷ elapsed time ✓)
  • Ø Moving Speed = 32.5 km/h — above the total, as expected once standstill is excluded

One note: I initially scoped Ø Moving Speed by car_id + $__timeFilter (leftover from the old panel) — that broke your own "per-drive shouldn't follow the time range" point (widening the range pulled in adjacent drives). Fixed to drive_id in the second commit. Ready for another look 🙏

@JakobLichterfeld
JakobLichterfeld marked this pull request as ready for review July 23, 2026 06:55

@JakobLichterfeld JakobLichterfeld left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: 0 might read better.

With that fixed I'm happy to merge.

🤖 Review drafted with Claude Code (Opus 5 high) — sponsored by Claude for Open Source

@JakobLichterfeld JakobLichterfeld modified the milestones: v4.1.0, v4.2.0 Aug 13, 2026
@JakobLichterfeld JakobLichterfeld modified the milestones: v4.2.0, v4.3.0 Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:dashboard Related to a Grafana dashboard kind:bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Drive Details 'Ø Speed' overstates average speed: point average is biased by standstill downsampling

3 participants