thu, 29-aug-2019, 17:43
Pick me up!

Pick me up!

Tallys died today after a short fight with an unknown disease. We got him and his two brothers from a shelter foster litter and as soon as we met him as a tiny kitten, we knew he was one of the kittens we wanted to adopt. He was the smallest kitten in the litter, was extremely friendly and very snuggly, and he remained that way for his short life. He would spend all night curled up at my feet, or under the covers with Andrea, and in the morning he’d come downstairs and ask to be picked up while I made breakfast. He loved to be up on my shoulders, purring and kneading, and occasionally scolding me if I wasn’t petting him enough. He played with my feet under the covers, and would bring cat toys downstairs during the day while we were at work and the dogs were outside. He also made friends with all the dogs, and especially Monte, who would come in the house and bark until his friend Tallys came down to nuzzle him and play.

He also loved heat, so he was always the cat baking himself in the direct sun in the window, and especially liked winter because of the heat the wood stove put out. He’d lay in front of it (or sometimes directly under it) and his fur would get hot enough it seemed like he should be melting. And when the wood stove wasn’t cranking he’d flop down in front of our Toyo and soak up as much heat as he could.

He is survived by his brothers, Jenson and Caslon, and his two sisters living in another home in Fairbanks. Tallys, Jenson, and Caslon all got along at our house, sleeping in a ball on the bed, or chasing each other around. Tallys and Caslon would often sit opposite each other and lazily swat at each other until someone got in a good punch and they’d go bolting after each other.

He was a super sweet little cat, and our time with him was too short, but in those years he was a constant bright spot in our lives. We will miss him.

Monte and Tallys enjoing the wood stove

Monte and Tallys enjoying the wood stove

Tallys bug hunting

Tallys bug hunting

Tallys bug hunting

Tallys looking out the window

tags: Tallys  memorial 
sat, 10-nov-2018, 10:02

Introduction

It’s November 10th in Fairbanks and we have only an inch of snow on the ground. The average depth on this date is 6.1 inches, but that a little deceptive because snow depth doesn’t follow a normal distribution; it can never be below zero, and has a long tail toward deeper snow depths. In the 92 years of snow depth data for the Fairbanks Airport, we’ve had less than an inch of snow only six times (6.5%). At the other end of the distribution, there have been seven years with more than 14 inches of snow on November 10th.

My question is: what does snow depth on November 10th tell us about how much snow we are going to get later on in the winter? Is there a relationship between depth on November 10th and depths later in the winter, and if there is, how much snow can we expect this winter?

Data

We’ll use the 92-year record of snow depth data from the Fairbanks International Airport station that’s in the Global Historical Climate Network.

The correlation coefficients (a 1 means a perfect correlation, and a 0 is no correlation) between snow depth on November 10th, and the first of the months of December, January and February of that same winter are shown below:

  nov_10 dec_01 jan_01 feb_01
nov_10 1.00 0.65 0.49 0.46
dec_01 0.65 1.00 0.60 0.39
jan_01 0.49 0.60 1.00 0.74
feb_01 0.46 0.39 0.74 1.00

Looking down the nov_10 column, you can see a high correlation between snow depth on November 10th and depth on December 1st, but lower (and similar) correlations with depths in January and February.

This makes sense. In Fairbanks, snow that falls after the second week in October is likely to be around for the rest of the winter, so all the snow on the ground on November 10th, will still be there in December, and throughout the winter.

But what can a snow depth of one inch on November 10th tell us about how much snow we will have in December or later on?

Here’s the data for those six years with a snow depth of 1 inch on November 10th:

wyear dec_01 jan_01 feb_01
1938 5 11 24
1940 6 8 9
1951 12 22 31
1953 1 5 17
1954 9 15 12
1979 3 8 14

Not exactly encouraging data for our current situation, although 1951 gives us some hope of a good winter.

Methods

We used Bayesian linear regression to predict snow depth on December 1st, January 1st and February 1st, based on our snow depth data and the current snow depth in Fairbanks. We used the rstanarm R package, which mimics the glm function that’s part of base R.

Because of the non-zero, skewed nature of the distribution of snow depths, a log-linked Gamma distribution is appropriate. We used the rstanarm defaults for priors.

One of the great things about Bayesian linear regression is that it incorporates our uncertainty about the model coefficients to produce a distribution of predicted values. The more uncertainty there is in our model, the wider the range of predicted values. We examine the distribution of these predicted snow depth values and compare them with the distribution of actual values.

The code for the analysis appears at the bottom of the post.

Results

The following figure shows a histogram and density function plot for the predicted snow depth on December 1st (top pane) for this year, and the actual December 1st snow depth data in past years (bottom).

December Snow Depth

The predicted snow depth ranges from zero to almost 27 inches of snow, but the distribution is concentrated around 5 inches. The lower plot showing the distribution of actual snow depth on December 1st isn’t as smooth, but it has a similar shape and peaks at 9 inches.

If we run the same analysis for January and February, we get a set of frequency distributions that look like the following plot, again with the predicted snow depth distribution on top and the distribution of actual data on the bottom.

Snow Depth Distribution

The December densities are repeated here, in red, along with the January (green) and February (blue) results. In the top plot, you can clearly see that the shape of the distribution gets more spread out as we get farther from November, indicating our increasing uncertainty in our predictions, although some of that pattern is also from the source data (below), which also gets more spread out in January and February.

Despite our increasing uncertainty, it’s clear from comparing the peaks in these curves that our models expect there to be less snow in December, January and February this year, compared with historical values. By my reckoning, we can expect around 5 inches on December 1st, 10 inches on January 1st, and 12 or 13 inches by February. In an average year, these values would be closer to 9, 12, and 15 inches.

Conclusion

There is a relationship between snow depth on November 10th and depths later in the winter, but the distributions of predicted values are so spread out that we could easily receive as much or more snow as we have in previous years. Last year on this date we had 5 inches, on December 1st we had 11 inches, 13 inches on New Year’s Day, and 20 inches on February 1st. Here’s hoping we quickly reach, and surpass those values in 2018/2019.

Appendix

library(tidyverse)
library(glue)
library(ggpubr)
library(scales)
library(lubridate)
library(RPostgres)
library(rstanarm)

noaa <- dbConnect(Postgres(),
                  dbname = "noaa")

ghcnd_stations <- noaa %>%
    tbl("ghcnd_stations") %>%
    filter(station_name == "FAIRBANKS INTL AP")

ghcnd_variables <- noaa %>%
    tbl("ghcnd_variables") %>%
    filter(variable == "SNWD")

ghcnd_obs <- noaa %>%
    tbl("ghcnd_obs") %>%
    inner_join(ghcnd_stations, by = "station_id") %>%
    inner_join(ghcnd_variables, by = "variable") %>%
    mutate(month = date_part("month", dte),
           day = date_part("day", dte)) %>%
    filter((month == 11 & day == 10) |
           (month == 12 & day == 1) |
           (month == 1 & day == 1) |
           (month == 2 & day == 1),
           is.na(meas_flag) | meas_flag == "") %>%
    mutate(value = raw_value * raw_multiplier) %>%
    select(dte, month, day, variable, value) %>%
    collect()

snow_depths <- ghcnd_obs %>%
    mutate(wyear = year(dte - days(91)),
           mmdd = factor(glue("{str_to_lower(month.abb[month])}",
                              "_{sprintf('%02d', day)}"),
                         levels = c("nov_10", "dec_01",
                                    "jan_01", "feb_01")),
           value = value / 25.4) %>%
    select(wyear, mmdd, value) %>%
    spread(mmdd, value) %>%
    filter(!is.na(nov_10))

write_csv(snow_depths, "snow_depths.csv", na = "")

dec <- stan_glm(dec_01 ~ nov_10,
                data = snow_depths,
                family = Gamma(link = "log"),
                # prior = normal(0.7, 3),
                # prior_intercept = normal(1, 3),
                iter = 5000)

# What does the model day about 2018?
dec_prediction_mat <- posterior_predict(dec,
                                        newdata = tibble(nov_10 = 1))
dec_prediction <- tibble(pred_dec_01 = dec_prediction_mat[,1])
dec_hist <- ggplot(data = dec_prediction,
                   aes(x = pred_dec_01, y = ..density..)) +
    theme_bw() +
    geom_histogram(binwidth = 0.25, color = 'black',
                   fill = 'darkorange') +
    geom_density() +
    scale_x_continuous(name = "Snow depth (inches)",
                       limits = c(0, 40),
                       breaks = seq(0, 40, 5)) +
    scale_y_continuous(name = "Frequency") +
    theme(plot.margin = unit(c(1, 1, 0, 0.5), 'lines')) +
    theme(axis.text.x = element_blank(),
          axis.title.x = element_blank(),
          axis.ticks.x = element_blank()) +
    labs(title = "December Snow Depth",
         subtitle = "Fairbanks Airport Station")

actual_december <- ggplot(data = snow_depths,
                          aes(x = dec_01, y = ..density..)) +
    theme_bw() +
    geom_histogram(binwidth = 1, color = 'black',
                   fill = 'darkorange') +
    geom_density() +
    scale_x_continuous(name = "Snow depth (inches)",
                       limits = c(0, 40),
                       breaks = seq(0, 40, 5)) +
    scale_y_continuous(name = "Frequency") +
    theme(plot.margin = unit(c(0, 1, 0.5, 0.5), 'lines'))

height <- 9
width <- 16
rescale <- 0.75
heights <- c(0.5, 0.5) * height
gt <- ggarrange(dec_hist, actual_december,
                ncol = 1, nrow = 2, align = "v",
                widths = c(1, 1), heights = heights)
svg('december_comparison.svg',
    width=width*rescale, height=height*rescale)
gt
dev.off()

jan <- stan_glm(jan_01 ~ nov_10,
                data = snow_depths,
                # family = gaussian(link = "identity"),
                family = Gamma(link = "log"),
                # prior = normal(0.7, 3),
                # prior_intercept = normal(1, 3),
                iter = 5000)

jan_prediction_mat <- posterior_predict(jan,
                                        newdata = tibble(nov_10 = 1))
jan_prediction <- tibble(pred_jan_01 = jan_prediction_mat[,1])

feb <- stan_glm(feb_01 ~ nov_10,
                data = snow_depths,
                # family = gaussian(link = "identity"),
                family = Gamma(link = "log"),
                # family = poisson(link = "identity"),
                # prior = normal(0.7, 3),
                # prior_intercept = normal(1, 3),
                iter = 5000)

feb_prediction_mat <- posterior_predict(feb,
                                        newdata = tibble(nov_10 = 1))
feb_prediction <- tibble(pred_feb_01 = feb_prediction_mat[,1])

all_predictions <- bind_cols(dec_prediction,
                             jan_prediction,
                             feb_prediction) %>%
    rename(`Dec 1` = pred_dec_01,
           `Jan 1` = pred_jan_01,
           `Feb 1` = pred_feb_01) %>%
    gather(prediction, snow_depth_inches) %>%
    mutate(prediction = factor(prediction,
                               levels = c("Dec 1", "Jan 1", "Feb 1")))

pred_density_plot <- ggplot(data = all_predictions,
                       aes(x = snow_depth_inches, colour = prediction)) +
    theme_bw() +
    geom_density() +
    scale_x_continuous(name = "Snow depth (inches)",
                       limits = c(0, 55),
                       breaks = pretty_breaks(n = 10)) +
    theme(axis.text.x = element_blank(), axis.title.x = element_blank(),
          axis.ticks.x = element_blank()) +
    labs(title = "Predicted and actual snow depths based on November 10 depth",
         subtitle = "Fairbanks Airport Station")


actual_data <- snow_depths %>%
    transmute(`Dec 1` = dec_01,
              `Jan 1` = jan_01,
              `Feb 1` = feb_01) %>%
    gather(actual, snow_depth_inches) %>%
    mutate(actual = factor(actual, levels = c("Dec 1", "Jan 1", "Feb 1")))

actual_density_plot <- ggplot(data = actual_data,
                       aes(x = snow_depth_inches, colour = actual)) +
    theme_bw() +
    geom_density() +
    scale_x_continuous(name = "Snow depth (inches)",
                       limits = c(0, 55),
                       breaks = pretty_breaks(n = 10)) +
    theme(plot.margin = unit(c(0, 1, 0.5, 0.5), 'lines'))

height <- 9
width <- 16
rescale <- 0.75
heights <- c(0.5, 0.5) * height
gt <- ggarrange(pred_density_plot, actual_density_plot,
                ncol = 1, nrow = 2, align = "v",
                widths = c(1, 1), heights = heights)
svg('dec_jan_feb.svg',
    width=width*rescale, height=height*rescale)
gt
dev.off()
thu, 13-sep-2018, 17:40

Introduction

A couple years ago I wrote a post about past Equinox Marathon weather. Since that post Andrea and I have run the relay twice, and I plan on running the full marathon in a couple days. This post updates the statistics and plots to include two more years of the race.

Methods

Methods and data are the same as in my previous post, except the daily data has been updated to include 2016 and 2017. The R code is available at the end of the previous post.

Results

Race day weather

Temperatures at the airport on race day ranged from 19.9 °F in 1972 to 35.1 °F in 1969, but the average range is between 34.1 and 53.1 °F. Using our model of Ester Dome temperatures, we get an average range of 29.5 and 47.3 °F and an overall min / max of 16.1 / 61.3 °F. Generally speaking, it will be below freezing on Ester Dome, but possibly before most of the runners get up there.

Precipitation (rain, sleet or snow) has fallen on 16 out of 55 race days, or 29% of the time, and measurable snowfall has been recorded on four of those sixteen. The highest amount fell in 2014 with 0.36 inches of liquid precipitation (no snow was recorded and the temperatures were between 45 and 51 °F so it was almost certainly all rain, even on Ester Dome). More than a quarter of an inch of precipitation fell in three of the sixteen years when it rained or snowed (1990, 1993, and 2014), but most rainfall totals are much smaller.

Measurable snow fell at the airport in four years, or seven percent of the time: 4.1 inches in 1993, 2.1 inches in 1985, 1.2 inches in 1996, and 0.4 inches in 1992. But that’s at the airport station. Five of the 12 years where measurable precipitation fell at the airport and no snow fell, had possible minimum temperatures on Ester Dome that were below freezing. It’s likely that some of the precipitation recorded at the airport in those years was coming down as snow up on Ester Dome. If so, that means snow may have fallen on nine race days, bringing the percentage up to sixteen percent.

Wind data from the airport has only been recorded since 1984, but from those years the average wind speed at the airport on race day is 4.8 miles per hour. The highest 2-minute wind speed during Equinox race day was 21 miles per hour in 2003. Unfortunately, no wind data is available for Ester Dome, but it’s likely to be higher than what is recorded at the airport.

Weather from the week prior

It’s also useful to look at the weather from the week before the race, since excessive pre-race rain or snow can make conditions on race day very different, even if the race day weather is pleasant. The year I ran the full marathon (2013), it snowed the week before and much of the trail in the woods before the water stop near Henderson and all of the out and back were covered in snow.

The most dramatic example of this was 1992 where 23 inches (!) of snow fell at the airport in the week prior to the race, with much higher totals up on the summit of Ester Dome. Measurable snow has been recorded at the airport in the week prior to six races, but all the weekly totals are under an inch except for the snow year of 1992.

Precipitation has fallen in 44 of 55 pre-race weeks (80% of the time). Three years have had more than an inch of precipitation prior to the race: 1.49 inches in 2015, 1.26 inches in 1992 (most of which fell as snow), and 1.05 inches in 2007. On average, just over two tenths of an inch of precipitation falls in the week before the race.

Summary

The following stacked plots shows the weather for all 55 runnings of the Equinox marathon. The top panel shows the range of temperatures on race day from the airport station (wide bars) and estimated on Ester Dome (thin lines below bars). The shaded area at the bottom shows where temperatures are below freezing.

The middle panel shows race day liquid precipitation (rain, melted snow). Bars marked with an asterisk indicate years where snow was also recorded at the airport, but remember that five of the other years with liquid precipitation probably experienced snow on Ester Dome (1977, 1986, 1991, 1994, and 2016) because the temperatures were likely to be below freezing at elevation.

The bottom panel shows precipitation totals from the week prior to the race. Bars marked with an asterisk indicate weeks where snow was also recorded at the airport.

Equinox Marathon Weather

Here’s a table with most of the data from the analysis. A CSV with this data can be downloaded from all_wx.csv

Date min t max t ED min t ED max t awnd prcp snow p prcp p snow
1963-09-21 32.0 54.0 27.5 48.2   0.00 0.0 0.01 0.0
1964-09-19 34.0 57.9 29.4 51.8   0.00 0.0 0.03 0.0
1965-09-25 37.9 60.1 33.1 53.9   0.00 0.0 0.80 0.0
1966-09-24 36.0 62.1 31.3 55.8   0.00 0.0 0.01 0.0
1967-09-23 35.1 57.9 30.4 51.8   0.00 0.0 0.00 0.0
1968-09-21 23.0 44.1 19.1 38.9   0.00 0.0 0.04 0.0
1969-09-20 35.1 68.0 30.4 61.3   0.00 0.0 0.00 0.0
1970-09-19 24.1 39.9 20.1 34.9   0.00 0.0 0.42 0.0
1971-09-18 35.1 55.9 30.4 50.0   0.00 0.0 0.14 0.0
1972-09-23 19.9 42.1 16.1 37.0   0.00 0.0 0.01 0.2
1973-09-22 30.0 44.1 25.6 38.9   0.00 0.0 0.05 0.0
1974-09-21 48.0 60.1 42.5 53.9   0.08 0.0 0.00 0.0
1975-09-20 37.9 55.9 33.1 50.0   0.02 0.0 0.02 0.0
1976-09-18 34.0 59.0 29.4 52.9   0.00 0.0 0.54 0.0
1977-09-24 36.0 48.9 31.3 43.4   0.06 0.0 0.20 0.0
1978-09-23 30.0 42.1 25.6 37.0   0.00 0.0 0.10 0.3
1979-09-22 35.1 62.1 30.4 55.8   0.00 0.0 0.17 0.0
1980-09-20 30.9 43.0 26.5 37.8   0.00 0.0 0.35 0.0
1981-09-19 37.0 43.0 32.2 37.8   0.15 0.0 0.04 0.0
1982-09-18 42.1 61.0 37.0 54.8   0.02 0.0 0.22 0.0
1983-09-17 39.9 46.9 34.9 41.5   0.00 0.0 0.05 0.0
1984-09-22 28.9 60.1 24.6 53.9 5.8 0.00 0.0 0.08 0.0
1985-09-21 30.9 42.1 26.5 37.0 6.5 0.14 2.1 0.57 0.0
1986-09-20 36.0 52.0 31.3 46.3 8.3 0.07 0.0 0.21 0.0
1987-09-19 37.9 61.0 33.1 54.8 6.3 0.00 0.0 0.00 0.0
1988-09-24 37.0 45.0 32.2 39.7 4.0 0.00 0.0 0.11 0.0
1989-09-23 36.0 61.0 31.3 54.8 8.5 0.00 0.0 0.07 0.5
1990-09-22 37.9 50.0 33.1 44.4 7.8 0.26 0.0 0.00 0.0
1991-09-21 36.0 57.0 31.3 51.0 4.5 0.04 0.0 0.03 0.0
1992-09-19 24.1 33.1 20.1 28.5 6.7 0.01 0.4 1.26 23.0
1993-09-18 28.0 37.0 23.8 32.2 4.9 0.29 4.1 0.37 0.3
1994-09-24 27.0 51.1 22.8 45.5 6.0 0.02 0.0 0.08 0.0
1995-09-23 43.0 66.9 37.8 60.3 4.0 0.00 0.0 0.00 0.0
1996-09-21 28.9 37.9 24.6 33.1 6.9 0.06 1.2 0.26 0.0
1997-09-20 27.0 55.0 22.8 49.1 3.8 0.00 0.0 0.03 0.0
1998-09-19 42.1 60.1 37.0 53.9 4.9 0.00 0.0 0.37 0.0
1999-09-18 39.0 64.9 34.1 58.4 3.8 0.00 0.0 0.26 0.0
2000-09-16 28.9 50.0 24.6 44.4 5.6 0.00 0.0 0.30 0.0
2001-09-22 33.1 57.0 28.5 51.0 1.6 0.00 0.0 0.00 0.0
2002-09-21 33.1 48.9 28.5 43.4 3.8 0.00 0.0 0.03 0.0
2003-09-20 26.1 46.0 22.0 40.7 9.6 0.00 0.0 0.00 0.0
2004-09-18 26.1 48.0 22.0 42.5 4.3 0.00 0.0 0.25 0.0
2005-09-17 37.0 63.0 32.2 56.6 0.9 0.00 0.0 0.09 0.0
2006-09-16 46.0 64.0 40.7 57.6 4.3 0.00 0.0 0.00 0.0
2007-09-22 25.0 45.0 20.9 39.7 4.7 0.00 0.0 1.05 0.0
2008-09-20 34.0 51.1 29.4 45.5 4.5 0.00 0.0 0.08 0.0
2009-09-19 39.0 50.0 34.1 44.4 5.8 0.00 0.0 0.25 0.0
2010-09-18 35.1 64.9 30.4 58.4 2.5 0.00 0.0 0.00 0.0
2011-09-17 39.9 57.9 34.9 51.8 1.3 0.00 0.0 0.44 0.0
2012-09-22 46.9 66.9 41.5 60.3 6.0 0.00 0.0 0.33 0.0
2013-09-21 24.3 44.1 20.3 38.9 5.1 0.00 0.0 0.13 0.6
2014-09-20 45.0 51.1 39.7 45.5 1.6 0.36 0.0 0.00 0.0
2015-09-19 37.9 44.1 33.1 38.9 2.9 0.01 0.0 1.49 0.0
2016-09-17 34.0 57.9 29.4 51.8 2.2 0.01 0.0 0.61 0.0
2017-09-16 33.1 66.0 28.5 59.5 3.1 0.00 0.0 0.02 0.0
sun, 09-sep-2018, 10:54

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
wed, 03-jan-2018, 19:32

Well that was disappointing. I’ve read some of George Saunders’s short stories and was entertained, but I didn’t much enjoy Lincoln in the Bardo. It’s the story of Abraham Lincoln coming to the graveyard to visit his newly dead son William, told from the perspective of a variety of lost souls that don’t believe they’re dead. There was no plot to speak of, and none of the large cast of characters was appealing. I did enjoy the sections that were fictional quotes from contemporary histories, many of which contradicted each other on the details, and some of the characters told funny stories, but it didn’t hold together as a novel.

Widely acclaimed, winner of the Man Booker Prize, on many best of 2017 lists. Not my cup of tea.

Music I listened to while reading this:

  • Carlow Town, Seamus Fogarty
  • You’ve Got Tonight, Wiretree

Meta Photolog Archives