--- title: "The change-classification system" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{The change-classification system} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") ``` ## Design principles Classification in `trialdiff` is deliberately boring and auditable: * every change is a row in a long *change register* ([as_register()]); * every category comes from an explicit [td_rule()]; * the first matching rule (by priority) sets the primary category, but **all** matching rules are retained in `category_all`; * each change carries a plain-language `reason`. There is no machine learning and no hidden scoring. ## The built-in categories ```{r} library(trialdiff) td_categories() ``` ## The change register ```{r} diff <- compare_cut(adsl_cut1, adsl_cut2, by = "USUBJID", dataset = "ADSL") reg <- as_register(diff) reg[, c(".change_id", "record_type", "USUBJID", "variable", "old_value", "new_value", "change")] ``` ## Applying the default rules ```{r} classified <- classify_changes(diff) classified$register[, c(".change_id", "category", "category_label", "reason")] ``` ## Writing a custom rule A rule is a predicate over the register. The `context` contains the dataset name, keys and the sets of subjects present in each cut. ```{r} age_rule <- td_rule( name = "age_change", label = "Age change", priority = 1L, test = function(register, context) { register$record_type == "modified" & register$variable == "AGE" }, reason = function(register, context) { sprintf("Age changed for %s.", register$.subject) } ) custom <- classify_changes( diff, rules = c(list(age_rule), td_default_rules()) ) custom$register$category[custom$register$variable == "AGE"] ``` ## Prioritisation Rules are evaluated in ascending priority order. This matters when more than one rule could apply. For example, `AVAL` is a derived variable *and* a value can transition from missing to non-missing. Missingness transitions have a higher priority than the generic derived-variable rule, because they are more actionable for review: ```{r} old <- data.frame(USUBJID = c("S1", "S2"), AVAL = c(NA, 5)) new <- data.frame(USUBJID = c("S1", "S2"), AVAL = c(3, NA)) classified_missing <- classify_changes(compare_cut(old, new, by = "USUBJID", dataset = "ADLB")) classified_missing$modified[, c("USUBJID", "variable", "change", "category")] ``` Any change that no rule matches is marked `unclassified` and surfaced in the report as an item requiring manual review, so the rule set can never silently hide a change.