Shiny Applications

Shiny is an R package developed for building interactive web applications. Visual outputs resemble an easy-to-visit website offering instant feedback from user input. Within the framework, the Shiny package offers highly interactive tools such as widgets for simplicity of use for application programmers and users. Without needing to adjust the source code, users can interact with the flexible user interface and see the change of inputs immediately reflected in the output.

Building Randomization in a Shiny application

When studying the best practices for teaching R, a randomized controlled trial assesses whether there is a relationship between the order students learn R tools, and their ability to complete data manipulation tasks. Based on randomization, students learn either tidyverse or base first and then cross over to learn the other modality.

Figure  1: Crossover randomized trial.

Figure 1: Crossover randomized trial.

This study used the Shiny package to create this cross-random trial, which can host an application as a website that runs R code behind the scenes. For example, the study’s informed consent page automatically randomizes the participant into the tidyverse first group or base first group. A Shiny app is a directory structured by two main functions: a server function and a user interface (UI) function.

shinyApp(ui = ui, server = server)

The server function is a set of instructions that build the application’s computational components and run R code behind the scenes. Here, the server function acts as a random number generator to randomize participants into groups A or B. The function runs a reactive expression containing an R statement, which creates a random uniform variable. If the uniform variable is greater than 0.5, A will be the output; otherwise, B will be the output. The last line of the function makes sure that the created random variable will not suspend when hidden.

server <- function(input, output) { 
  output$random <- reactive({
    ifelse(runif(1) > 0.5, "A", "B") })
  
  outputOptions(output, "random", suspendWhenHidden = FALSE) }

The UI function is a set of instructions for the webpage’s layout and appearance; hence, it builds the user-facing side of the application. The conditional panels allow for a set of elements to dynamically show or hide, depending on if meeting the given conditions. In this study, the UI function contains two conditional panels for groups A and B. The selection control only appears when meeting the requirements.

If the random output is A, then the action button opens a new window to the Shiny app of order A.

ui <- shinyUI( ...
               conditionalPanel(
                 condition = "output.random == 'A'", 
                 actionButton(
                   inputId = "yes",
                   label = "Yes, I agree to this study",
                   icon = icon("check"),
                   onclick = "window.open('http://link-to-order-A.html')"
                   ))
               ...)

If the random output is B, then the action button opens a new window to the Shiny app of order B using a similar code as above, only replacing ‘A’ with ‘B.’ With the server function performing the calculations, and the UI function building the user-facing side of the application, a dynamic and interactive Shiny app solves many statistical problems tied together with one software mechanism.


Learnr Package

Building on R Markdown, the learnr package enables interactive tutorials featuring various interactive Shiny components. Users can access these interactive tutorials from their browser, via the same mechanisms as a Shiny application. The code exercises created in the learner package display as R code blocks; the user can edit and execute them directly. Progress is automatically preserved, allowing users to save and keep track of completed exercises or questions. In the context of this study, we utilize the learnr package as a tool for hosting online experiments designed to examine best practices for teaching statistical or programming concepts.

Collecting data using learnr

This section describes how to collect demographic, assessment, and exercise data from a Shiny application and store it for analysis. The three main tools that permit automatic and continuous data collection are shiny, rdrop2, which allows integration with Dropbox, and learnr. Shiny provides the overall platform, the integration with Dropbox allows the user to collect and store participant data, and learnr enables the participant to input responses.

Dropbox integration

In order to continuously save data on Dropbox, the user needs:

  1. A Dropbox account
  2. A token to allow access to this account from R

Outside of the Shiny app, we create a token for the authentications and save it as an .RDS file (Code 1). This process allows the application to communicate with Dropbox. In the app itself, we then reference the token (Code 2). Once authenticated, we can tie into learnr to pull data from each participant’s interactions: clicks, completions, skips, attempts, results, etc.

library(rdrop2)

token <- drop_auth()
saveRDS(token, "droptoken.rds")

Code 1. Creating a Dropbox token. This code is run outside the Shiny application.

library(rdrop2)

# inside the shiny app
drop_auth(rdstoken = "droptoken.rds")

Code 2. Referencing a Dropbox token to authenticate a user. This code is run once inside the Shiny application.


Recording responses with learnr

In order to save user input, we need to set up an event recorder. Code 3 outlines a function used to record every participant interaction in the application. The function has five inputs: the tutorial id, tutorial version, user id, event, and data. For any given participant, the user id, events, and data will be unique. Hence, each participant classifies under a unique identifier, completes exclusive events, and links to individual data that lists each event’s results. Then we set up the tutorial event recorder by specifying the created function in options (code 4).

event_recorder <- function(tutorial_id, 
                           tutorial_version,
                           user_id,
                           event, 
                           data)
  {
  
    # code goes here
  
  }

options(tutorial.event_recorder = event_recorder)

Code 4. Code scaffolding for including an event recorder.

Now, we’ll define what goes inside of the event_recorder function. The first piece defines the ‘if’ statement; this is where the function searches Dropbox for existing user data. Using the glue package, we instruct the code, in a single command, to search the designated Dropbox folder for previous data under the user’s unique id. If there’s data, the function proceeds to download and override that data to the indicated path.

... {
  if (drop_exists(glue("teaching-r-study/data_{user_id}.rds"))) {
    drop_download(path = glue("teaching-r-study/data_{user_id}.rds"),
                  local_path = glue("data_{user_id}.rds"),
                  overwrite = TRUE) }
  
  ... }

Next, the rds file reads into a local data frame called \(t\). We use the glue package to link the data to the user id, ensuring the dataset remains unique for each student.

... {
  
  t <- readRDS(glue("data_{user_id}.rds"))
  
  ... }
Figure  2: Local data frame, $\small t$.

Figure 2: Local data frame, \(\small t\).

 

Now, we define the else statement, which provides alternative commands when data doesn’t exist in the Dropbox. When there’s no data to download, the function creates an empty data frame called \(t\). The data set is unique to the user, and it accepts six inputs: current time, tutorial id, tutorial version, user id, event, and data.

... {
  
  ...
  
  else {
    t <- tibble(
      time = .POSIXct(numeric(0)),
      tutorial_id = character(),
      tutorial_version = character(),
      user_id = character(),
      event = character(),
      data = list())
    }
  
  ... }

Defining the above if-else statement pulls in the data frame \(t\) that either includes a former user’s data frame or creates a new user’s empty data frame. We define a mechanism to bind the rows of the data frame, which takes the original data frame \(t\) and adds an extra row.

... {
  
  ...

  t <- bind_rows(t, tibble(
    time = Sys.time(),
    tutorial_id = tutorial_id,
    tutorial_version = tutorial_version,
    user_id = user_id,
    event = event,
    data = list(data)
  ))
  
  ... }
Figure  3: Binding rows of the data frame by taking $t$ and adding an extra row.

Figure 3: Binding rows of the data frame by taking \(t\) and adding an extra row.

 

Lastly, we instruct the code to save the RDS so that, whenever a user interacts with the tutorial, the function automatically appends a new row to the user’s data frame. This process documents constant updates to the data frame and uploads it back onto Dropbox.

... {
  
  ...
  
  
  saveRDS(t, file = glue("data_{user_id}.rds"))
  drop_upload(file = glue("data_{user_id}.rds"),
              path = "teaching-r-study")
  }

 

Putting it all together
event_recorder <- function(tutorial_id, tutorial_version, user_id, event, data) {
  
  if (drop_exists(glue("teaching-r-study/data_{user_id}.rds"))) {
    drop_download(path = glue("teaching-r-study/data_{user_id}.rds"),
                  local_path = glue("data_{user_id}.rds"),
                  overwrite = TRUE)}
  
  t <- readRDS(glue("data_{user_id}.rds"))
  
  else {t <- tibble(
    time = .POSIXct(numeric(0)),
    tutorial_id = character(),
    tutorial_version = character(),
    user_id = character(),
    event = character(),
    data = list())}
  
  t <- bind_rows(t, tibble(
    time = Sys.time(),
    tutorial_id = tutorial_id,
    tutorial_version = tutorial_version,
    user_id = user_id,
    event = event,
    data = list(data)))
  
  saveRDS(t, file = glue("data_{user_id}.rds"))
  drop_upload(file = glue("data_{user_id}.rds"),
              path = "teaching-r-study")
}

Now, we look into the deployment of questions. By default, the learnr module writes questions with correct answers and provides instant feedback that reveals the solution. However, this study uses assessments that aren’t supposed to give away solutions immediately. To change the module’s default, we create a new keyword class in the R space that marks every answer as correct. Regardless of whether answered correctly, the user will get the correct answer, letting them know they completed their submission and can move on.

question_is_correct.always_correct <- function(question, value, ...) {
  return(mark_as(TRUE, message = NULL))
}
question("This is your question?",
         answer("This is an answer.", correct = TRUE),
         type = c("always_correct", "radio_button"),
         correct = "Submitted")

We have a sneak peek at what the data will look like when going through the tutorial. There’s a column for each function input: current time, tutorial id, tutorial version, user id, event, and data. The data, a different list for each user event, holds the most critical information for analyzing results. We can sift, organize, and analyze through the copious amounts of data using the tidycode and Matahari packages.

time tutorial_id tutorial_version user_id event data
2020-05-27 18:12:52 /Useres/lucymcg… 1.0 lucymcg… exerc… <nam…
2020-05-27 18:12:57 /Useres/lucymcg… 1.0 lucymcg… exerc… <nam…
2020-05-27 18:13:02 /Useres/lucymcg… 1.0 lucymcg… exerc… <nam…
2020-05-27 18:13:07 /Useres/lucymcg… 1.0 lucymcg… exerc… <nam…
2020-05-27 18:13:17 /Useres/lucymcg… 1.0 lucymcg… exerc… <nam…
2020-05-28 10:39:19 /Useres/lucymcg… 1.0 lucymcg… exerc… <nam…

In conclusion, this study emphasizes the simplicity of using R Shiny, rdrop2, and learner packages to deploy a discrete method for collecting user participation data. By providing access to all user attempts, operations, clicks, skips, errors, time, etc. in a tutorial, R furthers valuable data collection, leading to vital analysis for answering complex questions.


References

McGowan, L.D. (2020) Best practices for teaching r a randomized controlled trial. Lucymcgowan.com/Talks.