Replace fixed DSL fields with parameterized metric expressions
This commit is contained in:
+18
-24
@@ -16,15 +16,9 @@ from .stats_dsl import QueryError
|
||||
|
||||
def catalog_for(q):
|
||||
catalog = dict(METRICS)
|
||||
if q["source"] in {"days", "health"}:
|
||||
for days in (7, 28):
|
||||
for field in ("distance", "count"):
|
||||
key = f"rolling_{days}d_{field}"
|
||||
label, unit, _ = catalog[key]
|
||||
catalog[key] = (label, unit, f"Complete recorded running {field} over this local day and the preceding {days - 1} local days, before filters. Rest days are included.")
|
||||
for spec in q["derived"]:
|
||||
catalog[spec["name"]] = (
|
||||
spec["name"].replace("_", " ").capitalize(),
|
||||
spec["expression"] if spec.get("inline") else spec["name"].replace("_", " ").capitalize(),
|
||||
spec["unit"],
|
||||
f"Per-run calculation: {spec['expression']}. Missing inputs and zero divisors produce missing data.",
|
||||
)
|
||||
@@ -80,7 +74,7 @@ def formatted(number, key, q):
|
||||
return "—"
|
||||
spec = q["aggregates"].get(key)
|
||||
raw = spec["field"] if spec and spec["function"] not in {"count", "stddev", "sum"} else key
|
||||
if raw in {"pace", "gap"} or raw == "effort_time" or raw.startswith("best_"):
|
||||
if raw in {"pace", "gap", "effort_time"} or any(s["name"] == raw and s["unit"] == "time" for s in q["derived"]):
|
||||
return format_value(number, "pace")
|
||||
if key == "count" or (spec and spec["function"] == "count"):
|
||||
return f"{number:,.0f}"
|
||||
@@ -137,14 +131,15 @@ def ordered(rows, ordering):
|
||||
|
||||
|
||||
def prepare(raw, zone, q, catalog):
|
||||
rows, windows, totals = [], {7: deque(), 28: deque()}, {7: 0.0, 28: 0.0}
|
||||
rows = []
|
||||
previous, last_day, streak = None, None, 0
|
||||
trees = {
|
||||
spec["name"]: expression_tree(spec["expression"], catalog)[0]
|
||||
for spec in q["derived"]
|
||||
if "window" not in spec
|
||||
if not any(key in spec for key in ("window", "since", "best_distance"))
|
||||
}
|
||||
custom_windows = {spec["name"]: deque() for spec in q["derived"] if "window" in spec}
|
||||
last_matching = {}
|
||||
for record in raw:
|
||||
row = SimpleNamespace(**record._mapping)
|
||||
row.instant = (
|
||||
@@ -169,10 +164,6 @@ def prepare(raw, zone, q, catalog):
|
||||
)
|
||||
row.values.update({key: (row.session_context or {}).get(key)
|
||||
for key in ("perceived_effort", "fatigue")})
|
||||
for km in (1, 2, 5, 10):
|
||||
efforts = [e.get("seconds") for e in row.best_efforts or []
|
||||
if e.get("distance") == km * 1000 and valid(e.get("seconds"))]
|
||||
row.values[f"best_{km}km"] = min(efforts) if efforts else None
|
||||
for field in (
|
||||
"pace",
|
||||
"heart_rate",
|
||||
@@ -196,20 +187,23 @@ def prepare(raw, zone, q, catalog):
|
||||
):
|
||||
now, before = row.values[field], previous.values[field] if previous else None
|
||||
row.values[key] = now - before if valid(now) and valid(before) else None
|
||||
for days in (7, 28):
|
||||
window = windows[days]
|
||||
while window and (row.instant - window[0].instant).total_seconds() >= days * 86400:
|
||||
totals[days] -= window.popleft().distance_m / 1000
|
||||
window.append(row)
|
||||
totals[days] += row.distance_m / 1000
|
||||
row.values[f"rolling_{days}d_distance"] = max(0.0, totals[days])
|
||||
row.values[f"rolling_{days}d_count"] = len(window)
|
||||
if row.day != last_day:
|
||||
streak = streak + 1 if last_day and (row.day - last_day).days == 1 else 1
|
||||
last_day = row.day
|
||||
row.values["run_day_streak"] = streak
|
||||
for spec in q["derived"]:
|
||||
name = spec["name"]
|
||||
if "best_distance" in spec:
|
||||
efforts = [e.get("seconds") for e in row.best_efforts or []
|
||||
if e.get("distance") == spec["best_distance"] and valid(e.get("seconds"))]
|
||||
row.values[name] = min(efforts) if efforts else None
|
||||
continue
|
||||
if "since" in spec:
|
||||
if matches(row, spec["since"]):
|
||||
last_matching[name] = row.day
|
||||
last = last_matching.get(name)
|
||||
row.values[name] = (row.day - last).days if last else None
|
||||
continue
|
||||
if "window" not in spec:
|
||||
row.values[name] = evaluate(trees[name], row.values)
|
||||
continue
|
||||
@@ -476,7 +470,7 @@ def explore(owner_id, timezone, q, page=1, selected_bucket=None, export=False, r
|
||||
number, coverage = summarize(current, key, q)
|
||||
before, _ = summarize(prior, key, q)
|
||||
title, unit, definition = descriptors[key]
|
||||
if unit == "runs" and key != "sessions" and not key.startswith("rolling_"):
|
||||
if unit == "runs" and (key == "count" or q["aggregates"].get(key, {}).get("function") == "count"):
|
||||
unit = "runs" if q["reduce"] else q["source"]
|
||||
cards.append(
|
||||
{
|
||||
@@ -539,7 +533,7 @@ def explore(owner_id, timezone, q, page=1, selected_bucket=None, export=False, r
|
||||
return results
|
||||
|
||||
title, unit, _ = descriptors[key]
|
||||
if unit == "runs" and key != "sessions" and not key.startswith("rolling_"):
|
||||
if unit == "runs" and (key == "count" or q["aggregates"].get(key, {}).get("function") == "count"):
|
||||
unit = "runs" if q["reduce"] else q["source"]
|
||||
pace_axis = unit == "/km"
|
||||
if q["trend"] == "percent_change":
|
||||
|
||||
Reference in New Issue
Block a user