126 lines
7.1 KiB
Markdown
126 lines
7.1 KiB
Markdown
# 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 an actually observed load increment are inferred
|
||
from the recent 24 sessions. 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.
|
||
|
||
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.312 | 0.692 | 1.893 |
|
||
| 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 | 1.022 | 1.084 | 3.243 |
|
||
| 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.782 | 1.174 | 4.114 |
|
||
| 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.
|
||
|
||
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.
|