Files
workout/docs/forecasting.md

8.4 KiB
Raw Permalink Blame History

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: fit only on observations strictly before the held-out session. Random train/test splits can give optimistic results for lagged time series. Damping 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

.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.