Author: Dr. Wolfgang Rolke
Date: 2024-05-25

# moodlequizR

#### Dr. Wolfgang Rolke

#### 2024-05-25

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.

3. 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:

- If a quiz was previously created with the app before and one wants to either run more copies or make changes to the quiz, entering the name and folder of the quiz is enough to read all the information into the app, so one doesn’t have to start from scratch each time.

- in the name of the quiz and the folder empty spaces and special symbols such as -, @, back and forward slashes should be avoided, so for example don’t use ‘Year 1’ as a name of an object but use ‘Year1’.

## 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:

- Name of Quiz / File : MyFirstQuiz (this will be the name of the files stored on the computer)
- Folder for Files: c:\Moodle (folder where files are to be stored)
- Category: My First Quiz / Try 1 (the category in the Moodle Question Bank where the quiz will appear. For subcategories use /)
- Quizzes: 25 (the number of randomized quizzes that should be generated)

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:

- Sample size: 50 (this can also be random, 50, 100, 1 means a random sample size from 50 to 100 in steps of 1. For a fixed sample size just use that number)
- Type of Data: Normal (a drop down box where we can choose the distribution. Also includes the option R Code, which allows us to generate data from non-standard distributions)
- Show data in moodle? Yes (Sometimes we want to hide the raw data)
- Mean 20 (can be randomize)
- Std 5 (can be randomize)
- Round data to 1 digit (can be randomize)
- Sort data? Yes
- General Calculations: (any R calculations that you might want to do early.)

Here again is a screen shot:

- Start on new line: Yes (should this question appear on the same line as the last one?)
- Text of Question: (the text that should appear in moodle)
- R Calculations for this Question: (R commands to find correct answers. Whether a sequence of R commands appears here or in the box above is usually just a question of organization.)
- Type of Question: Numeric (here the answer is a number, in other questions it might be multiple choice etc)
- Point(s): 100 (how many points is the question worth? This allows for partial credit, so for example we might give 50 points for answers that were not rounded)
- Precision: 0 (how precise does the answer have to be)
- Text of Answer: (what the student sees once they submitted their solution. If empty it is the same text as the question but often we want to show the student the correct R command)

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: lower, not equal to, higher, can’t tell
- 2: lower, not equal to, higher
- 3: is statistically significant, is not statistically significant
- 4: is statistically significant, is not statistically significant, can’t tell
- 5: is, is not
- 6: Male, Female
- 7: true, false
- 8: has, does not have
- 9: \(\ne, <, >\), can’t tell
- 10: \(\ne, <, >\)
- 11: \(\mu, \pi, \sigma, \lambda, \rho\), other

2. 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).

3. 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:

- moodle.table(x, DoRowNames = FALSE, DoBorder = FALSE, ncols = 10) creates the html code to properly display the data in Moodle. x is either a vector of a data.frame, DoRowNames and DoBorder are obvious and ncol is used if x is a vector to decide how many numbers appear in one row.

- paste.data is used to read data copied from Moodle into R. Just run

    dta=paste.data()

- qamatrix(tbl, points = 100, precision = 0, Border = 1, before, after) displays a matrix of questions and answers, for example if the students have to find a sample correlation matrix. Border=0 means no border. before and after can be used to rows before and after tbl, for example to add names and row totals.

- RtoHTML(method, x, y, n, varnames, …) creates the html code to display the output of a number of R routines in Moodle the same as they appear in R. x, y, n are the variables as they are required by the respective methods, varnames is used to get the correct variable names.

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.0002 -1.8665 -0.0302  0.9981  4.5181
    #>
    #> Coefficients:
    #>             Estimate Std. Error t value Pr(>|t|)
    #> (Intercept) -0.97226    1.17867  -0.825     0.42
    #> x            1.94583    0.09839  19.776 1.17e-13 ***
    #> ---
    #> Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
    #>
    #> Residual standard error: 2.537 on 18 degrees of freedom
    #> Multiple R-squared:  0.956,  Adjusted R-squared:  0.9536
    #> F-statistic: 391.1 on 1 and 18 DF,  p-value: 1.17e-13

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!