---
title: "Knowledge Tracing:  Jackknife, Bootstrap and Cross Validation"
author: "Russell Almond"
date: "1/12/2022"
output: html_document
runtime: shiny
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = TRUE)
library(tidyverse)
library(DiagrammeR)

## This is syntaxtic sugar, so I can use this idiom in a pipe.
applyappend <- function(listOfData)
  do.call(rbind,listOfData)


```

## Knowledge Tracing Model

Corbett and Anderson (1995)

Hidden Markov Model

Markov means past is independent of future given present.

Hidden means one variable is latent.

```{r hmm-dot, fig.cap="Knowledge Tracing (Hidden Markov) model"}
DiagrammeR::grViz("digraph hmm {

graph[layout=dot]

node[shape=circle]
subgraph hidden {
  rank=min;
  L0; L1; L2; L3; L4; L5
  L0 -> L1 -> L2 -> L3 -> L4 -> L5
}

X0; X1; X2; X3; X4; X5
L0 -> X0; L1 -> X1; L2 -> X2; L3 -> X3; L4 -> X4; L5->X5;

}")

```


$i$ represents opportunity to practice Knowledge, Skill or Ability (KSA)

$L_i=1$ is mastery, $L_i=0$ is non-mastery at Time $i$

$X_i$ is response at $i$th opportunity (1=success, 0=failure)

[Knowledge Tracing App](https://catherinesyeh.github.io/bkt-balloon/)

## Parameters of the Knowledge Tracing Model



* Assumption 1:  No forgetting -- $L_{i+1} \ge L_i$

* Assumption 2: Constant learning rate $P(L_{i+1}=1|L_i=0) = p(T)$, `learn`

* Assumption 3: Constant item parameters

 - `guess` $P(X_i=1|L_i=0)$
 
 - `slip` $P(X_i=0|L_i=1)$
 
 Need one more parameter, $P(L_0=1)$, `init`
 
## Stopping Rule (Russell Addition)

Original Corbett and Anderson paper assumed all students do same number of practices.

This simulation is based on a tutoring system, where students can work on as many examples as they like.

- `quit0` $P(quit|L_i=0)$

- `quit1` $P(quit|L_i=1)$

```{r generator}

simkt <- function(id=0,init=.5,learn=.25,
                  guess=.2,slip=.2,
                  quit0=.1,quit1=.25) {
  result <- numeric() # Result vector
  mastery <- runif(1) < init
  mvec <- numeric()
  done <- FALSE
  while (!done) {
    mvec <- c(mastery,mvec)
    result <- c(
      runif(1) < ifelse(mastery,1-slip,guess),
      result)
    if (!mastery) mastery <- runif(1) < learn
    done <- runif(1) < ifelse(mastery,quit1,quit0)
  }
  data.frame(id=id,
             trial=1:length(result),
             result=rev(result),
             mastery=rev(mvec))
}
simkt(-1)
```
   
## Now generate sample data

Use `lapply` as this makes it easier to make things parallel;  load `parallel` package and use `mclappy` instead of `lapply`.

Wrap call to `simkt` in an anonymous function to make changing the parameters easier.

The `do.call(rbind,...)` trick glues the data together.

```{r generate}
N <- 100
lapply(1:N,
       function (id) {
         simkt(id)
       }) %>% applyappend -> simktdat
```


## Learning curve

Plot trial versus error rate.

```{r learning curve}
errRate <- function (trials,results) {
  utrials <- sort(unique(trials))
  er <- sapply(utrials, function (tri) 
    1 - sum(results[trials==tri])/sum(trials==tri))
  data.frame(Trial=utrials,ErrorRate=er)
}
errrate <- errRate(simktdat$trial,simktdat$result)
```
```{r traceplot}
ggplot(errrate,aes(Trial,ErrorRate))+geom_point()+geom_line()+
    labs(title="Knowledge Trace Standard Errors.")

```

## Jackknife Error Bars

```{r jackknife errors}
subjects <- unique(simktdat$id)
maxtrials <- nrow(errrate)
lapply(subjects, function( subj) {
    jdat <- filter(simktdat,id!=subj) 
    er <- errRate(pull(jdat,"trial"),
                pull(jdat,"result"))
    if (nrow(er) < maxtrials) {
      ## Pad out extra rows in matrix if needed.
      er <- add_row(er,Trial=(nrow(er)+1):maxtrials)
    }
    pull(er,"ErrorRate")
}) %>% applyappend -> jest

upper <- apply(jest,2,quantile,probs=.95,na.rm=TRUE)
lower <- apply(jest,2,quantile,probs=.05,na.rm=TRUE)
errrate %>%add_column(JackUpper=upper,JackLower=lower) -> errrate
```
  
Now add them to the plot:

```{r traceplotJack}
ggplot(errrate,aes(Trial,ErrorRate))+
  geom_pointrange(aes(ymin=JackLower,ymax=JackUpper))+
  geom_line() +
  labs(title="Knowledge Trace with Jackknife Standard Errors.")

```

## Bootstrap standard errors

```{r bootstrap errors}
subjects <- unique(simktdat$id)
maxtrials <- nrow(errrate)
B <- 100
lapply(1:B, function(b) {
    bdat <- slice_sample(simktdat,prop=1,replace=TRUE) 
    er <- errRate(pull(bdat,"trial"),
                pull(bdat,"result"))
    if (nrow(er) < maxtrials) {
      ## Pad out extra rows in matrix if needed.
      er <- add_row(er,Trial=(nrow(er)+1):maxtrials)
    }
    pull(er,"ErrorRate")
}) %>% applyappend -> best

upper <- apply(best,2,quantile,probs=.95,na.rm=TRUE)
lower <- apply(best,2,quantile,probs=.05,na.rm=TRUE)
errrate %>%add_column(BootUpper=upper,BootLower=lower) -> errrate
```

Now Graph it.

```{r traceplotBoot}
ggplot(errrate,aes(Trial,ErrorRate))+
  geom_pointrange(aes(ymin=BootLower,ymax=BootUpper))+
  geom_line() +
  labs(title="Knowledge Trace with Bootstrap Standard Errors.")
```



