Dynamic Exercises

Unique code exercises for every learner

The quarto-live extension’s OJS integration allows an instructor to write dynamic exercises with parametrised questions and solution code. Such questions would provide a different experience for each learner, or the same learner over time, creating long-term value.

Parametrised Exercises

To create a parametrised exercise, first define some input variables in OJS for each parameter you wish to create. Then, use those parameters throughout your exercise.

Example: Write an R function

First, create a “hidden” randomised variable using an ojs cell with the cell option echo: false.

param_ex.qmd
```{ojs}
//| echo: false
rand_int = Math.floor(Math.random() * 10000);
```

Next, write your exercise using the randomised parameter as an input variable.

You can use the value of OJS variables in your question text and template solution using string interpolation. For example, in this question the string ${rand_int} becomes .

Source

param_ex.qmd
Write an R function that takes a single variable `x`
and returns that value multiplied by ${rand_int}.

```{webr}
#| exercise: ex1
#| input:
#|   - rand_int
my_function <- function(x) {
  ______
}
```

```{webr}
#| exercise: ex1
#| check: true
test_value <- rnorm(1)
correct_function <- function(x) { x * rand_int }
if (!exists("my_function")) {
  list(correct = FALSE, message = "I can't see your function!")
} else if (identical(my_function(test_value), correct_function(test_value))) {
  list(correct = TRUE, message = "Correct!")
} else {
  list(correct = FALSE, message = "Incorrect.")
}
```

::: { .solution exercise="ex1" }
**Solution**:
```r
my_function <- function(x) {
  x * ${rand_int}
}
```
:::

Output

Write an R function that takes a single variable x and returns that value multiplied by .

Solution:

my_function <- function(x) {
  x * ${rand_int}
}

Example: Penquins

This is an example of a dynamic exercise using input from the learner to change the details in the exercise later.

Source
penguins.qmd
```{webr}
#| edit: false
#| output: false
library(dplyr)
library(ggplot2)
library(plotly)
library(tidyr)
library(palmerpenguins)
penguins_clean <- penguins |> drop_na()
```

Choose a species of penguin, we'll work with that species for the rest of the tutorial.

```{ojs}
//| echo: false
viewof input_species = Inputs.select(["Adelie", "Chinstrap", "Gentoo"])
penguin_info = {
  const description = {
    Adelie: md`The **Adelie** is the most widespread penguin species, and, along with the emperor penguin, is the most southerly distributed of all penguins.`,
    Chinstrap: md`The **chinstrap** penguin's name stems from the narrow black band under its head, which makes it appear as if it were wearing a black helmet, making it easy to identify.`,
    Gentoo: md`The **gentoo** penguin calls in a variety of ways, but the most frequently heard is a loud trumpeting, which the bird emits with its head thrown back.`,
  }
  return description[input_species]
}
```

```{webr}
#| edit: false
#| echo: false
#| input:
#|  - input_species
penguins_alpha <- penguins |>
  drop_na() |>
  mutate(alpha = ifelse(species == input_species, 0.8, 0.2))

p <- ggplot(data = penguins_alpha, aes(x = flipper_length_mm, y = body_mass_g)) +
  geom_point(
    aes(color = species, shape = species, alpha = alpha),
    size = 3
  ) +
  scale_color_manual(values = c("darkorange","purple","cyan4")) +
  scale_alpha(guide = 'none') +
  labs(title = "Penguin size, Palmer Station LTER",
       subtitle = "Flipper length and body mass for Adelie, Chinstrap and Gentoo Penguins",
       x = "Flipper length (mm)",
       y = "Body mass (g)",
       color = "Penguin species",
       shape = "Penguin species",
       alpha = "Penguin species")

ggplotly(p)
```

### Data manipulation with `dplyr`

Filter your dataset so that it only contains the species of penguin you selected above, <strong>${input_species}</strong>.

::: {.panel-tabset}

## Exercise

```{webr}
#| exercise: ex2
#| caption: Exercise 2
#| input:
#|  - input_species
penguins_clean |> ______
```

```{webr}
#| exercise: ex2
#| check: true
answer <- penguins_clean |> filter(species == input_species)
if (identical(.result, answer)) {
  list(correct = TRUE, message = "Correct!")
} else {
  list(correct = FALSE, message = "Incorrect.")
}
```

## Hints

::: { .hint exercise="ex2" }
Filter datasets using dplyr's `filter()` function

```r
penguins_clean |> filter(______)
```
:::

::: { .hint exercise="ex2" }
Specify the column to filter over as the argument to the `filter()` function. Here we're using "non-standard evaluation".

```r
penguins_clean |> filter(species == ______)
```
:::

## Solution

::: { .solution exercise="ex2" }

**Solution:**

A full solution filtering for the ${input_species} species is shown below

```{webr}
#| exercise: ex2
#| solution: true
penguins_clean |> filter(species == "${input_species}")
```

:::
:::

Output

Choose a species of penguin, we’ll work with that species for the rest of the tutorial.

Data manipulation with dplyr

Filter your dataset so that it only contains the species of penguin you selected above, .

Filter datasets using dplyr’s filter() function

penguins_clean |> filter(______)

Specify the column to filter over as the argument to the filter() function. Here we’re using “non-standard evaluation”.

penguins_clean |> filter(species == ______)

Solution:

A full solution filtering for the species is shown below

penguins_clean |> filter(species == "${input_species}")
penguins_clean |> filter(species == "${input_species}")

Example: Tangle

This is an example of an exercise with reactive inputs taken from an existing Observable notebook, in this case a “Tangle” input.

Source
tangle.qmd
### The Normal Distribution

The normal distribution has 2 parameters, the mean $\mu$ and the standard deviation $\sigma$.
For a normal distribution with mean $\mu =$
`{{ojs}} meanTgl` and $\sigma =$ `{ojs} sdTgl`, here is a plot of the probability density:

```{ojs}
//| echo: false
import {Tangle} from "@mbostock/tangle"

// Setup Tangle reactive inputs
viewof mean = Inputs.input(0);
viewof sd = Inputs.input(1);
meanTgl = Inputs.bind(Tangle({min: -5, max: 5, minWidth: "1em", step: 0.1}), viewof mean);
sdTgl = Inputs.bind(Tangle({min: 0, minWidth: "1em", step: 0.01}), viewof sd);

// draw plot in R
draw_plot(mean, sd)
```

::: {.cell edit='false' define='draw_plot'}
```{webr}
#| edit: false
#| output: false
#| define:
#|   - draw_plot
draw_plot <- function(mean, sd) {
  x <- seq(-5, 5, length = 100)
  y <- dnorm(x, mean = mean, sd = sd)
  plot(x, y, type = "l", lwd = 2, ylim = c(0,1))
}
```
:::

Recreate the plot above in base R graphics.

```{webr}
#| exercise: tangle
#| input:
#|  - mean
#|  - sd
#| caption: Plot Normal Distribution
x <- seq(-5, 5, length = 100)
y <- ______
______
```

::: {.solution exercise="tangle"}
**Solution**:

```{webr}
#| exercise: tangle
#| solution: true
x <- seq(-5, 5, length = 100)                                 # <1>
y <- dnorm(x, mean = ${mean}, sd = ${sd})                     # <2>
plot(x, y, type = "l", lwd = 2, ylim = c(0,1))                # <3>
```
1. Generate values for the x axis.
2. Generate values for the y axis using the normal distribution density function `dnorm()`, with mean ${mean} and standard deviation ${sd}.
3. Plot the values as a line (`type = "l"`) and with thicker width (`lwd = 2`).
:::

The Normal Distribution

The normal distribution has 2 parameters, the mean \(\mu\) and the standard deviation \(\sigma\). For a normal distribution with mean \(\mu =\) and \(\sigma =\) , here is a plot of the probability density:

Recreate the plot above in base R graphics.

Solution:

x <- seq(-5, 5, length = 100) # <1> y <- dnorm(x, mean = ${mean}, sd = ${sd}) # <2> plot(x, y, type = "l", lwd = 2, ylim = c(0,1)) # <3>
1x <- seq(-5, 5, length = 100)
2y <- dnorm(x, mean = ${mean}, sd = ${sd})
3plot(x, y, type = "l", lwd = 2, ylim = c(0,1))
1
Generate values for the x axis.
2
Generate values for the y axis using the normal distribution density function dnorm(), with mean and standard deviation .
3
Plot the values as a line (type = "l") and with thicker width (lwd = 2).