Elevate: Fitness trend "Form" has to be based on yesterday's "Fitness" and "Fatigue"

Created on 23 Apr 2018  ·  18Comments  ·  Source: thomaschampagne/elevate

Describe your environment

  • Plugin version: 6.1.2 stable
  • Chrome/Opera/Chromium version: Chrome
  • OS version: Mac OS

Describe the problem:

Right now, the Form seems to be calculated as the difference of the same day's Fitness and Fatigue. However, it may be better to base it on yesterday's Fitness and Fatigue. This is debatable, of course. I can only suggest these two points:
a) it feels more correct to me subjectively, as the training fatigues is felt more the next day
b) TrainingPeaks is doing that (see here, "Form" section)

bug major

Most helpful comment

@aprokop Thanks a lot for this deep analysis! It's clear, unambiguous!

All 18 comments

Form by definition is (TrainingPeaks):
Training Stress Balance (TSB) or Form represents the balance of training stress.

Form (TSB) = Yesterday's Fitness (CTL) - Yesterday's Fatigue (ATL)

https://help.trainingpeaks.com/hc/en-us/articles/204071764-Form-TSB-

@aprokop Yes it should with the math model used. Formulas are in the helper

@thomaschampagne Not sure I understand your comment. Are you saying that's already in?

@aprokop Yes. Elevate fitness model = Training peaks model

image

@thomaschampagne I don't think so. The TrainingPeaks model is

Form(day+1) = Fitness(day) - Fatigue(day)

It also seems that the other two formulas are not exactly right, and should be

Fitness(day+1) = Fitness(day) + (StressScore(day+1)-Fitness(day)) x ...
Fatigue(day+1) = Fatigue(day) + (StressScore(day+1)-Fatigue(day)) x ...

I look at my TrainingPeaks curve, and major effort significantly affects both Fitness and Fatigue on the same day, and Form next day.

screen shot 2018-10-16 at 9 33 13 am

yupp that's also my point:

@thomaschampagne I don't think so. The TrainingPeaks model is

Form(day+1) = Fitness(day) - Fatigue(day)

Making a PR is something possible to you or i handle it myself?

Never worked with this programming language, but I could try if you think it's easy enough. I guess my main concern is the scope of affected code, and whether it requires any reorganization.

@aprokop The scope should be only that method https://github.com/thomaschampagne/elevate/blob/990b5d0fc11113b2c4d120e6aec9f0ba3dc0e844/plugin/app/src/app/fitness-trend/shared/services/fitness.service.ts#L226

But don't worry i'll do it. Just need your help to test and validate changes.

@aprokop Are you sure at 100% about this below?

Fitness(day+1) = Fitness(day) + (StressScore(day+1)-Fitness(day)) x ...
Fatigue(day+1) = Fatigue(day) + (StressScore(day+1)-Fatigue(day)) x ...

I fixed the code with your recommendations (via commit https://github.com/thomaschampagne/elevate/commit/ec73ee0c5a6d3c78662c41b94a4e090c70a1572b). To simplify the implementation in existing code, i implemented your formulas like this:

Fitness(day) = Fitness(day-1) + (StressScore(day)-Fitness(day-1)) x ...
Fatigue(day) = Fatigue(day-1) + (StressScore(day)-Fatigue(day-1)) x ...
Form(day) = Fitness(day-1) - Fatigue(day-1)

@aprokop @jayti74

  • Also can you test the below build and compare it with your TrainingPeaks to make sure everything is OK without regressions? (The build includes commit https://github.com/thomaschampagne/elevate/commit/ec73ee0c5a6d3c78662c41b94a4e090c70a1572b ofc)

Test Build: v6.6.0_stable_ec73ee0_2018-10-17-16-05.zip

Thanks for your help :)

@thomaschampagne

Are you sure at 100% about this below?

Yes and no. Yes, I'm sure that it's wrong in the sense that it does not match TrainingPeaks as the effort in the same day would not affect Fitness/Fatigue in the same day. It certainly is necessary to change StressScore(day) to StressScore(day+1). However, I'm not sure if that's the exact formula. One could think that formula like

Fitness(day+1) = Fitness(day) + (StressScore(day+1)-Fitness(day+1)) x ...

could also make sense (one would then need to solve for Fitness(day+1)). But I think it is good as it is.

The commit implements the suggested formulas correctly. You could move out updates of prevCtl and friends out of if (isPreStartDay) clauses, but that's minor.

I'll test it in the evening when I get home.

Just tried it out, and it behaves as expected. The form dropped next day, while fitness and fatigue went up the same day. So, I think it's all good now.
screenshot_20181017_202434

@aprokop Thanks for your tests and review! So we have 2 formulas:

Fitness(day+1) = Fitness(day) + (StressScore(day+1)-Fitness(day+1)) x ...
-- or --
Fitness(day+1) = Fitness(day) + (StressScore(day+1)-Fitness(day)) x ...

But i think we have a problem with the first one :) It's strange to calculate Fitness(day+1) if Fitness(day+1) is also part of the expression... This is what we want to find.

And if we solve expression, we have:

Fitness(day+1) = (Fitness(day) - k * StressScore(day+1)) /(1 - k)  where k = exp(-1/42)

... does that make sense?!

Is there a way to export csv data from TrainingPeaks day by day? Or get more info from your TrainingPeaks graph screenshot?

On your TrainingPeaks graph screenshot, red dots are day of effort(s)? Do you confirm Fitness and Fatigue curve goes up the day of your effort?

Thanks for your help :)

@thomaschampagne I agree with you that Fitness(day+1) = Fitness(day) + (StressScore(day+1)-Fitness(day+1)) x ... is a weird one. I only mentioned it because I am not 100% sure.

Is there a way to export csv data from TrainingPeaks day by day? Or get more info from your TrainingPeaks graph screenshot?

This is a really good idea. I believe it's possible, and I've done that in the past. Let me try to get the recent data.

On your TrainingPeaks graph screenshot, red dots are day of effort(s)? Do you confirm Fitness and Fatigue curve goes up the day of your effort?

Yes, red dots are the values of TSS for each day, and blue ones are the intensity.

Do you confirm Fitness and Fatigue curve goes up the day of your effort?

Yes.

OK, so one can export workouts but not the curve values. Fine. Lets test it.

Lets look at the recent hard race (marking as - non-important data). The data had to be taken from a graph, and it was rounded to the nearest integer by TrainingPeaks.

| | pre race day | race day | after race day |
|-|-|-|-|
| TSS | - | 395 | - |
| Fatigue | 54 | 102 | - |
| Fitness | 61 | 69 | - |
| Form | - | 8 | -33 |

So:

  1. Fatigue (day) - Fitness (day) = 69 - 102 = 33 = Form(day+1), so we have the correct formula for Form.
  2. Fatigue(day-1) + (TSS - Fatigue(day-1)) x ... = 54 + (395-54)x(1-e**(-1/7)) = 99, so approximately 102?
  3. (Fatigue(day-1) + TSS(1-k))/(2-k) = (54 + 395(1-k))/(2-k) = 94, so way off. (k = exp(-1/7)). I fixed the fomula calculations here, as the one in the previous comment is wrong.
  4. Fitness(day-1) + (TSS - Fitness(day-1) x ... = 61 + (395-61)x(1-e**(-1/42) = 69, so that's correct.
  5. (Fitness(day-1) + TSS(1-K))/(2-K) = (61 + 395(1-K))/(2-K) = 69, so it's also about right (here, K = exp(-1/42)).

In summary, these seem the best option:

Form(day) = Fitness(day-1) - Fatigue(day-1)
Fitness(day) = Fitness(day-1) + (StressScore(day)-Fitness(day-1)) x (1-exp(-1/42))
Fatigue(day) = Fatigue(day-1) + (StressScore(day)-Fatigue(day-1)) x (1-exp(-1/7))

P.S. I still don't quite get why in 2. we only get 99 and not 102.

Took a look at GoldenCheetah:

double lte = (double)exp(-1.0/ltsDays_);
double ste = (double)exp(-1.0/stsDays_);
 ...
// LTS
if (day) lastLTS = lts_[day-1];
lts_[day] = (stress_[day] * (1.0 - lte)) + (lastLTS * lte);

// STS
if (day) lastSTS = sts_[day-1];
sts_[day] = (stress_[day] * (1.0 - ste)) + (lastSTS * ste);

There is also this.

So, I think we are good.

@aprokop Thanks a lot for this deep analysis! It's clear, unambiguous!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ashdriver picture ashdriver  ·  35Comments

ndaman picture ndaman  ·  14Comments

puzanart picture puzanart  ·  5Comments

Djaouws picture Djaouws  ·  6Comments

cmagnuson picture cmagnuson  ·  19Comments