The Periodization Algorithm

Founded in the 1960’s periodization training is widely used by athletes of all skill levels to this day and it’s concepts can be found in most sport science textbooks. Since periodization training has a set of scientifically tested principles, we can break them down into an algorithm that computes daily training data leading to an optimal performance on race day. Many coaches (sometimes unknowingly) already use a form of the “periodization algorithm” to program workouts for their athletes in a seasonal calendar. Our goal is to demystify the process of planning a race season by sharing this algorithm publicly through open source code.

A Brief Overview

Before we take a look under the hood at the different components of this algorithm let’s review the definition of periodization training.

In Matveyev’s model the fundamental concept is that training should gradually progress from high-volume/low-intensity to low-volume/high-intensity to prepare an athlete for peak performance. Tools such as Hans Selye’s General Adaptation Syndrome (GAS), Specific Adaptations to Imposed Demands (SAID) Principle, and Progressive Overloading (PO) are great examples of mechanisms that support the periodization model. ~NASM

Additional resources on the science of periodization can be found below in the appendix of this notebook.

So what goes into the algorithm?

  • Input race data. Before the algorithm can compute daily workouts it needs input values for all races planned this season. Races should be broken into priority_race, secondary_races, and tertiary_races. These races should be mapped to a season_calendar for use later on in the algorithm.
  • Estimated difficulty and safety. The human body needs time to adapt to the stress of training in order to develop performance and avoid injury. The number of days between training_start_date and priority_race will determine if the algorithm has enough time to allocate to each phase of periodization.
  • Defined periodization phases. The phases: Prep, Base, Build, Peak, and Race will each have the following properties:
  • start_date
  • end_date
  • total_days
  • weekly_workouts

These phases will map to the season_calendar with a start_date and end_date value that depends on the priority_race date.

  • Compute daily workouts. The weekly_workouts property for each phase is a starting point to ensure overall volume is met. However there will need to be some refinement based on the overall_training_duration we estimated earlier.
  • Output results. At this point the algorithm can provide the user with customized training workouts based on their race season. In a future version the algorithm will accept input data during training to update workouts in real time.

That’s nice now show me the algorithm!

The Algorithm

The first step is to collect input data about the user’s priority race. This can be a simple html form that feeds values into the algorithm.

For this example we need to import several javascript utility functions which will make it easier to work with dates. Calendar dates are integral to this algorithm as you’ll see later on, but working with data related to date & time can be tricky. If using this algorithm in production make sure to thoroughly review the output dates.

import "date-fns"

const dateFormatter = (date, days) => {
    return dateFns.format(dateFns.sub(new Date(date), {days}), "MM/dd/yyyy")
}

Now we need to declare some default values. The durations of each phase are set to a max number of days that should be spent in each, but these values will later be modified depending on the form’s training_start and race_date inputs.

const training_duration_total = dateFns.differenceInDays(
                                new Date(form_values.race_date),
                                new Date(form_values.training_start),
                             );
const phase_durations = ({
          prep_phase: 28,
          base_phase: 84,
          build_phase: 56,
          peak_phase: 14,
          race_phase: 7,
      });

const priority_race = ({
  "type": form_values.sport,
  "date": new Date(form_values.race_date),
  "distance_in_miles": form_values.distance
});

Next we should determine a “safety score” also known as a “difficulty score”. As mentioned earlier the human body needs time to adapt to the stress of training in order to develop performance and avoid injury. The number of days between training_start and race_date will determine if the algorithm has enough time to allocate to each phase of periodization. If there isn’t enough time to train properly the risk of injury goes up, hence the term “safety score”.

functioncalculateDaysInPhases() {
    let calculated_durations = phase_durations;
    let phases_duration_total = Object.values(calculated_durations).reduce((a, b) => a + b, 0);
    const phases = [
        { name: 'prep_phase', min_value: 7 },
        { name: 'base_phase', min_value: 63 },
        { name: 'build_phase', min_value: 42 },
        { name: 'peak_phase', min_value: 7 }
    ];

    while (phases_duration_total > training_duration_total) {
        for (let phase of phases) {
            if (calculated_durations[phase.name] > phase.min_value) {
                calculated_durations = {
                    ...calculated_durations,
                    [phase.name]: calculated_durations[phase.name] - 1,
                };
                phases_duration_total--;
                break;
            }
        }
    }
    return calculated_durations;
}

With the dates of each periodization phase defined we can begin mapping workouts to each day in the training season.

Above each function are sample workouts for the first week in that phase. Each function maps over these workouts and increments the values for the total duration of the phase. The default workouts provided are malleable, if you know what you’re doing and want to modify them you can customize the algorithm to your liking. For everyone else these workouts can be followed out of the box. They use a fairly standard format that includes:

  • several short to medium distance runs
  • speed work/track workouts
  • weekly long run
  • recovery day
const prep_workouts = ({
  "period_1": [
    {
      "date": "2023-12-28T19:47:19.253Z",
      "activity": "easy run",
      "distance": 2,
      "heart_rate": 0,
      "duration": "",
      "details": "",
      "periodization_phase": "Base"
    },
    {
      "date": "2023-12-28T19:47:19.253Z",
      "activity": "easy run",
      "distance": 3,
      "heart_rate": 0,
      "duration": "",
      "details": "",
      "periodization_phase": "Base"
    },
    {
      "date": "2023-12-28T19:47:19.253Z",
      "activity": "sprints",
      "distance": 0.7,
      "heart_rate": 0,
      "duration": "",
      "details": "2 x 800 meter repeats",
      "periodization_phase": "Base"
    },
    {
      "date": "2023-12-28T19:47:19.253Z",
      "activity": "easy run",
      "distance": 3,
      "heart_rate": 0,
      "duration": "",
      "details": "",
      "periodization_phase": "Base"
    },
    {
      "date": "2023-12-28T19:47:19.253Z",
      "activity": "easy run",
      "distance": 2,
      "heart_rate": 0,
      "duration": "",
      "details": "run negative splits",
      "periodization_phase": "Base"
    },
    {
      "date": "2023-12-28T19:47:19.253Z",
      "activity": "long run",
      "distance": 4,
      "heart_rate": 0,
      "duration": "",
      "details": "Long slow run",
      "periodization_phase": "Base"
    },
    {
      "date": "2023-12-28T19:47:19.253Z",
      "activity": "Rest",
      "distance": 0,
      "heart_rate": 0,
      "duration": "",
      "details": "Rest and recover",
      "periodization_phase": "Base"
    }
  ]
})
functionmapWorkoutsInPrepPhase(duration) {
  let workouts = {};
  let increment = 0;

  for (let i = 0; i <= duration; i++) {
    let resetDistance = 6 + increment * 2;
    if (resetDistance > 20) {
      resetDistance = 6;
      increment = 0;
    } else {
      increment += 1;
    }
    workouts[period_{i + 1}] = prep_workouts.period_1.map((workout, idx) => {
      if (workout.activity === "long run") {
        let incrementedDistance = workout.distance + i;
        let adjustedDistance =
          incrementedDistance > 20 ? resetDistance : incrementedDistance;
        return {
          ...workout,
          distance: adjustedDistance,
        };
      } else {
        return {
          ...workout,
        };
      }
    });
  }

  return { workouts };
}

To keep this notebook digestible the remaining phases have been left out but the same logic above applies to every phase (prep, base, build, peak, race) the only difference being what workouts are fed into each mapping function.

Future Possibilities

Initially the algorithm was programmed for running but it can be expanded into other endurance sports. Theoretically this form of training could even be used on hybrid events like Hyrox or any sport that has seasons and race/game days with a 1+ hour duration. The only major change needed to make this work for alternative sports would be the daily workouts.

With some additional effort this algorithm has many different applications including:

  • browser plugin to populate your calendar with daily workouts.
  • mobile coaching app that synchronizes with your health data to modify the periodization workouts in real time.
  • a simple webpage (like this one) to get instant feedback while planning your season.

There’s also plenty of room for improvement in making the code behind this algorithm more efficient. Next step would be to explore what this could look like with real time data and/or integration of a large language model (LLM) for fine tuning the workouts. This notebook is dynamic in nature and can be updated at anytime.

For more information or to contribute, please email trenton@remainstheday.org

Appendix