22 KiB
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_time(1 km), best_time(2 km), best_time(5 km), and best_time(10 km),
which accept second/minute filters and display elapsed min:sec. Unsupported
distances produce a validation error; supported distances without an attempt remain missing. These
efforts are windows inside runs, not standardized races.
days
during last 16 weeks
summarize latest(days_since_run(distance >= 10 km)), latest(rolling_sum(distance, 28 days))
by day
compare none
chart line
display overlay
days includes rest days. sessions, days_since_run(), days_since_run(distance >= 10 km),
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 rolling windows include the current local day plus the previous
N−1 local days, including history before the query. Whole-run rolling expressions
use elapsed N×24-hour windows. days_since_run(condition) accepts one existing
numeric, availability or title condition, evaluated against individual runs before
query filters; days_since_run() matches any run. Multiple definitions can appear
in the same query. Missing prior qualifying 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
Metric functions can appear directly in summaries, filters, sorting and let
calculations. For example, min(best_time(5 km)) selects each month's fastest
5 km effort, while latest(rolling_sum(distance, 28 days)) selects its latest
trailing distance. The editor obtains function suggestions from the same registry
as the parser. No field or clause is added for a new window or condition.
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. On daily sources, windows use local
calendar days; rolling_sum(sessions, 7 days) counts running sessions, while
rolling_count(distance, 7 days) counts days with usable distance. 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_sum(distance, N days) |
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_count(distance, N days) |
runs |
elevation_per_km |
m/km |
calories |
kcal |
training_effect |
score |
Changes compare with the immediately preceding recorded run before filtering. Rolling N-day expressions include this run and earlier runs less than N×24 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 can call the registered metric functions described above. They cannot
call arbitrary 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/descorders runs; missing values sort last.dateis allowed.take Nselects the first sorted runs before grouping, also for comparison.having SUMMARY OP VALUE UNITfilters groups after aggregation. Multiple having clauses combine with AND; a summary need not be displayed to filter on it.rank by SUMMARY asc/descorders groups; the ranking summary must be selected.limit Nlimits 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
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.