---
title: "Examples: Basic LKT Models"
author: "Philip I. Pavlik Jr."
date: "2026-06-07"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Examples: Basic LKT Models}
  %\VignetteEngine{knitr::rmarkdown}
  \usepackage[utf8]{inputenc}
---

```{r setup}
source("lkt-vignette-setup.R")
check_true("LKT namespace available", requireNamespace("LKT", quietly = TRUE))
check_true("data.table namespace available", requireNamespace("data.table", quietly = TRUE))
check_true("pROC namespace available", requireNamespace("pROC", quietly = TRUE))
```

# Load data

```{r bundled-data}
val <- prepare_largeraw_sample()
check_true("bundled data row count", nrow(val) == 55122)
check_true("bundled data folds", identical(sort(unique(val$fold)), 1:5))
check_true("bundled data spacing columns", all(c(
  "KC..Default.spacing",
  "KC..Cluster.spacing",
  "Anon.Student.Idspacing",
  "CF..Correct.Answer.spacing"
) %in% names(val)))
```

# Spacing predictor fixture

```{r spacing-predictor-fixture}
spacing_fixture <- data.frame(
  Anon.Student.Id = c("s1", "s1", "s1", "s1", "s2", "s2"),
  CF..ansbin. = c(1, 0, 1, 1, 0, 1),
  KC = c("a", "a", "a", "b", "a", "a"),
  CF..Time. = c(0, 10, 25, 30, 0, 12),
  CF..reltime. = c(0, 10, 25, 30, 0, 12)
)
spacing_fixture <- computeSpacingPredictors(spacing_fixture, "KC")

check_true("spacing fixture columns", all(c(
  "KCspacing",
  "KCrelspacing",
  "KCprev",
  "KCmeanspacing",
  "KCrelmeanspacing",
  "KCspacinglagged"
) %in% names(spacing_fixture)))
check_true("spacing fixture spacing values", identical(
  as.numeric(spacing_fixture$KCspacing),
  c(0, 10, 15, 0, 0, 12)
))
check_true("spacing fixture previous responses", identical(
  as.numeric(spacing_fixture$KCprev),
  c(0, 1, 0, 0, 0, 0)
))
check_true("spacing fixture lagged spacing", identical(
  as.numeric(spacing_fixture$KCspacinglagged),
  c(0, 0, 10, 0, 0, 0)
))

missing_component_error <- tryCatch(
  {
    computeSpacingPredictors(spacing_fixture, "MissingKC")
    FALSE
  },
  error = function(err) grepl("missing component column", conditionMessage(err), fixed = TRUE)
)
check_true("spacing fixture missing component fails clearly", missing_component_error)
```

# API boundary checks

```{r api-boundary-checks}
unknown_feature_error <- tryCatch(
  {
    LKT(
      data = val,
      interc = FALSE,
      components = "KC..Default.",
      features = "notafeature",
      verbose = FALSE
    )
    FALSE
  },
  error = function(err) grepl("Unknown LKT feature", conditionMessage(err), fixed = TRUE)
)
check_true("unknown feature fails clearly", unknown_feature_error)

component_feature_length_error <- tryCatch(
  {
    LKT(
      data = val,
      interc = FALSE,
      components = c("KC..Default.", "Anon.Student.Id"),
      features = "intercept",
      verbose = FALSE
    )
    FALSE
  },
  error = function(err) grepl("same length", conditionMessage(err), fixed = TRUE)
)
check_true("component/feature length mismatch fails clearly", component_feature_length_error)

fixedpars_length_error <- tryCatch(
  {
    LKT(
      data = val,
      interc = FALSE,
      components = c("KC..Default.", "KC..Default."),
      features = c("logitdec", "recency"),
      fixedpars = 0.5,
      verbose = FALSE
    )
    FALSE
  },
  error = function(err) grepl("fixedpars must have length 2", conditionMessage(err), fixed = TRUE)
)
check_true("fixedpars length mismatch fails clearly", fixedpars_length_error)

seedpars_length_error <- tryCatch(
  {
    LKT(
      data = val,
      interc = FALSE,
      components = c("KC..Default.", "KC..Default."),
      features = c("logitdec", "recency"),
      fixedpars = NA,
      seedpars = 0.5,
      verbose = FALSE
    )
    FALSE
  },
  error = function(err) grepl("seedpars must have length 2", conditionMessage(err), fixed = TRUE)
)
check_true("seedpars length mismatch fails clearly", seedpars_length_error)

no_duration <- as.data.frame(val)
no_duration$Duration..sec. <- NULL
dualfit_duration_error <- tryCatch(
  {
    LKT(
      data = no_duration,
      interc = FALSE,
      dualfit = TRUE,
      components = "KC..Default.",
      features = "intercept",
      verbose = FALSE
    )
    FALSE
  },
  error = function(err) grepl("dualfit requires a Duration..sec.", conditionMessage(err), fixed = TRUE)
)
check_true("dualfit duration requirement fails clearly", dualfit_duration_error)

model_called <- FALSE
model_saw_request <- FALSE
custom_model <- LKTCustomModel(
  name = "custom-liblinear",
  fit = function(request, verbose = FALSE) {
    model_called <<- TRUE
    model_saw_request <<- all(c(
      "feature_input",
      "design_matrix",
      "data",
      "response",
      "usefolds",
      "options",
      "formula",
      "feature_spec"
    ) %in% names(request))
    model_saw_request <<- model_saw_request &&
      all(c("formula", "data", "response", "feature_spec", "design_matrix", "column_names") %in%
            names(request$feature_input))
    LibLinearModel()$fit(request, verbose = verbose)
  }
)
custom_model_fit <- LKT(
  data = val[seq_len(500), ],
  interc = FALSE,
  components = "KC..Default.",
  features = "intercept",
  model = custom_model,
  verbose = FALSE
)
check_true("custom model was called", model_called)
check_true("custom model received fit request", model_saw_request)
check_true("custom model name is reported", custom_model_fit$model_name == "custom-liblinear")

prediction_only_model_adapter <- LKTCustomModel(
  name = "prediction-only",
  fit = function(request, verbose = FALSE) {
    LKTModelFit(
      model = list(kind = "constant-probability"),
      pred = rep(mean(request$response), length(request$response))
    )
  }
)
prediction_only_model <- LKT(
  data = val[seq_len(500), ],
  interc = FALSE,
  components = "KC..Default.",
  features = "intercept",
  model = prediction_only_model_adapter,
  verbose = FALSE
)
check_true("prediction-only model gets log likelihood", is.finite(prediction_only_model$loglike))
check_true("prediction-only model name is reported", prediction_only_model$model_name == "prediction-only")

unknown_model_error <- tryCatch(
  {
    LKT(
      data = val,
      interc = FALSE,
      components = "KC..Default.",
      features = "intercept",
      model = "notamodel",
      verbose = FALSE
    )
    FALSE
  },
  error = function(err) grepl("Unknown LKT model", conditionMessage(err), fixed = TRUE)
)
check_true("unknown model fails clearly", unknown_model_error)
```

# Additive Factors Model

```{r afm-fixed-effect}
model_afm <- LKT(
  data = val,
  interc = FALSE,
  components = c("Anon.Student.Id", "KC..Default.", "KC..Default."),
  features = c("intercept", "intercept", "lineafm")
)
check_lkt_fit(model_afm, expected_r2 = 0.280024, expected_loglike = -27347.20717315)
check_has_coefficient_matching(model_afm, "^interceptAnon[.]Student[.]Id")
check_has_coefficients(model_afm, c("lineafmKC..Default."))
check_true("default model is LibLinear", model_afm$model_name == "LibLinear")

pred_stats <- predict_lkt(model_afm, val, return_stats = TRUE)
check_true("predict_lkt returns predictions", length(pred_stats$predictions) == nrow(val))
check_true("predict_lkt returns finite log likelihood", is.finite(pred_stats$LL))
check_true("predict_lkt returns finite RMSE", is.finite(pred_stats$RMSE))
```

# Performance Factors Analysis

```{r pfa-fixed-effect}
model_pfa <- LKT(
  data = val,
  interc = TRUE,
  components = c(
    "Anon.Student.Id",
    "KC..Default.",
    "KC..Default.",
    "KC..Default."
  ),
  features = c("intercept", "intercept", "linesuc$", "linefail$")
)
check_lkt_fit(model_pfa, expected_r2 = 0.295228, expected_loglike = -26769.69416527)
check_has_coefficient_containing(model_pfa, ":linesucKC..Default.")
```

# Difficulty-sensitive PFA

```{r pfa-difficulty-sensitive}
val$pred <- model_pfa$prediction

model_diffcor <- LKT(
  data = val,
  interc = TRUE,
  components = c(
    "Anon.Student.Id",
    "KC..Default.",
    "KC..Default.",
    "KC..Default."
  ),
  features = c("intercept", "intercept", "diffcorComp", "linefail")
)
check_lkt_fit(model_diffcor, expected_r2 = 0.283896, expected_loglike = -27200.10757759)
check_has_coefficients(model_diffcor, c("diffcorCompKC..Default."))
```
