Extend run stats DSL with statistical and temporal analysis

This commit is contained in:
Peter Stockings
2026-10-02 17:37:01 +10:00
parent 987f470ea5
commit a06189849e
12 changed files with 1807 additions and 214 deletions
+193
View File
@@ -0,0 +1,193 @@
# Run analysis DSL
Write queries in **Stats**, use **Ctrl+Space** for completion and **Ctrl/Cmd+Enter**
to execute. **Format** produces a canonical query. Live validation uses the same
parser as execution. Comments occupy their own lines starting with `#`.
## Largest gaps between runs
```text
runs
during all time
summarize gap_since_previous_run, distance
by run
sort by gap_since_previous_run desc
take 10
compare none
```
Each gap is actual elapsed start-to-start time, expressed in days. Its predecessor
comes from your complete running history, even if it falls outside the period or
fails the query's filters. The first recorded run has a missing gap. Each result
links to the preceding run; this cannot reveal unrecorded activities.
## Statistical analysis
```text
runs
during last 24 months
where distance >= 5 km
summarize count(), median(pace), p90(heart_rate), sum(distance)
by weekday
having count() >= 5 runs
rank by median(pace) asc
limit 4
compare none
plot speed vs heart_rate
```
Functions: `sum`, `mean`, `weighted_mean`, `median`, `min`, `max`, `stddev`,
`count`, `p10`, `p25`, `p75`, `p90`, `p95`, `p99`. Use `count()` for all runs or
`count(power)` for runs with that metric. Percentiles use linear interpolation
at `(n-1)*p`; standard deviation is population standard deviation.
`mean(pace)` gives each run equal weight. `weighted_mean(pace)` weights by
recorded distance. Other weighted means use timer duration. Bare `pace` is total
timer time / distance; bare heart rate and running dynamics are weighted by timer
time. Bare distance, duration, ascent, calories and count are totals. Other bare
metrics are unweighted means. Calculated-field bare summaries are means.
Missing values are excluded, not replaced by zero. Empty sums and means remain
missing; counts are zero. Nonpositive recorded pace and sensor values are missing.
Each summary card shows usable-run coverage and its definition.
## Metrics and units
| Metric | Input unit(s) |
|---|---|
| `distance`, `distance_change`, `rolling_7d_distance`, `rolling_28d_distance` | `km`, `m` |
| `duration` | `h`, `min`, `s` |
| `pace` | `/km`, such as `5:30 /km` or `330 /km` |
| `pace_change` | `sec/km`, signed; negative is faster |
| `heart_rate`, `heart_rate_change`, `max_heart_rate` | `bpm` |
| `elevation`, `stride_length` | `m` |
| `cadence` | `spm` |
| `power` | `W` |
| `ground_contact` | `ms` |
| `vertical_oscillation` | `cm` |
| `speed` | `km/h`, `m/s` |
| `gap_since_previous_run` | `days`, `h`, `min` |
| `run_day_streak` | `days` |
| `count`, `rolling_7d_count`, `rolling_28d_count` | `runs` |
| `elevation_per_km` | `m/km` |
| `calories` | `kcal` |
| `training_effect` | `score` |
Changes compare with the immediately preceding recorded run before filtering.
Rolling 7/28-day metrics include this run and earlier runs less than 168/672 hours
old; the exact boundary is excluded. Streaks count consecutive local calendar days
with a run. Multiple runs on one day don't lengthen the streak.
Pace standard deviations and sums use numeric `sec/km`, not pace formatting.
Garmin calories and aerobic training effect remain missing when not recorded.
## Calculated fields
```text
runs
during last 12 weeks
let efficiency = speed / heart_rate
let pace_ratio = pace / pace
where efficiency > 0.07 (km/h/bpm)
summarize mean(efficiency), median(pace), count()
by week
plot efficiency vs distance
compare none
```
Define fields before referring to them. Arithmetic supports `+`, `-`, `*`, `/`,
parentheses, numeric constants, and earlier fields. Addition/subtraction requires
matching units. Pace arithmetic uses seconds/km, duration uses hours; division of
equal units gives `ratio`. Scalar multiplication keeps the field's unit. Combined
units appear in validation and completion, e.g. `(km/h/bpm)` for efficiency.
Missing inputs, zero divisors and nonfinite results yield missing values.
Expressions cannot call functions, access attributes, index values or execute code.
## Filters and periods
```text
runs
during 2026-07-01 to 2026-09-30
where distance between 8 and 20 km
and heart_rate < 150 bpm
or title contains "Race"
and has power
summarize median(pace), count()
by month
compare previous period
```
Numeric operators: `=`, `!=`, `<`, `<=`, `>`, `>=`, and inclusive `between`.
`has` / `missing` test metric availability; title search is case-insensitive and
uses a double-quoted string. AND binds more strongly than OR: the example selects
8–20 km runs below 150 bpm **or** race-titled runs with power. Parenthesized boolean
filters are not supported; use separate AND chains joined by OR.
Periods: `during last N days/weeks/months`, inclusive explicit date ranges, or
`during all time`. Rolling periods may use `ending YYYY-MM-DD`. Dates and groups
use your account timezone. Supported relative ranges are 1–732 days, 1–104 weeks,
1–24 calendar months; explicit ranges cover at most 732 days. All time covers the
complete recorded history through today and requires `compare none`.
Comparisons otherwise use an equal-length preceding period. Time series align
previous dates by elapsed calendar days; categorical groups use matching categories.
## Grouping, ranking and limits
Group with `by day/week/month/year`, `by weekday`, `by hour`, `by run`, `by all`, or
`by distance bins 5 km`. Weeks start Monday. Numeric bins are half-open ranges
`[lower, upper)` anchored at zero; negative inputs use negative bins. Missing bin
values are excluded. Time groups include empty calendar buckets, weekday/hour
include all categories; numeric bins include observed bins.
- `sort by FIELD asc/desc` orders runs; missing values sort last. `date` is allowed.
- `take N` selects the first sorted runs **before** grouping, also for comparison.
- `having SUMMARY OP VALUE UNIT` filters groups after aggregation. Multiple having
clauses combine with AND; a summary need not be displayed to filter on it.
- `rank by SUMMARY asc/desc` orders groups; the ranking summary must be selected.
- `limit N` limits groups; without ranking, time groups retain the most recent N,
categorical groups retain the first N in category order.
The order is history-derived metrics → date/per-run filters → sort/take → groups
→ having → rank/limit → displayed cohort → chart trends. Cards and contributing
runs reflect the displayed cohort. Limits are 1–1,000 runs/groups, six displayed
summaries, eight calculated fields, twenty numeric filter conditions and ten
having clauses. Wider grouping or limits are required above 1,000 displayed groups.
## Trends, relationships and export
```text
runs
during last 24 weeks
summarize distance, count(), weighted_mean(pace)
by week
trend rolling 4 buckets
compare none
```
Time groups support `trend cumulative`, `trend rolling N buckets` (1–52),
`trend change`, and `trend percent_change`. Rolling/cumulative summaries pool
contributing runs and recalculate the selected statistic; they do not average
bucket means. These windows operate on displayed buckets after having/limits.
Change compares consecutive displayed buckets; the first, missing inputs, and
percentage division by zero remain missing. Percentage change uses the absolute
previous value as denominator. Trends cannot also rank groups.
Charts support `chart auto/line/bar/none`. `plot X vs Y` adds a run-level scatter
plot and Pearson correlation using complete pairs. Constant axes or fewer than
two pairs have no correlation. Select a scatter point to open its activity.
Select a chart point, group row, or period picker to inspect contributing runs.
For cumulative/rolling chart points, drill-through shows the pooled runs; group
rows show only the original group's runs. The grouped
results table and cards always retain untransformed statistics. Evidence shows
individual input values, not an aggregate applied to one run.
**Export CSV** downloads untransformed grouped results. **Export runs** downloads
all runs in the displayed cohort, not only the evidence page. CSV contains raw
numeric precision; pace is seconds/km, duration is hours, and missing values are
blank. Run titles are escaped against spreadsheet formula interpretation. All
queries, predecessor links, scatter points and exports remain owner-scoped.
Analysis operates on persisted whole-run summaries; it does not load raw samples
or routes. Saved views are stored in this browser; bookmark URLs to share a query
with yourself across devices. Queries cannot join other users or raw sample data.