--- title: "Implementing the Maths Garden Update Algorithm" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Maths Garden Update Algorithm} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") library(meow) ``` The Maths Garden algorithm (Klinkenberg, Straatemeier, and van der Maas, 2011) is a gradient-based, Elo-style estimation method for computer adaptive practice systems. It updates person abilities and item difficulties on the fly, which makes it well suited to real-time educational applications. # Mathematical foundation The algorithm updates both abilities ($\theta$) and difficulties ($b$) from prediction errors: $$\theta_j^{new} = \theta_j + K_\theta \sum_{i \in I_j} (S_{ij} - E(S_{ij})), \qquad b_i^{new} = b_i + K_b \sum_{j \in J_i} (E(S_{ij}) - S_{ij}),$$ where $S_{ij} \in \{0, 1\}$ is the observed response, $I_j$ and $J_i$ are the items answered by person $j$ and the people who answered item $i$, and $K_\theta$, $K_b$ are learning rates. The expected response follows the Rasch (1PL) model, $$E(S_{ij}) = \frac{1}{1 + e^{-(\theta_j - b_i)}}.$$ Intuitively, performing better than expected raises an ability estimate; an item answered correctly more often than expected becomes easier. # Implementation in `meow` `update_maths_garden()` follows the parameter update contract (`vignette("parameter-update")`): it reads the administered responses from the matrix state and returns updated `pers` and `item` data frames. The prediction errors are aggregated per respondent and per item with `tapply()` rather than explicit loops: ```{r, eval = FALSE} update_maths_garden <- function(pers, item, R, admin, K_theta = 0.1, K_b = 0.1) { idx <- which(admin != 0, arr.ind = TRUE) person <- idx[, 1] itm <- idx[, 2] resp <- R[idx] E_Sij <- stats::plogis(pers$theta[person] - item$b[itm]) dtheta <- tapply(resp - E_Sij, person, sum) pers$theta[as.integer(names(dtheta))] <- pers$theta[as.integer(names(dtheta))] + K_theta * dtheta db <- tapply(E_Sij - resp, itm, sum) item$b[as.integer(names(db))] <- item$b[as.integer(names(db))] + K_b * db list(pers = pers, item = item) } ``` # Using it Learning rates are passed through `update_args`: ```{r} sim <- meow( select_fun = select_max_info, update_fun = update_maths_garden, data_loader = data_simple_1pl, data_args = list(N_persons = 100, N_items = 50), update_args = list(K_theta = 0.05, K_b = 0.05) ) head(sim$results[, 1:3]) ``` # Extending the algorithm Because update functions are ordinary R functions, variations are easy. The following adds **adaptive learning rates** (shrinking as a respondent answers more items) and **bounds** on the estimates, while staying within the matrix contract: ```{r, eval = FALSE} update_maths_garden_adaptive <- function(pers, item, R, admin, base_K = 0.1, decay = 0.05, bounds = c(-4, 4)) { idx <- which(admin != 0, arr.ind = TRUE) person <- idx[, 1] itm <- idx[, 2] resp <- R[idx] E_Sij <- stats::plogis(pers$theta[person] - item$b[itm]) n_person <- tapply(resp, person, length) err_p <- tapply(resp - E_Sij, person, sum) who_p <- as.integer(names(err_p)) K_p <- base_K / (1 + n_person * decay) pers$theta[who_p] <- pers$theta[who_p] + K_p * err_p pers$theta <- pmin(pmax(pers$theta, bounds[1]), bounds[2]) n_item <- tapply(resp, itm, length) err_i <- tapply(E_Sij - resp, itm, sum) who_i <- as.integer(names(err_i)) K_i <- base_K / (1 + n_item * decay) item$b[who_i] <- item$b[who_i] + K_i * err_i item$b <- pmin(pmax(item$b, bounds[1]), bounds[2]) list(pers = pers, item = item) } ``` # Practical notes * Start with modest learning rates ($K_\theta = K_b = 0.1$) and check that estimates stabilize across iterations. * Large learning rates can make estimates oscillate; bounding the estimates helps. * The algorithm assumes a Rasch model; if your data need discrimination parameters, consider an MLE updater or a 2PL extension.