Reverse engineering the Strava Suffer Score algorithm

3 March 2017

Few months ago I purchased a Suunto Ambit3 Peak after four years, and thousands of kilometres, with my faithful Polar RC3. The fact anyone could write small apps for this device is one of the main reasons that made me opt for this model instead of its competitors.

Around the same time, Strava introduced the Suffer Score to quantify the training effort subjectively based on the heart rate and provides a score that would allow athletes from different levels and specialities to compare how hard they have performed. This feature is only available for premium users, which I’m not. Luckily for me, an acquaintance of mine has recently upgraded to premium and he was happy to share his training data with me. From here the decision to reverse engineer the Suffer Score algorithm and then build a Movescount app out of it so I can find out how hard I’m training (according to Strava) despite not being a premium member.

Strava uses five customisable heart rate zones (only available for Premium users) that well adapts to the three most-known systems: Allen & Coggan’s five zones described in Training and Racing with a Power Meter, and the simplified versions of Friel’s Training Bible seven zones and Fitzgerald’s 80/20 Endurance 5+2 zones.

Finding out the formula

After reading their official blogpost describing the new feature, it is clear that the Suffer Score is the sum of the time spent in each zone multiplied by a corresponding zone constant:

\[score=\sum_{n=1}^{5} t_{n} \cdot z_{n}\]

What I was missing were the values of \(z_{n}\), which I was able to infer by interpolating the aforementioned training data. Luckily for me the dataset was well structured and diversified, ranging from easy trainings fully spent in Z1 and Z2 to interval sessions that included all the five zones. Thanks to those easy runs I was able to extrapolate the value of the first two zones constants (\(z_{1}\) and \(z_{2}\)) by using the binomial equation \(score=t_{1} \cdot z_{1}+t_{2} \cdot z_{2}\) with \(score\), \(t_{1}\) and \(t_{2}\) being known values. From there I worked my way up until I got all the five constants.

Unfortunately I’m not allowed to share the data I used since it belongs to an athlete who prefers keeping his trainings private, but these are the end results:

Movescount integration

Since only two fixed heart parameters were available, rest (SUUNTO_USER_REST_HR) and max (SUUNTO_USER_MAX_HR), I wasn’t able to use the lactate threshold heart rate or other advanced metrics to calculate the five zones. I instead used a more simplistic approach using the max heart rate as a reference point:

The app runs once per second, providing the current heart rate (SUUNTO_HR) and showing on screen the result value (RESULT). From here, building a Movescount app was quite straightforward despite the limited syntax1.

RESULT = SCORE;
if(SUUNTO_HR >= SUUNTO_USER_REST_HR && SUUNTO_HR <= 60*SUUNTO_USER_MAX_HR/100) {
  SCORE = SCORE + 25/3600;
} else if(SUUNTO_HR > 60*SUUNTO_USER_MAX_HR/100 && SUUNTO_HR <= 70*SUUNTO_USER_MAX_HR/100) {
  SCORE = SCORE + 60/3600;
} else if(SUUNTO_HR > 70*SUUNTO_USER_MAX_HR/100 && SUUNTO_HR <= 80*SUUNTO_USER_MAX_HR/100) {
  SCORE = SCORE + 115/3600;
} else if(SUUNTO_HR > 80*SUUNTO_USER_MAX_HR/100 && SUUNTO_HR <= 90*SUUNTO_USER_MAX_HR/100) {
  SCORE = SCORE + 250/3600;
} else {
  SCORE = SCORE + 300/3600;
}

You can find the app in the Movescount Apps section under the name Strava Suffer Score, or by clicking here2.

  1. A switch statement would have been better, unfortunately the available syntax is really limited. 

  2. Movescount doesn’t let you edit the content of your app once it has been downloaded by one or more users, so the code currently available there doesn’t look as clean as the one above.