Classification in trialdiff is deliberately boring and
auditable:
category_all;reason.There is no machine learning and no hidden scoring.
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 reviewdiff <- 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: 4 × 7
#> .change_id record_type USUBJID variable old_value new_value change
#> <chr> <chr> <chr> <chr> <chr> <chr> <chr>
#> 1 CHG00001 added 01-718-1371 <NA> <NA> <NA> recor…
#> 2 CHG00002 added 01-718-1427 <NA> <NA> <NA> recor…
#> 3 CHG00003 modified 01-701-1015 TRT01P Placebo Xanomeline Low D… value
#> 4 CHG00004 modified 01-701-1015 TRT01A Placebo Xanomeline Low D… valueclassified <- classify_changes(diff)
classified$register[, c(".change_id", "category", "category_label", "reason")]
#> # A tibble: 4 × 4
#> .change_id category category_label reason
#> <chr> <chr> <chr> <chr>
#> 1 CHG00001 new_subject New subject Subject '0…
#> 2 CHG00002 new_subject New subject Subject '0…
#> 3 CHG00003 treatment_assignment_change Treatment-assignment change Treatment …
#> 4 CHG00004 treatment_assignment_change Treatment-assignment change Treatment …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 NARules 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.