Playbook: Epley’s equations - include 1RM or not?

Mladen Jovanović

Epley’s equation, popularized by Jim Wendler with his 5/3/1 books, is oftentimes used to estimate 1RM from maximal number of repetitions using sub-max load (ideally under 10 max reps, or nRM).

Epley’s equation has multiple forms:

\[ \begin{equation} \begin{split} 1RM &= (Load \times nRM \times 0.0333) + Load \\ \\ \%1RM &= \frac{1}{(0.0333 \times nRM + 1)} \\ \\ nRM &= \frac{30.03}{\%1RM} - 30.03 \end{split} \end{equation} \]

Using the equations, we can devise the Max Reps Table:

nRM %1RM
2 94
3 91
4 88
5 86
6 83
7 81
8 79
9 77
10 75

One of the problems with this equation, is that it predicts 0 reps with 100% 1RM. This is the result of estimating parameter k (i.e., 0.0333) using the following model definition:

\[ \%1RM = \frac{1}{(k \times nRM + 1)} \]

Let’s estimate parameter k, using simulated data, assuming Epley’s equation as data-generating-process (DGP)

df <- df %>%
  # add small noise so the model can be fit
  mutate(
    `%1RM` = `%1RM` + rnorm(n(), 0, 10^-4)
  )

Now, let’s fit two non-linear models: (1) original Epley’s model definition, and (2) modified model definition with \(nRM - 1\). To provide practical model fit, mean-absolute-error (MAE) estimator will be provided:

\[ \%1RM = \frac{1}{(k \times (nRM - 1) + 1)} \]

MAE <- function(model) {
  mean(abs(resid(model)))
}

m1 <- nls(
  `%1RM` ~ 100 * 1/(k * nRM + 1),
  data =  df,
  start = list(k = 0)
    )

summary(m1)
#> 
#> Formula: `%1RM` ~ 100 * 1/(k * nRM + 1)
#> 
#> Parameters:
#>    Estimate Std. Error t value Pr(>|t|)    
#> k 3.330e-02  4.693e-08  709524   <2e-16 ***
#> ---
#> Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#> 
#> Residual standard error: 5.861e-05 on 8 degrees of freedom
#> 
#> Number of iterations to convergence: 4 
#> Achieved convergence tolerance: 8.284e-06
MAE(m1)
#> [1] 4.602527e-05
m2 <- nls(
  `%1RM` ~ 100 * 1/(k * (nRM-1) + 1),
  data =  df,
  start = list(k = 0)
    )

summary(m2)
#> 
#> Formula: `%1RM` ~ 100 * 1/(k * (nRM - 1) + 1)
#> 
#> Parameters:
#>   Estimate Std. Error t value Pr(>|t|)    
#> k 0.039162   0.001242   31.54 1.11e-09 ***
#> ---
#> Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#> 
#> Residual standard error: 1.305 on 8 degrees of freedom
#> 
#> Number of iterations to convergence: 5 
#> Achieved convergence tolerance: 1.129e-07
MAE(m2)
#> [1] 1.002432

The estimate parameter k for the model 1 is 0.033 and 0.039 for the model 2. The following forms of equations can be used for the model 1:

\[ \begin{equation} \begin{split} 1RM &= (Load \times nRM \times k) + Load \\ \\ \%1RM &= \frac{1}{(k \times nRM + 1)} \\ \\ nRM &= \frac{1 - \%1RM}{k \times \%1RM} \end{split} \end{equation} \]

While the following forms of equations can be used for the model 2:

\[ \begin{equation} \begin{split} 1RM &= (Load \times (nRM-1) \times k) + Load \\ \\ \%1RM &= \frac{1}{(k \times (nRM-1) + 1)} \\ \\ nRM &= \frac{(k - 1) \times \%1RM + 1}{k \times \%1RM} \end{split} \end{equation} \]

Now, let’s create a table with original and corrected Epley’s equation:

df <- tibble(
  nRM = seq(1, 10),
  `%1RM (Epley)` = 1/(coef(m1) * nRM + 1) * 100,
  `%1RM (Epley corrected)` = 1/(coef(m2) * (nRM-1) + 1) * 100
)

kable(df, digits = 0)
nRM %1RM (Epley) %1RM (Epley corrected)
1 97 100
2 94 96
3 91 93
4 88 89
5 86 86
6 83 84
7 81 81
8 79 78
9 77 76
10 75 74

So far we have used data generated using Epley’s equation as underlying DGP. Let’s now consider a single individual tested in the bench press, using 150, 135, 120, and 105kg. This corresponds to athlete’s 1RM, 3RM, 6RM and 12RM. Let’s see how the two model definitions differ.

athlete_df <- tibble(
  weight = c(150, 135, 120, 105),
  nRM = c(1, 3, 6, 12),
  `1RM` = 150,
  `%1RM` = weight / `1RM` * 100
) %>%
  select(-`1RM`)
m1 <- nls(
  `%1RM` ~ 100 * 1/(k * nRM + 1),
  data =  athlete_df,
  start = list(k = 0)
    )

summary(m1)
#> 
#> Formula: `%1RM` ~ 100 * 1/(k * nRM + 1)
#> 
#> Parameters:
#>   Estimate Std. Error t value Pr(>|t|)   
#> k 0.036958   0.003145   11.75  0.00132 **
#> ---
#> Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#> 
#> Residual standard error: 2.356 on 3 degrees of freedom
#> 
#> Number of iterations to convergence: 4 
#> Achieved convergence tolerance: 3.314e-07
MAE(m1)
#> [1] 1.539243
m2 <- nls(
  `%1RM` ~ 100 * 1/(k * (nRM-1) + 1),
  data =  athlete_df,
  start = list(k = 0)
    )

summary(m2)
#> 
#> Formula: `%1RM` ~ 100 * 1/(k * (nRM - 1) + 1)
#> 
#> Parameters:
#>   Estimate Std. Error t value Pr(>|t|)   
#> k 0.043161   0.003431   12.58  0.00108 **
#> ---
#> Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#> 
#> Residual standard error: 2.167 on 3 degrees of freedom
#> 
#> Number of iterations to convergence: 5 
#> Achieved convergence tolerance: 3.492e-06
MAE(m2)
#> [1] 1.624158

Here is the max reps table combining observed max number of reps and model predicted:

athlete_df <- athlete_df %>%
  mutate(
    `%1RM (Epley)` = 1/(coef(m1) * nRM + 1) * 100,
     `%1RM (Epley corrected)` = 1/(coef(m2) * (nRM-1) + 1) * 100
  )

kable(athlete_df, digits = 0)
weight nRM %1RM %1RM (Epley) %1RM (Epley corrected)
150 1 100 96 100
135 3 90 90 92
120 6 80 82 82
105 12 70 69 68

What if we remove the 1RM from the table and repeat the estimation?

athlete_df <- athlete_df %>%
  filter(nRM != 1)
m1 <- nls(
  `%1RM` ~ 100 * 1/(k * nRM + 1),
  data =  athlete_df,
  start = list(k = 0)
    )

summary(m1)
#> 
#> Formula: `%1RM` ~ 100 * 1/(k * nRM + 1)
#> 
#> Parameters:
#>   Estimate Std. Error t value Pr(>|t|)   
#> k 0.037565   0.001857   20.23  0.00243 **
#> ---
#> Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#> 
#> Residual standard error: 1.369 on 2 degrees of freedom
#> 
#> Number of iterations to convergence: 5 
#> Achieved convergence tolerance: 1.944e-07
MAE(m1)
#> [1] 0.9354522
m2 <- nls(
  `%1RM` ~ 100 * 1/(k * (nRM-1) + 1),
  data =  athlete_df,
  start = list(k = 0)
    )

summary(m2)
#> 
#> Formula: `%1RM` ~ 100 * 1/(k * (nRM - 1) + 1)
#> 
#> Parameters:
#>   Estimate Std. Error t value Pr(>|t|)   
#> k 0.043161   0.004202   10.27  0.00935 **
#> ---
#> Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#> 
#> Residual standard error: 2.654 on 2 degrees of freedom
#> 
#> Number of iterations to convergence: 5 
#> Achieved convergence tolerance: 3.551e-06
MAE(m2)
#> [1] 2.165544

Here is the max reps table combining observed max number of reps and model predicted:

athlete_df <- athlete_df %>%
  mutate(
    `%1RM (Epley)` = 1/(coef(m1) * nRM + 1) * 100,
     `%1RM (Epley corrected)` = 1/(coef(m2) * (nRM-1) + 1) * 100
  )

kable(athlete_df, digits = 0)
weight nRM %1RM %1RM (Epley) %1RM (Epley corrected)
135 3 90 90 92
120 6 80 82 82
105 12 70 69 68

The conclusion is the following: