Files
Stride/docs/STATS_DSL.md
T

455 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Run analysis DSL
## Deep training signals
`intervals` reads persisted, non-overlapping continuous moving windows of up to
30 seconds. Recording breaks over ten seconds, distance resets, stops and
implausible movement end a window; windows never join across those breaks.
Filter `duration >= 25 s` to remove short fragments. `elapsed` and
`moving_offset` are minutes since recording start and usable moving start.
Elapsed offsets use relative time even when FIT timestamps are Unix timestamps.
```text
intervals
during last 16 weeks
reduce by run min 3 min
where elapsed >= 10 min
and duration >= 25 s
and pace between 6:30 and 7:00 /km
and grade between -2 and 2 %
and grade_variability <= 2 %
and pace_variability <= 15 %
and sensor_coverage >= 90 %
and hr_artifact_percent = 0 %
summarize mean(heart_rate) as "Matched HR"
by run
sort by date asc
compare none
chart line
```
Filters run **before** `reduce by run`. A session needs the specified total
matched moving minutes to contribute one observation. Within-session HR and
mechanics are weighted by usable sensor time; `mean` across reduced observations
weights each session equally. `count()` then counts independent sessions rather
than windows. Matched windows can be separated in time. This is different from
requiring one uninterrupted three-minute section. Charts show distinct-session
and usable-minute coverage, and run plots preserve calendar spacing.
`grade` is duration-weighted signed grade using the existing smoothed elevation
model; `grade_variability` is its population SD in percentage points. GAP and
grade require at least 80% terrain coverage. Window-level cadence, power,
stride length, vertical oscillation and ground contact include sensor coverage
metrics. HR outside 30–240 bpm or a jump exceeding 20 bpm per second is excluded
and counted in `hr_artifact_percent`. These coarse checks do not detect every
sensor artifact or cadence lock. Terrain, HR lag, weather and session intent
remain confounders; the matcher does not claim to remove them.
After reduction, `heart_rate_drift` is recomputed over matched moving-time
halves, excluding any filtered warm-up. Each half needs at least ten minutes of
joint usable HR and terrain, and 80% of the half's time. Halves must differ by
at most 10% in speed and one percentage point of mean grade. Otherwise the
result is missing. Positive means lower speed/HR efficiency later. This is a
descriptive comparison, not an overtraining diagnosis. Recording-level flags
require reduction to independent sessions; a baseline still needs ten runs.
`efforts` expands the persisted best elapsed windows. Use `where distance = 5 km`
and `min(effort_time)` for a 5 km progression. Whole runs expose `best_1km`,
`best_2km`, `best_5km`, and `best_10km`, which accept second/minute filters and
display elapsed min:sec. No attempt at that distance remains missing. These
efforts are windows inside runs, not standardized races.
```text
days
during last 16 weeks
long run distance >= 10 km
summarize latest(days_since_long_run), latest(rolling_28d_distance)
by day
compare none
chart line
display overlay
```
`days` includes rest days. `sessions`, `days_since_run`, `days_since_long_run`,
`run_day_streak`, and `long_run_share` expose continuity and concentration.
`long_run_share` is the largest individual run's share of the current seven-day
distance. Daily 7/28-day windows include the current local day plus the previous
6/27 local days, including history before the query. Whole-run rolling metrics
continue to use elapsed 168/672-hour windows. Missing prior long-run history
does not become a fabricated gap. `latest(field)` selects the latest input by
timestamp regardless of sort order, retaining missing values on that input;
it is useful for current dashboard values and cannot define a baseline band.
`health` reads owner-only Garmin resting HR, overnight HRV, sleep duration and
stress observations (`resting_hr`, `hrv`, `sleep_hours`, `stress`). Enable the
optional recovery sync in Settings. The worker refreshes today/yesterday and
backfills a bounded batch of earlier dates. Provider gaps and unsupported watch
observations stay missing; no readiness score is inferred. Daily inputs link
to their contributing runs when present; rest/recovery-only days do not invent
activity links. Daily sources also support calendar rolling calculations.
Private activity notes expose `perceived_effort` (1–10) and `fatigue` (0–10).
They are never shown on public or friend activity pages. Settings accepts two
personal HR boundaries. `lower_hr_minutes`, `middle_hr_minutes`, and
`upper_hr_minutes` sum actual usable moving sample time, rather than assigning
an entire run/window according to its average HR. Boundary HR belongs to the
higher band. Missing thresholds mean missing results; physiological zones are
never assumed.
`route similar to ID` restricts recordings to an approximate full-course match
against one of your own runs. The comparison resamples compact geometry along
distance, requires endpoints within 100 m, length within 10%, mean course
separation ≤100 m and 90th-percentile separation ≤200 m. Different directions
and recording breaks are excluded; geometry shorter than 500 m is unavailable.
This is an exploratory same-start course matcher, not exact named-segment
recognition. Always inspect the contributing courses.
Operators can run `flask --app app install-training-signals --email ACCOUNT`
to save editable examples and pin the first five. `--pace-low`, `--pace-high`
and `--long-run` customize matching. Re-running preserves existing library
edits. Recompute old recordings with `backfill-analysis` (version 3).
`backfill-health --email ACCOUNT --days 90` imports bounded recovery history
using the normal account session lock and encrypted token rotation; only this
account's recovery sync is enabled.
## Account library and live dashboard
Save an analysis with a name, investigation collection and notes. **Save as new**
or **Duplicate** creates an independent query to explore another question.
**Pin live analysis** adds it to your personal dashboard, where it can display a
plot, summary values or a grouped table. Width and order are saved to your
account. All queries and widget requests enforce owner scope, including shared
friend profiles. Dashboard widgets refresh on load/focus and every five minutes
while visible; each widget retains its own query date range. Relative periods
advance with the calendar. Results are recomputed rather than saved as snapshots.
## Matched effort, reference ranges and persistent deviations
```text
runs
during last 12 weeks
where pace between 5:20 and 5:40 /km
and elevation_per_km <= 10 m/km
and has heart_rate
summarize weighted_mean(heart_rate) as "Matched-pace HR"
by week
compare none
baseline previous 90 days
flag above baseline 1.5 sd min 3 runs sustained 2 buckets
chart line
```
The baseline is the stated number of local calendar days immediately **before
the query starts**, filtered with the same per-input conditions. It never uses
the current cohort. The center uses the selected summary function; its range is
center ± threshold × population standard deviation of individual matched
inputs. This is a descriptive range, not a confidence interval or a diagnostic
threshold. At least ten usable baseline inputs, and nonzero variation, are
required. Totals, counts and standard deviations do not receive baseline bands.
`above`, `below` and `outside` are supported. Flags require the minimum usable
inputs in **each** consecutive calendar bucket; missing, insufficient, in-range
or opposite-direction buckets reset the streak. Only the bucket completing the
streak and following qualifying buckets are marked. The grammar uses `runs` for
the minimum count even for laps/segments; the displayed coverage uses the actual
source. Segments from one run are correlated observations, so a high segment
count does not imply many independent sessions. Calendar grouping is required;
trend transforms and group removal/ranking cannot be combined with flags.
Terrain, conditions, warm-up and sensor differences can still affect matched
effort. Inspect the actual inputs and workload before interpreting a departure.
## Recording-level exploration
```text
segments
during last 12 weeks
where pace between 5:20 and 5:40 /km
and pace_variability <= 5 %
and sensor_coverage >= 80 %
and duration >= 4 min
summarize weighted_mean(heart_rate)
by week
compare none
chart line
```
`segments` expands persisted five-minute **moving-time** segments; the final
partial segment is retained, so filter duration when complete segments matter.
The segments accumulate usable moving intervals across pauses/gaps rather than
claiming five uninterrupted minutes. Intervals require increasing timestamps
with at most ten seconds between recordings, positive plausible movement
(0.5–15 m/s), and increasing distance. Missing sensor values are not interpolated.
HR is duration weighted. Ascent requires elevation on at least 80% of usable
moving intervals; otherwise it is missing. Ascent is the sum of positive raw
altitude increments and can be sensitive to noise. Segment dates are the parent
run's local start date; selecting one opens that run.
`laps` expands recorded lap distance, timer duration, HR and persisted GAP. Other
sensor/terrain metrics remain missing when the lap does not record them. Neither
source loads raw samples during a query. Their `count()` counts inputs; `by run`
gives each lap/segment its own row. Run-history metrics and custom rolling
calculations require `runs`.
Additional recording-derived metrics:
- `pace_variability`: duration-weighted **speed** coefficient of variation (%).
- `sensor_coverage`: usable moving time with HR / usable moving time (%), excluding gaps and pauses.
- `heart_rate_drift`: percent loss of speed/HR efficiency between moving-time halves. At least four moving minutes and ≥80% HR coverage in each half are required. Positive means lower efficiency later; warm-up and terrain are included.
- `gap`: modelled grade-adjusted pace in seconds/km, formatted as min/km.
New imports calculate these; older activities require `backfill-analysis`.
## Visualization and named series
`chart auto|line|bar|area|histogram|box|heatmap|stacked|none` controls presentation.
`display overlay` combines series with an axis per unit; `display separate` is
the default. Summary labels use JSON double-quoted strings:
```text
summarize sum(distance) as "Weekly distance", count() as "Sessions"
reference heart_rate = 150 bpm
plot pace vs heart_rate
color by elevation_per_km
size by distance
```
References require the selected raw metric and compatible units. Scatter color
and size require a scatter plot; missing encoding values use midpoint styling.
Correlation uses paired per-input values, not grouped summaries.
Histograms bin **individual input values** using up to 30 equal-width bins;
the final bin includes its upper endpoint. Boxes show min, p25, median, p75 and
max per group, with no inferred outlier rule. Distribution charts show current
inputs, while comparison cards retain the previous cohort. Calendar heatmaps
require `by day` and retain every day. Missing data differs from a zero count.
Distribution/heatmap overlays are not supported. PNG download exports the plot;
CSV exports the raw underlying grouped or per-input values.
## Named training cohorts
```text
runs
during last 12 weeks
summarize sum(distance)
by week
compare none
cohort "Lower HR" where heart_rate <= 145 bpm
cohort "Higher HR" where heart_rate > 145 bpm
chart stacked
```
Up to four named numeric conditions partition inputs in declaration order: the
**first matching cohort wins**, avoiding double counting. Unmatched/missing
inputs appear in Other / missing. Stacking requires one additive summary
(sum/count, distance, duration, elevation or calories), with no comparison,
baseline or trend. Thresholds are user-selected; names do not infer physiological zones.
## Custom rolling calculations and yearly comparisons
```text
let hr_14d = rolling_mean(heart_rate, 14 days)
let km_10d = rolling_sum(distance, 10 days)
let hr_count = rolling_count(heart_rate, 14 days)
compare previous year
```
Rolling functions use complete owner-scoped run history before filters, include
the current run, and exclude the exact lower elapsed-time boundary. They accept
1–365 days and an earlier metric/calculation. Means are unweighted, sums exclude
missing inputs, and counts count usable inputs. Calendar-bucket rolling trends
remain separate operations. `compare previous year` uses the same local-date
interval one year earlier, clamping leap-day boundaries; time charts align by
elapsed days from each period's start.
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.