# Introduction

In previous posts (Fairbanks Race Predictor, Equinox from Santa Claus, Equinox from Gold Discovery) I’ve looked at predicting Equinox Marathon results based on results from earlier races. In all those cases I’ve looked at single race comparisons: how results from Gold Discovery can predict Marathon times, for example. In this post I’ll look at all the Usibelli Series races I completed this year to see how they can inform my expectations for next Saturday’s Equinox Marathon.

# Methods

I’ve been collecting the results from all Usibelli Series races since 2010. Using that data, grouped by the name of the person racing and year, find all runners that completed the same set of Usibelli Series races that I finished in 2018, as well as their Equinox Marathon finish pace. Between 2010 and 2017 there are 160 records that match.

The data looks like this. `crr` is that person’s Chena River Run pace in
minutes, `msr` is Midnight Sun Run pace for the same person and year, `rotv`
is the pace from Run of the Valkyries, `gdr` is the Gold Discovery Run, and
`em` is Equniox Marathon pace for that same person and year.

crr | msr | rotv | gdr | em |
---|---|---|---|---|

8.1559 | 8.8817 | 8.1833 | 10.2848 | 11.8683 |

8.7210 | 9.1387 | 9.2120 | 11.0152 | 13.6796 |

8.7946 | 9.0640 | 9.0077 | 11.3565 | 13.1755 |

9.4409 | 10.6091 | 9.6250 | 11.2080 | 13.1719 |

7.3581 | 7.1836 | 7.1310 | 8.0001 | 9.6565 |

7.4731 | 7.5349 | 7.4700 | 8.2465 | 9.8359 |

... | ... | ... | ... | ... |

I will use two methods for using these records to predict Equinox Marathon times, multivariate linear regression and Random Forest.

The R code for the analysis appears at the end of this post.

# Results

## Linear regression

We start with linear regression, which isn’t entirely appropriate for this analysis because the independent variables (pre-Equinox race pace times) aren’t really independent of one another. A person who runs a 6 minute pace in the Chena River Run is likely to also be someone who runs Gold Discovery faster than the average runner. This relationship, in fact, is the basis for this analysis.

I started with a model that includes all the races I completed in 2018, but pace time for the Midnight Sun Run wasn’t statistically significant so I removed it from the final model, which included Chena River Run, Run of the Valkyries, and Gold Discovery.

This model is significant, as are all the coefficients except the intercept, and the model explains nearly 80% of the variation in the data:

## ## Call: ## lm(formula = em ~ crr + gdr + rotv, data = input_pivot) ## ## Residuals: ## Min 1Q Median 3Q Max ## -3.8837 -0.6534 -0.2265 0.3549 5.8273 ## ## Coefficients: ## Estimate Std. Error t value Pr(>|t|) ## (Intercept) 0.6217 0.5692 1.092 0.276420 ## crr -0.3723 0.1346 -2.765 0.006380 ** ## gdr 0.8422 0.1169 7.206 2.32e-11 *** ## rotv 0.7607 0.2119 3.591 0.000442 *** ## --- ## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 ## ## Residual standard error: 1.278 on 156 degrees of freedom ## Multiple R-squared: 0.786, Adjusted R-squared: 0.7819 ## F-statistic: 191 on 3 and 156 DF, p-value: < 2.2e-16

Using this model and my 2018 results, my overall pace and finish times for Equinox are predicted to be 10:45 and 4:41:50. The 95% confidence intervals for these predictions are 10:30–11:01 and 4:35:11–4:48:28.

## Random Forest

Random Forest is another regression method but it doesn’t require independent variables be independent of one another. Here are the results of building 5,000 random trees from the data:

## ## Call: ## randomForest(formula = em ~ ., data = input_pivot, ntree = 5000) ## Type of random forest: regression ## Number of trees: 5000 ## No. of variables tried at each split: 1 ## ## Mean of squared residuals: 1.87325 ## % Var explained: 74.82 ## IncNodePurity ## crr 260.8279 ## gdr 321.3691 ## msr 268.0936 ## rotv 295.4250

This model, which includes all race results explains just under 74% of the variation in the data. And you can see from the importance result that Gold Discovery results factor more heavily in the result than earlier races in the season like Chena River Run and the Midnight Sun Run.

Using this model, my predicted pace is 10:13 and my finish time is 4:27:46. The 95% confidence intervals are 9:23–11:40 and 4:05:58–5:05:34. You’ll notice that the confidence intervals are wider than with linear regression, probably because there are fewer assumptions with Random Forest and less power.

# Conclusion

My number one goal for this year’s Equinox Marathon is simply to finish without injuring myself, something I wasn’t able to do the last time I ran the whole race in 2013. I finished in 4:49:28 with an overall pace of 11:02, but the race or my training for it resulted in a torn hip labrum.

If I’m able to finish uninjured, I’d like to beat my time from 2013. These results suggest I should have no problem acheiving my second goal and perhaps knowing how much faster these predictions are from my 2013 times, I can race conservatively and still get a personal best time.

# Appendix - R code

```
library(tidyverse)
library(RPostgres)
library(lubridate)
library(glue)
library(randomForest)
library(knitr)
races <- dbConnect(Postgres(),
host = "localhost",
dbname = "races")
all_races <- races %>%
tbl("all_races")
usibelli_races <- tibble(race = c("Chena River Run",
"Midnight Sun Run",
"Jim Loftus Mile",
"Run of the Valkyries",
"Gold Discovery Run",
"Santa Claus Half Marathon",
"Golden Heart Trail Run",
"Equinox Marathon"))
css_2018 <- all_races %>%
inner_join(usibelli_races, copy = TRUE) %>%
filter(year == 2018,
name == "Christopher Swingley") %>%
collect()
candidate_races <- css_2018 %>%
select(race) %>%
bind_rows(tibble(race = c("Equinox Marathon")))
input_data <- all_races %>%
inner_join(candidate_races, copy = TRUE) %>%
filter(!is.na(gender), !is.na(birth_year)) %>%
collect()
input_pivot <- input_data %>%
group_by(race, name, year) %>%
mutate(n = n()) %>%
filter(n == 1) %>%
ungroup() %>%
select(name, year, race, pace_min) %>%
spread(race, pace_min) %>%
rename(crr = `Chena River Run`,
msr = `Midnight Sun Run`,
rotv = `Run of the Valkyries`,
gdr = `Gold Discovery Run`,
em = `Equinox Marathon`) %>%
filter(!is.na(crr), !is.na(msr), !is.na(rotv),
!is.na(gdr), !is.na(em)) %>%
select(-c(name, year))
kable(input_pivot %>% head)
css_2018_pivot <- css_2018 %>%
select(name, year, race, pace_min) %>%
spread(race, pace_min) %>%
rename(crr = `Chena River Run`,
msr = `Midnight Sun Run`,
rotv = `Run of the Valkyries`,
gdr = `Gold Discovery Run`) %>%
select(-c(name, year))
pace <- function(minutes) {
mm = floor(minutes)
seconds = (minutes - mm) * 60
glue('{mm}:{sprintf("%02.0f", seconds)}')
}
finish_time <- function(minutes) {
hh = floor(minutes / 60.0)
min = minutes - (hh * 60)
mm = floor(min)
seconds = (min - mm) * 60
glue('{hh}:{sprintf("%02d", mm)}:{sprintf("%02.0f", seconds)}')
}
lm_model <- lm(em ~ crr + gdr + rotv,
data = input_pivot)
summary(lm_model)
prediction <- predict(lm_model, css_2018_pivot,
interval = "confidence", level = 0.95)
prediction
rf <- randomForest(em ~ .,
data = input_pivot,
ntree = 5000)
rf
importance(rf)
rfp_all <- predict(rf, css_2018_pivot, predict.all = TRUE)
rfp_all$aggregate
rf_ci <- quantile(rfp_all$individual, c(0.025, 0.975))
rf_ci
```