The Scottish Hill Race Record Time Predictor

Jeff B
April 16, 2020

Introduction

This presentation is for the final course project for the Coursera course “Developing Data Products.” For this assignment, we developed a Shiny App and made a 5-slide presentation to accompany it. The presentation is structured as follows:

  • Title page
  • Introduction (this page)
  • Overview of the Shiny app itself
  • UI-side code of the app
  • Server-side code of the app

Overview of the Shiny app

Link to the app

This very simple app allows the user to predict record times of Scottish hill races using the dataset hills, available in the MASS package. This dataset contains data on Scottish hill races in 1984.

The app provides 2 key variables that the user can change: race distance (in miles) and climb (in feet), which is the vertical distance covered by the race.

As the user changes the variables, the user can view live changes on a plot. The plot displays distance in miles on the x axis and time in minutes on the y axis. Beneath the plot, the app displays the estimated record time in a convenient hours-minutes-seconds format.

The output plot looks like this: plot of chunk unnamed-chunk-1

UI-side code

library(shiny); library(tidyverse); library(ggrepel); library(MASS)

shinyUI(fluidPage(

  # Make horizontal lines more aesthetically pleasing
  tags$head(tags$style(HTML("hr {border-top: 1px solid #C0C0C0;}"))),

  # Application title
  titlePanel("Scottish hill race record time predictor"),

  # Sidebar with a slider input for variables
  sidebarLayout(
    sidebarPanel(
      h3("Race variables"),
      p("Set the total distance and climb of your race"),
      hr(),
      sliderInput("dist1", "Distance:", min = 2, max = 28, value = 15, step = 1),
      hr(),
      sliderInput("climb1", "Climb:", min = 0, max = 10000, value = 4000, step = 100),
    ),

    # Show the plot
    mainPanel(
      p("Your race will appear below among record times of other races in 1984"),
      plotOutput("racePlot"),
      p("Estimated record time of your race is:"),
      textOutput("raceTime"),
      p(),
      textOutput("worldRecord")
    )
  )
  )
)

The UI-side code is fairly straightforward.

In the side panel, we set up the input variables, along with some simple horizontal lines to provide a little more aesthetic to the design.

In the main panel, we display the plot. Beneath the plot we display the record time in text form.

A little bit of instruction is sprinkled throughout in lieu of formal documentation.

Server-side code

library(shiny); library(tidyverse); library(ggrepel); library(MASS); library(lubridate)
shinyServer(function(input, output) {
  # Load and tidy the data we need
  df <- data.frame(hills)
  df <- mutate(df, race = rownames(hills))
  # Create linear model
  fit <- lm(time ~ dist + climb, data = df)
  # Make prediction
  time1 <- reactive({
    raceDist <- input$dist1
    raceClimb <- input$climb1
    predict(fit, newdata = data.frame(dist = raceDist, climb = raceClimb))})
  # Convert the time prediction into something more nicely displayed
  time2 <- reactive({ str_to_lower(as.character(seconds_to_period(round(
    as.numeric(as.period(time1()))*60)))) })
  # Tell a joke if the time is too low
  worldRecord <- reactive({ ifelse(time1()*60 < 223,
        print("This is faster than the current world record time for 1 mile!"),
        ifelse(time1()*60 < 478,
        print("This is faster than the world record time for 2 miles!"),""))})
  # Create plot
  output$racePlot <- renderPlot({
    raceDist <- input$dist1
    raceClimb <- input$climb1
    ggplot(data = hills,
           aes(x = dist, y = time, alpha = 0.5)) + 
      geom_point(shape = 3) + 
      geom_smooth(method="lm", size = 1) +
      labs(title = "Record times in 1984 for 35 Scottish hill races",
           x = "Distances in miles",
           y = "Time in minutes") +
      geom_text(label = df$race, check_overlap=TRUE, size = 3, nudge_y = 8) +
      geom_point(aes(x = raceDist, y = time1(), color="red"), shape = 8) +
      geom_text(label = "Your Race", aes(x = raceDist, y = time1(), color = "red"), nudge_y = 8) +
      theme(legend.position = "none")})
  output$raceTime <- renderText({ time2() })
  output$worldRecord <- renderText({ worldRecord() })})

The server-side code is slightly more complicated.

First it loads necessary packages and data. It tidies and processes the data a bit. Then it creates a linear model. So far nothing out of the ordinary.

Then it resorts to some reactive() functions. The reactive functions are necessary to run calculations that use inputs from the UI side. For this app, there are three reactive functions. One is for the time prediction itself. Another is to convert the time prediction into a more readable format. And the third is for an Easter egg that checks if the time is beneath a world record.

Next it builds the plot, drawing on the outputs of the reactive functions. The final lines render the text for the formatted prediction time and for the statement about a world record.

The data on display in this presentation is messily formatted because it was squeezed to fit into the page.