moodlequizR

Dr. Wolfgang Rolke

2024-03-22

moodlequizR is a package that allows the user to easily create fully randomized tests for Moodle, or any other online assessment platform that uses the XML format to transfer questions. It includes a shiny web application that makes it possible for even those with limited knowledge of R to create simple quizzes and exams.

Basic Workflow

The idea is to create a file outside of Moodle that can then be imported. The steps are

  1. Create an R script quizxyz.R either with the shiny app by running shinymoodlequizR, with Rstudio or with any ASCII word processor.

  2. Create a file called quizxyz.xml. This is done automatically by the shiny app, or by running

make.xml(quizxyz, 20)

XML is one of the standard file types recognized by Moodle and several other online test systems.

  1. Go to moodle, the Question Bank, and Import quizxyz.xml.

shinymoodlequizR

The best way to get started is by using the interactive shiny app. Start it with

moodlequizR::shinymoodlequizR()

It is largely self-explanatory (I hope!) and also has a few hints on what to do. Here are some general suggestions:

My First Quiz

Let’s say we want to create a quiz where the students are presented with a random data set and are asked to find the mean with R. The data is to be 50 observations drawn from a normal distribution with mean 20 and standard deviation 5, all numbers rounded to 1 digit. After opening the shiny app we can enter the following information:

Here is a screenshot:

Notice the drop-down box Included MoodlequizR Quizzes. There are 15 quizzes of increasing complexity included in the package.

Next we enter:

Here again is a screen shot:

This is all we need for now, so we can go to the bottom of the app and hit the Create the Files! button. If all goes well we then see the MyFirstQuiz.R script which will be saved in Folder. In Folder there will also be a file MyFirstQuiz.dta, which has all the info we just entered as an R list and which can be read back into the app at a later time if we want to make changes. Finally there will be a file called MyFirstQuiz.xml, which we can read into Moodle.

Now we go to Moodle and import the file MyFirstQuiz.xml.xml into the Question bank. Previewing one quiz we see

The students can high-light the data, hit Copy, switch to R and run

Finally the student enters the answer into Moodle and hits Submit to see:

The package includes 15 quizzes of increasing complexity. The best way to get started to make your own quiz is to pick one of these and make the needed changes!

Question / Part of Question and @

In the app a Question / Part of Question is usually just that, a place where the students have to enter their answer to a question. However, a bit more general it is any place where the quiz is randomized, and sometimes that might appear as just text. For an example consider the built-in example 13, where the first “Question” just displays the output of an R command.

In the text that the user enters in the box the randomized part will appear at the @ symbol, or at the end if no @ is found.

R Scipts

The shiny app is a good way to get started and learn about moodlequizR. However, there are limits to the complexity of the randomization that can be done with the app. For example, the data that all students see in any one question will always come from the same distribution. Another limitation is discussed below in the multiple stories section. All these limitations can be overcome by writing your own R scripts. Again a good way to get started is using the shiny app to get a first version and then add to the R script directly.

The “heart” of moodlequizR are R scripts that can be turned into an XML file. These have to have a certain structure.

Here is a very simple example of a problemxxx.R:

example0=function() {
   category="Examples / 1"  # moodle category where quiz will be stored
   quizname="problem -" # running counter for problems
   n=50+1*sample(0:50, 1)  # randomized sample size
   m=round(runif(1, 90, 110), 1) # randomized mean
   s=runif(1, 8, 12) # randomized standard deviation
   d=sample(1:3) # randomized number of digits
   x=rnorm(n, m, s) # generate data
   x=round(x, d) # round it to d digits
   res=as.list(1:1)
   res[[1]]= round(mean(x), d+1) # calculate correct answer, to one digit more than the data.
   qtxt =  paste0( "<p>The mean of the data is  ",
        moodlequizR::nm(res[[1]], w = c(100,80), eps = c(0, 0.01))," </p>" )
# create question text with correct answer   
   atxt =  paste0( "<p>The mean of the data is  ", res[[1]]," </p>" )
# create answer text with correct answer      
   htxt = "Use the mean command"
# create hint   
   list(qtxt = paste0("<h5>", qtxt, "</h5>", moodle.table(x)),
       htxt = paste0("<h5>", htxt, "</h5>"),
       atxt = paste0("<h5>", atxt, "</h5>"),
       category = category, quizname = quizname)
}

The routine generates a data set of size n (randomly chosen between 50 and 100 and rounded to d (1-3) digits) from a normal distribution with a random mean (90-110) and a random standard deviation (8-12). The student has to find the mean and round to d+1 digits, and gets partial credit (80%) if the rounding is not done.

The output of the routine has to be a list with five elements:

  1. category: this is the category under which Moodle will file the the problem.

  2. quizname: the name of the problem (usually just problem - )

  3. qtxt: the ENTIRE text of the problem as a single string as it will appear to the students.

  4. htxt: Hints that students see after their first attempt

  5. atxt: the ENTIRE text of the answers that the students see after the deadline for the quiz has passed, again as a single string .

Note the <h5> at then end of the routine. This is html code for heading 5. I use this here to make the text a bit more readable in Moodle.But more generally we can use any html code and Moodle knows what to do with it. The same is true for latex, as we will see later.

Here is an example with two questions on the same page:

example1 <- function() {
   category <- "Examples / Percentage 1"
   quizname <- "problem - "
#---------------------- 
# Question 1
#----------------------
   n <- sample(200:500, 1)
   p <- runif(1, 0.5, 0.6)
   x <- rbinom(1, n, p)
   per <- round(x/n*100, 1)
   qtxt <- paste0("Question 1: In a survey of ", n, " 
      people ", x, " said that they prefer Coke 
      over Pepsi. So the percentage of people who prefer 
      Coke over Pepsi is 
      {:NM:%100%", per,":0.1~%80%", per, ":0.5}%")
   htxt <- "Don't forget to multiply by 100, don'tinlcude % sign in answer"
   atxt <- paste0("Question 1: x/n*100 = ", x, "/", n,
          "*100 = ", per, " (rounded to 1 digit)")
#---------------------- 
# Question 2
#----------------------
   p1 <- round(runif(1, 50, 60), 1)
   if(per<p1) mc <- c("{:MC:~%100%lower~%0%the same~%0%higher}", " < ") 
   if(p1==per) mc <- c("{:MC:~%0%lower~%100%the same~%0%higher}", " = ")
   if(per>p1) mc <- c("{:MC:~%0%lower~%0%the same~%100%higher}", " > ")   
   qtxt <- paste0(qtxt, "<p>Question 2: In a survey some
     years ago the percentage was ", p1, "%.
    So the percentage now is ", mc[1]) 
   htxt <- ""
   atxt <- paste0(atxt, "<p>Question 2: ", per , mc[2], p1)

   list(qtxt = paste0("<h5>",qtxt,"</h5>"), 
        htxt = paste0("<h5>", htxt,"</h5>"), 
        atxt = paste0("<h5>", atxt,"</h5>"), 
        category = category, quizname = quizname) 
} 

so one simply creates a long string with all the questions and another with all the answers.

CLOZE

Moodle has a number of formats for writing questions. moodlequizR uses the CLOZE format because it has a number of features that work well in the kinds of quizzes that are most common. There are three basic question types:

  1. Multiple Choice

Example: For this data set the mean is {1:MC:~%0%lower~%0%the same~%100%higher} than the median.

In this case the students are presented with a drop-down box with the three options: lower, the same and higher.

higher is the correct answer, so it gets 100%, the others get 0%. One can also give (say) 65% for partial credit. The 1 in the front means the problem is worth 1 point.

In the package moodlequizR there are some routines that make this easier. The routine mc creates the code needed for multiple choice questions. It has a number of standard choices already implemented.

moodlequizR::mc(options, w) 

here options is a character vector with the choices (or a number for some common ones that I predefined), w is the vector of percentages, usually 100 for the correct answer and 0 for the others.

So the following creates the text for the multiple choice question above:

moodlequizR::mc(1, c(0,0, 100))
#> $qmc
#> [1] "{1:MC:%0%lower~%0%not equal to~%100%higher~%0%can't tell}"
#> 
#> $amc
#> [1] "higher"

Notice that this routine creates both the question and the answer text.

The built-in options are:

  1. Numerical Answers

Example: The mean is {2:NM:%100%54.7:0.1~%80%54.7:0.5}.

Now the students see a box and have to type in a number.

Here 54.7 is the correct answer, which get’s 100%. Any answer within \(\pm 0.1\) is also correct, allowing for rounding to 1 digit. The answer 55 gets 80% (a bit to much rounding) .

moodlequizR::nm creates numerical questions:

nm(x, w, eps, ndigits, pts = 1) 

x is a vector of possible answers, w is the vector of percentages and eps is a vector of acceptable range \(\pm\). Alternatively one can use the argument ndigits. With (say) ndigits=1 the answer has to be rounded to one digit behind the decimal, all other rounding get partial credit.

So for the above example we need to run

moodlequizR::nm(c(54.7, 54.7), c(100, 80), c(0.1, 0.5), pts=2)
#> [1] "{2:NM:%100%54.7:0.1~%80%54.7:0.5}"

If just one number is correct, say 50, and the answer has to be given exactly, use nm(50).

  1. Text answers

Example: The correct method for analysis is the {1:SA:correlation coefficient}

Here the student sees a box and has to type in some text.

There is also {1:SAC:correlation coefficient} if the case has to match.

This type of problem has the issue of empty spaces. So if the student typed correlation coefficient with two spaces it would be judged wrong. The solution is to use *s: {1:SA:*correlation*coefficient*}

Use moodlequizR::sa for text answers

sa(txt, w=100, caps=TRUE, pts=1) 

the function automatically adds *s in a number of places. txt can be a vector.

moodlequizR::sa("correlation coefficient")
#> [1] "{1:SAC:%100%correlation*coefficient}"

With these routines we can update our quiz:

example2   <- function() {
   category <- "moodlequizR / Percentage 2"
   quizname <- "problem - "
#---------------------- 
# Question 1
#----------------------
   n <- sample(200:500, 1)
   p <- runif(1, 0.5, 0.6)
   x <- rbinom(1, n, p)
   per <- round(x/n*100, 1)
   qtxt <- paste0("Question 1: In a survey of ", n, "
       people ", x, " said that they prefer Coke over
       Pepsi. So the percentage of people who prefer
        Coke over Pepsi is ", 
      nm(c(per, per), c(100, 80), c(0.1, 0.5)), "%")
   htxt <- "Don't forget to multiply by 100, don'tinlcude % sign in answer"
   atxt <- paste0("Question 1: x/n*100 = ", x, "/", n, "*100 = ", per, " (rounded to 1 digit)")
#---------------------- 
# Question 2
#----------------------
   p1 <- round(runif(1, 0.5, 0.6)*100, 1)
   if(per<p1) {w <- c(100, 0, 0); amc <-"<"}
   if(per==p1) {w <- c(0, 100,  0); amc <-"="}
   if(per>p1) {w <- c(0, 0, 100); amc <-">"}
   opts <- c("lower", "the same", "higher") 
   
   qtxt <- paste0(qtxt, "<p>Question 2: In a survey some
        years ago the percentage was ", p1, "%.
        So the percentage now is ", mc(opts, w)) 
   htxt <- ""
   atxt <- paste0(atxt, "<p>Question 2: ", per , amc, p1)

   list(qtxt = paste0("<h5>",qtxt,"</h5>"), 
        htxt = paste0("<h5>", htxt,"</h5>"), 
        atxt = paste0("<h5>", atxt,"</h5>"), 
        category = category, quizname = quizname) 
}

Helper Routines

moodlequizR has a number of routines that help with making questions:

dta=paste.data()

The methods currently included are:

  1. t.test(x) one sample t test
  2. t.test(x, y) two-sample t test
  3. binom.test(x, n) test for binomial proportion
  4. summary(lm (y~x)) regression

All the usual arguments, for example mu=10 for a hypothesis test in t.test, can be passed to the methods. Say, for example, we want to ask the students a number of questions about the output of the regression command. In R we generate some random data and run

x=1:20
y=2*x+round(rnorm(20, 0, 3),2)
summary(lm(y~x))
#> 
#> Call:
#> lm(formula = y ~ x)
#> 
#> Residuals:
#>     Min      1Q  Median      3Q     Max 
#> -5.1925 -0.8243 -0.1786  0.9469  6.4416 
#> 
#> Coefficients:
#>             Estimate Std. Error t value Pr(>|t|)    
#> (Intercept)   0.4243     1.3281   0.319    0.753    
#> x             1.9120     0.1109  17.246 1.22e-12 ***
#> ---
#> Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#> 
#> Residual standard error: 2.859 on 18 degrees of freedom
#> Multiple R-squared:  0.9429, Adjusted R-squared:  0.9398 
#> F-statistic: 297.4 on 1 and 18 DF,  p-value: 1.223e-12

Now RtoHTML(lm, x, y) will generate code that in Moodle appears the same!

The way to use this in the shiny app is as a question with the Verbatim option. See the included example 13.

Creating the XML file

Once the quizxyz.R is written we can read it into R and then use the make.xml routine to create the Moodle input file quizxyz.xml:

make.xml(quizxyz, k=20, folder=getwd())

This creates 20 quizzes and saves a file called in the folder given by getwd(). The routine can also pass arguments to the .R file.

Next we need to open Moodle and import this file.

Note Moodle has two Import places. One is for importing existing questions from other courses. We need to go to Questions - Import, select XML and drop .

Multiple Stories

In statistics we like to use word problems. So how can we do that? Essentially we can make up a couple of stories:

example3 <- function(whichstory) {
   category <- paste0("moodlequizR / Percentage - Story - ", whichstory)
   quizname <- "problem -"
    
   if(whichstory==1) {  
      n <- sample(200:500, 1)
      p <- runif(1, 0.5, 0.6)
      x <- rbinom(1, n, p)
      per <- round(x/n*100, 1)
      qtxt <- paste0("In a survey of ", n, " people ", x, "
        said that they prefer Coke over Pepsi. So the 
        percentage of people who prefer Coke over 
        Pepsi is ", 
        nm(c(per, per), c(100, 80), c(0.1, 0.5)), "%")
   } 
   if(whichstory==2) {  
      n <- sample(1000:1200, 1)
      p <- runif(1, 0.45, 0.55)
      x <- rbinom(1, n, p)
      per <- round(x/n*100, 1)
      qtxt <- paste0("In a survey of ", n, " likely 
        voters ", x, " said that they would vote for
        candidate A. So the percentage of people who 
        will vote for candidate A is ", 
        nm(c(per, per), c(100, 80), c(0.1, 0.5)), "%")      
   }
   if(whichstory==3) {  
      n <- sample(100:200, 1)
      p <- runif(1, 0.75, 0.95)
      x <- rbinom(1, n, p)
      per <- round(x/n*100, 1)
      qtxt <- paste0("In a survey of ", n, " people ", 
        x, " said that they are planning a vacation 
        this summer. So the percentage of people who 
        are planning a vacation is ", nm(c(per, per),
        c(100, 80), c(0.1, 0.5)), "%")      
   }
   htxt <- ""
   atxt <- paste0("x/n*100 = ", x, "/", n, "*100 = ", 
          per, " (rounded to 1 digit)")

   list(qtxt = paste0("<h5>",qtxt,"</h5>"), 
        htxt = paste0("<h5>", htxt,"</h5>"), 
        atxt = paste0("<h5>", atxt,"</h5>"), 
        category = category, quizname = quizname) 
}

Note we can match each story with likely numbers, so in the Coke vs Pepsi story n is between 200 and 500 whereas in the votes story it is between 1000 and 1200.

Note that this routine has the argument whichstory, which we can add to genquiz or make.xml:

make.xml(example3, 20, whichstory=2)

Data Sets

Often in Statistics we want the students to work with data sets. So these data sets need to be displayed properly in the quiz and it must be easy for the students to “transfer” them to R.

To display the data in the quiz use the moodle.table function. If it is called with a vector it arranges it as a table with (ncol=) 10 columns. If it is called with a matrix or data frame it creates a table as is.

To get the data from the quiz into R moodlequizR has the routine paste.data(). All the students have to do is highlight the data (including column names if present) in the quiz with the mouse, right click copy, switch to R and run

moodledata = paste.data()

The routine correctly reads vectors, both numeric and character. Also tables with a mix of character and numeric columns. If on a arare occasion it reads a table as a vector we can use the argument is.table=TRUE. It works on both Windows and Apple operating systems.

So we could have another version of the above example, this time presenting the students with the the raw data:

example4 <- 
function() {
   category <- "moodlequizR / Percentage from Raw Data" 
   quizname <- "problem - "

   n <- sample(200:500, 1)
   p <- runif(1, 0.5, 0.6)
   x <- sample(c("Coke", "Pepsi"), size=n, 
               replace=TRUE, prob=c(p,1-p))
   per <- round(table(x)[1]/n*100, 1)

   qtxt <- paste0("In a survey people were asked whether 
    they prefer Coke over Pepsi. Their answers
    are below. So the percentage of people who 
    prefer Coke over Pepsi is ", 
    nm(c(per, per), c(100, 80), c(0.1, 0.5)), "%<hr>",
    moodle.table(x))
   htxt <- "" 
   atxt <- paste0("x/n*100 = ", table(x)[1], "/", n,
            "*100 = ", per, " (rounded to 1 digit)")
   
   list(qtxt = paste0("<h5>",qtxt,"</h5>"), 
        htxt = paste0("<h5>", htxt,"</h5>"),
        atxt = paste0("<h5>", atxt,"</h5>"), 
        category = category, quizname = quizname) 
}

Graphics

Often in statistics we use graphics. We can do this as follows: first you need to install the R package base64, available on CRAN.

Say we want the quiz to show a histogram and the student has to decide whether or not it is bell-shaped:

example5 <- function(bell=TRUE) {
  require(base64)
   category <- paste0("moodlequizR / 
      bell-shaped? - ", ifelse(bell, "Yes", "No")) 
   quizname <- "problem - "

   n <- 1000
   if(bell) x <- rnorm(n, 10, 3)
   else x<- rchisq(n, 2) + 8
   
   plt <- hplot(x, n=50, returnGraph=TRUE) 
   plt64 <- moodlequizR::png64(plt)
   if(bell) mmc <- mc(5, c(100, 0))
   else mmc <- mc(5, c(0, 100))
   qtxt <- paste0("The data shown in this histogram ", 
              mmc, " bell-shaped<hr>", plt64)
   
   htxt <- "" 
   if(bell) atxt <- "It is bell-shaped"
   else atxt <- "It is not bell-shaped"
   
   list(qtxt = paste0("<h5>", qtxt, "</h5>"), 
        htxt = paste0("<h5>", htxt, "</h5>"),         
        atxt = paste0("<h5>", atxt, "</h5>"), 
        category = category, quizname = quizname) 
}

Non Statistics Courses

The same basic ideas can be used to make random quizzes for other courses. In principle any other computer language can be used as well, but I will stick with R and make a quiz for calculus:

example6 <- function() {
   category <- "moodlequizR / Integral"
   quizname <- "problem - "

   A <- round(runif(1), 1)
   B <- round(runif(1, 1, 2), 1)
   fun <- function(x) {x*exp(x)}
   I <- round(integrate(fun, A, B)$value, 2)
   qtxt <- paste0("\\(\\int_{", A, "}^{", B, "} xe^x dx = \\)", 
   nm(I, eps=0.1))
   htxt <- ""
   atxt <- paste0("\\(\\int_{", A, "}^{", B, "} xe^x dx = \\)", I)
   list(qtxt = paste0("<h5>", qtxt, "</h5>"), 
        htxt = paste0("<h5>", htxt, "</h5>"),         
        atxt = paste0("<h5>", atxt, "</h5>"), 
        category = category, quizname = quizname) 
}

Note here we see that one can use latex notation in Moodle quizzes, and so display formulas!

Conclusions

If there are any problems with the package feel free to email me at wolfgang.rolke@upr.edu. Also, I am always interested to hear your opinion, good or bad!