455 lines
21 KiB
Markdown
455 lines
21 KiB
Markdown
# 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.
|