Add saved analysis workspace and live dashboard statistics
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
"""Transparent, descriptive deviations from a prior matched-run cohort."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from .stats_analysis import statistic, valid
|
||||
|
||||
|
||||
def enrich(chart, q, keys, tokens, groups, history, included, start, measure, catalog):
|
||||
baseline_rows = [
|
||||
row
|
||||
for row in history
|
||||
if q["baseline"]
|
||||
and start - timedelta(days=q["baseline"]) <= row.day < start
|
||||
and included(row)
|
||||
]
|
||||
insights = []
|
||||
for key, series in zip(keys, chart["series"], strict=True):
|
||||
spec = q["aggregates"].get(key)
|
||||
field = spec["field"] if spec else key
|
||||
field = field or "count"
|
||||
inputs = lambda rows, field=field: [
|
||||
row.values[field] for row in rows if valid(row.values.get(field))
|
||||
]
|
||||
if q["chart"] == "histogram":
|
||||
values = inputs([row for token in tokens for row in groups[token]])
|
||||
if values:
|
||||
import math
|
||||
|
||||
count = min(30, max(1, math.ceil(math.sqrt(len(values)))))
|
||||
lo, hi = min(values), max(values)
|
||||
width = (hi - lo) / count if hi > lo else max(abs(lo) * 0.05, 1)
|
||||
bins = [0] * count
|
||||
for number in values:
|
||||
bins[min(count - 1, int((number - lo) / width))] += 1
|
||||
series["histogram"] = {
|
||||
"edges": [lo + i * width for i in range(count + 1)],
|
||||
"counts": bins,
|
||||
"width": width,
|
||||
"field": catalog[field],
|
||||
}
|
||||
else:
|
||||
series["histogram"] = {"edges": [], "counts": [], "field": catalog[field]}
|
||||
if q["chart"] == "box":
|
||||
series["boxes"] = [
|
||||
{
|
||||
"low": statistic(inputs(groups[token]), "min"),
|
||||
"q1": statistic(inputs(groups[token]), "p25"),
|
||||
"median": statistic(inputs(groups[token]), "median"),
|
||||
"q3": statistic(inputs(groups[token]), "p75"),
|
||||
"high": statistic(inputs(groups[token]), "max"),
|
||||
"count": len(inputs(groups[token])),
|
||||
}
|
||||
for token in tokens
|
||||
]
|
||||
if not q["baseline"]:
|
||||
continue
|
||||
# Comparing totals/counts to a distribution of individual runs is meaningless.
|
||||
supported = (
|
||||
spec["function"] not in {"sum", "count", "stddev"}
|
||||
if spec
|
||||
else field not in {"count", "distance", "duration", "elevation", "calories"}
|
||||
)
|
||||
if not supported:
|
||||
continue
|
||||
numbers = inputs(baseline_rows)
|
||||
config = q["flag"] or {"sd": 2, "minimum": 3, "sustained": 1, "direction": "outside"}
|
||||
usable = supported and len(numbers) >= max(10, config["minimum"]) and len(set(numbers)) > 1
|
||||
center = measure(baseline_rows, key) if usable else None
|
||||
spread = statistic(numbers, "stddev") if usable else None
|
||||
low = center - config["sd"] * spread if valid(center) else None
|
||||
high = center + config["sd"] * spread if valid(center) else None
|
||||
counts = [len(inputs(groups[token])) for token in tokens]
|
||||
flags, streak, last_direction = [], 0, None
|
||||
for number, count in zip(series["values"], counts, strict=True):
|
||||
direction = (
|
||||
"above"
|
||||
if usable and valid(number) and number > high
|
||||
else "below"
|
||||
if usable and valid(number) and number < low
|
||||
else None
|
||||
)
|
||||
qualified = (
|
||||
direction
|
||||
and count >= config["minimum"]
|
||||
and config["direction"] in {direction, "outside"}
|
||||
)
|
||||
streak = (
|
||||
streak + 1 if qualified and direction == last_direction else 1 if qualified else 0
|
||||
)
|
||||
last_direction = direction if qualified else None
|
||||
flags.append(bool(q["flag"] and streak >= config["sustained"]))
|
||||
series["baseline"] = {
|
||||
"value": center,
|
||||
"low": low,
|
||||
"high": high,
|
||||
"count": len(numbers),
|
||||
"counts": counts,
|
||||
"flags": flags,
|
||||
"supported": supported,
|
||||
}
|
||||
flagged = [
|
||||
{"label": label, "value": number, "count": count}
|
||||
for label, number, count, flag in zip(
|
||||
chart["labels"], series["values"], counts, flags, strict=True
|
||||
)
|
||||
if flag
|
||||
]
|
||||
insights.append(
|
||||
{
|
||||
"key": key,
|
||||
"label": series["label"],
|
||||
"unit": series["unit"],
|
||||
"baseline": center,
|
||||
"source": q["source"],
|
||||
"low": low,
|
||||
"high": high,
|
||||
"count": len(numbers),
|
||||
"usable": usable,
|
||||
"supported": supported,
|
||||
"start": (start - timedelta(days=q["baseline"])).isoformat(),
|
||||
"end": (start - timedelta(days=1)).isoformat(),
|
||||
"flagged": flagged,
|
||||
"rule": config,
|
||||
}
|
||||
)
|
||||
return insights
|
||||
Reference in New Issue
Block a user