Files
workout/docs/forecasting.md
T

147 lines
8.4 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.
# Exercise forecasts
The forecast predicts joint **load × reps** outcomes for an exercise's next
sessions. E1RM is derived from those sets with fractional loads preserved. The
graph and table use the same forecast. Equivalent loads at other rep counts
are a separate, collapsed reference table, not predicted workout choices.
## Why this model
A small, recency-weighted transition model fits the data and the user workflow:
repeated sets, rep gains, difficult sessions, then load increases and rep resets.
It is a nonparametric learner (weighted neighbours), trained on demand on the
current person/exercise block. It needs only NumPy; no external training service,
cross-person data pooling or large ML dependency is involved.
We compared it with repeating the last set, an anchored full-block linear E1RM
trend, and a damped recent E1RM trend. A larger neural/boosted model was not
trained: the per-exercise histories are small, and this model already improves
the held-out metrics while preserving interpretable load/rep transitions.
The evaluation follows [rolling-origin validation](https://otexts.com/fpp3/tscv.html):
fit only on observations strictly before the held-out session. Random train/test
splits can give [optimistic results for lagged time series](https://scikit-learn.org/stable/auto_examples/applications/plot_time_series_lagged_features.html).
[Damping](https://otexts.com/fpp3/holt.html) motivates restraining long-horizon growth;
our load-transition damping is a conservative scenario assumption, not a learned
physiological law.
## Training breaks and data handling
- Take the best E1RM set per date, including deterministic ties. Duplicate sets
do not increase the session count.
- Split training blocks online when a gap exceeds three recent usual intervals,
with a 28-day minimum, 84-day maximum and 42-day sparse-history fallback.
The threshold uses only preceding gaps; cadence restarts after a break.
- Fit the latest block only. Require four valid sessions after a break. Older
blocks remain visible in the chart but cannot drive current forecasts.
- History-range controls change the chart and equivalent-rep map, not the
training window. A custom end date is also a strict training cutoff.
- Positive loads and integer reps 1–20 are supported. Missing, invalid and
unsupported readings do not train the model. The UI explains when a new
forecast is unavailable. This E1RM model is not appropriate for timed carries,
zero-load bodyweight work, or other metrics merely stored as reps.
- Old histories are labelled historical projections, not upcoming workouts.
## Model details
The last 32 current-block sessions supply joint load/rep transitions. A kernel
weights their similarity to the current reps, relative load and stall streak;
recency has an eight-session half-life. An explicit repeat-set prior keeps sparse
evidence conservative. A stall means failing to improve the rep best at the
current load, including drops/recoveries rather than counting every rebound as
new progress.
The usual pre-increase reps and most common load increment are inferred
from the recent 24 sessions. The model preserves the observed mix of load
increments from its transition window rather than replacing every increase with
the most common one. Jumps over 15% of their original source load are excluded
from progression evidence. Load increases are allowed at the inferred threshold;
their reset reps come from actual transitions. No fixed 8–12 prescription is
hardcoded. If there are no observed increases, the model cannot invent them.
Rep-building transitions are excluded at the ceiling, so clipping a rep gain
cannot turn it into artificial evidence of a stall. Actual repeats and the
explicit repeat-set prior remain in the model.
The next set is the most likely **joint** outcome. For longer horizons, 512
deterministically seeded trajectories produce scenario sets at the median load
and median reps conditional on that load. This avoids jumping between dissimilar
sets just because their E1RMs match. Each row describes a representative outcome
at that date, not one prescribed sequence. Dates use the median recent session
gap. A horizon extending between workout dates carries forward the previous
scenario without inventing another workout.
The band starts at the observed value and uses simulated 10th/90th E1RM quantiles,
broadened to at least the 80th percentile of recent absolute one-session backtest
errors. These are plausible scenario ranges, **not calibrated 80% confidence
intervals**. Long-horizon growth probabilities are damped after 12 sessions.
Intervals and one-year scenarios have not been prospectively validated.
## Production audit, 6 September 2026
Read-only SSH/SQL inspection of Peter's 3,050 recorded sets covered 132 exercises.
There were 3,015 distinct valid exercise/date observations and 53 detected breaks;
91 latest blocks had at least four usable sessions. The rest are withheld with
an explanation. Old but sufficiently populated blocks can produce historical
projections; this count is not a count of currently active exercises.
Every eligible historical block was tested, starting with eight training sessions.
Holdouts never cross a detected break. The first four to eight post-break sessions
therefore have less validated support. All methods use the same block segmentation,
fractional loads and estimated session cadence. Linear/damped baselines keep the
latest rep target, as the previous product did. These are retrospective development
results, not independent prospective validation or a guarantee for every exercise.
| Horizon | Holdouts | Model | Load MAE (kg) | Rep MAE | E1RM MAE (kg) |
| --- | ---: | --- | ---: | ---: | ---: |
| Next session | 1,929 | Learned transitions | 0.272 | 0.687 | 1.863 |
| Next session | 1,929 | Repeat last set | 0.506 | 0.800 | 2.395 |
| Next session | 1,929 | Damped trend | 0.374 | 0.800 | 2.133 |
| Next session | 1,929 | Linear trend | 0.422 | 0.800 | 2.118 |
| 4 sessions | 1,636 | Learned transitions | 0.953 | 1.030 | 3.112 |
| 4 sessions | 1,636 | Repeat last set | 1.884 | 1.145 | 4.159 |
| 4 sessions | 1,636 | Damped trend | 1.074 | 1.145 | 3.436 |
| 4 sessions | 1,636 | Linear trend | 1.201 | 1.145 | 3.581 |
| 8 sessions | 1,308 | Learned transitions | 1.716 | 1.173 | 4.151 |
| 8 sessions | 1,308 | Repeat last set | 3.545 | 1.158 | 5.906 |
| 8 sessions | 1,308 | Damped trend | 1.903 | 1.158 | 4.518 |
| 8 sessions | 1,308 | Linear trend | 2.056 | 1.158 | 4.845 |
The learned model improves aggregate load and E1RM errors at all tested horizons.
At eight sessions it does **not** beat persistence on reps; difficult sessions
remain unpredictable. The UI includes exercise-specific recent error metrics.
After correcting the collapsed load increments and artificial ceiling repeats,
next-session and four-session errors improved over the original transition model.
Eight-session load error improved from 1.782 to 1.716 kg, but E1RM error rose
slightly from 4.114 to 4.151 kg. This adjustment is not an accuracy gain at every
horizon. All 23 unit tests pass, including mixed increments, ceiling transitions,
outlier increases and unchanged plateau behaviour.
Peter's Stiff Leg Deadlift (TNG) eight-week scenario changes from 181 kg × 7 to
183 kg × 6, and its 26-week scenario from 183.5 kg × 7 to 188 kg × 6. These are
joint load/rep outcomes: a higher load does not necessarily mean a higher E1RM
at every horizon. DB Bulgarian Split Squat's 26-week historical scenario changes
from 39 kg × 10 to 39.5 kg × 11; its history ends on 11 January 2024. The current
Smith machine Bulgarian split squat has only two sessions and remains withheld
until four valid sessions are available.
For 30 degrees Smith Press, the latest block has 39 sessions. The current load
block contains seven sessions at 93.5 kg, progressing 4→5→6→6→6→6→7 reps.
Three sessions did not improve the rep best. The inferred threshold is nine reps
and the load increment is 0.5 kg. The next forecast is 93.5 kg × 7, around 13 Sep
2026 at an eight-day cadence. Its E1RM is 113.2 kg; the old SQL rounded the load
to 94 kg and then the E1RM to 114 kg.
## Reproduce
```powershell
.venv/Scripts/python -m unittest discover -s tests -v
.venv/Scripts/python scripts/audit_forecasts.py --person Peter
.venv/Scripts/python scripts/preview_forecasts.py
```
The audit and local preview keep raw production records in process memory.
Neither modifies the database. Cached forecasts are bounded, keyed by the full
training rows and horizon, and copied before rendering; new records invalidate
the key naturally.