ETC5523: Communicating with Data
Tutorial 9: Build a coin-flipping app
๐ฏ Objectives
By the end of this tutorial, you will be able to:
- connect a Shiny input to an output;
- simulate repeated coin flips; and
- use one reactive expression in more than one output.
You will build one small app, one piece at a time. Each time the user presses a button, the app will flip a biased coin 100 times, show the outcomes and calculate the proportion of heads.
Do not try to write the complete app at once. Run the app after every small change.
Preparation
You need the shiny and bslib packages. Install them in the Console if you do not already have them:
install.packages(c("shiny", "bslib"))Create a new folder called coin-app. Inside that folder, create a blank file called app.R.
The question behind the app
Imagine a coin for which the probability of heads is 0.8. We flip it 100 times and estimate the probability of heads as:
number of heads รท 100
Even though the true probability stays at 0.8, the estimate will not be exactly 0.8 every time. Our app will let a user repeat the experiment and see this variation.
1. Start with the app skeleton
Copy the following code into app.R.
library(shiny)
library(bslib)
ui <- page_sidebar(
title = "How much does an estimate change?",
sidebar = sidebar(
# We will put an input here.
),
card(
card_header("Flip a biased coin 100 times"),
p("The probability of heads is 0.8."),
# We will put outputs here.
)
)
server <- function(input, output, session) {
# We will add the app's instructions here.
}
shinyApp(ui, server)Run the app. You should see a title, an empty sidebar and a card containing some text.
The packages come first, ui describes the page, server describes what happens, and shinyApp(ui, server) assembles the app.
2. Practise flipping the coin in R
Before using Shiny, complete this ordinary R code. Replace the blanks so it makes 100 flips, samples with replacement, and gives heads probability 0.8 and tails probability 0.2.
flips <- sample(
c("H", "T"),
size = _____,
replace = _____,
prob = c(_____, _____)
)Run flips, then use this comparison to check which results are heads:
flips == "H"R treats TRUE as 1 and FALSE as 0 when calculating a mean. Calculate the proportion of heads.
4. Reserve places for the results
Replace the output comment inside card() with the following headings:
h4("Estimate"),
# Put the estimate output here.
h4("The 100 outcomes"),
# Put the outcomes output here.Under the first heading, add textOutput() with the output ID "estimate". Under the second heading, add another textOutput() with the ID "flips".
Run the app again. textOutput() reserves a place on the page; it does not create the text.
5. Make the first connection
We will first use the estimate output to check that the button and server are connected.
Inside server(), add:
output$estimate <- renderText({
paste("The button has been pressed", input$flip, "times.")
})Run the app and press the button twice. Trace the path:
actionButton("flip", ...) โ input$flip โ renderText() โ output$estimate โ textOutput("estimate")
The input ID "flip" becomes input$flip. The output ID "estimate" becomes output$estimate. These names must match exactly.
6. Generate a new batch after each press
The app needs one batch of 100 flips that can be used by both outputs. Replace the contents of server() with this skeleton:
server <- function(input, output, session) {
flips <- reactive({
if (input$flip == 0) {
return(character(0))
}
# Simulate and return 100 flips here.
})
}Copy your sample() code from Step 2 into the marked position. Do not assign it to another object: the last expression in the reactive block is the value it returns.
Before the first press, input$flip is 0 and the reactive expression returns an empty character vector. After a press, input$flip changes and the code runs again.
flips is a reactive expression, not an ordinary vector. Inside the server, get its current value by writing flips() with parentheses.
7. Calculate and display the estimate
After the closing }) of the reactive expression, but still inside server(), recreate output$estimate.
Complete the three blanks in this skeleton:
output$estimate <- renderText({
if (input$flip == 0) {
return("Press the button to run the experiment.")
}
# Replace each blank with the required R code.
results <- _____
estimate <- _____
paste("Estimated probability of heads:", _____)
})Run the app. It should display an instruction at first and a new estimate after every press.
8. Display the individual outcomes
We also reserved textOutput("flips") in the UI. Create its matching server output with renderText().
Use:
paste(flips(), collapse = " ")to turn the vector of outcomes into one piece of text with spaces between the values. As with the estimate, show an instruction before the first press.
Run the app and press the button. Do the displayed outcomes and estimate come from the same experiment?
Both outputs call flips(). Shiny therefore generates one batch for each press and lets both outputs use it. Separate calls to sample() in the two outputs could produce results that do not agree.
Check that one press produces 100 outcomes, that the estimate agrees with them, and that another press replaces both results.
Optional extension: count the heads (5 minutes)
Add one more output that reports the number of heads, for example:
Number of heads: 83 out of 100
This output must use the existing flips() reactive expression. It should not call sample() again.
First, add a heading and output place inside card() in the UI:
h4("Number of heads"),
textOutput("heads")Then add the matching output$heads inside the server. These two expressions will help:
sum(flips() == "H")
paste("Number of heads:", number_of_heads, "out of 100")Remember to display an instruction rather than a count before the first button press.
If your app does not work
- Read the first error in the R Console.
- Check the most recent code you added, including commas and brackets.
- Check that
"flip","estimate"and"flips"match everywhere. - Check that you read the reactive value with parentheses:
flips().
If the app opens but part of the page is blank, trace that output backwards:
textOutput("estimate") โ output$estimate โ renderText() โ the values used inside it.
Wrap-up
For the coin-flipping app:
- the input is the button press;
- the shared reactive value is the batch of 100 flips; and
- the outputs are two text views of that batch.
The important pattern is:
UI input โ server calculation โ UI output