Shiny apps with math exercises

It is often very useful to practise mathematics by automatically generated exercises. One approach is multiple choice quizzes (MCQ), but it turns out to be fairly difficult to generate authentic wrong answers. Instead, we want the user to input the answer and be able to parse the answer and check whether this is the correct answer. There are many fun challenges in this, e.g. to verify that 2 is equal to 1 + 1 (as text strings the two are different, but mathematically they are equal, at least to a convenient approximation in this case).

In this post I will demonstrate how to use R package Ryacas (computer algebra system, CAS) and the R package iomath under development (as well as shiny) to make a small, powerful Shiny app. The resulting app is available at https://github.com/r-cas/shinymathexample.

First I will show the app, and then I will show a few central lines of code.

The shinymathexample app

First, the shinymathexample app presents the question:

The answer can the be written and checked:

It even works for mathematical/numerical equality, not just text/string equality:

Finally wrong answers are caught, too:

Exercise generation

The exercise generation code (boiled down) is something like this:

choices_x_coef <- c("a", "2*a", "3*a")
choices_x_pow <- 1:3

generate_f <- function() {
  x_coef <- sample(choices_x_coef, 1)
  x_pow <- sample(choices_x_pow, 1)
  x_part <- paste0(x_coef, "*x^", x_pow)
  eq <- ysym(x_part)
  eq
}

problem_f_eq <- generate_f()

true_ans <- list(
  x = deriv(problem_f_eq, "x")
)

output$problem <- renderUI({
  problem <- paste0("Let $$f(x) = ", tex(problem_f_eq), ".$$",
                    "Calculate the derivative with respect ", 
                    "to \\(x\\) and enter the result below.")
  
  res <- withMathJax(
    helpText(problem)
  )
  
  return(res)
})

Validation

The validation code (boiled down) is something like this:

reply <- input$answer_x

parsed_input <- iomath::prepare_input(reply)

if (inherits(parsed_input, "error")) {
  stop("Could not prepare the input (remember that I'm simple-minded!).")
}

reply_sym <- tryCatch(Ryacas::ysym(parsed_input), 
                      error = function(e) e)

if (inherits(reply_sym, "error")) {
  stop("Could not understand the input (remember that I'm simple-minded!).")
}

compare_grid <- expand.grid(
    x = seq(-10, 10, len = 6),
    a = seq(-10, 10, len = 6)
)
is_correct <- tryCatch(iomath::compare_reply_answer(reply = reply, 
                                                    answer = true_ans$x, 
                                                    compare_grid = compare_grid), 
                       error = function(e) e)

if (inherits(is_correct, "error")) {
  stop(paste0("Error: ", is_correct$message))
}

is_correct # TRUE/FALSE

Remarks

Take a look at the complete code at https://github.com/r-cas/shinymathexample.

The provided example should illustrate that it is fairly easy to make something relatively sophisticated.

Beside the central aspect of Ryacas (e.g. for derivatives etc.), iomath has the important function compare_reply_answer that compares reply to answer over the grid of values defined by compare_grid. Thus, equality of expressions are measured as point-wise equality over a finite number of points (e.g. 100) for different values of variables including an allowed tolerance.

Avatar
Mikkel Meyer Andersen
Assoc. Professor of Applied Statistics

My research interests include applied statistics and computational statistics.

Related