Add saved analysis workspace and live dashboard statistics
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
"""Compact five-minute moving segments; gaps over ten seconds are excluded."""
|
||||
|
||||
import math
|
||||
from itertools import pairwise
|
||||
|
||||
from .analytics import numeric
|
||||
|
||||
SEGMENT_SECONDS = 300
|
||||
|
||||
|
||||
def summary(intervals):
|
||||
duration = sum(item["dt"] for item in intervals)
|
||||
distance = sum(item["ds"] for item in intervals)
|
||||
hr = [(item["hr"], item["dt"]) for item in intervals if numeric(item["hr"]) and item["hr"] > 0]
|
||||
hr_time = sum(dt for _, dt in hr)
|
||||
mean_speed = distance / duration if duration else None
|
||||
variation = (
|
||||
math.sqrt(
|
||||
sum((item["ds"] / item["dt"] - mean_speed) ** 2 * item["dt"] for item in intervals)
|
||||
/ duration
|
||||
)
|
||||
/ mean_speed
|
||||
* 100
|
||||
if duration and mean_speed
|
||||
else None
|
||||
)
|
||||
drift = None
|
||||
if duration >= 240 and hr_time >= duration * 0.8:
|
||||
halves = [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]
|
||||
elapsed = 0.0
|
||||
for item in intervals:
|
||||
for index, begin, end in ((0, 0, duration / 2), (1, duration / 2, duration)):
|
||||
dt = max(0, min(elapsed + item["dt"], end) - max(elapsed, begin))
|
||||
if dt and numeric(item["hr"]) and item["hr"] > 0:
|
||||
halves[index][0] += item["ds"] * dt / item["dt"]
|
||||
halves[index][1] += item["hr"] * dt
|
||||
halves[index][2] += dt
|
||||
elapsed += item["dt"]
|
||||
if all(part[2] >= duration * 0.4 and part[0] > 0 and part[1] > 0 for part in halves):
|
||||
efficiency = [ds / hr_total for ds, hr_total, _ in halves]
|
||||
drift = (efficiency[0] - efficiency[1]) / efficiency[0] * 100
|
||||
return {
|
||||
"distance": distance,
|
||||
"duration": duration,
|
||||
"heart_rate": sum(value * dt for value, dt in hr) / hr_time if hr_time else None,
|
||||
"elevation": sum(item["ascent"] for item in intervals)
|
||||
if duration
|
||||
and sum(item["dt"] for item in intervals if item["has_elevation"]) >= duration * 0.8
|
||||
else None,
|
||||
"pace_variability": variation,
|
||||
"heart_rate_drift": drift,
|
||||
"sensor_coverage": hr_time / duration * 100 if duration else None,
|
||||
}
|
||||
|
||||
|
||||
def analyze(samples):
|
||||
intervals, segments, pending, elapsed, offset = [], [], [], 0.0, 0.0
|
||||
for before, after in pairwise(samples):
|
||||
if not all(
|
||||
numeric(row.get(key)) for row in (before, after) for key in ("time", "distance")
|
||||
):
|
||||
continue
|
||||
dt, ds = after["time"] - before["time"], after["distance"] - before["distance"]
|
||||
if not 0 < dt <= 10 or not 0.5 <= ds / dt <= 15:
|
||||
continue
|
||||
ascent = (
|
||||
max(0, after["elevation"] - before["elevation"])
|
||||
if all(numeric(row.get("elevation")) for row in (before, after))
|
||||
else None
|
||||
)
|
||||
original = {
|
||||
"dt": dt,
|
||||
"ds": ds,
|
||||
"hr": after.get("heart_rate"),
|
||||
"ascent": ascent or 0,
|
||||
"has_elevation": ascent is not None,
|
||||
}
|
||||
intervals.append(original)
|
||||
remaining = dt
|
||||
while remaining > 1e-9:
|
||||
portion = min(remaining, SEGMENT_SECONDS - elapsed)
|
||||
pending.append(
|
||||
{
|
||||
**original,
|
||||
"dt": portion,
|
||||
"ds": ds * portion / dt,
|
||||
"ascent": (ascent or 0) * portion / dt,
|
||||
}
|
||||
)
|
||||
elapsed += portion
|
||||
remaining -= portion
|
||||
if elapsed >= SEGMENT_SECONDS - 1e-9:
|
||||
segments.append({**summary(pending), "offset": offset})
|
||||
offset += elapsed
|
||||
pending, elapsed = [], 0.0
|
||||
if pending:
|
||||
segments.append({**summary(pending), "offset": offset})
|
||||
return summary(intervals), segments
|
||||
Reference in New Issue
Block a user