Wednesday, July 12, 2017

Interactive R programs

In the past, we have looked at using R to analyze audit data. The programs are kind of like batch processing. Whatever they do is predefined and you can't tell it to change without modifying the source code. Today we are going to take a look at how to make R application that respond to user input.


Shiny
The developers at RStudio created a way to marry web programming with R so that you have a web presentation layer and an R backend that responds to the changes. This brings a much needed capability because sometimes you want to see the data differently right away.

The shiny interface does bring with it a number of controls like Radio Buttons, drop down text boxes, sliders, charts, and boxes for grouping. You can take a look at a gallery of controls here.

To create a basic shiny app, open RStudio. Click on File|New File and then select "Shiny Web App". That brings up a dialog asking some basic questions. It asks what the application's name is. I put in Test. Then it asks if you want 1 file or 2. I select 1. If you choose 2, then it makes one file for the UI and one file for the back end. The last thing is to select the directory for the file. When you click on Create, it will open a file fully populated with a simple working app.

If you click "Run App", then you should have a program that looks something like this:




Moving the slider causes the histogram to change. Let's look at the code.

library(shiny)

# Define UI for application that draws a histogram
ui <- fluidPage(

   # Application title
   titlePanel("Old Faithful Geyser Data"),

   # Sidebar with a slider input for number of bins
   sidebarLayout(
      sidebarPanel(
         sliderInput("bins",
                     "Number of bins:",
                     min = 1,
                     max = 50,
                     value = 30)
      ),

      # Show a plot of the generated distribution
      mainPanel(
         plotOutput("distPlot")
      )
   )
)

# Define server logic required to draw a histogram
server <- function(input, output) {

   output$distPlot <- renderPlot({
      # generate bins based on input$bins from ui.R
      x    <- faithful[, 2]
      bins <- seq(min(x), max(x), length.out = input$bins + 1)

      # draw the histogram with the specified number of bins
      hist(x, breaks = bins, col = 'darkgray', border = 'white')
   })
}

# Run the application
shinyApp(ui = ui, server = server)



There are 2 parts to this program. The first part is the GUI. There is a call to fluid page that takes an undefined number of arguments that describe the widgets on the page. Each widget is itself a function call that takes parameters or other objects created by other functions. In the basic design, we have a title, a slider, and a plot.

On the server side we have a server object created by a function that has input and output objects. To make the GUI change, we define a distPlot sub-variable to output. We can call this anything. It just has to match what's on the GUI side. This variable is initialized by a renderPlot function which takes a few parameters to describe what to plot. It knows what to plot based on a sub-variable from the input argument, bins. This could be named anything but it has to match what the slide control has or nothing will happen.

The server side and GUI side are tied together with a function call to ShinyApp at the bottom. This is what runs the program. Under the hood, RStudio starts up a little web server that runs a cgi-bin application with an R environment that your app gets loaded into. On the front end it opens a little web browser and connects to the web server on localhost. The cgi-bin starts your session and sends a web page to draw. When you change anything in the web page, it sends a post to the cgi-bin with a new copy of all the variables in the GUI. This  immediately triggers the server code and it responds with an updated web page.

There is a nice and detailed tutorial video created by the RStudio developers if you wanted to learn more. I found it very helpful when learning Shiny. You can also browse around the widget gallery mentioned earlier. In it you can see the source code for all of these little examples.

Now let's do a simple program that does something with audit data. A long time ago, we learned how to do bar charts. That was a pretty simple program. Let's re-fit that code to run as a shiny app so that we tell it how to group the audit data.

library(shiny)
library(ggplot2)

# Read in the data and don't let strings become factors
audit <<- read.csv("~/R/audit-data/audit.csv", header=TRUE, stringsAsFactors = FALSE)
fnames <<- colnames(audit)
fnames[5] <<- "HOUR" # Change serial number to HOUR
audit$one <<- rep(1,nrow(audit))
# Create time series data frame for aggregating
audit$posixDate=as.POSIXct(paste(audit$DATE, audit$TIME), format="%m/%d/%Y %H:%M:%S")
# Create a column of hour and date to aggregate an hourly total.
audit$HOUR <- format(audit$posixDate, format = '%Y-%m-%d %H')
ourColors <<- c("red", "blue", "green", "cyan", "yellow", "orange", "black", "gray", "purple" )

# Define UI for application
ui <- shinyUI(fluidPage(
  # Application title
  titlePanel("Audit Barcharts"),

  sidebarLayout(
    sidebarPanel(
      selectInput("groupBy", "Group By", fnames, selected = "HOUR"),
      selectInput("lowColor", "Low Color", ourColors, selected = "blue"),
      selectInput("highColor", "High Color", ourColors, selected = "red"),
      width = 3
    ),
    # Show a plot of the generated distribution
    mainPanel(
      plotOutput("barPlot", width = "auto", height = "600px"),
      width = 9
    )
  )
))


# Define our server side code

server <- shinyServer(function(input, output) {
  observeEvent(c(input$groupBy, input$lowColor, input$highColor), {
    # Now summarize it
    grp <- input$groupBy

    temp <- aggregate(audit$one, by = audit[grp], FUN = length)
    temp$t <- as.character(temp[,grp])

    if (grp == "HOUR") {
      # Time based needs special handling
      final = data.frame(date=as.POSIXct(temp$t, format="%Y-%m-%d %H", tz="GMT"))
      final$num <- temp$x
      final$day <- weekdays(as.Date(final$date))
      final$oday <- factor(final$day, levels = unique(final$day))
      final$hour <- as.numeric(format(final$date, "%H"))

      output$barPlot<-renderPlot({
        pl <- ggplot(final, aes(x=final[,1], y=final$num, fill=final$num)) +
          geom_bar(stat="identity") + ggtitle(paste("Events by", grp)) +
          scale_x_datetime() + xlab("") + labs(x=grp, y="Number of Events") +
          scale_fill_gradient(low=input$lowColor, high = input$highColor, name=paste("Events/", grp, sep=""))
        print(pl)
      })
    } else {
      # non-time conversion branch
      final <- temp[,1:2]
      colnames(final) = c("factors", "num")
      final$factors <- abbreviate(final$factors, minlength = 20, strict = TRUE)

      # We will rotate based on how dense the labels are
      rot <- 90
      if (nrow(final) < 20)
        rot <- 60
      if (nrow(final) < 10)
        rot <- 45

      # Plot it
      output$barPlot<-renderPlot({
        pl <- ggplot(final, aes(x=final[,1], y=final$num, fill=final$num)) +
          geom_bar(stat="identity") + ggtitle(paste("Events by", grp)) +
          scale_x_discrete() + xlab("") + labs(x=grp, y="Number of Events") +
          scale_fill_gradient(low=input$lowColor, high = input$highColor, name=paste("Events/", grp, sep="")) +
          theme(axis.text.x = element_text(angle = rot, hjust = 1, size = 18))
        print(pl)
      })
    }
  })
})

# Run the application
shinyApp(ui = ui, server = server)



Make sure you have ~/R/audit-data/audit.csv filled with audit data. Save the above code as app.R and run it. You should see something like this:




Also notice that you can change the selection in the text drop downs and the chart is immediately redrawn. Briefly, the way this works is we setup some global data in the R environment. Next we define a GUI that has 3 selector inputs. All of the hard work is in the server function. What it does is wait for either of the 3 variables to change and if so re-draws the screen. We split the charting into 2 branches, time and everything else. The main difference is time variables need special handling. Basically we format the data to what's expected by the plotting function and pass it in. On the non-time side of things, we can get very dense groups. So what we do is rotate the text labels on the bottom if we start running out of room to fit more in.

Conclusion
This shows the basics of how a shiny app works. You can create very elaborate and complicate programs using this API. Now that we've been over Shiny basics, I'll talk about Audit Explorer next time.

No comments: