99 lines
3.7 KiB
Python
99 lines
3.7 KiB
Python
"""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
|