R

Begin by starting R from http://academic.uprm.edu/wrolke/Resma3/Resma3.RData

This will open Resma3. I need that because I need all the routines in it for my courses and their quizzes.

Next you need to download the package moodler from

http://academic.uprm.edu/wrolke/moodler/moodler.zip

It includes a couple of helper functions for making quizzes, see below.

If you are not familiar with R I have a lengthy introduction to R that I gave at a workshop in Hamburg, Germany last year http://academic.uprm.edu/wrolke/research/Introduction%20%20to%20R.pdf, or you can study the (much more detailed) material of my R course at http://academic.uprm.edu/wrolke/Computing-with-R

Basic Workflow

  1. Create a file problemxxx.R with an ASCII word processor.

  2. Read into R, run genquiz(10, problemxxx) to create a file newquiz.xml.

  3. Import newquiz.xml into moodle.

problemxxx.R

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

example1 <- function() {
   category <- "moodler / 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) 
} 

The output of the routine is a list with four 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 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.

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.

CLOZE

Moodle has a number of formats for writing questions. I use the cloze format exclusively because it has a number of features that work well in the kinds of quizzes I typically do. 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}.

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.

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

  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 problem 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*}

In the package moodler there are some routines that make this easier:

mc <- function (options, w, which.true, pts = 1) 
{
    option.list <- list(o1 = c("lower", "not equal to", "higher", 
        "can't tell"), o2 = c("lower", "not equal to", "higher"), 
        o3 = c("is statistically significant", "is not statistically significant"), 
        o4 = c("is statistically significant", "is not statistically significant", 
            "can't tell"), o5 = c("is", "is not"), o6 = c("Male", 
            "Female"), o7 = c("true", "false"), o8 = c("has", 
            "does not have"), o9 = c("!=", "<", ">", "can't tell"), 
        o10 = c("!=", "<", ">"), o11 = c("&mu;", "&pi;", "&sigma;", 
            "&lambda;", "&rho;", "other"))
    if (is.numeric(options)) 
        options <- option.list[[options]]
    if (!missing(which.true)) {
        if (is.logical(which.true)) {
            if (which.true) 
                which.true <- 1
            else which.true <- 2
        }
        w <- rep(0, length(options))
        w[which.true] <- 100
    }
    qmc <- paste0("{", pts, ":MC:", paste0("%", w, "%", options, 
        "~", collapse = ""))
    qmc <- paste0(substring(qmc, 1, nchar(qmc) - 1), "}")
    list(qmc = qmc, amc = options[w == 100])
}

here opts is a character vector with the choices (or a number for some common ones that I predefined), w is the vector of percentages and pts is how much the problem is worth.

nm <- function (x, w, eps, ndigits, pts = 1) 
{
    n <- length(x)
    if (missing(w)) 
        w <- rep(100, n)
    if (!missing(ndigits)) {
        x <- rep(x, each = 2)
        eps <- rep(10^(-ndigits) * c(0, 0.49), n)
        w <- rep(w, each = 2) * rep(c(1, 0.75), n)
    }
    w <- round(w)
    if (missing(eps)) 
        out <- paste0("{", pts, ":NM:", paste0("%", w, "%", x, 
            "~", collapse = ""))
    else out <- paste0("{", pts, ":NM:", paste0("%", w, "%", 
        x, ":", eps, "~", collapse = ""))
    paste0(substring(out, 1, nchar(out) - 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 roundings get partial credit.

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

sa <- function (txt, w=100, caps=TRUE, pts=1) 
{
    txt <- gsub(" ", "*", txt)
    txt <- gsub("\\(", "\\(*", txt)
    txt <- gsub("\\)", "*\\)", txt)
    txt <- gsub(",", "*,*", txt)
    if(caps)
      out <- paste0("{", pts, ":SAC:", paste0("%", w, "%", txt, "~", collapse = ""))
    else
      out <- paste0("{", pts, ":SA:", paste0("%", w, "%", txt, "~", collapse = ""))
    out <- paste0(substring(out, 1, nchar(out)-1), "}")
    out
}

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

With these routines we can update out moodle quiz:

example2   <- function() {
   category <- "moodler / 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) 
}

Creating the .xml file

Once the problemxxx.R is written we can read it into R and then use the genquiz routine to create the moodle input file

genquiz <- function (B = 1, fun, folder, Show = FALSE, ...) 
{
    if (missing(folder)) 
        folder <- getwd()
    outfile <- c("<?xml version=\"1.0\" encoding=\"UTF-8\"?>", 
        "<quiz>")
    for (i in 1:B) {
        info <- fun(...)
        if (Show) 
            print(info)
        category <- info$category
        quizname <- info$quizname
        lns <- c("<!-- question: 0   -->", "<question type=\"category\">", 
            "<category>", paste0("<text>$course$/", category, 
                "</text>"), "</category>", "</question>", " ", 
            "<!-- question: ", sample(1:1e+06, 1), "  -->", "<question type=\"cloze\">", 
            "<name>", paste0("<text>", quizname, i, "</text>"), 
            "</name>", "<questiontext format=\"html\">", paste0("<text><![CDATA[", 
                info$qtxt, "]]></text>"), "</questiontext>", 
            "<generalfeedback format=\"html\">", paste0("<text><![CDATA[", 
                info$atxt, "]]></text>"), "</generalfeedback>", 
            "</question>")
        outfile <- c(outfile, "", lns)
    }
    outfile <- c(outfile, "</quiz>")
    write(outfile, paste0(folder, "/newquiz.xml"))
}

here B is how many quizzes you want (I usually do about 20). fun is the name of the R routine we just wrote. folder is the folder where the .R file is located. Set Show=TRUE to see the output (this is useful for debugging). And … is for any additional parameters that are needed in fun.

genquiz essentially creates a vector of character strings, with each element corresponding to one line. This is then saved on the hard drive as the file newquiz.xml.

So now we run

genquiz(1, example1)

and we should have a file newquiz.xml in the folder from which R was started (you can check with getwd()).

I also have a routine that does it all, read the function from the folder, generate the xml and (if desired) do some cleanup:

make.xml <-
function (fun, k = 1, delete.fun = TRUE, ...) 
{
    whichcomp <- strsplit(getwd(), "/")[[1]][3]
    folder <- paste0("c:/users/", whichcomp, "/Dropbox/teaching/moodle/")
    funname <- deparse(substitute(fun))
    source(paste0(folder, funname, ".R"))
    genquiz(k, fun, folder = folder, ...)
    if (delete.fun) 
        remove(list = funname, pos = .GlobalEnv)
}

and so

make.xml(example1, 20)

does it all!



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, drop newquiz.xml into the box and hit enter.

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("moodler / 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 have data sets the students need to use. 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 colums. If it is called with a matrix or data frame it makes a table as is.

To get the data from the quiz into R Resma3 has the routine get.moodle.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

get.moodle.data()

There is now an object called x in R. If it is a single vector it can be used as is, say

mean(x)

If the data was a table with several columns the dataframe is also automatically attached, so the students can immediately start using them.

So we could have another version of

example4 <- 
function() {
   category <- "moodler / 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. Then we need the function

png64 <- function(plt) {
  pngfile <- tempfile()
  png(pngfile, width = 400, height = 400)
  print(plt)
  dev.off()
  pltout <- img(pngfile, Rd = TRUE, alt = "a")
  m <- nchar(pltout)
  pltout <- substring(pltout, 6, m-1)
  pltout
}

Now we can write

example5 <- function(bell=TRUE) {
  require(base64)
   category <- paste0("moodler / 
      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 <- 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 <- "moodler / 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!