Analyzing Running Efficiency
Decoding running paths for efficiency
Code
# loading necessary libraries
::p_load("sf", "dplyr", "ggplot2", "spatstat.geom", "spatstat.explore",
pacman"gstat", "tidyr", "terra", "tmap","readr", "leaflet", "zoo", "gganimate", "gifski", "leaflet.extras2", "ggmap", "osmdata", "tmap", "geojsonio", "htmlwidgets", "cowplot")
1 Abstract
This thesis analyzes data of an ultra-marathon runner to assess endurance using metrics TRIMP (training impulse) and efficiency (speed/HR). By evaluating metrics on a repeated route, trends in efficiency and training load are identified. The results show a increase in efficiency over a season on the same route and reveal a structured training pattern suggesting possible improvement of running endurance.
2 Introduction
Among professional and hobby runners, collecting training data has become common practice for monitoring training effect (Emig and Peltonen 2020; Torres-Ronda et al. 2022). As regular training leads to a lower heart rate (Almeida and Araújo 2003), physiological marker heart rate (HR) in combination with movement data play a crucial role in assessing performance in running (Berglund et al. 2019; Brabandere et al. 2018). The HR during endurance activities can be divided into five HR-zones distinguished by their physiological impact on the human body and utility for training assessment (Lucía et al. 2000; Marx et al. 2018).
Important training characteristics for ultra-marathon runners are high running volume and the increase in average speed while maintaining a low HR (Knechtle and Nikolaidis 2015; Tanda and Knechtle 2015). The term “ultra-marathon” is used for races that surpass the traditional marathon running distance of 42.2 km (Roebuck et al. 2018). For assessing endurance in ultra-marathon training, HR-zones in combination with a weighted training impulse (TRIMP) and efficiency-scoring based on HR and speed can be used to evaluate a training effect Boudet et al. (2004).
2.1 Research Questions
In this thesis, training data of an ultra-marathon runner is compared and training impulse (TRIMP) as well as efficiency (speed/HR) per run is analyzed. Main questions include:
How does heart rate change over a run?
Is a training effect visible over time when running on the same route?
3 Methods
Generative AI systems were used for refinement of concepts (e.g., TRIMP, efficiency scoring) as well as visualizations. Specifically, ChatGPT (OpenAI 2025) was used. AI-generated content was reviewed, edited, and verified.
3.1 Data
We use GPS data collected by a marathon runner who recorded runs with the Strava1 app. In addition, we have further training data from a wearable. The Strava data is stored in .fit.gz
files which are derived to .gpx
as well as .fit
files. To extract the data and make it usable for further analysis, we used a workflow and function suggested by a fellow student of the module Paterns & Trends in Environmental Data. This workflow is based on the FITfileR
package (Smith 2025).
Each run or .fit
file must be processed in R with following functions:
Code
# example code that will not be executed
# Funciton to process a single fit file
<- function(file_path) {
process_fit_file <- tryCatch({
fit readFitFile(file_path)
error = function(e) {
}, warning(paste("Error reading file:", basename(file_path), ":", e$message))
return(NULL)
})
if (is.null(fit)) return(NULL)
# Extract geolocation and heart rate data
<- records(fit)
record_data
# Define crucial and optional variables
<- c("timestamp", "position_lat", "position_long")
crucial_vars <- c("heart_rate", "enhanced_altitude", "gps_accuracy", "altitude",
optional_vars "grade", "distance", "cadence", "speed", "enhanced_speed", "ascent", "descent")
# Check if data is a data frame or list
if (is.data.frame(record_data)) {
<- setdiff(crucial_vars, names(record_data))
missing_vars
if (length(missing_vars) == 0) {
<- record_data %>%
geo_data select(any_of(c(crucial_vars, optional_vars))) %>%
mutate(sequence_number = row_number())
else {
} warning(paste("Skipping file (missing crucial variables in record_data df):", paste(missing_vars, collapse = ", "), ":", basename(file_path)))
return(NULL)
}else if (is.list(record_data)) {
} <- tryCatch({
geo_data <- record_data %>%
flattened_data ::rbindlist(fill = TRUE) %>%
data.table::as_tibble()
tibble
<- setdiff(crucial_vars, names(flattened_data))
missing_vars
if (length(missing_vars) == 0) {
%>%
flattened_data select(any_of(c(crucial_vars, optional_vars))) %>%
mutate(sequence_number = row_number())
else {
} warning(paste("Skipping file (missing crucial variables in record_data list):", paste(missing_vars, collapse = ", "), ":", basename(file_path)))
return(NULL)
}error = function(e) {
}, warning(paste("Error processing records in:", basename(file_path)))
return(NULL)
})else {
} warning(paste("Skipping file (invalid record data):", basename(file_path)))
return(NULL)
}
# Extract session data
<- getMessagesByType(fit, message_type = "session") %>%
session as.data.frame() %>%
select(timestamp, sport) %>%
rename(session_id = timestamp)
if (nrow(session) == 0) {
warning(paste("Skipping file (no session data):", basename(file_path)))
return(NULL)
}
<- session[1, ]
session
# Merge session data with geo_data
<- geo_data %>%
geo_data mutate(session_id = session$session_id,
sport = session$sport,
file_name = basename(file_path))
return(geo_data)
}
#################################
#################################
# Function to process a folder with fit files
<- function(folder_path) {
process_fit_folder <- list.files(folder_path, pattern = "\\.fit$", full.names = TRUE)
fit_files
<- lapply(fit_files, function(file) {
results message(paste("Processing:", basename(file))) # Print progress
process_fit_file(file)
})
# Remove NULL results (skipped files)
<- Filter(Negate(is.null), results)
results
if (length(results) == 0) {
warning("No valid FIT files processed.")
return(NULL)
}
# Combine all data frames into one
<- dplyr::bind_rows(results)
combined_data
return(combined_data)
}
#################################
#################################
The output is one run per .fit
file, consisting of a trajectory of 1-sec timestamps. Following columns were extracted and used:
- timestamp: every 1 sec – “2024-04-29 16:44:38”
- position_lat: coordinates – “46.96658”
- position_long: coordinates – “7.446189”
- heart_rate: in bpm – “161”
- altitude: in m asl – “566.6”
- speed: in m/s – “3.60”
- sequence_number: number of sequence within this run – “176”
- session_id: the unique identifier of this perticular run – “2024-04-29 17:33:22”
- sport: what sport has been practiced – “running”
- file_name: from which .fit file this run derives – “12064329079.fit”
The original Strava global export contains 1’295 .fit.gz
(“runs”) files spread over several years, resulting in a tremendous amount of time stamps. We have therefore decided to use solely the runs of one year. We filtered runs conducted in the year 2024 to have the most up-to-date training information.
3.2 Preprocessing
After processing all year 2024 .fit
files, we arranged the time stamps per run and assigned each run a unique identifier (run_ID)
.
Code
# example code that will not be executed
# load .fit folder (2024)
<- process_fit_folder("../data/strava_export_18824964/activities/2024_fit_files")
fit_data
# arrange timestamps
<- fit_data %>%
fit_data arrange(timestamp)
# assign each run a unique ID
<- fit_data %>%
all_runs_ID mutate(run_id = as.integer(factor(file_name)))
We then converted the data set from the .fit
files into a spatial object containing one linestring for each run (runs_linestring.24.gpkg)
. To localize suitable routes for our analysis, we plotted all runs and carried out an exploratory data analysis. We identified clusters of runs and selected a route frequented with runs. We then identified an ‘optimal’ run within this route. With the geometry processing tool ‘Buffer’ we created a buffer around the example run and identified all runs on the same route. This way we identified 44 equal runs.
Code
#load linestring spatial object
<- st_read(dsn = "runs_linestring_24.gpkg",layer = "runs_linestring_24")
runs_all <- st_transform(runs_all, crs = 2056)
runs_all
<- runs_all |>
run_example filter(run_id == 140)
<- st_buffer(run_example, dist = 100)
buffer_100m
<- runs_all %>% #select all runs that belong to the route
route_north filter(st_within(.,buffer_100m, sparse = FALSE))
Figure 1: Comparison of the run example with selected runs in a 100 m buffer.
In order to obtain runs that were as comparable as possible, we refined the selection further. We supplemented the selected runs with the data from the .fit
files and carried out another exploratory data analysis with the heart rates. It was necessary for this task to standardize the time stamps into seconds since start of the run.
The exploratory analysis made it obvious that some runs contained outliers or particularly long runs that must be removed.
Code
<- read_delim("route_north_runs_24.csv")
route_north_runs_24
# calculate seconds since start
<- route_north_runs_24 |>
data mutate(timestamp = as.POSIXct(timestamp)) |>
group_by(run_id) |>
mutate(seconds_since_start = as.numeric(difftime(timestamp, min(timestamp), units = "secs"))) |>
ungroup()
<- data |>
data_1 filter(seconds_since_start >= 180) |> # remove first warmup minutes
filter(run_id !=65) |> # remove outlier
group_by(run_id) |>
filter(max(seconds_since_start, na.rm = TRUE) <= 3500) |> # remove verry long runs
ungroup()
length(unique(data$run_id))
length(unique(data_1$run_id))
When looking at the runs in Figure 1, it becomes apparent that data points are consistently missing at the same location. At this point, the route passes through a tunnel, GPS was unable to track the position. Therefore, we performed an interpolation for the missing points.
Code
# Interpolate all runs at 1-second resolution
<- data_1 |>
runs_interp group_by(run_id) |>
arrange(timestamp, .by_group = TRUE) |>
complete(timestamp = seq(min(timestamp), max(timestamp), by = "1 sec")) |>
mutate(t = as.numeric(timestamp)) |>
mutate(
position_lat = na.approx(position_lat, x = t, na.rm = FALSE, rule = 2),
position_long = na.approx(position_long, x = t, na.rm = FALSE, rule = 2),
heart_rate = na.approx(heart_rate, x = t, na.rm = FALSE, rule = 2),
altitude = na.approx(altitude, x = t, na.rm = FALSE, rule = 2),
distance = na.approx(distance, x = t, na.rm = FALSE, rule = 2),
cadence = na.approx(cadence, x = t, na.rm = FALSE, rule = 2),
speed = na.approx(speed, x = t, na.rm = FALSE, rule = 2)
|>
) select(-t) |>
ungroup()
The data is now cleaned and .fit
for the next steps.
3.3 HR-Zones
The HR-zones and HRmax were calculated for our ultra-marathon subject and was based on (Marx et al. 2018) and (Fornasiero et al. 2018). Because of highest measured HR of 192 in our data, we took this as the HRmax threshold.
HR-Zone | Heartrate | % of HRmax | ||
Zone 1 – Moderate | < 113 | < 59% | ||
Zone 2 – Aerobic Base | 113–131 | 59–68% | ||
Zone 3 – Aerobic Threshold | 132–150 | 69–78% | ||
Zone 4 – Anaerobic | 151–170 | 79–89% | ||
Zone 5 – Red Line | 171–192 | 89–100% | ||
Code
# Heartrate-Zones
<- function(hr) {
hr_zones case_when(
< 113 ~ "Zone 1 - Moderate",
hr < 132 ~ "Zone 2 - Aerobic Base",
hr < 151 ~ "Zone 3 - Aerobic Threshold",
hr < 171 ~ "Zone 4 - Anaerobic",
hr <= 192 ~ "Zone 5 - Red Line",
hr TRUE ~ "Above MAX"
)
}# Redline = max effort HR zone (90–100% HRmax)
3.4 TRIMP
The TRIMP (training impulse) was calculated from the time spent in weighted HR-zones per run (Impellizzeri, Rampinini, and Marcora 2005; Lucía et al. 2000). For ultra-marathon runners, it is advised to focus on HR-zones 1-3 during training (Fornasiero et al. 2018; Lucía et al. 2000).
In zone weighted TRIMP (training impulse), each HR-zone has a weight multiplier assigned (zone-weights)
. The weighting has been adapted a for ultra runners, favoring HR-Zone 1 - HR-Zone 3. This reflects the ultra runner’s focus on low-intensity aerobic runs. Other zones were recognized as well.
Zone weighting (based on (Fornasiero et al. 2018; Lucía et al. 2000)
Code
#weighting the defined zones. weights according to literature.
<- c("Zone 1 - Moderate"=0.5, "Zone 2 - Aerobic Base"=1.5, "Zone 3 - Aerobic Threshold"=1.25, "Zone 4 - Anaerobic"=1.75, "Zone 5 - Red Line"=2) zone_weights
3.5 Efficiency-Score
For monitoring changes in endurance, an efficiency-score per run was calculated using average running speed (m/s) divided by average heart rate (bpm) of the runner (Efficiency = mean_speed/mean_HR)
according to (Vesterinen et al. 2014).
4 Results
4.1 HR-Zones
Time spent in HR-zones per run
Code
# assign different HR-Zones to run
# Note: time_diff gives you seconds spent until the next point.
<- runs_interp |>
HR_zones_runs arrange(run_id, timestamp) |>
group_by(run_id) |>
mutate(
hr_zone = hr_zones(heart_rate),
time_diff = as.numeric(difftime(lead(timestamp), timestamp, units = "secs"))
|>
) ungroup()
#include mean speed per run_id
<- HR_zones_runs |>
mean_speed_per_id group_by(run_id) |>
summarise(mean_speed = mean(speed, na.rm = TRUE))
#summary by runs and HR-zones
<- HR_zones_runs |>
HR_summary2 filter(!is.na(time_diff), !is.na(hr_zone)) |>
group_by(run_id, hr_zone) |>
summarise(
total_time_sec = sum(time_diff, na.rm = TRUE),
.groups = "drop"
|>
) mutate(
total_time_min = total_time_sec / 60) |>
left_join(mean_speed_per_id, by = "run_id")
<- colorFactor(
zone_colors palette = c("blue", "green", "orange", "red", "purple"),
levels = c("Zone 1 - Moderate", "Zone 2 - Aerobic Base", "Zone 3 - Aerobic Threshold", "Zone 4 - Anaerobic", "Zone 5 - Red Line")
)
Figure 4 shows the time the runner spent in each HR-Zone per run. It is apparent that most time was spent in Zone 3 and 4 while Zone 2 and 5 are represented only in small quantities and Zone 1 missing completely.
HR-Zones Compared
Code
# run 115 for visualisation
<- HR_zones_runs |>
one_run_interp filter(run_id == "115") |> # Replace with desired run_id
arrange(timestamp)
<- HR_zones_runs |>
one_run_interp_2 filter(run_id == "42") |> # Replace with desired run_id
arrange(timestamp)
<- HR_zones_runs |>
one_run_interp_3filter(run_id == "92") |> # Replace with desired run_id
arrange(timestamp)
Code
#plotting
leaflet() |>
addTiles() |>
# Run 1
addCircleMarkers(
data = one_run_interp,
lng = ~position_long,
lat = ~position_lat,
color = ~zone_colors(hr_zone),
radius = 3,
stroke = FALSE,
fillOpacity = 0.8,
group = "Run 115"
|>
)
# Run 2
addCircleMarkers(
data = one_run_interp_2,
lng = ~position_long,
lat = ~position_lat,
color = ~zone_colors(hr_zone),
radius = 3,
stroke = FALSE,
fillOpacity = 0.8,
group = "Run 42"
|>
)
# Run 3
addCircleMarkers(
data = one_run_interp_3,
lng = ~position_long,
lat = ~position_lat,
color = ~zone_colors(hr_zone),
radius = 3,
stroke = FALSE,
fillOpacity = 0.8,
group = "Run 92"
|>
)
# Legend
addLegend(
position = "bottomright",
pal = zone_colors,
values = one_run_interp$hr_zone,
title = "HR Zone"
|>
)
# Layer-Control for Checkboxes
addLayersControl(
overlayGroups = c("Run 115", "Run 42", "Run 92"),
options = layersControlOptions(collapsed = FALSE)
)
Figure 5: Heart Rate profile of Runs no. 42, 92, 115
In Figure 5, runs with varying distribution of heart rate zones are interactively visualized. In run 92 the runners was mainly in the Red Line Zone, showing high effort and high HR. In run number 42
, the runner shows distribution over 3 Zones with main duration spent in Zone 4 - Anaerobic. Run 115 is the most evenly distributed, with time spent mostly in Zone 3 - Aerobic Threshold.
Animated Run
Code
<- zone_colors(one_run_interp$hr_zone)
zone_color_vals
<- paste0(
latlng_js_array "[",
$position_lat, ",",
one_run_interp$position_long, ",",
one_run_interp$heart_rate, ",",
one_run_interp"'", one_run_interp$hr_zone, "'", ",",
"'", zone_color_vals, "'",
"]",
collapse = ",\n"
)
<- leaflet(one_run_interp) %>%
m addTiles() %>%
addPolylines(lng = ~position_long, lat = ~position_lat, color = "darkred") %>%
addCircleMarkers(
lng = one_run_interp$position_long[1],
lat = one_run_interp$position_lat[1],
layerId = "runner",
radius = 6,
color = "red"
)
onRender(m, sprintf("
function(el, x) {
var map = this;
// prepare data
var latlngs = [%s].map(function(coord) {
return {
lat: coord[0],
lng: coord[1],
hr: coord[2],
zone: coord[3],
color: coord[4]
};
});
// Insert Control
var container = document.createElement('div');
container.style.position = 'absolute';
container.style.top = '10px';
container.style.left = '60px';
container.style.zIndex = 999;
container.style.backgroundColor = 'rgba(255,255,255,0.8)';
container.style.padding = '6px';
container.style.borderRadius = '4px';
// Slider
var slider = document.createElement('input');
slider.type = 'range';
slider.min = 1;
slider.max = 50;
slider.step = 1;
slider.value = 25;
// Start/Stop Buttons
var startBtn = document.createElement('button');
startBtn.innerText = '▶ Start';
startBtn.style.marginRight = '4px';
var stopBtn = document.createElement('button');
stopBtn.innerText = '⏸ Stop';
container.appendChild(slider);
container.appendChild(document.createElement('br'));
container.appendChild(startBtn);
container.appendChild(stopBtn);
el.appendChild(container);
// prepare animation
var marker = map.layerManager.getLayer('marker', 'runner');
var i = 0;
var playing = false;
var timeout;
function move() {
if (!playing || i >= latlngs.length) return;
var point = latlngs[i];
marker.setLatLng([point.lat, point.lng]);
marker.setStyle({color: point.color}); // <-- Farbanpassung
marker.bindPopup('Heart Rate: ' + point.hr + ' bpm<br>Zone: ' + point.zone).openPopup();
i++;
timeout = setTimeout(move, parseInt(slider.value));
}
startBtn.onclick = function() {
if (!playing) {
if (i >= latlngs.length) i = 0;
playing = true;
move();
}
};
stopBtn.onclick = function() {
playing = false;
clearTimeout(timeout);
};
}
", latlng_js_array))
Figure 6: Animated Heart Rate profile of Run no. 115. (Note: embedded Java Script created with ChatGPT)
To visualize how the heart rate changes over the course of a run, an interactive map with adjustable speed was created. The color of the dot represents the HR-Zone, the pop-up shows heart rate in bpm and the HR-zone.
4.2 TRIMP
Code
<- HR_summary2 |>
trimp_df mutate(TRIMP = total_time_min * zone_weights[hr_zone]) |>
group_by(run_id) |>
summarise(TRIMP = sum(TRIMP))
TRIMP Bar-Plot with trend line:
Figure 7 shows a bar plot of the TRIMP. Runs 92, 42 and 96 show highest TRIMP score. Looking at all runs, the trend line displays a decreasing TRIMP towards the end of training season. Also, for the majority of consecutive runs a soft saw blade pattern can be detected. Stagnation of training impulse is shown between Runs 102 - 110, and 115 - 125.
4.3 Efficiency-Score
Code
<- runs_interp |>
eff_df group_by(run_id) |>
summarise(
duration_min = as.numeric(difftime(max(timestamp), min(timestamp), units="mins")),
total_km = max(distance, na.rm=TRUE)/1000,
avg_speed_ms = mean(speed, na.rm=TRUE),
avg_hr = mean(heart_rate, na.rm=TRUE),
pace_min_per_km= (1/(avg_speed_ms*60))*1000,
efficiency = avg_speed_ms/avg_hr
)
Efficiency score Bar-Plot with trend line:
In Figure 8, Run 99, 143 and 119 show highest value. Also over the year, the trend line shows a slight gain in running efficiency.
Both metrics in one plot
Code
<- eff_df |>
comparison_df left_join(trimp_df, by="run_id") |>
left_join(HR_summary2 |>
group_by(run_id) |>
summarise(across(starts_with("total_time"), sum)),
by="run_id")
All runs: BAR-Chart for TRIMP, Efficiency
Code
#plotting
#created with help from ChatGPT
# transforming the data into a long format
<- comparison_df |>
comparison_long gather(key = "Metric", value = "Value", TRIMP, efficiency)
# 2 small barcharts of eff and trimp
ggplot(comparison_long, aes(x = factor(run_id), y = Value, fill = Metric)) +
geom_bar(stat = "identity", position = "dodge") +
geom_smooth(aes(group = 1), method = "lm", color = "darkred", se = FALSE) + # Add trendline
facet_wrap(~ Metric, scales = "free_y") +
labs(title = "Comparison of TRIMP, Efficiency per Run",
x = "Run ID",
y = "Value") +
theme_minimal() +
theme(
axis.text.x = element_text(angle = 90, hjust = 1),
strip.text = element_text(size = 12)
)
Together, both can quantify and visualize the training effect. Here, the clear upwards trend of efficiency and downwards trend of TRIMP over the season is illustrated, as well as the saw-blade pattern of both.
Dual-axis plot: TRIMP and Efficiency.
Code
# 1. Merge TRIMP, efficiency and extract run date
<- trimp_df %>%
runs_summary left_join(eff_df, by = "run_id") %>%
left_join(
%>%
runs_interp group_by(run_id) %>%
summarise(run_date = as.Date(min(timestamp))),
by = "run_id"
)
# 2. Compute scaling factor so both series share a common plot range
<- max(runs_summary$TRIMP, na.rm = TRUE) /
scale_factor max(runs_summary$efficiency, na.rm = TRUE)
Code
# plotting
# created with help from ChatGPT
ggplot(runs_summary, aes(x = run_date)) +
# TRIMP line + points
geom_line(aes(y = TRIMP, color = "TRIMP"), size = 1) +
geom_point(aes(y = TRIMP, color = "TRIMP"), size = 2) +
# Efficiency (scaled) dashed line + points
geom_line(aes(y = efficiency * scale_factor, color = "Efficiency"),
linetype = "dashed", size = 1) +
geom_point(aes(y = efficiency * scale_factor, color = "Efficiency"), size = 2) +
# Dual y-axis
scale_y_continuous(
name = "TRIMP",
sec.axis = sec_axis(~ . / scale_factor,
name = "Efficiency (m/s per bpm)")
+
)
# Color legend mapping
scale_color_manual(
name = "Metric",
values = c("TRIMP" = "steelblue", "Efficiency" = "red")
+
)
labs(
title = "TRIMP (blue) and Efficiency (red) Over Time",
x = "Date"
+
) theme_minimal() +
theme(
axis.text.x = element_text(angle = 45, hjust = 1),
legend.position = "right"
)
This dual-axis plot shows efficiency score vs. TRIMP on a unified scale over the course of year 2024. While both have a saw blade-like pattern, TRIMP differs greatly in height between points as Efficiency displays more of a “upwards” climbing pattern.
5 Discussion
5.1 HR-Zones
Over all, Figure 4 shows a relatively sound training for long distance endurance as most time spent running falls into Zone 3 - Aerobic Threshold (Fornasiero et al. 2018). However, it could be argued that for longtime endurance improvement, Zone 1 and Zone 2 could be activated more frequently, combined with focus on bursts into Zone 5 and Zone 6 (Manzi et al. 2015).
5.2 TRIMP
A varying TRIMP score as seen in Figure 7 indicates a good training distribution for long-distance endurance sports. A constant high TRIMP load is counterproductive as there arises risk of overtraining, fatigue and injury (Manzi et al. 2015; Craddock, Buchholtz, and Burgess 2020).
A slowly decreasing trend line in Figure 7 shows a possible intentional reduction of training impact for reaching lower HR-zones that favor aerobic runs (Zone 1-3), important for long-distance endurance (Fornasiero et al. 2018; Lucía et al. 2000). Another interpretation is that the stagnation between run 99 and 110 as well as 115 and 126 has great influence on the downward course of the trend line.
In general the TRIMP scores display a good alternating structure of consecutive training sessions (saw-blade) which shows an adequate overall training load for ultra-runners. As single, high-loaded runs push O2 threshold, moderate runs in between allow for recovery, minimizing strain and injury risk for athletes (Manzi et al. 2015; Craddock, Buchholtz, and Burgess 2020; Talsnes, Sandbakk, and Tillaar 201AD).
5.3 Efficiency
A higher efficiency-score indicates more speed per heartbeat which is a mark for increased endurance (Vesterinen et al. 2014). In Figure 8, the Efficiency-score of the runner shows an upwards trend, suggesting a improvement in endurance over the recorded year. This could either mean that the speed of our runner stayed constant while the heart rate decreased or speed increased while heart rate was constant, which indicates a objective in endurance training (Boudet et al. 2004).
5.4 Is a training effect visible over time when running on the same route?
It has to be noted that gains in efficiency-score could also be misinterpreted. A run with relatively short duration, cooler weather or a longer recovery time before would also mark up as efficient even though it could simply be less cardiovascular strain through named reasons.
The goal for runners is to stabilize their HR to optimize performance (Boudet et al. 2004).
When considered in context with Figure 4, the efficiency upwards trend as well as the decrease in training load (TRIMP) over the season in Figure 9, shows a gain in endurance and focus on low-intensity runs. As the aerobic base improves (efficiency), less work is needed from the athlete to produce the same power (Buchheit 2014).
In dual-axis plot Figure 10, the conversion of of training impulse into endurance can be read off the rising red efficiency trend after the high TRIMP score. This is supported by the alternating TRIMP (saw tooth pattern) and an efficiency line gradually wandering upwards without striking out. This also implies that the runner has found his sound form of training and continued to train more effectively after the peak in TRIMP.
These findings show the possibility of analyzing performance for long- distance running with TRIMP and efficiency-scores (mean speed/mean HR). However, Boudet et al. (2004) state that heart rate reacts differently in training vs. competitive running and results may not be directly transferable to performance in races.
Over all, a good long-distance endurance training consist of a high number of weekly running hours in low intensity zones with interspersed high-intensity runs (Fornasiero et al. 2018; Manzi et al. 2015).]
6 References
Footnotes
https://www.strava.com↩︎