| Title: | Tools for Analyzing Lactate Thresholds |
| Version: | 1.0.0 |
| Description: | Set of tools for analyzing lactate thresholds from a step incremental test to exhaustion. Estimate Log-log, Onset of Blood Lactate Accumulation (OBLA), Baseline plus (Bsln+), the Dmax family, Lactate Turn Points (LTP), the Lactate / Intensity ratio (LTratio), and the Individual Anaerobic Thresholds of Dickhuth (IAT) and Stegmann in cycling, running, swimming, or rowing, with bootstrap confidence intervals, data-quality checks, training zones, test comparisons, training load, and the maximal lactate accumulation rate (VLamax). Beaver WL, Wasserman K, Whipp BJ (1985) <doi:10.1152/jappl.1985.59.6.1936>. Heck H, Mader A, Hess G, Mücke S, Müller R, Hollmann W (1985) <doi:10.1055/s-2008-1025824>. Kindermann W, Simon G, Keul J (1979) <doi:10.1007/BF00421101>. Skinner JS, Mclellan TH (1980) <doi:10.1080/02701367.1980.10609285>. Berg A, Jakob E, Lehmann M, Dickhuth HH, Huber G, Keul J (1990) PMID 2408033. Zoladz JA, Rademaker AC, Sargeant AJ (1995) <doi:10.1113/jphysiol.1995.sp020959>. Cheng B, Kuipers H, Snyder A, Keizer H, Jeukendrup A, Hesselink M (1992) <doi:10.1055/s-2007-1021309>. Bishop D, Jenkins DG, Mackinnon LT (1998) <doi:10.1097/00005768-199808000-00014>. Hughson RL, Weisiger KH, Swanson GD (1987) <doi:10.1152/jappl.1987.62.5.1975>. Jamnick NA, Botella J, Pyne DB, Bishop DJ (2018) <doi:10.1371/journal.pone.0199794>. Hofmann P, Tschakert G (2017) <doi:10.3389/fphys.2017.00337>. Hofmann P, Pokan R, von Duvillard SP, Seibert FJ, Zweiker R, Schmid P (1997) <doi:10.1097/00005768-199706000-00005>. Pokan R, Hofmann P, Von Duvillard SP, et al. (1997) <doi:10.1097/00005768-199708000-00009>. Dickhuth H-H, Yin L, Niess A, et al. (1999) <doi:10.1055/s-2007-971105>. Stegmann H, Kindermann W, Schnabel A (1981) <doi:10.1055/s-2008-1034604>. Heuberger JAAC, Gal P, Stuurman FE, et al. (2018) <doi:10.1371/journal.pone.0206846>. Meixner B, Nusser V, Koehler K, et al. (2024) <doi:10.14814/phy2.16086>. |
| License: | MIT + file LICENSE |
| Copyright: | see file COPYRIGHTS |
| URL: | https://fmmattioni.github.io/lactater/, https://github.com/fmmattioni/lactater |
| BugReports: | https://github.com/fmmattioni/lactater/issues |
| Encoding: | UTF-8 |
| LazyData: | true |
| VignetteBuilder: | knitr |
| Suggests: | covr, dplyr, grDevices, knitr, lintr, pagedown, readxl, rmarkdown, gt, htmltools, spelling, systemfonts, testthat (≥ 3.0.0) |
| Imports: | ggplot2 (≥ 4.0.0), ggtext (≥ 0.2.0), grid, patchwork, pillar, rlang, splines, stats, tibble, tools, utils, vctrs |
| Depends: | R (≥ 4.1.0) |
| Language: | en-US |
| Config/testthat/edition: | 3 |
| Config/Needs/website: | purrr, tidyr |
| Config/roxygen2/version: | 8.1.1 |
| NeedsCompilation: | no |
| Packaged: | 2026-11-12 21:07:02 UTC; felipe |
| Author: | Felipe Mattioni Maturana
|
| Maintainer: | Felipe Mattioni Maturana <felipe.mattioni@med.uni-tuebingen.de> |
| Repository: | CRAN |
| Date/Publication: | 2026-11-13 09:10:24 UTC |
lactater: Tools for Analyzing Lactate Thresholds
Description
Set of tools for analyzing lactate thresholds from a step incremental test to exhaustion. Estimate Log-log, Onset of Blood Lactate Accumulation (OBLA), Baseline plus (Bsln+), the Dmax family, Lactate Turn Points (LTP), the Lactate / Intensity ratio (LTratio), and the Individual Anaerobic Thresholds of Dickhuth (IAT) and Stegmann in cycling, running, swimming, or rowing, with bootstrap confidence intervals, data-quality checks, training zones, test comparisons, training load, and the maximal lactate accumulation rate (VLamax). Beaver WL, Wasserman K, Whipp BJ (1985) doi:10.1152/jappl.1985.59.6.1936. Heck H, Mader A, Hess G, Mücke S, Müller R, Hollmann W (1985) doi:10.1055/s-2008-1025824. Kindermann W, Simon G, Keul J (1979) doi:10.1007/BF00421101. Skinner JS, Mclellan TH (1980) doi:10.1080/02701367.1980.10609285. Berg A, Jakob E, Lehmann M, Dickhuth HH, Huber G, Keul J (1990) PMID 2408033. Zoladz JA, Rademaker AC, Sargeant AJ (1995) doi:10.1113/jphysiol.1995.sp020959. Cheng B, Kuipers H, Snyder A, Keizer H, Jeukendrup A, Hesselink M (1992) doi:10.1055/s-2007-1021309. Bishop D, Jenkins DG, Mackinnon LT (1998) doi:10.1097/00005768-199808000-00014. Hughson RL, Weisiger KH, Swanson GD (1987) doi:10.1152/jappl.1987.62.5.1975. Jamnick NA, Botella J, Pyne DB, Bishop DJ (2018) doi:10.1371/journal.pone.0199794. Hofmann P, Tschakert G (2017) doi:10.3389/fphys.2017.00337. Hofmann P, Pokan R, von Duvillard SP, Seibert FJ, Zweiker R, Schmid P (1997) doi:10.1097/00005768-199706000-00005. Pokan R, Hofmann P, Von Duvillard SP, et al. (1997) doi:10.1097/00005768-199708000-00009. Dickhuth H-H, Yin L, Niess A, et al. (1999) doi:10.1055/s-2007-971105. Stegmann H, Kindermann W, Schnabel A (1981) doi:10.1055/s-2008-1034604. Heuberger JAAC, Gal P, Stuurman FE, et al. (2018) doi:10.1371/journal.pone.0206846. Meixner B, Nusser V, Koehler K, et al. (2024) doi:10.14814/phy2.16086.
Author(s)
Maintainer: Felipe Mattioni Maturana felipe.mattioni@med.uni-tuebingen.de (ORCID)
Authors:
Felipe Mattioni Maturana felipe.mattioni@med.uni-tuebingen.de (ORCID)
See Also
Useful links:
Report bugs at https://github.com/fmmattioni/lactater/issues
Plot lactate thresholds
Description
Plots the result of lactate_threshold(). Without method, an overview: the fitted lactate
curve on top and every threshold below, grouped by what the method aims at, on the same intensity axis, with a
shaded band for how far the methods aimed at LT1 and at LT2 lie apart, so you can see at a glance how much the
methods agree; the two that mark LT1 and LT2 (lt1, lt2, as chosen in
lactate_threshold()) are drawn in green. With method, the plot of that one method:
what the method fitted, the measured stages, and the threshold with its intensity, lactate and
heart rate.
Usage
## S3 method for class 'lactate_thresholds'
autoplot(object, method = NULL, lt1 = NULL, lt2 = NULL, ...)
## S3 method for class 'lactate_thresholds'
plot(x, method = NULL, ...)
Arguments
object, x |
The result of |
method |
A method to plot (one of |
lt1, lt2 |
The thresholds that mark LT1 and LT2, drawn in green in the overview. Default to those
chosen in |
... |
Not used. |
Details
The plots are ggplot2 objects: add layers, scales or a theme() to change them. See
theme_lactater() for the dark style (options(lactater.style = "night")) and the font.
Value
a ggplot2 (one method) or patchwork (overview) object.
Examples
results <- lactate_threshold(
demo_data,
intensity_column = "intensity",
lactate_column = "lactate",
heart_rate_column = "heart_rate",
plot = FALSE
)
autoplot(results)
autoplot(results, method = "Dmax")
Bootstrap intervals
Description
Wild bootstrap around the user-selected curve: each replicate keeps the fitted values and adds to each stage its
own residual, scaled by 1 / sqrt(1 - leverage) and widened by qt(level) / qnorm(level) for the n - p degrees of
freedom of the curve, with a random sign (Rademacher weights; Wu, 1986; Davidson & Flachaire, 2008). Residuals never
move between stages, because the error of a lactate sample grows with its concentration. The widening brought the
coverage of 95% intervals from 75-93% to 84-97% in data-raw/bootstrap_coverage.R. Davidson & Flachaire (2008) used
residuals divided by 1 - leverage (HC3), which they found made little difference; in that simulation it gave
intervals two to three times too wide for Bsln+ (coverage 95-99%). A resting sample outside the
fit gets the residual size of the first stage. Every method is then run again on the replicate. The interval is the
percentile interval of the replicate intensities. For internal use only.
Usage
bootstrap_intervals(
data,
settings,
method,
curve,
n_rows,
n_boot,
level,
seed = NULL
)
Arguments
data |
The data retrieved from |
settings |
The analysis settings. |
method |
The method categories to run. |
curve |
The user-selected curve fitted to the original data. |
n_rows |
Number of thresholds the methods return. |
n_boot |
Number of replicates. |
level |
Confidence level. |
seed |
A seed for the replicates, or |
Value
a matrix with one row per threshold and the columns low and high.
Layers of a breakpoint method
Description
The fitted straight lines (solid where they were fitted, dashed beyond), and for LTP the other turn point. For internal use only.
Usage
breakpoint_layers(fit, method, intensity, log_axes, colours, sport)
Arguments
fit |
A fit from |
method |
The method label. |
log_axes |
Whether the fit is in log-log space. |
colours |
The plot colours. |
sport |
The sport, for the unit of the other turn point. |
Value
a list with the layers, the lactate y where the threshold sits, and the top.
The straight lines of a breakpoint fit
Description
The straight lines of a breakpoint fit
Usage
breakpoint_lines(fit)
Arguments
fit |
A fit from |
Value
a list of functions, one line per segment, each defined over all x.
Calibrate your own performance equation
Description
Fits the relationship between a threshold and race performance on your own athletes, for any threshold, sport and
test protocol, so that predict_performance() can use it for new tests. This is the most valid way to predict
performance from a step test: the published equations only apply to tests run like those of their studies, while
yours applies to your protocol, your analyzer and your athletes.
Usage
calibrate_performance(
data,
threshold,
performance,
method,
sport = "running",
distance = NULL,
event = NULL,
protocol = NULL
)
## S3 method for class 'lactate_calibration'
autoplot(object, ...)
Arguments
data |
A data frame with one row per athlete. |
threshold |
The column of |
performance |
The column of |
method |
The lactater method the thresholds come from (e.g. |
sport |
|
distance |
The race distance in meters, when |
event |
A name for the race. Defaults to the distance (e.g. |
protocol |
A short description of your test protocol and population, printed with every prediction. |
object |
The result of |
... |
Not used. |
Details
Give one row per athlete: the threshold from lactate_threshold() and the result of a race run soon after the
test (e.g. within 2 months, as in Roecker et al., 1998). With distance, performance is the race time in seconds
and is converted to the mean race intensity of the sport (speed in km/h for running, m/s for swimming, and power
in W for rowing, with the Concept2 relation watts = 2.80 / pace^3, pace in s/m); without it, performance is
already a mean intensity (e.g. time-trial power in W). The equation is a straight line, intensity = intercept + slope * threshold, as in the published equations. Use the same protocol for the new tests as for the athletes it
was fitted on, and more athletes make a narrower, more stable range.
Value
a lactate_calibration tibble with one row: the equation (intercept, slope), its residual SD
(sigma), n, the correlation r, the leave-one-out prediction error (loo_error), and what
predict_performance() needs. Plot it with autoplot().
Examples
## thresholds and 10 km times of 12 runners from your lab (simulated here)
set.seed(1)
lab <- data.frame(ltp2 = round(rnorm(12, 15, 1.5), 1))
lab$time_10km <- round(10000 / ((0.5 + 1.02 * lab$ltp2 + rnorm(12, 0, 0.4)) / 3.6))
own <- calibrate_performance(
lab, "ltp2", "time_10km", method = "LTP2", sport = "running", distance = 10000,
protocol = "3-min stages, +1 km/h, 1% grade, earlobe"
)
own
autoplot(own)
The thresholds that mark LT1 and LT2
Description
What the caller asked for, otherwise what lactate_threshold() was given, otherwise the package defaults (a result
saved before the choice was kept). For internal use only.
Usage
chosen_pair(info, lt1 = NULL, lt2 = NULL)
pair_text(info)
Arguments
info |
The |
lt1, lt2 |
A method, an intensity, or |
Value
a list with lt1 and lt2.
pair_text(): "83 W (Bsln + 0.5)" for each of the two that was found, named LT1 and LT2.
Column name from a bare name, a string, or a variable that holds a string
Description
Column name from a bare name, a string, or a variable that holds a string
Usage
column_name(expr, .data, env)
Arguments
expr |
The captured argument. |
.data |
The data. |
env |
Where to look up a variable. |
Value
the column name, or NULL.
Compare two lactate tests
Description
Compares two tests of the same athlete (e.g. before and after a training block): the change of each threshold, whether it is larger than the noise of the method, the heart rate at each threshold, and notes on what could make the comparison unfair.
Usage
compare_tests(before, after, cv = NULL, swc = 0, level = 0.9)
## S3 method for class 'lactate_comparison'
autoplot(object, ...)
Arguments
before, after |
The results of |
cv |
The test-retest coefficient of variation (%) of each method: one number for all methods, or a
named vector (e.g. |
swc |
The smallest worthwhile change, in % of the intensity before. Defaults to |
level |
The confidence level of the interval. Defaults to |
object |
The result of |
... |
Not used. |
Details
A threshold moves between two tests even without any change in fitness, by the day-to-day variation of
the athlete and the error of the analyzer and the method. Each change is therefore given with an
interval, change +/- z * sqrt(2) * typical error, where the typical error is the coefficient of
variation (cv) times the intensity before (Hopkins, 2000; Swinton et al., 2018). This is the model behind the
repeatability of Bland & Altman (1996), for whom two measurements of the same person differ by less than
1.96 * sqrt(2) within-subject standard deviations in 95% of pairs, a size also known as the minimal detectable
change (Sainani, 2017). With a cv from lactate_reliability(), z is the t quantile for its degrees of freedom,
because a typical error from few athletes is itself uncertain (Swinton et al., 2018): with 10 athletes tested
twice, the normal quantile would call 13% of unchanged pairs "beyond the noise" at the 90% level instead of 10%.
Three columns say how the change stands against that noise, without a probability attached to them:
-
noise:sqrt(2)x the typical error, the typical difference between two tests when nothing changed, in the unit of the intensity. -
ratio: the change divided by the noise. At 1, the change is as large as two tests usually differ by anyway. -
verdict:"beyond the noise"when the whole interval lies beyond the smallest worthwhile change (swc; beyond zero by default), which at the default 90% is a ratio above 1.64, and"within the noise"otherwise. A change within the noise is not a sign that nothing changed: one pair of tests cannot tell. The words describe this one comparison against thecvin the table; graded terms such as "likely" are not used, because they read a frequentist interval as a probability of the true change (Lohse et al., 2020).
Three things decide how often a verdict is wrong, so settle them before looking at the result:
-
The level. At the default 90%, an unchanged threshold is called "beyond the noise" in 10% of comparisons: 5% as a gain and 5% as a loss, which is a one-sided test for a gain at 5%, the test Sainani (2018) recommends in place of graded terms. No level is agreed on: 95% is the classic choice, and narrower intervals are defended for decisions in practice (Swinton et al., 2018).
-
The smallest worthwhile change. With
swcabove zero, the verdict asks for a change that is certainly larger thanswc, not merely larger than zero. No value is agreed on for lactate thresholds; one convention is 0.2 times the standard deviation between athletes (Swinton et al., 2018). Choosingswcorlevelafter seeing the verdict lets one pick the verdict one likes (Sainani, 2018). -
The threshold. Each method gets its own verdict, and with every method in the table some of them will lie beyond the noise by chance. In 157 pairs of consecutive tests of the cyclists of Heuberger et al. (2018), each method was beyond the noise in 6% to 15% of pairs, but at least one of the 18 was in 50%, and the default LT1 or LT2 in 19%. The LT1 and LT2 chosen in
lactate_threshold()are the ones to read, as the header does; a method picked because its verdict stands out proves nothing.
The default cv of each method is lactater's own calculation: lactate_reliability() on the five step tests of
each of 47 well-trained male cyclists in the open data of Heuberger et al. (2018; 204 tests, 5-min stages, venous
blood, about two weeks apart), so that it is the noise of each method as lactater computes it, on a step test. The
output gives it in the cv column.
| Method | Default cv (%) | Published (%) |
| OBLA 2.0, 2.5, 3.0, 3.5 | 7.7, 5.2, 4.6, 4.8 | |
| OBLA 4.0 | 4.4 | 3.8 (Heuberger et al., 2018) |
| Bsln + 0.5, 1.0, 1.5 | 8.2, 5.0, 4.5 | 3.7, 3.4, 3.4 (Pallares et al., 2016; ramp test) |
| Dmax | 5.9 | 3.4 (Heuberger et al., 2018) |
| ModDmax | 5.1 | 4.3 (Heuberger et al., 2018) |
| Exp-Dmax, Log-Poly-ModDmax, Log-Exp-ModDmax | 4.3, 5.5, 4.9 | |
| IAT | 4.4 | 3.6 (Heuberger et al., 2018) |
| LTP1, LTP2 | 10.6, 6.0 | |
| Log-log, LTratio | 7.8, 9.4 |
The defaults are on the careful side. Heuberger et al. computed their values on the riders who received placebo,
with their own implementation of each method; the open data do not say which riders received erythropoietin, so the
defaults use every rider and are, if anything, too high (pairs of consecutive tests gave values of the same size,
so a drift between the two groups does not explain them). Those tests have no resting sample, so Bsln+ rose from
the first stage. To use the published values instead, give them: cv = c("IAT" = 3.6, "Dmax" = 3.4).
Stegmann, Keul, Simon and your own fixed or baseline_plus values have no default: their verdict is NA
unless you give one.
The defaults come from cycling and are used for every sport, as a percentage of the intensity; the notes say so
for other sports. No open test-retest data of running step tests were found to compute running values the same way.
Published running values are of a similar size in trained runners: typical errors of about 0.3 to 0.8 km/h (2% to
6%) for 22 trained men on a treadmill with 1-min stages, from the SDs of their test-retest differences (the CVs
they report, 1.2% to 3.5%, are smaller; Cerezuela-Espejo et al., 2018), and 0.4 to 1.4 km/h for 19 healthy,
untrained people with 4-min and 8-min stages (Gavin et al., 2014).
The best cv is the one of your own athletes, measured with your protocol and analyzer. A cv assumes that the
noise grows with the intensity, which is common for such measures but should be checked where the data allow
(Atkinson & Nevill, 1998).
Value
a lactate_comparison tibble with one row per method found in both tests: method, before,
after, change, change_percent, the interval of the change (change_low, change_high), the cv (%) it
was computed with, the noise, the ratio of the change to it, the verdict,
and the heart rate at each threshold in both tests (when recorded). The notes attribute lists what differs
between the two tests (stages, step, resting sample, sample type) and a fall in peak lactate. Plot both
curves with autoplot().
References
Atkinson G, Nevill AM. Statistical methods for assessing measurement error (reliability) in variables relevant to sports medicine. Sports Medicine. 1998;26(4):217-238.
Bland JM, Altman DG. Measurement error. BMJ. 1996;312(7047):1654. doi:10.1136/bmj.312.7047.1654
Cerezuela-Espejo V, Courel-Ibáñez J, Morán-Navarro R, Martínez-Cava A, Pallarés JG. The relationship between lactate and ventilatory thresholds in runners: validity and reliability of exercise test performance parameters. Frontiers in Physiology. 2018;9:1320. doi:10.3389/fphys.2018.01320
Gavin JP, Willems MET, Myers SD. Reproducibility of lactate markers during 4 and 8 min stage incremental running: a pilot study. Journal of Science and Medicine in Sport. 2014;17(6):635-639. doi:10.1016/j.jsams.2013.08.006
Heuberger JAAC, Gal P, Stuurman FE, de Muinck Keizer WAS, Mejia Miranda Y, Cohen AF. Repeatability and predictive value of lactate threshold concepts in endurance sports. PLOS ONE. 2018;13(11):e0206846.
Hopkins WG. Measures of reliability in sports medicine and science. Sports Medicine. 2000;30(1):1-15.
Lohse KR, Sainani KL, Taylor JA, Butson ML, Knight EJ, Vickers AJ. Systematic review of the use of "magnitude-based inference" in sports science and medicine. PLOS ONE. 2020;15(6):e0235318.
Pallares JG, Moran-Navarro R, Ortega JF, Fernandez-Elias VE, Mora-Rodriguez R. Validity and reliability of ventilatory and blood lactate thresholds in well-trained cyclists. PLOS ONE. 2016;11(9):e0163389.
Sainani KL. Reliability statistics. PM&R. 2017;9(6):622-628. doi:10.1016/j.pmrj.2017.05.001
Sainani KL. The problem with "magnitude-based inference". Medicine & Science in Sports & Exercise. 2018;50(10):2166-2176. doi:10.1249/MSS.0000000000001645
Swinton PA, Hemingway BS, Saunders B, Gualano B, Dolan E. A statistical framework to interpret individual response to intervention: paving the way for personalized nutrition and exercise prescription. Frontiers in Nutrition. 2018;5:41.
Examples
series <- subset(lactate_tests, athlete == "cyclist_series")
before <- lactate_threshold(subset(series, day == 0), intensity, lactate, heart_rate, plot = FALSE)
after <- lactate_threshold(subset(series, day == 58), intensity, lactate, heart_rate, plot = FALSE)
compare_tests(before, after)
What could make two tests hard to compare
Description
What could make two tests hard to compare
Usage
comparison_notes(before, after)
Arguments
before, after |
The |
Value
a character vector of notes (empty when nothing stands out).
Compute thresholds
Description
Fits the user-selected curve and runs the requested methods. For internal use only.
Usage
compute_thresholds(data, settings, method)
Arguments
data |
The data retrieved from |
settings |
The analysis settings. |
method |
The method categories to run. |
Value
a list with the user curve, the result rows (with method_category) and the
plot_data for each row.
Convert intensity to pace
Description
swimming: speed in m/s to pace in s/100 m;
running: speed in km/h to pace in s/km;
rowing: power in W to split in s/500 m, with the Concept2 relation
watts = 2.80 / pace^3(pace in s/m).
Usage
convert_to_pace(intensity, sport = "swimming")
Arguments
intensity |
The intensity to convert. |
sport |
The sport. |
Value
the pace, rounded to 0.1 s.
Layers of a method drawn on the fitted curve
Description
The curve, plus the Dmax line and the gap to it, the Bsln+ baseline and rise, or the IAT rise from LTratio. For internal use only.
Usage
curve_layers(plot_data, intensity, lactate, colours, sport)
Arguments
plot_data |
What the method drew on: the fitted |
colours |
The plot colours. |
sport |
The sport, for the unit of the LTratio label. |
Value
a list with the layers, the lactate y where the threshold sits, and the top of the drawing.
Points along a fitted curve
Description
Points along a fitted curve
Usage
curve_points(curve, from = curve$domain[1], n = 200)
Arguments
curve |
A curve from |
from |
Where to start (default: the start of the curve's domain). |
n |
Number of points. |
Value
a data frame with intensity and lactate.
Did the lactate curve shift, or change its shape?
Description
Tests whether two tests of the same athlete differ only by a horizontal shift of the lactate curve, or also in its
shape. Both tests are fitted with the exponential plus constant, lactate = a + b * exp(c * intensity) (Hughson et
al., 1987), in which a pure horizontal shift by d changes only b:
Usage
curve_shape(before, after, level = 0.95)
## S3 method for class 'lactate_shape'
autoplot(object, ...)
Arguments
before, after |
The results of |
level |
The confidence level of the shift. Defaults to |
object |
The result of |
... |
Not used. |
Details
-
Shift only: one shape for both tests (shared
a,bandc), with the second test moved byd. -
Free shape: each test with its own
a,bandc.
The two fits are compared with an extra-sum-of-squares F test (Motulsky & Ransnas, 1987) and with AICc, the
Akaike criterion corrected for small samples (Hurvich & Tsai, 1989); the verdict is "shape changed" only when
both prefer the free shape. The shift d comes with a profile confidence interval.
Read the result with care:
The test finds a change of shape only when it is large and the tests are long. In simulated pairs of tests of a trained cyclist (analyzer error of 0.1 mmol/L plus 4%), a baseline 0.5 mmol/L higher was found in 96% of pairs with 8 stages per test and in 20% with 6; a curve steep enough to double the peak lactate in 55% and 10%. So "no shape change detected" is not proof of the same shape, least of all with 6 stages.
The p-value is approximate: the F test is exact only for models that are linear in their parameters, and a low p-value from one pair of curves needs a repeat before it counts (Motulsky & Ransnas, 1987). In the same simulation it fell below 0.05 in 8% to 10% of pairs with no change of shape; with AICc beside it, the verdict was wrong in 1% to 6%.
In this model, a shift to the right is the same as a proportional fall of lactate above baseline. Better endurance, a lower glycolytic rate (VLamax) at the same VO2max, low glycogen stores and overreaching can all produce it, so check the peak lactate, the diet before the test and the performance too (Maassen & Busse, 1989; Faude et al., 2009; Wackerhage et al., 2025). A shift to the left can likewise come from a higher VLamax, not only from lost endurance.
The exponential cannot follow an early lactate dip; check both tests with
lactate_diagnostics()first.
Value
a lactate_shape list with shift (intensity units, positive when the curve moved to the right),
shift_low and shift_high, f, df1, df2, p_value, aicc_shift and aicc_free, verdict
("shape changed" when the F test and AICc both prefer the free shape, "shift" otherwise: none
was detected), the peak lactate of both tests and the two
exponential curves. Print it for a summary, and plot it with autoplot(): the two fitted curves, and the first one
moved by the shift (dashed).
References
Faude O, Kindermann W, Meyer T. Lactate threshold concepts: how valid are they? Sports Medicine. 2009;39(6):469-490.
Hurvich CM, Tsai CL. Regression and time series model selection in small samples. Biometrika. 1989;76(2):297-307. doi:10.1093/biomet/76.2.297
Hughson RL, Weisiger KH, Swanson GD. Blood lactate concentration increases as a continuous function in progressive exercise. Journal of Applied Physiology. 1987;62(5):1975-1981.
Maassen N, Busse MW. The relationship between lactic acid and work load: a measure for endurance capacity or an indicator of carbohydrate deficiency? European Journal of Applied Physiology and Occupational Physiology. 1989;58(7):728-737.
Motulsky HJ, Ransnas LA. Fitting curves to data using nonlinear regression: a practical and nonmathematical review. FASEB Journal. 1987;1(5):365-374.
Wackerhage H, Kabasakalis A, Seiler S, Heck H. Is the vLamax for glycolysis what the VO2max is for oxidative phosphorylation? Sports Medicine. 2025;55(8):1853-1866.
Examples
series <- subset(lactate_tests, athlete == "cyclist_series")
before <- lactate_threshold(subset(series, day == 0), intensity, lactate, plot = FALSE)
after <- lactate_threshold(subset(series, day == 58), intensity, lactate, plot = FALSE)
curve_shape(before, after)
Shift of the whole lactate curve between two tests
Description
How far the fitted lactate curve moved between two tests: the intensity gained at each lactate value, and,
when intensity is given, the lactate saved at each intensity. A rightward shift (more intensity for the
same lactate) is the classic sign of better endurance; check the peak lactate too (see compare_tests()).
Usage
curve_shift(before, after, lactate = seq(1.5, 4, by = 0.5), intensity = NULL)
Arguments
before, after |
The results of |
lactate |
Lactate values (mmol/L) at which to compare the intensity. Values outside either curve are
|
intensity |
Intensities at which to compare the lactate, or |
Value
a tibble with lactate, intensity_before, intensity_after and shift; with intensity,
rows with intensity, lactate_before, lactate_after and lactate_change are added.
Examples
series <- subset(lactate_tests, athlete == "cyclist_series")
before <- lactate_threshold(subset(series, day == 0), intensity, lactate, plot = FALSE)
after <- lactate_threshold(subset(series, day == 58), intensity, lactate, plot = FALSE)
curve_shift(before, after)
Demo data
Description
A dataset containing the lactate and heart rate data collected from a step-incremental test as an example, with a simulated rating of perceived exertion
Usage
demo_data
Format
A data frame with 8 rows and 6 variables:
- step
the number of the step, starting at zero for the baseline.
- length
the length of each step.
- intensity
the intensity performed on each step (in this case in watts).
- lactate
the blood lactate concentration.
- heart_rate
the heart rate associated with that step.
- rpe
the rating of perceived exertion (Borg 6-20). Simulated: about 11 at 2 mmol/L and 14 at 4 mmol/L, close to the means of 2,560 adults at the first rise in lactate (10.8) and at 4 mmol/L (14.1; Scherr et al., 2013), and 19 at the last stage.
Dmax point
Description
Finds where the curve lies furthest below the straight line from (x0, y0) to (x1, y1).
The perpendicular distance is the vertical distance times a constant, so both peak at the
same intensity. For internal use only.
Usage
dmax_point(curve, x0, y0, x1, y1)
Arguments
curve |
A function returning the fitted lactate at given intensities. |
x0, y0 |
The start of the line. |
x1, y1 |
The end of the line. |
Value
the intensity, or NA when the curve never dips below the line.
The position text of the zones, once per domain
Description
Zones 1 and 2 of the five-zone model are both "below LT1": the text is drawn once, centered over the two.
Usage
domain_spans(bands, span)
Arguments
bands |
The zone bands of a plot: |
span |
The width of the plot, in intensity. |
Value
a data frame with from, to and description (empty where there is no room for it).
The two splits inside the moderate and the severe domain (five-zone model)
Description
The two splits inside the moderate and the severe domain (five-zone model)
Usage
domain_splits(split, lt, info)
Arguments
split |
|
lt |
LT1 and LT2. |
info |
The |
Value
the zone 1|2 and zone 4|5 intensities.
Fit a continuous piecewise-linear model by exhaustive breakpoint search
Description
For fixed breakpoints the model y = b0 + b1 x + sum(c_j max(x - psi_j, 0)) is linear, so the
residual sum of squares can be computed for every candidate. All breakpoint positions on a grid are
tried and the best one is refined locally. Unlike iterative fits (e.g. segmented), the result is the
global least-squares optimum (up to the grid) and is deterministic. For internal use only.
Usage
fit_breakpoints(x, y, n_breakpoints = 2, flat_first = FALSE, min_points = 2)
breakpoint_design(x, psi, flat_first = FALSE)
predict_breakpoints(fit, newx)
Arguments
x, y |
The measured points. |
n_breakpoints |
Number of breakpoints (1 or 2). |
flat_first |
Whether the first segment is flat (slope fixed at 0). |
min_points |
Minimum number of measured points in each segment. |
psi |
The breakpoints. |
fit |
A fit from |
newx |
Where to evaluate the fitted lines. |
Value
a list with the breakpoints psi, the coefficients, x, y, and flat_first.
Fit a lactate curve
Description
Fits the lactate curve used by the threshold methods. For internal use only.
Usage
fit_curve(
points,
fit = c("3rd degree polynomial", "4th degree polynomial", "B-spline", "Exponential")
)
Arguments
points |
A data frame with |
fit |
The curve type. |
Details
-
"3rd degree polynomial"and"4th degree polynomial": ordinary least squares on raw powers. -
"B-spline": a natural cubic regression spline with 4 degrees of freedom (splines::ns()). -
"Exponential": the exponential plus constanta + b * exp(c * x)(Hughson et al., 1987). For a fixedcthe model is linear inaandb, so onlycis searched (variable projection), and only "in the positive range" as in Hughson et al.: no starting values are needed and the fit cannot fail to converge.
Value
a list with the fit type, the points, the domain (intensity range of the points),
the coefficients, predict, a function returning the fitted lactate at given intensities, and,
for the fits that are linear in their parameters, the leverage of each point.
Fit two independent straight lines that share a division point
Description
Each measured point in turn is the division point, which belongs to both segments (Beaver et al., 1985).
Each segment gets its own least-squares line, and the division with the lowest total residual sum of
squares wins (Jamnick et al., 2018). The breakpoint psi is where the two lines cross. For internal use
only.
Usage
fit_two_lines(x, y, min_points = 3)
Arguments
x, y |
The measured points, sorted by |
min_points |
Minimum number of points per line, the division point included. A line through two points has no residuals, so fewer than three would favor the shortest segments. |
Value
a list with the breakpoint psi (NA when the lines cross outside the range of x), the
intercept and slope of each line (lines, one row per line), x, and y.
Format an intensity with the decimals of its sport
Description
Format an intensity with the decimals of its sport
Usage
format_intensity(x, sport)
Arguments
x |
Intensities. |
sport |
The sport. |
Format a pace as minutes and seconds
Description
Format a pace as minutes and seconds
Usage
format_pace(seconds, sport)
Arguments
seconds |
The pace in seconds. |
sport |
The sport, which sets the distance unit. |
Value
e.g. "4:30 /km".
The look of lactater's tables
Description
Gives a gt table the look of the tables in lactater's reports and articles, so your own tables can match them:
IBM Plex Sans, the full width of the page, bold column labels over one dark rule, light rules between rows, bold
row groups, and no fills or colors. Needs the gt package. options(lactater.web_font = FALSE) leaves out the link
to the web font (IBM Plex Sans is then used when installed), which keeps a self-contained page small.
Usage
gt_lactater(table, size = 14)
Arguments
table |
A gt table. |
size |
The font size, in pixels. |
Value
the gt table.
Examples
gt::gt(head(demo_data)) |>
gt_lactater()
The heart rate at a threshold, labeled beside its point
Description
Its value over "at LT1", right of the point and under the line, where no stage sits, or left of it and above near the right edge or the bottom of the panel. The offsets are in points, so the label hugs its point at any plot size. For internal use only.
Usage
hr_point_label(x, y, lt, xlim, ylim, colours)
Arguments
x, y |
The point: the threshold and its heart rate. |
lt |
|
xlim, ylim |
The limits of the panel. |
colours |
The plot colours. |
Value
an annotation_custom() layer.
A table of the report
Description
A table of the report
Usage
html_table(table, size = 13, compact = FALSE)
Arguments
table |
A gt table. |
size |
The font size, in pixels. |
compact |
Whether to set the rows closer, for the tables kept for the record. |
Value
its HTML.
Decimals shown for the intensity of each sport
Description
Whole watts, km/h to 0.1 and m/s to 0.01: finer than that is below what a step test can resolve.
Usage
intensity_digits(sport)
Arguments
sport |
The sport. |
Training intensity distribution label
Description
Training intensity distribution label
Usage
intensity_distribution(share)
Arguments
share |
The fractions of time in zones 1, 2 and 3. |
Value
"polarized", "pyramidal", "threshold", "high intensity", or "other".
Intensity axis label
Description
Intensity axis label
Usage
intensity_label(sport)
Arguments
sport |
The sport. |
An intensity with its unit
Description
An intensity with its unit
Usage
intensity_text(x, sport)
Arguments
x |
An intensity. |
sport |
The sport. |
Intensity unit of each sport
Description
Intensity unit of each sport
Usage
intensity_unit(sport)
Arguments
sport |
The sport. |
Lactate axis breaks
Description
Whole numbers: every 1, 2 or 4 mmol/L depending on the range.
Usage
lactate_breaks(top)
Arguments
top |
The top of the axis. |
Check a lactate threshold analysis
Description
Runs a set of checks on the test and the results of lactate_threshold(), so problems that are easy
to miss in a table of numbers are flagged: too few stages, no resting sample, thresholds that were not
reached or sit at the edge of the tested range, an early lactate dip or a final plateau, a test that may
have stopped before the second lactate turn point, and methods that rely on fixed blood lactate
concentrations used on another sample type.
Usage
lactate_checks(x)
Arguments
x |
The result of |
Value
a tibble with one row per check: check, status ("ok", "note", or "warning"), and
details. It prints as a checklist, with a tick for each check that passed.
References
Hofmann P, Pokan R, von Duvillard SP, Seibert FJ, Zweiker R, Schmid P. Heart rate performance curve during incremental cycle ergometer exercise in healthy young male subjects. Medicine & Science in Sports & Exercise. 1997;29(6):762-768.
Bonaventura JM, Sharpe K, Knight E, Fuller KL, Tanner RK, Gore CJ. Reliability and accuracy of six hand-held blood lactate analysers. Journal of Sports Science and Medicine. 2015;14:203-214.
Foxdal P, Sjodin B, Rudstam H, Ostman C, Ostman B, Hedenstierna GC. Lactate concentration differences in plasma, whole blood, capillary finger blood and erythrocytes during submaximal graded exercise in humans. European Journal of Applied Physiology. 1990;61(3-4):218-222.
Examples
results <- lactate_threshold(demo_data, intensity, lactate, heart_rate, plot = FALSE)
lactate_checks(results)
Lactate curve
Description
It retrieves the lactate curve for plotting purposes.
Usage
lactate_curve(
.data,
intensity_column,
lactate_column,
heart_rate_column,
fit = c("3rd degree polynomial", "4th degree polynomial", "B-spline"),
include_baseline = FALSE,
sport = c("cycling", "running", "swimming", "rowing")
)
Arguments
.data |
The raw data. |
intensity_column |
The name of the intensity column. |
lactate_column |
The name of the lactate column. |
heart_rate_column |
The name of the heart rate column, if applicable. |
fit |
The fit you would like to use for plotting the lactate curve.
Options are |
include_baseline |
A boolean to indicate whether to include the resting (baseline) sample in the fit. The resting sample is the row with intensity 0; it is placed one median step before the first stage. Data without a resting sample is fine. Rows with a missing intensity or lactate are dropped with a warning. |
sport |
The sport at which the incremental test was performed. One of |
Value
a list with the following elements:
data |
a tibble containing the raw data with the columns |
lactate_curve |
a tibble containing the data with the columns |
heart_rate_response |
a tibble with the columns |
Examples
lactate_curve(demo_data, intensity, lactate, heart_rate)
Check the lactate curve and how much each threshold depends on single stages
Description
Regression diagnostics for the fitted lactate curve, adapted to tests of 6 to 10 stages, and a leave-one-out check
of every threshold. Print it for a short summary, and plot it with autoplot() for six panels, each with a line on
what to look for; they need a tall plot, about 9 x 10 inches, and the article Checking the analysis shows them panel by panel.
Usage
lactate_diagnostics(x)
## S3 method for class 'lactate_diagnostics'
autoplot(object, ...)
Arguments
x |
The result of |
object |
The result of |
... |
Not used. |
Details
-
Residuals: the measured lactate minus the curve. A run of residuals of the same sign, or an arch against the fitted values, means the shape of the curve is wrong for this test.
-
Standardized residuals: residuals divided by their standard error. With so few stages, normality and equal-variance tests have almost no power, so a check that "passes" means "could not tell", not "fine"; analyzer error also grows with lactate (Bonaventura et al., 2015).
-
Leverage and Cook's distance: how much one stage pulls the curve (Cook, 1977). The first and last stages always have high leverage, by design. A stage is flagged when its Cook's distance is above the median of the F(p, n - p) distribution, where p is the number of curve terms; rules such as 2p/n for leverage cannot flag anything when p is close to n.
-
Leave one stage out: every threshold is computed again without each stage in turn (a jackknife; Efron, 1982). The range shows how much a threshold rests on one stage, and the jackknife standard error is
sqrt((n - 1) / n * sum((theta_i - mean(theta_i))^2)). Dropping the first or last stage changes the Dmax line itself, so large changes there are expected for the Dmax family. The resting sample is left out too (column "rest"), which shows how much Bsln+ rests on that one baseline sample (withinclude_baseline = TRUEthe curve changes too, so the curve methods move as well); it is not part of the jackknife. -
Which curve predicts best: the leave-one-out prediction error of every curve type, from
lactate_fit_quality(). It rewards curves that follow the data without overfitting it, which the RMSE of the fit alone cannot do.
Value
a lactate_diagnostics object: a list with stages (one row per stage used in the fit: intensity,
lactate, fitted, residual, standardized, leverage, cooks_distance, influential), leave_one_out
(method, stage left out, intensity without it, change), jackknife (method, intensity, lowest,
highest, jackknife_se), and fits (lactate_fit_quality(), best prediction error first).
References
Bonaventura JM, Sharpe K, Knight E, Fuller KL, Tanner RK, Gore CJ. Reliability and accuracy of six hand-held blood lactate analysers. Journal of Sports Science and Medicine. 2015;14:203-214.
Cook RD. Detection of influential observation in linear regression. Technometrics. 1977;19(1):15-18.
Efron B. The jackknife, the bootstrap and other resampling plans. Philadelphia: SIAM; 1982.
Examples
female <- subset(lactate_tests, test == "cycling_female")
results <- lactate_threshold(female, intensity, lactate, heart_rate, plot = FALSE)
diagnostics <- lactate_diagnostics(results)
diagnostics
## six panels: draw them in a tall window, about 9 x 10 inches
if(interactive()) autoplot(diagnostics)
Goodness of fit of the lactate curves
Description
Fits every available curve to the same points and reports how well each follows the measured
lactate, to help choose fit in lactate_threshold(). The statistics are computed on the measured
stages (plus the resting sample when include_baseline = TRUE).
Usage
lactate_fit_quality(x)
Arguments
x |
The result of |
Details
A curve with more parameters always follows the points more closely, so the RMSE favors it. The prediction error does not: each stage is predicted by the curve fitted without it, which punishes a curve that bends to follow single points (leave-one-out cross-validation).
Value
a tibble with one row per curve: fit, used (the curve chosen in lactate_threshold()),
n_points, parameters, r_squared, rmse and see (standard error of the estimate) in mmol/L, and
max_abs_residual and the leave-one-out prediction_error (root-mean-square) in mmol/L.
Examples
results <- lactate_threshold(demo_data, intensity, lactate, plot = FALSE)
lactate_fit_quality(results)
Maximal lactate steady state from constant-load bouts
Description
Finds the maximal lactate steady state (MLSS) from two or more constant-load bouts of at least 30 min on separate
days: a bout is steady when blood lactate rises by no more than 1.0 mmol/L from minute 10 to minute 30, and the MLSS
is the highest steady bout. Heck et al. (1985), who introduced the test and the term, allowed a rise of 1 mmol/L over
the last 20 min of 3 + 25-min runs; the minute 10 to 30 window is that of Beneke (2003). Against
the highest steady oxygen uptake, a window from minute 15 or 20 to minute 30 was more accurate
(Iannetta et al., 2022); set it with from. With the result of lactate_threshold() for a step test of the same
athlete, it also gives how far each threshold lies from the MLSS, your own check of which threshold estimates it.
Usage
lactate_mlss(bouts, x = NULL, rise = 1, from = 10)
## S3 method for class 'lactate_mlss'
autoplot(object, ...)
Arguments
bouts |
A data frame with one row per sample: |
x |
The result of |
rise |
The largest rise in lactate (mmol/L) from minute |
from |
The minute the rise is measured from, to minute 30. Defaults to |
object |
The result of |
... |
Not used. |
Details
The lactate at minute from and at minute 30 is interpolated linearly between the samples, so sample at or around
both. A bout
that ended before minute 30 (exhaustion) is not steady. The true MLSS lies between the highest steady bout and the
next bout up, so bouts closer together pin it down better. Thresholds against a measured MLSS land from about 40 W
below to 40 W above it on average, depending on the method and the protocol (Hauser et al., 2014; Ploszczyca et al.,
2020). Whether the MLSS or the critical power marks the boundary between the heavy and severe domains is debated
(Keir et al., 2015; Mattioni Maturana et al., 2016; Jones et al., 2019).
Value
a lactate_mlss list with mlss (the highest steady bout, NA when none was steady), next_bout (the
lowest bout above it, which was not steady, or NA), bouts (a tibble with one row per bout: intensity,
lactate_from (at minute from), lactate_30, rise, minutes (the last sample), and steady), the samples,
rise and from, and, with x,
thresholds (a tibble with method, intensity, and the gap to the MLSS in intensity units and in %, positive
when the threshold lies above it). Plot it with autoplot(); the article Beyond the step test shows it.
References
Beneke R. Methodological aspects of maximal lactate steady state - implications for performance testing. European Journal of Applied Physiology. 2003;89(1):95-99. doi:10.1007/s00421-002-0783-1
Hauser T, Adam J, Schulz H. Comparison of selected lactate threshold parameters with maximal lactate steady state in cycling. International Journal of Sports Medicine. 2014;35(6):517-521.
Heck H, Mader A, Hess G, Mücke S, Müller R, Hollmann W. Justification of the 4-mmol/l lactate threshold. International Journal of Sports Medicine. 1985;6(3):117-130. doi:10.1055/s-2008-1025824
Iannetta D, Ingram CP, Keir DA, Murias JM. Methodological reconciliation of CP and MLSS and their agreement with the maximal metabolic steady state. Medicine & Science in Sports & Exercise. 2022;54(4):622-632. doi:10.1249/MSS.0000000000002831
Jones AM, Burnley M, Black MI, Poole DC, Vanhatalo A. The maximal metabolic steady state: redefining the 'gold standard'. Physiological Reports. 2019;7(10):e14098.
Keir DA, Fontana FY, Robertson TC, Murias JM, Paterson DH, Kowalchuk JM, Pogliaghi S. Exercise intensity thresholds: identifying the boundaries of sustainable performance. Medicine & Science in Sports & Exercise. 2015;47(9):1932-1940. doi:10.1249/MSS.0000000000000613
Mattioni Maturana F, Keir DA, McLay KM, Murias JM. Can measures of critical power precisely estimate the maximal metabolic steady-state? Applied Physiology, Nutrition, and Metabolism. 2016;41(11):1197-1203. doi:10.1139/apnm-2016-0248
Ploszczyca K, Jazic D, Piotrowicz Z, Chalimoniuk M, Langfort J, Czuba M. Comparison of maximal lactate steady state with anaerobic threshold determined by various methods based on graded exercise test with 3-minute stages in elite cyclists. BMC Sports Science, Medicine and Rehabilitation. 2020;12:70.
Examples
bouts <- data.frame(
intensity = rep(c(130, 140, 150), each = 4),
time = rep(c(5, 10, 20, 30), 3),
lactate = c(2.4, 2.6, 2.7, 2.8, 3.1, 3.4, 3.8, 4.1, 3.9, 4.5, 5.4, 6.3)
)
results <- lactate_threshold(demo_data, intensity, lactate, plot = FALSE)
mlss <- lactate_mlss(bouts, results)
mlss
mlss$thresholds
## the plot is short and wide: see it in the article Beyond the step test
if(interactive()) autoplot(mlss)
Test-retest reliability of each threshold, from your own repeated tests
Description
The typical error of each threshold, from athletes who did the same test two or more times while their fitness did
not change (e.g. a few days apart): the residual standard deviation of a two-way analysis of athletes and tests,
which leaves out the mean change from one test to the next (Hopkins, 2000). It is given in intensity units and as a
coefficient of variation, 100 * (exp(s) - 1), with s the typical error of the log intensities. Pass the result
to compare_tests(cv = ) so that its verdicts use the noise of your own protocol, analyzer and athletes rather than
the default values, from trained male cyclists.
Usage
lactate_reliability(x, athlete, test)
Arguments
x |
The result of |
athlete, test |
The columns of |
Details
A typical error from few athletes is itself uncertain: with 10 athletes and 2 tests (9 degrees of freedom) its 90%
confidence interval spans 0.73 to 1.65 times its value (from the chi-square distribution of a variance). The
result therefore gives the degrees of freedom (df) and that interval for the cv (cv_low, cv_high), as
reliability statistics should always come with one (Sainani, 2017), and compare_tests() widens its intervals for
a small df. Atkinson & Nevill (1998) recommend more than 40 athletes for a reliability study; with fewer, read
the interval.
The bias is the mean change from each test to the next, which the typical error leaves out. Report it beside the
random error (Atkinson & Nevill, 1998): a bias that is not near zero is a learning or order effect, or a change
in fitness between the tests. The cv assumes that the error grows with the intensity, as it usually does for
such measures; with athletes of a similar level the two cannot be told apart, and typical_error is the same
noise in intensity units.
Value
a tibble with one row per method: method, n_athletes (with two or more tests), n_tests, bias
(the mean change from one test to the next, intensity units), typical_error (intensity units), cv (%) with
its 90% confidence interval (cv_low, cv_high), and the degrees of freedom of the typical error (df).
Methods with fewer than two such athletes get NA.
References
Atkinson G, Nevill AM. Statistical methods for assessing measurement error (reliability) in variables relevant to sports medicine. Sports Medicine. 1998;26(4):217-238.
Hopkins WG. Measures of reliability in sports medicine and science. Sports Medicine. 2000;30(1):1-15.
Sainani KL. Reliability statistics. PM&R. 2017;9(6):622-628. doi:10.1016/j.pmrj.2017.05.001
Examples
## the same athlete tested twice, a few days apart (simulated here as small changes in lactate)
set.seed(1)
repeats <- do.call(rbind, lapply(1:4, function(a) do.call(rbind, lapply(1:2, function(t) {
transform(demo_data, athlete = a, test = t, intensity = intensity * (0.9 + 0.05 * a),
lactate = lactate * exp(rnorm(nrow(demo_data), 0, 0.05)))
}))))
results <- lactate_threshold(repeats, intensity, lactate, method = c("OBLA", "Dmax"), plot = FALSE,
.by = c("athlete", "test"))
reliability <- lactate_reliability(results, "athlete", "test")
reliability
Lactate threshold report table
Description
Formats the result of lactate_threshold() as a publication-ready table: thresholds grouped by method
family, with the confidence interval when available, and the checks that need attention as notes.
Needs the gt package.
Usage
lactate_report(x, title = "Lactate thresholds")
Arguments
x |
The result of |
title |
The table title, or |
Value
a gt_tbl object (print it, or save it with gt::gtsave()).
Examples
results <- lactate_threshold(demo_data, intensity, lactate, heart_rate, plot = FALSE)
lactate_report(results)
Lactate step tests from different sports, athletes and scenarios
Description
Sixteen incremental step tests in long format, one row per stage, to try every part of lactater on
real data: cycling, running, rowing and swimming; elite, trained and untrained athletes; one athlete
tested six times over five years; and the cases that need care (an early lactate dip, no resting
sample, 5-min steps, a submaximal test). Analyze one test with subset(), or several tests of one
sport with .by = test.
Usage
lactate_tests
Format
A tibble with one row per stage and 13 columns:
- test
the test:
cycling_elite,cycling_female,cycling_early_dip,cycling_series_1tocycling_series_6,running,rowing,cycling_5min_no_rest,swimming,untrained,submaximal,running_recreational.- athlete
the athlete; the six
cycling_seriestests sharecyclist_series.- sport
cycling,running,rowing, orswimming, as inlactate_threshold(sport = ).- sex
MorF.- body_mass
body mass (kg) on the test day,
NAwhen not reported.- day
days since the athlete's first test (0 for single tests).
- description
who was tested, and how.
- source
Trinity College Dublin (CC BY 4.0),Heuberger et al. 2018 (CC BY 4.0), orsimulated.- step
the stage, 0 for the resting sample.
- intensity
power (W) for cycling and rowing, speed (km/h) for running, speed (m/s) for swimming; 0 for the resting sample.
- lactate
blood lactate (mmol/L).
- heart_rate
heart rate (bpm),
NAwhen not recorded.- rpe
rating of perceived exertion (Borg 6-20), simulated tests only;
NAfor the real tests.
Details
Twelve tests are real: eleven from the Trinity College Dublin graded exercise test data (3-min stages,
lactate sampled at 2 min, YSI analyzer; running on a treadmill at 0% grade; rowing with 1 min of rest
between stages) and one from Heuberger et al. (2018) (5-min steps from 175 W). Names, birth dates and
test dates were removed. Four tests are simulated, where no open data exists: a swimmer's 7 x 200 m
step test (+0.05 m/s per step; Pelarigo et al., 2017), an untrained client tested to exhaustion and
stopped early (25 W steps; untrained men peaked near 250 W in Messonnier et al., 2013), and a recreational runner
on the protocol of Roecker et al. (1998) (treadmill at 2% grade, +2 km/h every 3 min from 6 km/h), so that
their race-time equations apply as published. Their lactate
rises exponentially through about 2.5 mmol/L near the first threshold, with analyzer scatter of 3% from
2 mmol/L and 5% below (Bonaventura et al., 2015). Their simulated RPE rises with lactate, from about 11 at
2 mmol/L to 14 at 4 mmol/L (close to the cohort means at the first rise in lactate and at 4 mmol/L of Scherr et
al., 2013) and 19 to 20 at the last stage. See data-raw/lactate_tests.R.
Source
Trinity College Dublin graded exercise test data. Zenodo. doi:10.5281/zenodo.10841412. License CC BY 4.0.
Heuberger JAAC, Gal P, Stuurman FE, de Muinck Keizer WAS, Mejia Miranda Y, Cohen AF. Repeatability and predictive value of lactate threshold concepts in endurance sports. PLOS ONE. 2018;13(11):e0206846. Data: figshare, doi:10.6084/m9.figshare.7240571. License CC BY 4.0.
References
Bonaventura JM, Sharpe K, Knight E, Fuller KL, Tanner RK, Gore CJ. Reliability and accuracy of six hand-held blood lactate analysers. Journal of Sports Science and Medicine. 2015;14:203-214.
Messonnier LA, Emhoff CAW, Fattor JA, Horning MA, Carlson TJ, Brooks GA. Lactate kinetics at the lactate threshold in trained and untrained men. Journal of Applied Physiology. 2013;114(11):1593-1602.
Pelarigo JG, Machado L, Fernandes RJ, Greco CC, Vilas-Boas JP. Oxygen uptake kinetics and energy system's contribution around maximal lactate steady state swimming intensity. PLOS ONE. 2017;12(2):e0167263.
Roecker K, Schotte O, Niess AM, Horstmann T, Dickhuth HH. Predicting competition performance in long-distance running by means of a treadmill test. Medicine & Science in Sports & Exercise. 1998;30(10):1552-1557.
Scherr J, Wolfarth B, Christle JW, Pressler A, Wagenpfeil S, Halle M. Associations between Borg's rating of perceived exertion and physiological measures of exercise intensity. European Journal of Applied Physiology. 2013;113(1):147-155.
Examples
unique(lactate_tests[c("test", "sport", "description")])
rowing <- subset(lactate_tests, test == "rowing")
lactate_threshold(rowing, intensity, lactate, heart_rate, sport = "rowing", plot = FALSE)
Lactate thresholds from a step test
Description
Estimates the lactate thresholds of a step incremental test with every method at once, or the methods you choose,
and returns them with the lactate, heart rate and RPE at each one. How each method is defined, how lactater computes
it and what the evidence says is in the article
The methods and the evidence; the essentials are
under Details.
Usage
lactate_threshold(
.data,
intensity_column,
lactate_column,
heart_rate_column = NULL,
rpe_column = NULL,
rpe_scale = c("6-20", "CR10"),
method = c("Log-log", "OBLA", "Bsln+", "Dmax", "LTP", "LTratio", "IAT", "Keul",
"Simon", "Stegmann"),
fit = c("3rd degree polynomial", "4th degree polynomial", "B-spline"),
include_baseline = FALSE,
sport = c("cycling", "running", "swimming", "rowing"),
loglog_restrainer = 1,
plot = TRUE,
dmax_anchor = c("original", "fitted", "observed"),
moddmax_rise = 0.4,
ltp_fix_first_slope = FALSE,
dmax_methods = c("Dmax", "ModDmax", "Exp-Dmax", "Log-Poly-ModDmax", "Log-Exp-ModDmax"),
fixed = c(2, 2.5, 3, 3.5, 4),
baseline_plus = c(0.5, 1, 1.5),
iat_rise = 1.5,
ltp_breakpoints = 2,
recovery = NULL,
stage_duration = NULL,
last_stage_duration = NULL,
site = NULL,
grade = NULL,
max_intensity = NULL,
sample = c("blood", "plasma", "sweat", "interstitial fluid"),
ci = FALSE,
n_boot = 500,
ci_level = 0.95,
seed = NULL,
lt1 = "Bsln + 0.5",
lt2 = "Log-Poly-ModDmax",
.by = NULL
)
Arguments
.data |
One row per stage: the intensity, the lactate at the end of the stage and, if recorded, the heart rate and the rating of perceived exertion (RPE). The resting sample, if any, is the row with intensity 0. Rows with a missing intensity or lactate are dropped with a warning. |
intensity_column |
The name of the intensity column: bare, as a string, or a variable that holds the name. |
lactate_column |
The name of the lactate column. |
heart_rate_column |
The name of the heart rate column, if recorded. The heart rate at each threshold is
interpolated linearly between the measured stages, and is |
rpe_column |
The name of the RPE column, if recorded. The RPE at each threshold is interpolated like the heart rate. A heart rate or RPE column with no values, as when none was written on the recording form, is left out. |
rpe_scale |
The RPE scale: |
method |
The method families to estimate, one or more of |
fit |
The curve fitted to the lactate values: |
include_baseline |
Whether the resting sample is part of the curve fitted with |
sport |
One of |
loglog_restrainer |
Deprecated. Log-log uses all stages, as Beaver et al. (1985) did. A value below |
plot |
Whether to draw the plot of each method into a |
dmax_anchor |
Where the ends of the Dmax lines sit: |
moddmax_rise |
The rise in lactate (mmol/L) between two consecutive stages that marks the first rise in
ModDmax. Defaults to |
ltp_fix_first_slope |
Whether the first LTP segment (before LTP1) is flat instead of estimated. Defaults to
|
dmax_methods |
Which Dmax variants to estimate when |
fixed |
The fixed lactate concentrations (mmol/L) for OBLA. Defaults to |
baseline_plus |
The rises above baseline (mmol/L) for Bsln+. Defaults to |
iat_rise |
The rise (mmol/L) above the lactate at LTratio that defines IAT. Defaults to |
ltp_breakpoints |
The number of lactate turn points to fit: |
recovery |
The lactate samples taken after the end of the test, for Stegmann: a data frame with the columns
|
stage_duration |
The duration of each stage, in minutes, or |
last_stage_duration |
The minutes completed in the last stage when the test ended before the stage did, or
|
site |
Where the blood was sampled: |
grade |
The treadmill grade (%) of a running test, or |
max_intensity |
Stages above this intensity are left out of every method (an incomplete last stage, or one
where lactate plateaus). Defaults to |
sample |
The sample the lactate was measured in: |
ci |
Whether to add bootstrap confidence intervals for the intensities ( |
n_boot |
Number of bootstrap replicates when |
ci_level |
Confidence level when |
seed |
A seed for the bootstrap replicates, so that the intervals are reproducible, or |
lt1, lt2 |
The thresholds that mark LT1 and LT2 for this athlete: a method (e.g. |
.by |
Names of columns that identify separate tests ( |
Details
Log-log
Two straight lines through log(lactate) against log(intensity), split at a measured stage that belongs to both
lines, at the split with "the lowest residuals sum of squares" (Beaver et al., 1985; Jamnick et al., 2018), with
at least three stages per line, so at least five stages. The threshold is where the lines cross, and the lactate
is read off the lines; it is NA when they cross outside the measured stages. Beaver et al. plotted lactate
against oxygen uptake; lactater uses the intensity.
OBLA
The intensity at which the fitted curve reaches each concentration in fixed, 2.0 to 4.0 mmol/L by default
(Heck et al., 1985; Kindermann et al., 1979; Skinner & McLellan, 1980), solved exactly. When the curve crosses a
target more than once (an early lactate dip), the last crossing where lactate rises through it is used: from there
on, lactate stays above the target. NA when the curve never reaches it.
Bsln+
The intensity at which the fitted curve is each rise in baseline_plus above the baseline (Berg et al., 1990;
Zoladz et al., 1995), solved like OBLA. The baseline is one sample: the resting sample when there is one, otherwise
the first stage. Studies differ here: Heuberger et al. (2018) used the mean of the samples before lactate rose, and
Jamnick et al. (2018) wrote only "baseline value(s)". One sample carries the analyzer error, about 0.1 mmol/L at
low concentrations (Bonaventura et al., 2015): on demo_data, a baseline 0.1 mmol/L lower or higher moves
Bsln + 0.5 from 83 W to 77 or 88 W. lactate_diagnostics() shows how much each threshold rests on the baseline
sample, and lactate_checks() says when the LT1 methods disagree.
The Dmax family
Every variant takes the point of its curve that lies furthest below a straight line ending at the last stage (the
perpendicular distance peaks at the same point as the vertical one). The variants differ in the curve, which fit
does not change, and in where the line starts:
Dmax: a 3rd degree polynomial, from the first stage (Cheng et al., 1992).
ModDmax: the polynomial, from the stage preceding the first rise of at least
moddmax_rise(Bishop et al., 1998). Bishop et al. and Jamnick et al. (2018) wrote a rise of "> 0.4"; lactater counts a rise of exactly 0.4 too, as Heuberger et al. (2018) did.Exp-Dmax: an exponential plus constant, from the first stage (Hughson et al., 1987).
Log-Poly-ModDmax and Log-Exp-ModDmax: the polynomial or the exponential, from the Log-log threshold, computed without the resting sample (Jamnick et al., 2018).
LTP1-Poly-ModDmax: the polynomial, from LTP1 (only when requested in
dmax_methods).
With dmax_anchor = "original" (default) the ends of the line sit where each paper put them: on the fitted curve
for Dmax and Exp-Dmax ("the two end points of the curve", Cheng et al., 1992), and at the measured lactate for
ModDmax ("the LT and the final lactate point", Bishop et al., 1998) and for the end of the Log-log variants ("the
final lactate point", Jamnick et al., 2018). "fitted" puts every end on the curve and "observed" every end at
the measured values (Heuberger et al., 2018); a line starting at the Log-log threshold or LTP1 always starts on the
curve. NA when the curve never dips below the line. The resting sample is never used.
LTP
Three connected straight segments through the exercise stages, with the two breakpoints found by exhaustive search
for the lowest residual sum of squares, at least two stages per segment (so at least six stages; four with
ltp_breakpoints = 1, which fits LTP1 only). ltp_fix_first_slope = TRUE keeps the first segment flat. LTP1 is
the first rise of lactate above resting levels and LTP2 the second, accelerated rise (Hofmann et al., 1997; Pokan
et al., 1997; Hofmann & Tschakert, 2017). The original papers fitted each turn point in its own window, on tests
with 1-min or 90-s stages; with the 6 to 10 stages of a step test the windows hold too few stages, so lactater fits
the three segments together. The fit has 6 parameters, so with 6 or 7 stages a single stage can move the turn
points a lot: lactate_checks() says so, and lactate_diagnostics() shows by how much.
LTratio and IAT
LTratio is the intensity where lactate / intensity is lowest on a B-spline through the stages (Dickhuth et al.,
1999). IAT, the individual anaerobic threshold of Dickhuth et al. (1999), is the intensity iat_rise (1.5 mmol/L)
above the lactate at LTratio, on the same spline (as described by Heuberger et al., 2018). Like OBLA and Bsln+, IAT
uses an absolute lactate step, so it assumes blood lactate.
Keul and Simon
The individual anaerobic thresholds of Keul et al. (1979) and Simon et al. (1981), for running only: the speed at
which a 3rd degree polynomial through the exercise stages rises by 1.26 mmol/L per km/h (Keul; a tangent at
51°34') or by 1 mmol/L per km/h (Simon; 45°). Keul et al. took the mean slope at 4 mmol/L of 60 tests of 20
cross-country skiers; Simon et al. took 45° as the point of the largest change in slope. Both tested on a treadmill
at a 5% grade with 3-min stages of 2 km/h, sampling earlobe blood, and set the slope on that protocol. A slope in
watts or m/s has no counterpart in these papers, so the methods run only with sport = "running". When the slope
reaches the value more than once, the last crossing where it rises through it is used; NA when it never does
within the stages.
Stegmann
The individual anaerobic threshold of Stegmann et al. (1981). After the test, blood lactate first rises and then
falls. From the point in recovery where it is back at its end-of-exercise value, a tangent is drawn to the lactate
curve of the test; the point of contact is the threshold, and the slope of the tangent the maximal rate of lactate
elimination. Stegmann et al. plotted lactate against time, so lactater puts the recovery minutes on the intensity
axis at the rate the test rose (the median increment between stages divided by stage_duration), interpolates
the return to the end-of-exercise value between the recovery samples after the post-exercise peak, and draws the
tangent to the curve fitted with fit. When lactate never falls back to its end value, or never rises after the
test, the threshold is NA.
Bootstrap intervals
With ci = TRUE, each threshold gets an approximate percentile bootstrap interval for the noise of the samples.
Each replicate keeps the curve fitted to the data and gives every stage its own residual with a random sign (a
wild bootstrap; Wu, 1986; Davidson & Flachaire, 2008), because the error of a lactate sample grows with its
concentration; the residuals are scaled for their leverage and widened for the few degrees of freedom a step test
leaves. Every method is then run again on each replicate, and the bounds are rounded outward. Replicates on which
a method fails are left out, with a warning. The interval does not include the bias of the fitted curve, errors
that are the same in every sample (analyzer bias, sampling site), or the day-to-day variation of the athlete,
which is larger (about 3.5-4% of the intensity; Heuberger et al., 2018; see compare_tests()). In simulated tests,
95% intervals held the true threshold in 84% to 97% of tests, depending on the method and the analyzer error; how
they were calibrated is in Checking the analysis.
Value
a lactate_thresholds tibble (plot it with autoplot() or plot()) with one row per threshold:
method_category |
the method family. |
method |
the method. |
fitting |
the curve the method used. |
intensity |
the intensity at the threshold, in whole watts, km/h to 0.1 or m/s to 0.01; |
intensity_low, intensity_high |
with |
pace |
for running, swimming and rowing, the pace at the threshold. |
lactate |
the lactate at the threshold. |
heart_rate |
the heart rate at the threshold, when |
rpe |
the RPE at the threshold, when |
plot |
the plot of the method, when |
With .by, the columns that identify each test come first.
References
Beaver WL, Wasserman K, Whipp BJ. Improved detection of lactate threshold during exercise using a log-log transformation. Journal of Applied Physiology. 1985;59(6):1936–40.
Heck H, Mader A, Hess G, Mücke S, Müller R, Hollmann W. Justification of the 4-mmol/l Lactate Threshold. International Journal of Sports Medicine. 1985;06(03):117–30.
Kindermann W, Simon G, Keul J. The significance of the aerobic-anaerobic transition for the determination of work load intensities during endurance training. European Journal of Applied Physiology and Occupational Physiology. 1979;42(1):25–34.
Skinner JS, Mclellan TH. The Transition from Aerobic to Anaerobic Metabolism. Research Quarterly for Exercise and Sport. 1980;51(1):234–48.
Bonaventura JM, Sharpe K, Knight E, Fuller KL, Tanner RK, Gore CJ. Reliability and accuracy of six hand-held blood lactate analysers. Journal of Sports Science and Medicine. 2015;14:203-214.
Berg A, Jakob E, Lehmann M, Dickhuth HH, Huber G, Keul J. Current aspects of modern ergometry. Pneumologie. 1990;44(1):2–13.
Zoladz JA, Rademaker AC, Sargeant AJ. Non-linear relationship between O2 uptake and power output at high intensities of exercise in humans. The Journal of Physiology. 1995;488(1):211–7.
Cheng B, Kuipers H, Snyder A, Keizer H, Jeukendrup A, Hesselink M. A New Approach for the Determination of Ventilatory and Lactate Thresholds. International Journal of Sports Medicine. 1992;13(07):518–22.
Bishop D, Jenkins DG, Mackinnon LT. The relationship between plasma lactate parameters, Wpeak and 1-h cycling performance in women. Med Sci Sports Exerc. 1998;30(8):1270–5.
Hughson RL, Weisiger KH, Swanson GD. Blood lactate concentration increases as a continuous function in progressive exercise. Journal of Applied Physiology. 1987;62(5):1975–81.
Jamnick NA, Botella J, Pyne DB, Bishop DJ. Manipulating graded exercise test variables affects the validity of the lactate threshold and VO2peak. PLOS ONE. 2018;13(7):e0199794.
Davidson R, Flachaire E. The wild bootstrap, tamed at last. Journal of Econometrics. 2008;146(1):162-169. doi:10.1016/j.jeconom.2008.08.003
Wu CFJ. Jackknife, bootstrap and other resampling methods in regression analysis. The Annals of Statistics. 1986;14(4):1261-1295. doi:10.1214/aos/1176350142
Heuberger JAAC, Gal P, Stuurman FE, de Muinck Keizer WAS, Mejia Miranda Y, Cohen AF. Repeatability and predictive value of lactate threshold concepts in endurance sports. PLOS ONE. 2018;13(11):e0206846.
Hofmann P, Tschakert G. Intensity- and Duration-Based Options to Regulate Endurance Training. Front Physiol. 2017;8:337.
Hofmann P, Pokan R, von Duvillard SP, Seibert FJ, Zweiker R, Schmid P. Heart rate performance curve during incremental cycle ergometer exercise in healthy young male subjects. Med Sci Sports Exerc. 1997;29(6):762–8.
Pokan R, Hofmann P, Von Duvillard SP, et al. Left ventricular function in response to the transition from aerobic to anaerobic metabolism. Med Sci Sports Exerc. 1997;29(8):1040–7.
Dickhuth H-H, Yin L, Niess A, et al. Ventilatory, Lactate-Derived and Catecholamine Thresholds During Incremental Treadmill Running: Relationship and Reproducibility. International Journal of Sports Medicine. 1999;20(02):122–7.
Kuipers H, Rietjens G, Verstappen F, Schoenmakers H, Hofman G. Effects of stage duration in incremental running tests on physiological variables. International Journal of Sports Medicine. 2003;24(7):486–91. doi:10.1055/s-2003-42020
Stegmann H, Kindermann W, Schnabel A. Lactate kinetics and individual anaerobic threshold. International Journal of Sports Medicine. 1981;2(3):160–5. doi:10.1055/s-2008-1034604
Keul J, Simon G, Berg A, Dickhuth HH, Goerttler I, Kübel R. Bestimmung der individuellen anaeroben Schwelle zur Leistungsbewertung und Trainingsgestaltung. Deutsche Zeitschrift für Sportmedizin. 1979;30(7):212–8.
Simon G, Berg A, Dickhuth HH, Simon-Alt A, Keul J. Bestimmung der anaeroben Schwelle in Abhängigkeit vom Alter und von der Leistungsfähigkeit. Deutsche Zeitschrift für Sportmedizin. 1981;32(1):7–14.
Examples
results <- lactate_threshold(demo_data, intensity, lactate, heart_rate, plot = FALSE)
results
lactate_checks(results)
autoplot(results, method = "Dmax")
Lactate thresholds by group
Description
Runs lactate_threshold() once per group and binds the results, with the group columns first.
For internal use only.
Usage
lactate_threshold_by(.data, .by, arguments)
Arguments
.data |
The raw data. |
.by |
The grouping column names. |
arguments |
The other |
Thresholds over a series of tests
Description
Fits a straight line through each threshold over three or more tests of the same athlete, to show the trend with its uncertainty instead of reading noise from one test to the next.
Usage
lactate_trend(..., day, level = 0.9)
## S3 method for class 'lactate_trend'
autoplot(
object,
method = NULL,
view = NULL,
cv = NULL,
lt1 = NULL,
lt2 = NULL,
...
)
Arguments
... |
The results of |
day |
The date of each test, in the same order as |
level |
The confidence level of the interval of the trend, and of the noise band in the |
object |
The result of |
method |
The methods to plot in the |
view |
|
cv |
The test-retest coefficient of variation (%) for the noise band, as in |
lt1, lt2 |
The thresholds that mark LT1 and LT2 in the |
Value
a lactate_trend tibble with one row per method found in every test: method, tests, the
change per 30 days (per_30_days) with its confidence interval at level (low, high), and the first and last
fitted values. Plot it with autoplot(): up to five tests as their lactate curves and training zones, more as each
threshold over time.
References
Hopkins WG. Measures of reliability in sports medicine and science. Sports Medicine. 2000;30(1):1-15.
Examples
series <- subset(lactate_tests, athlete == "cyclist_series")
days <- c(0, 163, 1107, 1865, 2046)
tests <- lapply(days, function(d) {
lactate_threshold(subset(series, day == d), intensity, lactate, plot = FALSE)
})
## the test dates are not in the data: an example start date
trend <- lactate_trend(!!!tests, day = as.Date("2019-01-14") + days)
trend
autoplot(trend, lt1 = "Bsln + 0.5", lt2 = "ModDmax")
autoplot(trend, view = "thresholds")
Training zones from two lactate thresholds
Description
Builds three or five training zones from a first (LT1) and a second (LT2) lactate threshold, with the zone boundaries placed at the boundaries of the exercise intensity domains: below LT1 is the moderate domain, from LT1 to LT2 the heavy domain, and above LT2 the severe domain (Jamnick et al., 2020). You choose which thresholds mark LT1 and LT2.
Usage
lactate_zones(x, lt1 = NULL, lt2 = NULL, model = 3, split = "relative")
## S3 method for class 'lactate_zones'
autoplot(
object,
heart_rate = getOption("lactater.heart_rate", "panel"),
view = c("curve", "ruler"),
...
)
Arguments
x |
The result of |
lt1, lt2 |
The thresholds that mark LT1 and LT2: a method in |
model |
The number of zones: |
split |
How to split the moderate and severe domains in the five-zone model: |
object |
The result of |
heart_rate |
How the plot shows the heart rate of the stages, when the test has it: |
view |
|
... |
Not used. |
Details
-
Three zones (
model = 3): zone 1 below LT1, zone 2 from LT1 to LT2, zone 3 above LT2 (Hofmann & Tschakert, 2017). -
Five zones (
model = 5): zones 1 and 2 split the moderate domain, zone 3 is the heavy domain, and zones 4 and 5 split the severe domain, as in the five-zone scale described by Seiler (2010) and Tonnessen et al. (2014), where zones 1-2 lie below LT1, zone 3 between LT1 and LT2, and zones 4-5 above LT2. The two splits inside a domain are set bysplit. The five zones are not named: the scale gives them no names, and the names of other scales belong to other boundaries (the "threshold" level of the five-level scale in Jamnick et al., 2020, Table 1, lies around LT2, not above it). The two splits:-
"relative"(default): zone 1|2 at 75% of LT1, and zone 4|5 halfway between LT2 and the highest intensity of the test. In the five-zone scale of Seiler (2010, Table 1), zone 2 starts at 66% of the maximal oxygen uptake and zone 3 at 81%; converted with the oxygen cost of cycling and running (Glass & Dwyer, 2007), our own calculation, that puts zone 2 at about 77% of the power at LT1 and 80% of the running speed. Sitko et al. (2025) place zone 2 "immediately below" LT1, at about 80% to 90% of the heart rate at LT1, which on the cycling tests of lactate_tests (our count, where that heart rate falls within the stages) starts it at a median of about 65% of the power at LT1. 75% lies between the two and needs no heart rate. Zone 5 starts at 94% of the maximal oxygen uptake, halfway between the start of zone 4 (88%) and the maximum. Both cuts are approximations: no physiological marker separates them (Bishop et al., 2025), and the scale's zones are "somewhat arbitrary" (Seiler, 2010). -
"seiler": where the fitted curve reaches 1.5 mmol/L (zone 1|2) and 6.0 mmol/L (zone 4|5), the blood lactate limits of the five-zone scale in Seiler (2010). A cut that falls outside its domain, or is not reached, uses the relative split instead, with a warning. Two intensities,
c(zone 1|2, zone 4|5), for your own cuts.
-
LT1 is usually a "first rise" threshold ("Log-log", "Bsln + 0.5", "Bsln + 1.0", "LTratio", "LTP1"), and
LT2 a threshold aimed at the maximal lactate steady state (MLSS) or the second, abrupt rise (the Dmax family,
"LTP2", "IAT", "Stegmann", "Keul" and "Simon" (running), and "Bsln + 1.5", which estimated the MLSS
in two studies; Jamnick et al., 2020).
These are two of the three categories of threshold concepts of Faude et al. (2009); fixed values such as
"OBLA 4.0" are the third. Against a measured MLSS these landed from about 40 W below to 40 W above it on average,
by
method, protocol and athletes (Hauser et al., 2014; Ploszczyca et al., 2020), and whether the MLSS or the critical
power marks the boundary between the heavy and severe domains is debated (Jones et al., 2019). Different choices
give different zones, so report which ones you used.
The default pair has the best evidence for each boundary and moves least with a single stage. Bsln + 0.5 is among
the most defensible markers of the moderate-heavy boundary (Jamnick et al., 2020), and Log-Poly-ModDmax estimated the
MLSS with 4-min stages (Jamnick et al., 2018). Left out one stage at a time on the cycling tests of
lactate_tests, Bsln + 0.5 moved by a median 1.7% (jackknife standard error) and Log-Poly-ModDmax by 8.6%, against
23% for LTP1, whose three-segment fit has few stages to spare in a step test. Bsln + 0.5 rests on a single baseline
sample, so check that sample (see lactate_diagnostics()).
Value
a lactate_zones tibble with one row per zone: zone, domain, description, intensity_from,
intensity_to, the matching pace range (running, swimming, rowing), the lactate range on the fitted curve
(NA at a boundary outside the stages, where the curve is not extrapolated), and the heart_rate and rpe ranges
(when available; a boundary below the first stage is extrapolated from the first three stages). The first zone has
no lower bound and the last no upper bound.
Plot it with autoplot().
References
Faude O, Kindermann W, Meyer T. Lactate threshold concepts: how valid are they? Sports Medicine. 2009;39(6):469-490.
Hofmann P, Tschakert G. Intensity- and duration-based options to regulate endurance training. Frontiers in Physiology. 2017;8:337.
Bishop DJ, Beck B, Biddle SJH, et al. Physical activity and exercise intensity terminology: a joint American College of Sports Medicine (ACSM) expert statement and Exercise and Sport Science Australia (ESSA) consensus statement. Medicine & Science in Sports & Exercise. 2025;57(11):2599-2613. doi:10.1249/MSS.0000000000003795
Glass S, Dwyer GB, editors. ACSM's metabolic calculations handbook. Baltimore: Lippincott Williams & Wilkins; 2007.
Hauser T, Adam J, Schulz H. Comparison of selected lactate threshold parameters with maximal lactate steady state in cycling. International Journal of Sports Medicine. 2014;35(6):517-521.
Jamnick NA, Botella J, Pyne DB, Bishop DJ. Manipulating graded exercise test variables affects the validity of the lactate threshold and VO2peak. PLOS ONE. 2018;13(7):e0199794.
Jamnick NA, Pettitt RW, Granata C, Pyne DB, Bishop DJ. An examination and critique of current methods to determine exercise intensity. Sports Medicine. 2020;50(10):1729-1756.
Jones AM, Burnley M, Black MI, Poole DC, Vanhatalo A. The maximal metabolic steady state: redefining the 'gold standard'. Physiological Reports. 2019;7(10):e14098.
Ploszczyca K, Jazic D, Piotrowicz Z, Chalimoniuk M, Langfort J, Czuba M. Comparison of maximal lactate steady state with anaerobic threshold determined by various methods based on graded exercise test with 3-minute stages in elite cyclists. BMC Sports Science, Medicine and Rehabilitation. 2020;12:70.
Seiler S. What is best practice for training intensity and duration distribution in endurance athletes? International Journal of Sports Physiology and Performance. 2010;5(3):276-291.
Sitko S, Artetxe X, Bonnevie-Svendsen M, et al. What is "zone 2 training"?: experts' viewpoint on definition, training methods, and expected adaptations. International Journal of Sports Physiology and Performance. 2025;20(11):1614-1617. doi:10.1123/ijspp.2024-0303
Tonnessen E, Sylta O, Haugen TA, Hem E, Svendsen IS, Seiler S. The road to gold: training and peaking characteristics in the year prior to a gold medal endurance performance. PLOS ONE. 2014;9(7):e101796.
Examples
results <- lactate_threshold(demo_data, intensity, lactate, heart_rate, plot = FALSE)
lactate_zones(results)
lactate_zones(results, model = 5)
lactate_zones(results, lt1 = "Log-log", lt2 = "ModDmax", model = 5, split = "seiler")
Colors of the current plot style
Description
Colors of the current plot style
Usage
lactater_colours(style = getOption("lactater.style", "light"))
Arguments
style |
|
Value
a named vector of colors.
The leave-one-stage-out grid
Description
One row per method and one column per stage left out, red where the threshold rises without that stage and blue where it falls, darker the further it moves, with the move written in the cells where it is at least 3% of the threshold. For internal use only.
Usage
leave_one_out_panel(object, colours, sport)
Arguments
object |
The result of |
colours |
The plot colours. |
sport |
The sport. |
A minimal result object from a stored lactater attribute, for helpers that take results
Description
A minimal result object from a stored lactater attribute, for helpers that take results
Usage
list_to_thresholds(info)
Arguments
info |
A |
Log-log breakpoint
Description
Fits two straight lines to log(lactate) vs log(intensity) of the measured points and returns the unrounded intensity where they cross. For internal use only.
Usage
loglog_breakpoint(points, loglog_restrainer = 1)
Arguments
points |
The measured points ( |
loglog_restrainer |
The share of the points to use (see |
Value
a list with the breakpoint intensity and the fit from fit_two_lines() (in log-log space).
LTratio minimum
Description
LTratio minimum
Usage
ltratio_minimum(data, curve, settings)
Arguments
data |
The data retrieved from |
curve |
The user-selected curve from |
settings |
The analysis settings. |
Value
a list with the B-spline spline, the intensity of the minimum ratio, its value
ratio_min, and the ratio curve over the exercise stages.
The threshold: dot, dotted guides to both axes, and the label joined by a curved arrow
Description
The label sits above the threshold, to the left when there is room (above a rising curve the plot is
empty) and to the right otherwise; with below, below the curve and to the right, when the threshold leaves room
under it. A label steps over (or, below the curve, under) the lines of avoid, the Dmax line and the drawn
curve, when one would cross it. For internal use only.
Usage
marker_layers(
x,
y,
label,
x_range,
y_range,
log_axes,
colours,
below = FALSE,
avoid = list()
)
Arguments
x, y |
Where the threshold sits. |
label |
A list with the |
x_range, y_range |
The ranges of the plot. |
log_axes |
Whether both axes are logarithmic. |
colours |
The plot colours. |
below |
Whether to put the label below the curve. |
avoid |
The lines ( |
Bsln+
Description
It applies the Bsln+ methods: the intensity at which the fitted curve reaches each rise in
settings$baseline_plus (0.5, 1.0, and 1.5 mmol/L by default) above the baseline. The baseline is the
first sample: the resting sample when there is one, otherwise the first stage.
Usage
method_bsln_plus(data, curve, settings)
Arguments
data |
The data retrieved from |
curve |
The user-selected curve from |
settings |
The analysis settings. |
Value
a list with the result rows (method, fitting, intensity, lactate) and the plot_data
for each row.
Dmax
Description
It applies the Dmax methods: "Dmax", "ModDmax", "Exp-Dmax", "Log-Poly-ModDmax", and "Log-Exp-ModDmax".
Usage
method_dmax(data, curve, settings)
Arguments
data |
The data retrieved from |
curve |
The user-selected curve from |
settings |
The analysis settings. |
Details
Every variant looks for the point on the fitted curve that lies furthest below a straight line between two anchors. The variants differ in the fitted curve and in where the line starts:
Dmax: 3rd degree polynomial, from the first stage.
ModDmax: 3rd degree polynomial, from the stage preceding the first rise of at least
moddmax_risemmol/L.Exp-Dmax: exponential plus constant, from the first stage.
LTP1-Poly-ModDmax: 3rd degree polynomial, from LTP1 (only when requested in
dmax_methods).Log-Poly-ModDmax: 3rd degree polynomial, from the Log-log threshold.
Log-Exp-ModDmax: exponential plus constant, from the Log-log threshold.
The line always ends at the last stage. The resting sample is never used. With
settings$dmax_anchor = "observed" the line's ends sit at the measured lactate of the start
and end stages instead of on the fitted curve; the Log-log and LTP1 starts have no measured
value, so they always sit on the curve. "original" follows each paper: on the curve for Dmax
and Exp-Dmax, at the measured values for the ModDmax variants.
Value
a list with the result rows (method, fitting, intensity, lactate) and the plot_data
for each row.
Individual Anaerobic Threshold (IAT)
Description
It applies the IAT method: the intensity at which the B-spline curve is settings$iat_rise
(1.5 mmol/L by default) above the lactate at LTratio (Dickhuth et al., 1999).
Usage
method_iat(data, curve, settings)
Arguments
data |
The data retrieved from |
curve |
The user-selected curve from |
settings |
The analysis settings. |
Value
a list with the result rows (method, fitting, intensity, lactate) and the plot_data
for each row.
Log-log
Description
It applies the Log-log method. Two straight lines are fitted to log(lactate) vs log(intensity),
split at the measured stage with the lowest total residual sum of squares, and the threshold is
where they cross (see fit_two_lines()). The lactate at the threshold is read off the lines. The resting
sample is never used: it has no exercise intensity.
Usage
method_loglog(data, curve, settings)
Arguments
data |
The data retrieved from |
curve |
The user-selected curve from |
settings |
The analysis settings. |
Value
a list with the result rows (method, fitting, intensity, lactate) and the plot_data
for each row.
Lactate turn points (LTP)
Description
It applies the LTP methods: LTP1, and LTP2. A three-segment continuous linear regression
is fitted to the measured points; the two breakpoints with the lowest residual sum of squares
are found by exhaustive search (see fit_breakpoints()). With ltp_fix_first_slope the first
segment is flat. Hofmann et al. (1997) and Pokan et al. (1997) fitted each turn point with its own
two-line regression inside a window, on tests with 1-min or 90-s stages; with the 6 to 10 stages of a
typical step test those windows hold too few stages, so the three segments are fitted together.
With settings$ltp_breakpoints = 1 (a submaximal test) two segments are fitted, the breakpoint is
LTP1 and LTP2 is NA. The lactate at each turn point is read off the fitted segments. The resting
sample is never used: it has no exercise intensity.
Usage
method_ltp(data, curve, settings)
Arguments
data |
The data retrieved from |
curve |
The user-selected curve from |
settings |
The analysis settings. |
Value
a list with the result rows (method, fitting, intensity, lactate) and the plot_data
for each row.
Minimum Lactate-Intensity Ratio (LTratio)
Description
It applies the LTratio method: the intensity where lactate / intensity, on a B-spline fit, is
lowest. The search runs over the exercise stages only.
Usage
method_ltratio(data, curve, settings)
Arguments
data |
The data retrieved from |
curve |
The user-selected curve from |
settings |
The analysis settings. |
Value
a list with the result rows (method, fitting, intensity, lactate) and the plot_data
for each row.
What each method does, in one line
Description
What each method does, in one line
Usage
method_note(method, settings)
Arguments
method |
The method label. |
settings |
The analysis settings. |
Value
the plot subtitle.
Onset of Blood Lactate Accumulation (OBLA)
Description
It applies the OBLA methods: the intensity at which the fitted curve reaches each fixed
concentration in settings$fixed (2.0, 2.5, 3.0, 3.5, and 4.0 mmol/L by default).
Usage
method_obla(data, curve, settings)
Arguments
data |
The data retrieved from |
curve |
The user-selected curve from |
settings |
The analysis settings. |
Value
a list with the result rows (method, fitting, intensity, lactate) and the plot_data
for each row.
Method names with their subscripts
Description
Plotmath, so that "ModDmax" is drawn as ModD with a subscript max and "LTratio" as LT with a subscript ratio.
Plotmath keeps the labels plain element_text(), which users can restyle. method_plotmath() gives the text to
parse (for label_parsed and parse = TRUE), method_label() the parsed expressions, and method_html() the same
names for HTML.
Usage
method_plotmath(x)
method_html(x)
method_label(x, bold = FALSE)
Arguments
x |
Method names. |
bold |
Whether to draw them in bold, for titles. |
Value
a character vector, or an expression vector.
Method result
Description
The common shape of every method_*() result. For internal use only.
Usage
method_result(method, fitting, intensity, lactate, plot_data)
Arguments
method, fitting, intensity, lactate |
The result columns. |
plot_data |
What the plot needs, either one list shared by all rows or a list per row. |
Keul and Simon
Description
The individual anaerobic thresholds of Keul et al. (1979) and Simon et al. (1981): the speed at which a 3rd degree polynomial through the exercise stages rises by 1.26 mmol/L per km/h (an angle of 51°34') or by 1 mmol/L per km/h (45°). Both fitted lactate against running speed in km/h, on treadmill tests at a 5% grade with 3-min stages of 2 km/h; a slope in other units has no meaning, so these methods run on running tests only. When the slope reaches the value more than once, the last crossing where it rises through it is used. For internal use only.
Usage
method_slope(data, method, slope)
Arguments
data |
The data retrieved from |
method |
The method name. |
slope |
The slope (mmol/L per km/h). |
Value
a list with the result rows (method, fitting, intensity, lactate) and the plot_data.
Individual anaerobic threshold of Stegmann et al. (1981)
Description
It applies the Stegmann method: blood lactate is followed into recovery until it falls back to its
end-of-exercise value, and from that point a tangent is drawn to the lactate curve of the test; the point of
contact is the threshold, and the slope of the tangent the maximal rate of lactate elimination (Stegmann et al.,
1981). Lactate is followed over time, so recovery minutes are put on the intensity axis at the rate the test rose
(the median increment between stages divided by settings$stage_duration). The time lactate is back at its
end-of-exercise value is interpolated linearly between the recovery samples after the post-exercise peak.
Usage
method_stegmann(data, curve, settings)
Arguments
data |
The data retrieved from |
curve |
The user-selected curve from |
settings |
The analysis settings. |
Value
a list with the result rows (method, fitting, intensity, lactate) and the plot_data.
Data used for modeling
Description
The exercise stages, plus the positioned resting sample when include_baseline = TRUE.
The prepared data keeps the resting sample at intensity 0; only the fit places it one step
before stage 1 (see position_rest()).
Usage
modeling_data(.data, include_baseline)
Arguments
.data |
The data retrieved from |
include_baseline |
Whether to include the resting sample. |
Build a lactate threshold result
Description
Adds the class and keeps what plotting needs (data, curve, settings, unrounded intensities). For internal use only.
Usage
new_lactate_thresholds(
out,
data,
curve,
settings,
plot_data,
intensity,
intervals = NULL
)
Arguments
out |
The result tibble. |
data |
The data retrieved from |
curve |
The user-selected curve. |
settings |
The analysis settings. |
plot_data |
What each row's plot needs. |
intensity |
The unrounded threshold intensities. |
intervals |
The unrounded confidence intervals (a |
A secondary point with a small label
Description
A secondary point with a small label
Usage
other_point(x, y, label, colours)
Arguments
x, y |
Where the point sits. |
label |
The label below it, as plotmath (a quoted string, or e.g. |
colours |
The plot colours. |
The table beside the overview plot
Description
The table beside the overview plot
Usage
overview_table(rows, colours, sport, intervals)
Arguments
rows |
The thresholds (one row each, with |
colours |
The plot colours. |
sport |
The sport. |
intervals |
Whether the thresholds have confidence intervals. |
The change of LT1 and LT2 in one line each
Description
The change of LT1 and LT2 in one line each
Usage
pair_change(x, info)
Arguments
x |
The comparison. |
info |
The |
Value
a character vector named LT1 and LT2 (only those chosen by method and found in both tests).
Combine lactate threshold methods into one plot
Description
The one-line note under each title is dropped: it would crowd a grid of small panels.
Usage
plot_methods(plots, ...)
Arguments
plots |
The |
... |
Additional arguments passed onto |
Value
a patchwork object
Overview plot
Description
Every threshold of the test on one intensity axis: the fitted curve on top, and below it one row per method,
grouped by what it aims at (the first rise, the second rise, or a fixed lactate), with the threshold as a dot, its
value beside it, and its confidence interval as a line (when computed). Two shaded bands, through the curve and the
rows, show how far the methods aimed at LT1 and at LT2 lie apart. The title gives LT1 and LT2 when both chosen
methods were computed. A table on the right prints the lactate, heart rate, RPE, percentage of the peak intensity
and of the highest heart rate at each threshold, with the lactate shaded by its value.
Usage
plot_overview(object, info, chosen = character())
Arguments
object |
The result of |
info |
Its |
chosen |
The methods that mark LT1 and LT2, drawn in green. |
The lactate curves and training zones of a few tests
Description
plot_series() draws every lactate curve (the latest test in blue, the first in dark gray, the tests between in
light gray), then the heart rate of the stages in the same colors with LT1 and LT2 as filled points (when the tests
have it), above the five zones of each test as one bar that ends at its highest stage, on one intensity axis.
series_zones() gives the five zones of each test, and names the test when a threshold is missing.
date_labels() names tests by date ("3 Nov 2026") or by day ("Day 58"). For internal use only.
Usage
plot_series(tests, labels, lt1, lt2)
series_zones(tests, labels, lt1, lt2)
date_labels(day)
Arguments
tests |
Results of |
labels |
The name of each test, from |
lt1, lt2 |
The thresholds that mark LT1 and LT2, as in |
day |
Dates, or days since the first test. |
Plot one lactate threshold
Description
Draws what the method fitted, the measured stages, and the threshold with a two-line label joined by an arrow: the intensity, then the pace, lactate and heart rate. For internal use only.
Usage
plot_threshold(
method,
intensity,
lactate,
heart_rate,
plot_data,
data,
settings,
interval = NULL,
rpe = NA
)
Arguments
method |
The method label. |
intensity, lactate, heart_rate, rpe |
The threshold values. |
plot_data |
What the method drew on: the fitted |
data |
The data retrieved from |
settings |
The analysis settings ( |
interval |
The confidence interval of the intensity ( |
Value
a ggplot2 object, or NA when the threshold could not be estimated.
The zones as a ruler
Description
One bar of the zones on the intensity axis, and under each boundary the intensity, pace, heart rate and RPE where one zone ends and the next begins (those the zones have). For internal use only.
Usage
plot_zone_ruler(object, zones, limits, colours)
Arguments
object |
The result of |
zones |
Its |
limits |
The intensity range of the plot. |
colours |
The plot colours. |
A curved arrow that ends on the edge of a dot
Description
The arrow is laid out in inches when the plot is drawn, so it stops at the edge of the dot and points at its center whatever the size of the plot. For internal use only.
Usage
pointer_layer(from, to, bend, colour, gap = 2.6)
Arguments
from, to |
The label end and the dot center, in data units (ggplot2 applies the scale transformation). |
bend |
How far the middle of the arrow bows out, as a share of its length; the sign sets the side. |
colour |
The arrow color. |
gap |
Where the arrow stops, in mm from the dot center. |
Polarization index (Treff et al., 2019)
Description
Polarization index (Treff et al., 2019)
Usage
polarization_index(share)
Arguments
share |
The fractions of time in zones 1, 2 and 3. |
Value
the index, 0 when zone 3 is empty, and NA when zone 3 exceeds zone 1.
Position the resting sample
Description
The resting sample (intensity 0) is placed one step before the first stage, so it can be
plotted next to the curve and, with include_baseline = TRUE, fitted. The step is the median
step between stages, so a missed stage or a short last stage does not shift it.
Usage
position_rest(.data)
Arguments
.data |
The data retrieved from |
Value
.data with the resting intensity replaced; unchanged when there is no resting sample.
Predict performance from a step test
Description
Estimates race times in running and time-trial power in cycling from the thresholds of a step test, with a 95% prediction range. Each estimate comes from an equation fitted on one threshold, in one population, with one test protocol, so it only applies to tests run the same way:
Usage
predict_performance(x, method = NULL, equation = NULL)
## S3 method for class 'lactate_performance'
autoplot(object, ...)
Arguments
x |
The result of |
method |
The thresholds to predict from. Defaults to every threshold of |
equation |
Your own equation(s) from |
object |
The result of |
... |
Not used. |
Details
-
Running: 5 km, 10 km, half marathon and marathon from IAT (Roecker et al., 1998; 427 competitive runners, treadmill at 2% grade, 3-min stages, +2 km/h, earlobe hemolyzed whole blood), and the marathon from OBLA 4.0 (Sjödin & Jacobs, 1981; 18 male marathon runners, 4-min stages, fingertip blood). lactater's
IATfollows the definition of Roecker et al. (the lowest lactate/speed ratio + 1.5 mmol/L). -
Cycling: 45-min time-trial power from LTP2, IAT, ModDmax, OBLA 4.0, Dmax or Log-Poly-ModDmax, fitted with lactater's own thresholds on the open data of Heuberger et al. (2018; 48 well-trained men, 5-min stages, +25 W, venous blood).
-
Your own equation, from
calibrate_performance(): any threshold, sport and protocol, fitted on your own athletes' tests and races. This is the best option when you have the data.
Other thresholds are not offered with the published equations: an equation fitted on one threshold gives wrong numbers for another.
The 95% range is the prediction interval of the regression: the predicted intensity +/- t x the standard error of
the estimate x sqrt(1 + 1/n + (threshold - mean)^2 / ((n - 1) * sd^2)). Roecker et al. (1998) reported neither
that error nor the mean of the thresholds per distance, so the error is derived from their tables as
SD x sqrt(1 - r^2), with the SD of race speed computed from the SD of race time, and the last term is left out.
Expect an individual error of about +/- 10% of race speed in running and +/- 25 W in cycling with the published
equations. A warning is given when the steps of the test differ from those of the source study (a different step
changes the threshold: Coen et al., 2001; Kuipers et al., 2003), and when a threshold lies outside the range of
the source sample. Stage duration, sampling site and treadmill grade are checked too when the test was analyzed with
stage_duration, site and grade (see lactate_threshold()); otherwise compare them with the protocol in the
printed header. A recent race is a better predictor of the next one than any step test
(Vickers & Vertosick, 2016).
Value
a lactate_performance tibble with one row per estimate: event, method, threshold, the predicted
mean race intensity (speed or power, in the unit of the sport) with its 95% range (intensity_low,
intensity_high), for races over a distance the time with its range (time_low, time_high), and the source.
Plot it with autoplot(); the article Predicting performance shows it.
References
Coen B, Urhausen A, Kindermann W. Individual anaerobic threshold: methodological aspects of its assessment in running. International Journal of Sports Medicine. 2001;22(1):8-16.
Heuberger JAAC, Gal P, Stuurman FE, de Muinck Keizer WAS, Mejia Miranda Y, Cohen AF. Repeatability and predictive value of lactate threshold concepts in endurance sports. PLOS ONE. 2018;13(11):e0206846.
Kuipers H, Rietjens G, Verstappen F, Schoenmakers H, Hofman G. Effects of stage duration in incremental running tests on physiological variables. International Journal of Sports Medicine. 2003;24(7):486-491.
Roecker K, Schotte O, Niess AM, Horstmann T, Dickhuth HH. Predicting competition performance in long-distance running by means of a treadmill test. Medicine & Science in Sports & Exercise. 1998;30(10):1552-1557.
Sjödin B, Jacobs I. Onset of blood lactate accumulation and marathon running performance. International Journal of Sports Medicine. 1981;2(1):23-26.
Vickers AJ, Vertosick EA. An empirical study of race times in recreational endurance runners. BMC Sports Science, Medicine and Rehabilitation. 2016;8:26.
Examples
runner <- data.frame(
intensity = c(0, 8, 10, 12, 14, 16, 18),
lactate = c(1.0, 1.1, 1.2, 1.6, 2.6, 4.6, 8.0)
)
results <- lactate_threshold(runner, intensity, lactate, sport = "running", plot = FALSE)
prediction <- predict_performance(results)
prediction
## the plot is short and wide: see it in the article Predicting performance
if(interactive()) autoplot(prediction)
Prepare raw data
Description
It organizes and renames the raw data.
Usage
prepare_data(
.data,
intensity_column,
lactate_column,
heart_rate_column = NULL,
rpe_column = NULL,
rpe_scale = "6-20"
)
Arguments
.data |
The raw data. |
intensity_column |
The name of the intensity column, as a string. |
lactate_column |
The name of the lactate column, as a string. |
heart_rate_column |
The name of the heart rate column, as a string, or |
rpe_column |
The name of the rating of perceived exertion column, as a string, or |
rpe_scale |
The RPE scale, |
Value
a tibble with the following columns:
intensity |
The intensity column. |
lactate |
The lactate column. |
heart_rate |
The heart rate column, if applicable. |
rpe |
The rating of perceived exertion column, if applicable. |
Check and tidy the recovery samples
Description
Check and tidy the recovery samples
Usage
prepare_recovery(recovery, stage_duration)
Arguments
recovery |
The |
stage_duration |
The |
Value
a data frame with time and lactate, ordered by time, without missing samples.
Read a recorded step test
Description
Reads a test recorded with the web form of recording_sheet() (an .xlsx with the sheets stages, recovery and
test), or a CSV in the columns of the template, and returns what lactate_threshold() and test_report() need.
Needs the readxl package for an .xlsx.
Usage
read_recording(file)
Arguments
file |
The |
Value
a list with
-
stages: one row per stage, the resting sample first, withintensity,lactate,heart_rateandrpe(andclock_timeandnotesfrom the form). When the form has a "time in the last stage" shorter than the stage duration, the intensity of the last stage is time-weighted, aslactate_threshold(last_stage_duration = )does, and a message says so. A last step that is already smaller than the one before is taken as weighted and kept; -
recovery: the recovery samples (time, in minutes, andlactate) for the Stegmann threshold, orNULL; -
athlete:name,date,sex,ageandbody_mass, as recorded, fortest_report(athlete = )orreport_header(); -
sport,sample,rpe_scale,stage_duration(min),siteandgrade(%), forlactate_threshold():"cycling","blood"and"6-20"when not recorded, andNULLfor the others; -
protocol: the protocol in one line, fortest_report(protocol = ), orNULL; -
details: every field of the form, as text.
See Also
recording_sheet() for the form.
Examples
## a test recorded with the CSV template
file <- tempfile(fileext = ".csv")
write.csv(demo_data[c("intensity", "lactate", "heart_rate", "rpe")], file, row.names = FALSE)
test <- read_recording(file)
results <- lactate_threshold(
test$stages, intensity, lactate, heart_rate, rpe_column = rpe, plot = FALSE
)
The protocol of a recorded test in one line
Description
The protocol of a recorded test in one line
Usage
recording_protocol(fields, sport)
Arguments
fields |
The fields of the recording form. |
sport |
The sport. |
Value
a string such as "Lode Excalibur, 3-min stages of +25 W from 50 W, earlobe whole blood, Biosen C-Line", or
NULL when nothing was recorded.
A sheet to record a step test
Description
Copies a printable recording sheet, a web form and a CSV template into a folder, so every test is recorded the same
way and goes straight into lactate_threshold(). The sheet is an A4 web page (open it in a browser and print it)
with the athlete, the 24 to 48 h before the test, the protocol, one row per stage for intensity, lactate, heart rate
and RPE, and how the test ended, as Best practices for a step test suggests. The form has the same fields to
fill in during the test, saves itself in the browser, and downloads an .xlsx that read_recording() reads, or a
CSV of the stages; it works offline and is also on the website. The template has the columns lactater reads,
with the resting sample as the first row.
Usage
recording_sheet(dir = ".", open = interactive())
Arguments
dir |
The folder to copy them into. |
open |
Whether to open the form in the browser. |
Value
the paths of the sheet, the form and the template, invisibly.
See Also
read_recording() to read a test recorded with the form.
Examples
files <- recording_sheet(tempfile(), open = FALSE)
read.csv(files[["template"]])
Objects exported from other packages
Description
These objects are imported from other packages. Follow the links below to see their documentation.
- ggplot2
The percentile of one value among the reference values
Description
Interpolates the nine published percentiles to the age, then the value between them on the normal scale.
Usage
reference_percentile(value, method, unit, sex, age)
Arguments
value |
The power (W) or power per kg. |
method |
|
unit |
|
sex |
|
age |
Age in years. |
Value
a list with the percentile (NA beyond the published ones) and the standing.
Thresholds against the population
Description
Places the power of a cycling test at 3 mmol/L ("OBLA 3.0") and at 1 mmol/L above the resting lactate
("Bsln + 1.0") among people of the same sex and age, on the reference percentiles of the German population aged 14
to 64 (Fiedler et al., 2025): in watts and, with body_mass, in watts per kg.
Usage
reference_percentiles(x, age, sex, body_mass = NULL)
Arguments
x |
The result of |
age |
Age in years, from 14 to 64. |
sex |
|
body_mass |
Body mass (kg), for the percentiles of power per kg, or |
Details
The percentiles come from 2,389 women and 2,801 men of two national health surveys (KiGGS wave 2 and DEGS1), tested
on a cycle ergometer from 25 W with 25 W more every 2 min, at 60-80 rpm, with earlobe blood analyzed on a Biosen
C-Line, until 85% of the age-predicted maximal heart rate. Their thresholds were computed with lactater "on a 3rd
degree polynomial with the baseline (step 0) data" included, so the two thresholds are computed here in the same way,
whatever fit and include_baseline the test was analyzed with: they can differ by a few watts from its table. Read
the result with three limits in mind:
-
The protocol. Percentiles transfer only between tests run the same way. Shorter stages put the thresholds at higher intensities (Bentley et al., 2007), so a test with 3-min stages looks a little weaker against these 2-min norms. A warning is given when the steps of the test are not 25 W, it has no resting sample, or the
stage_durationorsitegiven tolactate_threshold()are not 2 min and earlobe; without them, the stage length and the sampling site are yours to check. -
The population. These are values of the general population, not of athletes: most trained cyclists land above the 97.5th percentile.
-
The precision. The percentile is interpolated between the nine published percentiles, from the 2.5th to the 97.5th, on the normal scale and at the exact age. Beyond the 2.5th or the 97.5th it is
NA, andstandingsays "top 2.5%" or "bottom 2.5%".
Value
a lactate_reference tibble with one row per threshold and unit: method, value, unit, the
percentile, and the standing in plain words, from the published percentiles it falls between: "top 5%",
"top 10%", "top 25%", "upper half", "lower half", "bottom 25%", "bottom 10%", "bottom 5%", or beyond the
2.5th and 97.5th, "top 2.5%" and "bottom 2.5%".
References
Fiedler J, Thron M, Altmann S, Haertel S, Barsch F, Schlenker L, Krug S, Boes K, Woll A. Reference standards for power at the lactate threshold 2 for cycle ergometry throughout the lifespan in Germany. Journal of Sports Sciences. 2025;43(16):1654-1664. doi:10.1080/02640414.2025.2517913
Bentley DJ, Newell J, Bishop D. Incremental exercise test design and analysis: implications for performance diagnostics in endurance athletes. Sports Medicine. 2007;37(7):575-586.
Examples
results <- lactate_threshold(demo_data, intensity, lactate, heart_rate, plot = FALSE)
reference_percentiles(results, age = 31, sex = "male", body_mass = 72)
Refine the extremum of a function found on a grid
Description
The grid finds the basin of the extremum; optimize() between the neighbours of the best grid point makes it
exact. For internal use only.
Usage
refine_extremum(f, grid, values = f(grid), maximum = FALSE, tol = 1e-08)
Arguments
f |
The function of one variable. |
grid |
Where it was evaluated. |
values |
|
maximum |
Whether to look for a maximum. |
tol |
The tolerance of |
Value
the result of optimize(): minimum or maximum, and objective.
Thresholds relative to body mass, peak intensity and peak heart rate
Description
Expresses each threshold so that different athletes can be compared: per kg of body mass (power only), as a percentage of the highest intensity of the test, and the heart rate as a percentage of the highest heart rate of the test. A fixed lactate concentration falls at a similar percentage of peak power in trained and untrained people (Maassen & Busse, 1989), so percentages of peak do not rank fitness by themselves.
Usage
relative_thresholds(x, body_mass = NULL)
Arguments
x |
The result of |
body_mass |
Body mass (kg), or |
Value
x with intensity_per_kg (with body_mass, cycling and rowing), percent_peak and, with heart
rate, percent_peak_heart_rate.
References
Maassen N, Busse MW. The relationship between lactic acid and work load: a measure for endurance capacity or an indicator of carbohydrate deficiency? European Journal of Applied Physiology and Occupational Physiology. 1989;58(7):728-737.
Examples
elite <- subset(lactate_tests, test == "cycling_elite")
results <- lactate_threshold(elite, intensity, lactate, heart_rate, plot = FALSE)
relative_thresholds(results, body_mass = elite$body_mass[1])
Report internals
Description
check_report() stops when report is not one; add_section() adds a section that is drawn when the report is
saved, so the header can come in any order; html_*() draw the parts. For internal use only.
Usage
prepare_series(tests, dates)
check_report(report)
add_section(report, part, draw)
html_page(report)
html_header(report)
html_method(zones, boundary)
html_lede(r)
html_stats(r)
html_figure(zones)
html_plot(plot, caption, height = 3.5)
html_progress(r, cv)
html_reference(r)
html_zones(zones, sport)
html_prediction(r)
html_checks(x)
html_stages(info)
html_methods(x, info)
Arguments
tests |
A list of results of |
dates |
Their dates, or days since the first test. |
report, r |
A report from |
part |
The name of the section, for printing. |
draw |
A function of the report that returns the section's HTML. |
zones |
The result of |
boundary |
1 for LT1, 2 for LT2. |
plot |
A ggplot or a patchwork, drawn without its title and subtitle. |
caption |
The text under it. |
height |
Its height, in inches; it is 7 inches wide. |
sport |
The sport. |
x |
The result of |
info |
The |
Build a test report step by step
Description
Start a report with report_start(), then add the parts you want, in the order you want them, and write it with
report_save(). Printing a report previews it in the viewer. test_report() builds the default report in one call.
Usage
report_start(x, lt1 = NULL, lt2 = NULL, dates = NULL)
report_header(report, name = NULL, date = NULL, ..., lab = NULL, logo = NULL)
report_summary(report)
report_zones(report, model = 5, chart = TRUE)
report_progress(report, cv = NULL)
report_reference(report)
report_prediction(report)
report_checks(report)
report_test(report, protocol = NULL)
report_methods(report)
report_text(report, title, text)
report_page_break(report)
report_save(
report,
file = "lactate-test-report.html",
pdf = FALSE,
open = interactive()
)
Arguments
x |
The result of |
lt1, lt2 |
The thresholds that mark LT1 and LT2, as in |
dates |
For a series, the date of each test: dates, or days since the first test. Without them, the names of
|
report |
A report from |
name |
The athlete's name, the heading of the report. |
date |
The date of the test, as you want it printed. |
... |
Other details to list under the name, as |
lab |
Your lab, institution or business, printed at the top. |
logo |
The path to a logo image (PNG or JPEG), printed at the top right. |
model |
|
chart |
Whether to draw the zones chart above the table. |
cv |
The typical error of the methods, as in |
protocol |
One line on how the test was done: ergometer, stages, sampling site, analyzer. |
title |
The heading of a section of your own. |
text |
Its text: one string per paragraph. |
file |
Where to write the report, an |
pdf |
Whether to print a PDF next to the HTML file as well. Needs the pagedown package and Google Chrome or Chromium. |
open |
Whether to open the report in the browser. |
Details
To report a series of tests of one athlete, give report_start() a list of tests and their dates: the sections
above then describe the latest test, and report_progress() shows the progress over all of them.
-
report_header(): who was tested and where. The athlete's name, the date, any other details you name (sex,age,club,tester...), your lab or institution, and its logo. -
report_summary(): the answer: what LT1 and LT2 mean, and LT1, LT2 and the peak with their lactate, heart rate, RPE, percentage of peak and, withbody_mass, per kg. -
report_progress(): for a series, how LT1 and LT2 changed since the first test, every lactate curve with the zones of each test, test by test, and whether the latest change is larger than the noise of the method (compare_tests()). -
report_zones(): the zones chart and table, three or five zones. -
report_reference(): for cycling, the percentile of the power at 3 mmol/L and at baseline + 1 mmol/L among people of the same age and sex in Germany (reference_percentiles()), from theage,sexandbody_massgiven toreport_header(). For the general population, not for athletes. -
report_prediction(): the predicted race performance, from the chosen thresholds when an equation takes them. -
report_checks(): what to double-check in the data. -
report_test(): the protocol and the measured stages. -
report_methods(): the threshold of every method. -
report_text(): a section of your own, such as notes or a training plan. -
report_page_break(): start a new page when printed.
Value
a lactate_test_report to add to, and for report_save() the path of the report, invisibly.
Examples
results <- lactate_threshold(
demo_data, intensity, lactate, heart_rate, rpe_column = rpe, plot = FALSE
)
report <- report_start(results) |>
report_header(
name = "Demo athlete", date = "3 November 2026", sex = "male", age = 31, body_mass = 72,
lab = "Exercise physiology lab"
) |>
report_summary() |>
report_zones(model = 3) |>
report_text("Training", "Two sessions a week in zone 2, one in zone 3.")
report_save(report, tempfile(fileext = ".html"), open = FALSE)
Retrieve heart rate
Description
Heart rate at given intensities, interpolated linearly between the measured stages. A
straight line through the whole test misses the bend of the heart-rate curve at high
intensities (2-6 bpm on demo_data). Outside the stages (e.g. below the first one) it is NA.
Usage
retrieve_heart_rate(raw_data, intensity_value)
Arguments
raw_data |
The raw data, with a |
intensity_value |
The intensity values. |
Value
the heart rate (bpm, rounded) at each intensity.
Retrieve intensity
Description
Retrieves the intensity at which the fitted lactate curve reaches a given lactate value, by solving the fitted curve. When the curve crosses the value more than once (an early lactate dip, or a plateau at the end), the last crossing where lactate rises through the value is used: from there on, lactate stays above it.
Usage
retrieve_intensity(curve, lactate_value)
Arguments
curve |
A curve from |
lactate_value |
The lactate value. |
Value
the intensity associated with the estimated lactate threshold, or NA when the
curve never rises through lactate_value within the tested range.
Retrieve lactate
Description
Retrieves the fitted lactate at given intensities, rounded to 0.1 mmol/L.
Usage
retrieve_lactate(curve, intensity_value)
Arguments
curve |
A curve from |
intensity_value |
The intensity values. |
Value
the lactate associated with the estimated lactate threshold
Retrieve RPE
Description
The rating of perceived exertion at given intensities, interpolated linearly between the measured stages like the heart rate, to 0.1. For internal use only.
Usage
retrieve_rpe(raw_data, intensity_value)
Arguments
raw_data |
The raw data, with a |
intensity_value |
The intensity values. |
Value
the RPE at each intensity.
Measured stages and the resting sample
Description
The stages as open dots; the resting sample, placed one step before the first stage, fainter and labeled. For internal use only.
Usage
stage_layers(points, data, colours, label_rest = TRUE, rest_below = FALSE)
Arguments
points |
The stages to draw ( |
data |
The data retrieved from |
colours |
The plot colours. |
label_rest |
Whether to label the resting sample. |
rest_below |
Whether that label goes under the dot instead of over it. |
Measured stages layer
Description
The measured stages as open dots.
Usage
stage_points(points, colours, outline = colours[["curve"]])
Arguments
points |
The stages to draw ( |
colours |
The plot colours. |
outline |
The outline color. |
Layers of the Stegmann method
Description
The fitted curve, the recovery samples placed after the last stage (labeled with their minutes), the point where lactate is back at its end-of-exercise value, and the tangent from there to the curve. For internal use only.
Usage
stegmann_layers(plot_data, colours)
Arguments
plot_data |
The method's plot data, with the |
colours |
The plot colours. |
Value
a list with the layers, the lactate y where the threshold sits, the top and the x_range.
The Stegmann tangent
Description
The Stegmann tangent
Usage
stegmann_tangent(data, curve, settings)
Arguments
data |
The data retrieved from |
curve |
The user-selected curve from |
settings |
The analysis settings. |
Value
a list with the contact intensity (or NA when recovery lactate never fell back to its
end-of-exercise value after a rise), the back time (minutes after exercise), the point from which the tangent
is drawn, the recovery samples placed on the intensity axis, and the maximal elimination rate
(mmol/L/min).
Target label
Description
A lactate value as it appears in a method name, with at least one decimal (4 gives "4.0",
2.25 gives "2.25").
Usage
target_label(x)
Arguments
x |
Lactate values. |
A report of a step test, as a web page
Description
Writes the default report of a step test in one call: the header with the athlete's details, LT1 and LT2 with what
they mean, the training zones, the predicted performance, what to double-check, the test, and every method. Open it
in a browser, or print it to PDF from there, or set pdf = TRUE: two A4 pages, the answer and the zones on the
first, the record on the second (a section moves to the next page whole). To choose the sections, their order, or
to add your lab's name and logo, build the report step by step with report_start(). Needs the gt package.
Usage
test_report(
x,
file = "lactate-test-report.html",
athlete = list(),
lt1 = NULL,
lt2 = NULL,
protocol = NULL,
notes = NULL,
pdf = FALSE,
open = interactive(),
dates = NULL
)
Arguments
x |
The result of |
file |
Where to write the report, an |
athlete |
A named list for the header, passed to |
lt1, lt2 |
The thresholds that mark LT1 and LT2, as in |
protocol |
One line on how the test was done: ergometer, stages, sampling site, analyzer. Not shown for a series. |
notes |
Free text for the end of the report. |
pdf |
Whether to print a PDF next to the HTML file as well. Needs the pagedown package and Google Chrome or Chromium. |
open |
Whether to open the report in the browser. |
dates |
With a list of two to five tests in |
Value
the path of the report, invisibly.
See Also
report_start() to build a report step by step.
Examples
results <- lactate_threshold(
demo_data, intensity, lactate, heart_rate, rpe_column = rpe, plot = FALSE
)
report <- test_report(
results,
file = tempfile(fileext = ".html"),
athlete = list(name = "Demo athlete", date = "3 November 2026", body_mass = 72),
protocol = "Cycle ergometer, 3-min stages of +25 W from 50 W, earlobe capillary blood",
open = FALSE
)
lactater ggplot2 theme
Description
The theme used by every lactater plot: axis lines, no grid, and quiet text, so the lactate curve and the threshold carry the color. Use it to make your own plots match.
Usage
theme_lactater(
base_size = 11,
base_family = getOption("lactater.font", ""),
style = getOption("lactater.style", "light")
)
Arguments
base_size |
Base font size, in points. |
base_family |
Base font family. |
style |
|
Details
Two options change every lactater plot:
-
options(lactater.style = "night")switches to a dark version, and"journal"to a quieter print look (an ink curve with one amber accent)."light", a blue curve with a green threshold, is the default. -
options(lactater.font = "IBM Plex Sans")sets the font of the text and the annotations. The font must be installed (seesystemfonts::require_font()).
The subtitle and the caption are text boxes from ggtext, so they wrap by themselves to the width of the plot.
Restyle them with ggtext::element_textbox_simple() rather than ggplot2::element_text(), which cannot replace
them.
Plots are ggplot2 objects, so anything else can be changed by adding layers, scales or a theme().
Value
a ggplot2 theme.
Examples
library(ggplot2)
ggplot(demo_data, aes(intensity, lactate)) +
geom_point() +
theme_lactater()
Threshold label
Description
Threshold label
Usage
threshold_text(
intensity,
lactate,
heart_rate,
sport,
interval = NULL,
rpe = NA
)
Arguments
intensity, lactate, heart_rate, rpe |
The threshold values. |
sport |
The sport, which sets the intensity unit. |
interval |
The confidence interval of the intensity ( |
Value
a list with the value (the intensity, e.g. "140 W") and the detail line (the interval, pace,
lactate and heart rate, joined with middle dots).
Training load and intensity distribution from the lactate zones
Description
Sorts the samples of training sessions (heart rate, or power or speed) into the zones of lactate_zones(), and
returns the time in each zone and the training intensity distribution with its polarization index. It prints a
short summary and can be plotted with autoplot().
Usage
training_load(
data,
zones,
column = measure,
measure = c("heart_rate", "intensity"),
interval = 1,
.by = NULL
)
## S3 method for class 'lactate_load'
autoplot(object, ...)
Arguments
data |
A data frame with one row per sample, or a numeric vector of samples. |
zones |
The result of |
column |
The column of |
measure |
What the samples are: |
interval |
The time between two samples, in seconds. Defaults to |
.by |
Names of columns that group the samples (e.g. |
object |
The result of |
... |
Not used. |
Details
-
Time in zone: every sample counts for
intervalseconds in the zone of its value. Missing samples are left out. Heart rate lags behind intensity in short intervals, so heart-rate time in zone under-counts the highest zones in interval sessions; power or speed samples avoid that (Seiler, 2010). -
Intensity distribution across the three domains (moderate below LT1, heavy from LT1 to LT2, severe above LT2; with five zones, zones 1-2 and 4-5 are added up). The polarization index is
log10((moderate / heavy) * severe * 100), with each domain as a fraction of the total time; when the heavy domain is empty it is set to 0.01 and 0.01 is taken from the severe domain; it is 0 when the severe domain is empty, and not computed (NA) when the severe domain exceeds the moderate one. A distribution is"polarized"when the index is above 2.00 and moderate > severe > heavy (Treff et al., 2019). Otherwise it is labeled"pyramidal"(moderate > heavy > severe),"threshold"(heavy largest),"high intensity"(severe largest), or"other". Treff et al. (2019) note that a threshold distribution can also be pyramidal.
The distribution describes a training period, so give it the samples of all the sessions of that period (e.g.
.by = "week").
Value
a lactate_load tibble with one row per group: minutes, the minutes in each zone (zone_1, ...), the
percentage of time in each zone (zone_1_percent, ...), with five zones the percentage in each domain
(moderate_percent, heavy_percent, severe_percent), polarization_index, and distribution.
References
Seiler S. What is best practice for training intensity and duration distribution in endurance athletes? International Journal of Sports Physiology and Performance. 2010;5(3):276-291.
Treff G, Winkert K, Sareban M, Steinacker JM, Sperlich B. The polarization-index: a simple calculation to distinguish polarized from non-polarized training intensity distributions. Frontiers in Physiology. 2019;10:707.
Examples
results <- lactate_threshold(demo_data, intensity, lactate, heart_rate, plot = FALSE)
zones <- lactate_zones(results, lt1 = "Log-log", lt2 = "ModDmax")
## two sessions of heart rate sampled every 5 s
sessions <- data.frame(
session = rep(c("easy", "intervals"), each = 720),
heart_rate = c(rep(140, 720), rep(c(150, 185), times = 360))
)
load <- training_load(sessions, zones, interval = 5, .by = "session")
load
autoplot(load)
Label of the user-selected fit
Description
Label of the user-selected fit
Usage
user_fitting(settings)
Arguments
settings |
The analysis settings. |
Maximal lactate accumulation rate (VLamax)
Description
Estimates the maximal lactate accumulation rate (VLamax) from a short all-out sprint test (typically 15 s), not from the incremental test:
Usage
vlamax(lactate_pre, lactate_post, test_duration = 15, alactic_time = 3.5)
Arguments
lactate_pre |
Blood lactate before the sprint (mmol/L). |
lactate_post |
Blood lactate samples after the sprint (mmol/L); the highest is used. |
test_duration |
Sprint duration in seconds. Defaults to |
alactic_time |
Alactic time in seconds. Defaults to |
Details
VLamax = (La_{peak} - La_{pre}) / (t_{test} - t_{alactic})
VLamax needs its own protocol: a resting lactate sample before the sprint, an all-out sprint, and lactate sampled every minute for several minutes after it (Meixner et al. (2024) used up to 8 min). The highest post-sprint value is the peak.
The alactic time (alactic_time) is the part of the sprint assumed to be covered by phosphocreatine.
Fixing it at 3.5 s gave the most reliable VLamax in 50 cyclists (ICC 0.91, CV 3.1%); estimating it from
the power curve was much less reliable (CV 12-26%), and a 1-s change in it can change VLamax by up to
26% (Meixner et al., 2024). Protocols vary a lot between studies (Langley et al., 2025), and whether
VLamax measures the maximal glycolytic rate is still debated: Clark & Macdermid (2025) found it adds
little beyond the sprint power itself. Compare values only within the same protocol, ergometer and
lactate analyzer.
The default test_duration of 15 s is the sprint of Meixner et al. (2024) and of most cycling studies. A review of
60 studies recommends a sport-specific all-out sprint of 10-12 s instead, with lactate sampled every minute for 10
min, and calls the rate from 15 s or longer a peak rather than a maximal rate; it also found the use of VLamax to
predict performances of 1 min or longer inconclusive (Quittmann, 2026). Set test_duration to your sprint.
vlapeak is the same rise divided by the whole sprint, with no alactic time,
vLa_{peak} = (La_{peak} - La_{pre}) / t_{test}, so it is lower than vlamax. Wackerhage et al. (2025)
proposed it
partly because the alactic-time correction is error-prone. In 182 cyclists and triathletes, both gave similar
associations with the lactate thresholds (Röhrs et al., 2026).
Value
a tibble with vlamax and vlapeak (mmol/L/s), lactate_peak and delta_lactate (mmol/L), and
glycolytic_time (s).
References
Meixner B, Nusser V, Koehler K, Sablain M, Boone J, Sperlich B. Reliability of power output, maximal rate of capillary blood lactate accumulation, and phosphagen contribution time following 15-s sprint cycling in amateur cyclists. Physiological Reports. 2024;12(10):e16086. doi:10.14814/phy2.16086
Langley J, Haase R, Nitzsche N, Porter M. Methodological approaches in testing maximal lactate accumulation rate - vLamax: a systematic review. Journal of Science and Cycling. 2025;14(1):9. doi:10.28985/1425.jsc.09
Clark B, Macdermid PW. VLamax correlates strongly with glycolytic performance. Research Quarterly for Exercise and Sport. 2025:1-8.
Quittmann OJ. Maximal lactate accumulation rate (cLamax): current evidence and future directions for exercise testing and training. European Journal of Applied Physiology. 2026;126(1):1-36. doi:10.1007/s00421-025-06022-7
Röhrs D, Wagner CM, Reer R, Keiner M. Lactate accumulation rate is associated with lactate turnpoints in cyclists and triathletes: a comparison of vLapeak and vLamaxblood. European Journal of Applied Physiology. 2026. doi:10.1007/s00421-026-06444-x
Wackerhage H, Kabasakalis A, Seiler S, Heck H. Is the vLamax for glycolysis what the VO2max is for oxidative phosphorylation? Sports Medicine. 2025;55(8):1853-1866. doi:10.1007/s40279-025-02259-6
Examples
vlamax(lactate_pre = 1.1, lactate_post = c(8.9, 10.4, 10.9, 10.6, 10.1))
Time-weight the intensity of an incomplete last stage
Description
The stage before it plus the completed share of the increment: completed + t / T * increment (Kuipers et al.,
2003).
Usage
weight_last_stage(intensity, share)
Arguments
intensity |
The intensities, in increasing order. |
share |
The share of the last stage that was completed ( |
Value
intensity with its last value replaced.
Time in zone and intensity distribution from the minutes in each zone
Description
Time in zone and intensity distribution from the minutes in each zone
Usage
zone_summary(minutes, domain)
Arguments
minutes |
The minutes in each zone. |
domain |
The domain of each zone ( |
Value
a one-row data frame.
Heart rate or RPE at zone boundaries
Description
Interpolated between the stages, like the thresholds; a boundary below the first stage (e.g. the zone 1|2 split) is extrapolated from a straight line through the first three stages, where heart rate and RPE rise about linearly with intensity. For internal use only.
Usage
zone_value(data, column, x, digits, lowest = 0)
Arguments
data |
The data retrieved from |
column |
|
x |
The boundary intensities. |
digits |
Decimals. |
lowest |
The lowest value the measure can take. |