
The change-classification system
Source:vignettes/change-classification.Rmd
change-classification.RmdDesign 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
library(trialdiff)
td_categories()
#> # A tibble: 16 × 2
#> category label
#> <chr> <chr>
#> 1 new_subject New subject
#> 2 subject_removed Subject removed
#> 3 new_visit New visit
#> 4 new_assessment New assessment
#> 5 new_record New record
#> 6 record_removed Record removed
#> 7 treatment_assignment_change Treatment-assignment change
#> 8 derived_variable_change Derived-variable change
#> 9 corrected_value Corrected value
#> 10 missing_to_value Missing to non-missing
#> 11 value_to_missing Non-missing to missing
#> 12 variable_added Variable added
#> 13 variable_removed Variable removed
#> 14 type_change Type change
#> 15 label_change Label change
#> 16 unclassified Unclassified - requires reviewThe change register
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")]
#> # A tibble: 11 × 7
#> .change_id record_type USUBJID variable old_value new_value change
#> <chr> <chr> <chr> <chr> <chr> <chr> <chr>
#> 1 CHG00001 added TD001-S031 NA NA NA recor…
#> 2 CHG00002 added TD001-S032 NA NA NA recor…
#> 3 CHG00003 removed TD001-S003 NA NA NA recor…
#> 4 CHG00004 modified TD001-S007 AGE 44 45 value
#> 5 CHG00005 modified TD001-S011 AGE 22 21 value
#> 6 CHG00006 modified TD001-S014 SEX M <NA> value…
#> 7 CHG00007 modified TD001-S018 RACE BLACK OR AFRICAN… WHITE value
#> 8 CHG00008 modified TD001-S005 TRT01P Placebo Drug A value
#> 9 CHG00009 modified TD001-S005 TRT01A Placebo Drug A value
#> 10 CHG00010 schema NA REGION NA character varia…
#> 11 CHG00011 schema NA AGE integer numeric type_…Applying the default rules
classified <- classify_changes(diff)
classified$register[, c(".change_id", "category", "category_label", "reason")]
#> # A tibble: 11 × 4
#> .change_id category category_label reason
#> <chr> <chr> <chr> <chr>
#> 1 CHG00001 new_subject New subject Subject '…
#> 2 CHG00002 new_subject New subject Subject '…
#> 3 CHG00003 subject_removed Subject removed Subject '…
#> 4 CHG00004 corrected_value Corrected value Variable …
#> 5 CHG00005 corrected_value Corrected value Variable …
#> 6 CHG00006 value_to_missing Non-missing to missing Variable …
#> 7 CHG00007 corrected_value Corrected value Variable …
#> 8 CHG00008 treatment_assignment_change Treatment-assignment change Treatment…
#> 9 CHG00009 treatment_assignment_change Treatment-assignment change Treatment…
#> 10 CHG00010 variable_added Variable added Variable …
#> 11 CHG00011 type_change Type change Variable …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.
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"]
#> [1] NA NA NA "age_change" "age_change"
#> [6] "type_change"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:
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")]
#> # A tibble: 2 × 4
#> USUBJID variable change category
#> <chr> <chr> <chr> <chr>
#> 1 S1 AVAL missing_to_value missing_to_value
#> 2 S2 AVAL value_to_missing value_to_missingAny 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.