Files
Stride/docs/STATS_DSL.md
T

21 KiB
Raw Blame History

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.

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.

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

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

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:

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

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

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

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

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

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

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.

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.