Shiny
Shiny
is RStudio’s framework for creating interactive
graphics and web-like applications. There are several ways to use
Shiny
, and we’re going to focus on how to use it in
conjunction with flexdashboard
to make interactive
dashboards within R Markdown.
This page is related to content in plotly and dashboards.
As always, I’ll work on today’s example in a GitHub repo + local
directory / R Project. This template will be
the starting point for the flexdashboard
we’ll be creating
today; create a new .Rmd file with this structure and put it in your
directory.
---
title: "Shiny Dashboard"
output:
flexdashboard::flex_dashboard:
orientation: columns
vertical_layout: fill
runtime: shiny
---
```{r setup, include=FALSE}
library(flexdashboard)
```
Column {.sidebar}
-----------------------------------------------------------------------
```{r}
```
Column {data-width=650}
-----------------------------------------------------------------------
### Chart A
```{r}
```
Column {data-width=350}
-----------------------------------------------------------------------
### Chart B
```{r}
```
### Chart C
```{r}
```
This is very similar to the usual flexdashboard
template, with some small changes. First, we’ve added
runtime: shiny
to the YAML header. Second, we are using
slightly different layout because we have a sidebar column which will
hold our input widgets.
I’ll add the following libraries to the template, but we won’t need much else.
library(tidyverse)
library(viridis)
## Loading required package: viridisLite
library(p8105.datasets)
library(plotly)
##
## Attaching package: 'plotly'
## The following object is masked from 'package:ggplot2':
##
## last_plot
## The following object is masked from 'package:stats':
##
## filter
## The following object is masked from 'package:graphics':
##
## layout
As in plotly and dashboards, we’re going to focus on the Airbnb data. Our goal is to make a dashboard similar to the one produced there, but with interactive elements in place of hard-coded data manipulation choices.
The code below loads and cleans the data, and selects only a few of the variables.
data("nyc_airbnb")
nyc_airbnb =
nyc_airbnb |>
mutate(rating = review_scores_location / 2) |>
rename(latitude = lat, longitude = long) |>
select(
borough = neighbourhood_group, neighbourhood, rating, price, room_type,
latitude, longitude) |>
drop_na(rating)
Previously, we chose to focus on Manhattan rentals in a certain price range, and included those choices in our data manipulation. Now, we’re going to filter the dataset interactively based on user input, and use the resulting dataset as the basis for our plots.
We’ll place code for widgets in the sidebar, but this is a stylistic choice and they could be placed elsewhere in the dashboard.
The selectInput
widget creates a drop-down menu with
choices for the user to select from. By adding the code in the chunk
below to the sidebar panel in the dashboard, we can obtain user input
regarding the borough of interest.
boroughs = nyc_airbnb |> distinct(borough) |> pull()
# selectInput widget
selectInput(
"borough_choice",
label = h3("Select borough"),
choices = boroughs, selected = "Manhattan")
Now a user can select each of the different boroughs in New York,
although it’s not clear yet where this input goes. In the background,
Shiny is creating an object called input
of class
reactivevalues
– it’s not critical that you know much about
this, but it’s something you can treat like a named list and use inside
render
functions. The “named” part matters, too – your
input widget names are how you access elements of the input
object.
Try adding the code below to one of the panels (later we’ll use these for plots, but for now it’s helpful to get a sense for how Shiny works).
renderPrint({
input[["borough_choice"]]
})
This produces a string containing the selected borough, and updates when a user manipulates the input widget.
The sliderInput
function produces a slider input widget.
We’ll use this to get a user-specified price range, but if you provide
only a single initial value it will produce a “regular” slider. Add this
slider input to the sidebar, and then modify your
renderPrint
code chunk to examine the value of
input[["price_range"]]
.
max_price = 1000
min_price = nyc_airbnb |> distinct(price) |> min()
# sliderInput widget
sliderInput(
"price_range",
label = h3("Choose price range"),
min = min_price, max = max_price, value = c(100, 400))
The last input widget we’ll look at is radioButtons
,
which is helpful for getting users to select among several options. As
with the preceeding inputs, add a chunk containing the code below to the
sidebar and then update the renderPrint
function to inspect
the behavior of this widget.
room_choice = nyc_airbnb |> distinct(room_type) |> pull()
# radioButtons widget
radioButtons(
"room_choice",
label = h3("Choose room type"),
choices = room_choice, selected = "Entire home/apt")
For other types of widgets (and there are lots!) check out the widget gallery.
In plotly and dashboards, we made plots showing rental locations, number of rentals in each neighborhood, and the price range of rentals in some neighborhoods. We’re going to make similar plots now, but we want these to update based on user inputs.
We’ll start with the plot_ly
scatterplot showing rental
location and prices. As a first step, replace the
renderText
code in your dashboard with the
renderPlotly
code below.
renderPlotly({
nyc_airbnb |>
mutate(text_label = str_c("Price: $", price, '\nRating: ', rating)) |>
plot_ly(
x = ~longitude, y = ~latitude, type = "scatter", mode = "markers",
alpha = 0.5, color = ~price, text = ~text_label)
})
The plot does appear, but we haven’t incorporated user input yet. To
do that, we’ll add some data manipuation that uses the
input
object produced by the Shiny input widgets.
renderPlotly({
nyc_airbnb |>
filter(
borough == input[["borough_choice"]],
price %in% input[["price_range"]][1]:input[["price_range"]][2],
room_type == input[["room_choice"]]) |>
mutate(text_label = str_c("Price: $", price, '\nRating: ', rating)) |>
plot_ly(
x = ~longitude, y = ~latitude, type = "scatter", mode = "markers",
alpha = 0.5, color = ~price, text = ~text_label)
})
Learning Assessment:
Update the following boxplot code to react to user input and add it to your dashboard.
renderPlotly({
nyc_airbnb |>
count(neighbourhood) |>
mutate(neighbourhood = fct_reorder(neighbourhood, n)) |>
plot_ly(x = ~neighbourhood, y = ~n, color = ~neighbourhood, type = "bar")
})
The code chunk below, when added to the dashboard, will update the bar chart based on user input.
renderPlotly({
nyc_airbnb |>
filter(
borough == input[["borough_choice"]],
price %in% input[["price_range"]][1]:input[["price_range"]][2],
room_type == input[["room_choice"]]) |>
count(neighbourhood) |>
mutate(neighbourhood = fct_reorder(neighbourhood, n)) |>
plot_ly(x = ~neighbourhood, y = ~n, color = ~neighbourhood, type = "bar")
})
Lastly, the code provided below will produce a reactive boxplot showing price ranges in popular neighborhoods in the selected borough.
renderPlotly({
common_neighborhoods =
nyc_airbnb |>
filter(
borough == input[["borough_choice"]],
price %in% input[["price_range"]][1]:input[["price_range"]][2],
room_type == input[["room_choice"]]) |>
count(neighbourhood, sort = TRUE) |>
top_n(8) |>
select(neighbourhood)
nyc_airbnb |>
filter(
borough == input[["borough_choice"]],
price %in% input[["price_range"]][1]:input[["price_range"]][2],
room_type == input[["room_choice"]]) |>
inner_join(., common_neighborhoods, by = "neighbourhood") |>
plot_ly(y = ~price, color = ~neighbourhood, type = "box")
})
Now you have a flexdashboard
with Shiny elements!
As you’ve probably noticed by now, debugging Shiny documents is a
hassle. The code depends on an input
object, so testing
individual lines isn’t easy – I often have to create “placeholder”
inputs when building plots and other outputs, and then make sure these
plots react to changing user inputs. This was the approach in the Airbnb
dashboard, where I started with “working” plots and then updated them to
include input
values. I also typically double check (via
printing) that the input
object itself behaves the way I’m
expecting.
All that said, though, it can be frustrating to make these “work”
One of my favorite data science products is Julia Silge’s dashboard looking at power in A/B tests, which is explained in the accompanying blog post. I like this because it conveys real statistical concerns about power, sample size, and effect size in a user-friendly way. It’s also coded very nicely and made public for anyone to inspect, edit, or use.
The code for the dashboard is printed below; create a new .Rmd file and copy this code to reproduce the dashboard yourself.
---
title: "How do we measure differences?"
runtime: shiny
output:
flexdashboard::flex_dashboard:
theme: cosmo
orientation: columns
vertical_layout: fill
source_code: embed
---
```{r setup, include=FALSE}
library(flexdashboard)
library(tidyverse)
library(broom)
```
Column {.sidebar}
-----------------------------------------------------------------------
How are sample size, effect size, false positive, and false negative rates related?
The power of a test ($P$, $1-\beta$) is the probability that a test will detect an effect, if an effect is really there. When your power is high, your false negative rate is low.
The significance level of a test ($\alpha$) is the probability that a test will detect an effect, if an effect is really *not* there. When your significance level is low, your false positive rate is low.
We would like to not be fooled too often by either false negatives or false positives, so we choose large enough sample sizes for the effect size we expect to see.
#### Move the sliders to explore the relationships
```{r}
sliderInput("Power", "Power threshold", min = 1, max = 99,
value = 80, post = "%")
sliderInput("SigLevel", "Significance level", min = 1, max = 20,
value = 5, post = "%")
sliderInput("Baseline", "Baseline conversion rate", min = 1, max = 50,
value = 10, post = "%")
```
The sample sizes here are per variation (A and B in an A/B test), not the test as a whole.
Column
-----------------------------------------------------------------------
### Power calculation {data-height=800}
```{r}
renderPlot({
seq(1000, 1e4, by = 1000) %>%
map_df(~ power.prop.test(p1 = input$Baseline / 100,
p2 = seq(input$Baseline / 100, input$Baseline * 1.5 / 100,
by=0.001),
n = .x,
power = NULL,
sig.level = input$SigLevel / 100) %>%
tidy()) %>%
mutate(effect = (p2 / p1 - 1)) %>%
ggplot(aes(effect, power, color = n, group = n)) +
geom_hline(yintercept = input$Power / 100, linetype = 2, color = "gray50", alpha = 0.5, size = 1.5) +
geom_line(size = 1.5, alpha = 0.7) +
theme_minimal(base_size = 18) +
scale_y_continuous(labels = scales::percent_format(),
limits = c(0, NA)) +
scale_x_continuous(labels = scales::percent_format()) +
scale_color_gradient(high = "#0077CC", low = "#B8E0C5",
labels = scales::comma_format()) +
labs(x = "Effect size (relative % change in rate)", y = "Power", color = "Sample size")
})
```
### With those parameters, you can measure... {data-height=200}
```{r}
renderTable({
seq(1000, 1e4, by = 1000) %>%
map_df(~ power.prop.test(p1 = input$Baseline / 100,
p2 = NULL,
n = .x,
power = input$Power / 100,
sig.level = input$SigLevel / 100) %>%
tidy()) %>%
mutate(effect = scales::percent(p2 / p1 - 1),
n = scales::comma(n)) %>%
select(`A relative % change of` = effect,
`With a sample size in each group of` = n)
})
```
There are a few user inputs controlling the desired power level,
alpha level, and the baseline conversion rate (which is the probability
of “success” in the reference group). Given these inputs, code chunks
compute power for given sample sizes over a dense grid on effect sizes
using purrr
functions, power.prop.test
, and
broom::tidy
; the results are plotted using
ggplot
or printed as a table.
Unfortunately, you can’t email Shiny-based HTML files like you can a
static HTML file generated by R Markdown. This is because Shiny-based
documents need to run R
locally. However, you can host them
through Shinyapps.io. A slightly
fancier version of the Shiny-based Airbnb flexdashboard
for
this class is hosted here, and
Julia Silge’s A/B testing dashboard is hosted on Shinyapps.io as
well.
Shinyapps.io has a free user level and will host your app as long as it’s relatively small and has limited usage. Deploying an app to Shinyapps.io requires you to create an account and connect it to RStudio, but it’s not too bad – this walkthrough will help.
So far we’ve focused on adding Shiny to a flexdashboard
.
We refer to our completed dashboard as a Shiny document.
However, there other ways to use Shiny, and the most common is to make a
Shiny application, or app. So you know how these work,
we’re going to make a super-simple app; if you are interested in making
a more complicated Shiny app, check out the resources included
below.
Shiny apps use .R files, rather than
.Rmd. You can initialize a Shiny app from RStudio using
File > New File > Shiny Web App. This will create a template app
consisting of a ui
object and a server
object;
in some cases these are stored in separate ui.R
and
server.R
files, but that’s not necessary. The
ui
object controls the app layout, includes the input
widgets, and displays any outputs. The server
object holds
the code that performs computations on input
elements and
produces an output
object.
Given ui
and server
, the command
shinyApp(ui = ui, server = server)
will run the app.
Shiny apps can be quite flexible, if a little harder to get used to that Shiny documents, and can be hosted online through Shinyapps.io as well.
The code that I produced working examples in lecture is here.