Creating flexible e-learning quizzes with literate programming

15th December 2023 @ OZCOTS, Wollongong

Mitchell O’Hara-Wild

Show of hands 🙌

Keep your hands up you have…

  • used a learning management system (LMS)
  • used Moodle before
  • tried making a Moodle quiz
  • successfully made a Moodle quiz before

Making Moodle quizzes isn’t hard

However it is tedious and limits flexibility for creating good quizzes for teaching statistics

Why Moodle?

🎓 I use it at Monash University

I want to make my life easier when assessing students!

🎉 It’s free and open source

My software is also free and open source!

This improves accessibility.

Demo: creating a quiz in Moodle

Creating a quiz question in Moodle

Literate programming quizzes

Literate programming

An approach for creating documents or code using both writing and code in the same place

For R users, you might know this as Sweave/RMarkdown

For Python users, you probably think about notebooks

Statistics education ❤️ Literate programming

Literate programming for statistical quizzes is a great match! We work closely with data and code, and want to test students on these skills

Literate programming quizzes

💥 This isn’t a new idea…

The exams package for R has been on CRAN for over 15 years!

Lots of information at https://www.r-exams.org/

Literate programming quizzes

Why re-invent the R/exams wheel?

It ticks a lot of boxes

  • Uses R Markdown to create dynamic questions with code
  • Supports random repetition of quiz questions
  • Outputs to Moodle, Canvas, OpenOlat, or Blackboard

But it leaves some things to be desired

  • Defining questions, answers, and feedback is difficult
  • Each question requires separate R Markdown documents

Literate programming quizzes

Using R/exams

boxhist.Rmd [106 lines]
```{r data generation, echo = FALSE, results = "hide"}
## DATA
n <- sample(30:50, 1)
m <- sample(1:4, 1)
s <- runif(1, 0.5, 2)
delta <- ifelse(runif(1) < 0.2, sample(8:12), 0)
p2 <- runif(1, 0.45, 0.55)
skewed <- left <- FALSE
if(!delta) {
    skewed <- runif(1) < 0.6
    left <- runif(1) < 0.3
}

dgpBoxhist <- function(n = 40, mean = 0, sd = 1, delta = 0,
  p2 = 0.5, skewed = FALSE, left = FALSE)
{
  SK <- function(x) abs(diff(diff(fivenum(x)[2:4]))/diff(fivenum(x)[c(2, 4)]))
  sim <- function(x){
    x <- rnorm(n)
    if(skewed) exp(x) else x
  }

  x <- sim()
  if(skewed) while(SK(x) < 0.7) x <- sim() else while(SK(x) > 0.15) x <- sim()
  if(left) x <- -x

  x <- mean + sd * scale(x)
  k <- sample(1:n, round(p2 * n))
  x[k] <- x[k] + delta
  as.vector(sample(x))
}
x <- round(dgpBoxhist(n = n, mean = m, sd = s, delta = delta, 
  p2 = p2, skewed = skewed, left = left), digits = 2)

b <- boxplot(x, plot = FALSE)
spread <- tol <- signif(diff(range(c(b$stats, b$out)))/25, 1)

write.csv(data.frame(x), file = "boxhist.csv", quote = FALSE, row.names = FALSE)

## QUESTION/SOLUTION
questions <- solutions <- explanations <- rep(list(""), 6)
type <- rep(list("schoice"), 6)

questions[[1]] <- paste("The distribution is ", c("", "_not_ "), "unimodal.", sep = "")
solutions[[1]] <- c(delta < 1, delta > 1)

questions[[2]] <- paste("The distribution is", c("symmetric.", "right-skewed.", "left-skewed."))
solutions[[2]] <- c(!skewed, skewed & !left, skewed & left)
    
questions[[3]] <- paste("The boxplot shows ", c("", "_no_ "), "outliers.", sep = "")
solutions[[3]] <- c(length(b$out) > 0, length(b$out) < 1)

questions[[4]] <- "A quarter of the observations is smaller than which value?"
solutions[[4]] <- explanations[[4]] <- signif(b$stats[[2]], 3)
type[[4]] <- "num"
    
questions[[5]] <- "A quarter of the observations is greater than which value?"
solutions[[5]] <- explanations[[5]] <- signif(b$stats[[4]], 3)
type[[5]] <- "num"

questions[[6]] <- paste("Half of the observations are",
  sample(c("smaller", "greater"), 1), "than which value?")
solutions[[6]] <- explanations[[6]] <- signif(b$stats[[3]], 3)
type[[6]] <- "num"

explanations[1:3] <- lapply(solutions[1:3], function(x) ifelse(x, "True", "False"))
solutions[1:3] <- lapply(solutions[1:3], mchoice2string)
if(any(explanations[4:6] < 0)) explanations[4:6] <- lapply(solutions[4:6], function(x) paste("$", x, "$", sep = ""))
```

Question
========
For the `r n` observations of the variable `x` in the data file
[boxhist.csv](boxhist.csv) draw a histogram, a boxplot and a stripchart.
Based on the graphics, answer the following questions or check the correct
statements, respectively. _(Comment: The tolerance for numeric answers is
$\pm`r tol`$, the true/false statements are either about correct or clearly wrong.)_

```{r questionlist, echo = FALSE, results = "asis"}
answerlist(unlist(questions), markup = "markdown")
```

Solution
========
\
```{r boxplot_hist, echo = FALSE, results = "hide", fig.height = 4.5, fig.width = 9, fig.path = "", fig.cap = ""}
par(mfrow = c(1, 2))
boxplot(x, axes = FALSE)
axis(2, at = signif(b$stats, 3), las = 1)
box()
hist(x, freq = FALSE, main = "")
rug(x)
```

```{r solutionlist, echo = FALSE, results = "asis"}
answerlist(unlist(explanations), markup = "markdown")
```

Meta-information
================
extype: cloze
exsolution: `r paste(solutions, collapse = "|")`
exclozetype: `r paste(type, collapse = "|")`
exname: Boxplot and histogram
extol: `r tol`

Literate programming quizzes

Using R/exams

Introducing moodlequiz!

Literate programming for quizzes with literate questions

  • Questions are specified within the writing
  • An entire quiz with multiple questions in one file
  • Consistent with R Markdown and Quarto writing style
  • Prioritises creation of cloze question types
  • Supports random repetition of quiz questions

Some downsides (and future work)

  • Only outputs Moodle XML (but can work in other LMS).
  • In early development, things will change and break.
  • Available on GitHub, about to be published on CRAN.

Introducing moodlequiz!

How it works

demo.Rmd [88 lines]
---
title: Learning with penguins
output: 
  moodlequiz::moodlequiz:
    replicates: 1
moodlequiz:
  category: penguins
---

# Basics

## Introduction

```{r setup, include = FALSE}
library(moodlequiz)
knitr::opts_chunk$set(echo = FALSE)
```

**Meet the penguins!**

![The Palmer Archipelago penguins. Artwork by @allison_horst.](lter_penguins.png)

> The `palmerpenguins` data contains size measurements for three penguin species observed on three islands in the Palmer Archipelago, Antarctica.

```{r data, echo = TRUE}
library(palmerpenguins)
penguins
```

## Plotting weight

**Which penguins are heaviest?**

A summary of the penguins dataset is shown below.

```{r}
summary(penguins)
```

The average weight of a penguin is `r cloze(mean(penguins$body_mass_g, na.rm = TRUE), tolerance = 1)`, which is slightly `r cloze("more", c("more", "less"))` than the median weight of `r cloze(median(penguins$body_mass_g, na.rm = TRUE), tolerance = 1)`.

Complete the code to produce the graphic shown in figure \@ref(fig:boxplot).

```r
library(ggplot2)
penguins |> 
  ggplot(aes(x = `r cloze("body_mass_g", colnames(penguins))`, y = `r cloze("species", colnames(penguins))`, fill = `r cloze("species", colnames(penguins))`)) +
  `r cloze("geom_boxplot")`() +
  scale_fill_manual(values = c("darkorange","darkorchid","cyan4")) + 
  theme_minimal()
```

```{r boxplot, fig.cap="Boxplots of penguin weight by species"}
library(ggplot2)
penguins |> 
  ggplot(aes(x = body_mass_g, y = species, fill = species)) +
  geom_boxplot() +
  scale_fill_manual(values = c("darkorange","darkorchid","cyan4")) + 
  theme_minimal()
```

Modify the code to investigate differences in `species` weight by `sex` that produces figure \@ref(fig:boxplot-sex).

```r
library(ggplot2)
penguins |> 
  ggplot(aes(x = `r cloze("body_mass_g", colnames(penguins))`, y = `r cloze("sex", colnames(penguins))`, fill = `r cloze("species", colnames(penguins))`)) +
  `r cloze("geom_boxplot")`() +
  scale_fill_manual(values = c("darkorange","darkorchid","cyan4")) + 
  theme_minimal()
```

```{r boxplot-sex, fig.cap="Boxplots of penguin weight by species and sex"}
penguins |> 
  ggplot(aes(x = body_mass_g, y = sex, fill = species)) +
  geom_boxplot() +
  scale_fill_manual(values = c("darkorange","darkorchid","cyan4")) + 
  theme_minimal()
```

Male penguins `r cloze("tend to", c("almost always", "tend to"))` weigh `r cloze("more", c("more", "less"))` than female penguins. Within each species, male penguins `r cloze("almost always", c("almost always", "tend to"))` weigh `r cloze("more", c("more", "less"))` than female penguins.

## Telling the story {type=essay}

**Tell the story**

Write a short paragraph about how a penguin's weight relates to their species and sex.

Future work

✍️ Create a quarto template for Moodle XML

Allows use of quarto extensions and better support for other programming languages.

🧑‍🏫 Support quiz formats for other LMS

Conceptually literate programming of quizzes isn’t specific to Moodle.

🧑‍💻 Add capability of running code in the quiz

Using WebAssembly (WASM) we can run code in the web browser alongside a quiz.

Thanks for your time!

Final remarks

  • If you use literate programming for communicating statistics, use it for teaching statistics too!
  • Set low-pressure quizzes to track student performance, and give feedback at scale.
  • Try it out, install the package from GitHub with install_github("numbats/moodlequiz")

Unsplash credits

Thanks to these Unsplash contributors for their photos