tl;dr
Can you match the tidyverse function to its package? I used {learnr} to make a βtidyquizβ to test you.
A live version is available at https://mattdray.shinyapps.io/tidyquiz/
To run locally and get the very latest functions:
remotes::install_github("matt-dray/tidyquiz")
to install {tidyquiz} (itβs a package!)library(tidyquiz)
to load itlearnr::run_tutorial("tidy", package = "tidyquiz")
to open in your browser
The problem
I saw a (probably) tongue-in-cheek tweet recently from Ryan Timpe:
Hardest part about #rstats package development: remembering which functions are from {dplyr} and which are from {tidyr}.
β Ryan Timpe π¦π (@ryantimpe) March 4, 2019
Itβs easy enough to get out of this pickle, but maybe thereβs a deeper problem? What if the purpose of each tidyverse isnβt clear enough?1 Is there too much arbitrary jargon in the tidyverse?
Enjoy your existential crisis. Meanwhile, Iβve made a little quiz to see if you can remember whether unnest()
is from {dplyr} or {tidyr}2. In fact, itβs an interactive multi-choice test that presents you a random function from the tidyverse and challenges you to select the correct package.
Step 0: the approach
I wanted:
- To get a tidy data frame of all the tidyverse package-function combos
- A user to be presented with an interactive question about one of these tidyverse functions
- The ability to generate a new question from within the document
- To share this quiz easily, without a server
Read the rest of this post to see how I tackled these. Or, you know, spoilers:
- The
tidyverse_packages()
function from {tidyverse} - The {learnr} package
- An
actionButton()
and Shiny reactivity - You can put a {learnr} quiz in a package and call it from there!
Step 1: package-function combos
The {tidyverse} package is a package that loads packages.3 Itβs a convenient way to load the eight core packages of the tidyverse.
library(tidyverse)
## ββ Attaching packages βββββββββββββββββββββββββββββββββββββββββββββββββββ tidyverse 1.2.1 ββ
## β ggplot2 3.1.1 β purrr 0.3.2
## β tibble 2.1.3 β dplyr 0.8.1
## β tidyr 0.8.3 β stringr 1.4.0
## β readr 1.3.1 β forcats 0.4.0
## ββ Conflicts ββββββββββββββββββββββββββββββββββββββββββββββββββββββ tidyverse_conflicts() ββ
## β dplyr::filter() masks stats::filter()
## β dplyr::lag() masks stats::lag()
But thereβs more than these core eight. To access a list of functions for each package, we first need to load all the packages. We can get a character vector of them all with tidyverse_packages()
.
tidy_pkgs <- tidyverse_packages() %>% # calls character vector
# We can protect ourselves from any rogue characters in this vector
str_replace("\n", "") %>% # remove newline
str_replace(">=", "") %>% # remove greater than or equal to
str_replace("[:punct:]", "") # remove punctuation
tidy_pkgs
## [1] "broom" "cli" "crayon" "dplyr" "dbplyr"
## [6] "forcats" "ggplot2" "haven" "hms" "httr"
## [11] "jsonlite" "lubridate" "magrittr" "modelr" "purrr"
## [16] "readr" "readxl" "reprex" "rlang" "rstudioapi"
## [21] "rvest" "stringr" "tibble" "tidyr" "xml2"
## [26] "tidyverse"
We can pass this character vector to p_load()
. This convenient function from {pacman} installs and loads them all for us.
library(pacman)
p_load(
char = tidy_pkgs,
character.only = TRUE # read elements of character vector
)
Now we can get the functions from each package by mapping over them with {purrr} and {pacman}βs p_functions()
.
tidy_funs <- tidy_pkgs %>%
enframe(name = NULL, value = "package") %>% # make tibble
mutate(
functions = map(
package, # for each package...
~p_functions(.x, character.only = TRUE) # ...get the functions within
)
) %>%
unnest() # unpack the listcol elements
Hereβs a small sample:
sample_n(tidy_funs, 10) # random sample
## # A tibble: 10 x 2
## package functions
## <chr> <chr>
## 1 rstudioapi navigateToFile
## 2 rlang push_options
## 3 rstudioapi jobAddProgress
## 4 modelr bootstrap
## 5 rlang env_unlock
## 6 ggplot2 StatBoxplot
## 7 rvest html
## 8 rlang call_args_names
## 9 haven print_tagged_na
## 10 dbplyr sql_build
Out of interest we can look at the packages with the most and fewest functions:
count(tidy_funs, package, sort = TRUE) %>% slice(1:5)
## # A tibble: 5 x 2
## package n
## <chr> <int>
## 1 ggplot2 456
## 2 rlang 455
## 3 dplyr 261
## 4 lubridate 212
## 5 purrr 177
count(tidy_funs, package) %>% arrange(n) %>% slice(1:5)
## # A tibble: 5 x 2
## package n
## <chr> <int>
## 1 tidyverse 5
## 2 reprex 6
## 3 hms 7
## 4 broom 10
## 5 readxl 13
Another source of confusion might be that some functions exist in multiple packages. How many functions?
count(tidy_funs, functions, sort = TRUE) %>% filter(n > 1) %>% nrow()
## [1] 96
Okay, we have our data set, so letβs get quizzical.
Step 2: interactive questions with {learnr}
The {learnr} package helps you turn an R Markdown document into an interactive tutorial with a little help from Shiny. One option is to create a multiple-choice question, which is exactly what we need.
I should say that {learnr} wasnβt really intended for what Iβve done β itβs better suited to longform tutorials β but using it means that I didnβt have to write the logic for a multi-choice quiz question. Shrug.
Having installed the package and started a {learnr}-flavoured R Markdown4 we can create a question inside a code chunk in this form:
quiz(
caption = "Question 1",
question(
text = "What is Pokemon #399?", # question
answer("Bidoof"), correct = TRUE), # right answer
answer("Drifloom"), # wrong
answer("Pyukumuku"), # wrong
answer("Rayquaza"), # wrong
random_answer_order = TRUE # answers ordered randomly
)
)
But this example is hard-coded. In our case we want to replace the subject of the question and the answers any time we want to be presented with a new question.
Looks like weβll need a button for users to press to signal that they want a new question.
Step 3: generate new questions with Shiny
Since {learnr} operates in a Shiny runtime in our R Markdown file, itβs no problem to use Shinyβs actionButton()
.
actionButton("goButton", "Get Question") # button
You can press the button in the app to generate a new seed base don the current time and date. The seed is then used to randomly select a new question for the user.
To make this reactive β so that nothing will happen until the button is pressed β we can write Shiny server code in an R Markdown chunk by setting context="server"
in the chunk options. So hereβs how we get a new seed after clicking:
seed <- eventReactive(
input$goButton,
{
seed_temp <- as.numeric(Sys.time())
return(seed_temp)
}
)
Then our code needs to sample a row from the full data frame of package-function combos and isolate the name of the function the user will be quizzed on. This code is within eventReactive()
and will only trigger when the button has been activated. Second, we use renderText()
to take the function name and paste it into a string to create our question.
# Set the reactive element
fun_name <- eventReactive(
input$goButton, # on input
{
seed_val <- seed() # the newly-generated seed value
set.seed(seed_val) # user-selected value is seed value
fun_sample <- sample_n(tidy_funs, 1) # sample a package-function combo
fun_name <- select(fun_sample, functions) %>% pull() # just the function name
return(fun_name) # return the package value
}
)
# Set the output
# Generate a question that includes the sampled function name
output$fun_name_out <- renderText({
paste0("The function `", fun_name(), "` is from which tidyverse package?")
})
We can repeat this for getting the right answer and alter the code slightly to generate a few wrong answers. A wrong answer is selected randomly from the data frame of tidyverse functions, but only once the correct answer and already-selected wrong answers have been removed. Iβve also coded it so that any package that has a function with the same name β a conflict β will also be removed before a βwrongβ answer is chosen.
So rather than the hard-coded example of a multi-choice question in Step 2, our quiz question code will look like this:
quiz(
caption = "Question ",
question(
text = as.character(textOutput("fun_name_out")),
answer(as.character(textOutput("ans_correct_out")), correct = TRUE),
answer(as.character(textOutput("ans_wrong1_out"))),
answer(as.character(textOutput("ans_wrong2_out"))),
answer(as.character(textOutput("ans_wrong3_out"))),
random_answer_order = TRUE
)
)
So now the text outputs will be rendered into the quiz question and this wonβt change until the the βGet Questionβ button is clicked.
Actually, thatβs sort-of a lie. {learnr} remembers how itβs users have performed; it saves their progress. To erase this, we need to click βStart Overβ from the menu pane to clear that memory.
Get the code
Browse the code on GitHub and leave an issue with thoughts or suggestions.
For example, it could definitely be improved if the user got a set of 10 questions that were graded to give a final mark. Maybe Iβll implement this one day.
For now, give it a go and let me know if you ever find out if drop_na()
is in {dplyr} or {tidyr}.5
Seems even Hadley gets it wrong sometimes.β©
Am I tricking you? Is it actually from neither?β©
The meme writes itself. Or rather, you can do it for me.β©
After installing {learnr} you can go to new R Markdown > From Template > Interactive Tutorial.β©
Am I tricking you? Is it actually from neither?β©