| Title: | Clinical Trial Data-Cut Change Detection and Impact Assessment |
| Version: | 0.2.0 |
| Description: | A transparent, rule-based framework for detecting changes between successive data cuts of clinical trial datasets, classifying those changes into clinically meaningful categories, tracing user-defined data lineage, and assessing which downstream analyses and outputs may be affected. The package is designed to complement existing low-level data frame comparison tools by adding clinical-trial-specific classification, lineage and impact-assessment layers on top of deterministic comparison. The rule-based classification is similar in spirit to the data validation infrastructure of van der Loo and de Jonge (2021) <doi:10.18637/jss.v097.i10>. |
| License: | MIT + file LICENSE |
| URL: | https://github.com/Hirujan-R/trialdiff, https://Hirujan-R.github.io/trialdiff/ |
| BugReports: | https://github.com/Hirujan-R/trialdiff/issues |
| Depends: | R (≥ 4.1) |
| Imports: | cli, dplyr, htmltools, jsonlite, rlang, tibble |
| Suggests: | admiral, cards, covr, diffdf, haven, igraph, knitr, lintr, metacore, pharmaverseadam, pharmaversesdtm, pkgdown, rmarkdown, rtables, styler, tern, testthat (≥ 3.0.0), waldo, withr |
| Config/testthat/edition: | 3 |
| Config/Needs/website: | pkgdown |
| Encoding: | UTF-8 |
| Language: | en-GB |
| LazyData: | true |
| VignetteBuilder: | knitr |
| Config/roxygen2/version: | 8.1.0 |
| NeedsCompilation: | no |
| Packaged: | 2026-09-16 10:10:19 UTC; Hirujan |
| Author: | Hirujan Rangaraj [aut, cre] |
| Maintainer: | Hirujan Rangaraj <hirujanrangaraj@gmail.com> |
| Repository: | CRAN |
| Date/Publication: | 2026-09-27 16:30:02 UTC |
trialdiff: Clinical Trial Data-Cut Change Detection and Impact Assessment
Description
A transparent, rule-based framework for detecting changes between successive data cuts of clinical trial datasets, classifying those changes into clinically meaningful categories, tracing user-defined data lineage, and assessing which downstream analyses and outputs may be affected. The package is designed to complement existing low-level data frame comparison tools by adding clinical-trial-specific classification, lineage and impact-assessment layers on top of deterministic comparison.
Author(s)
Maintainer: Hirujan Rangaraj hirujanrangaraj@gmail.com
Authors:
Hirujan Rangaraj hirujanrangaraj@gmail.com
See Also
Useful links:
Report bugs at https://github.com/Hirujan-R/trialdiff/issues
Add edges to a lineage graph
Description
Merges edges (from lineage_edge(), td_output(), a registry or any edge
data frame) into an existing td_lineage object and rebuilds the node table.
Existing provenance and review information is preserved.
Usage
add_edges(x, ..., source = NULL)
Arguments
x |
A |
... |
Edge tables to add. |
source |
Optional value used to tag the source of every added edge. |
Value
A td_lineage object.
Add an analysis/output registry to a lineage graph
Description
Add an analysis/output registry to a lineage graph
Usage
add_outputs(x, outputs, ...)
Arguments
x |
A |
outputs |
A registry from |
... |
Additional registries. |
Value
A td_lineage object.
Examples
lineage <- define_lineage(
lineage_edge("ADLB.AVAL", "ADLB.CHG", relationship = "derives")
)
registry <- output_registry(
td_output("MMRM", depends_on = c("ADLB.AVAL", "ADLB.CHG")),
td_output("Table_14_2_1", depends_on = "MMRM", type = "output")
)
trace_dependencies(add_outputs(lineage, registry), from = "ADLB.AVAL")
Synthetic ADLB data cuts
Description
Two synthetic laboratory analysis datasets (ADLB) with a long structure
keyed by subject, parameter and visit. The later cut adds records for new
subjects and includes corrected, missing-to-value and value-to-missing
laboratory values.
Usage
adlb_cut1
adlb_cut2
Format
A data frame with one row per subject/parameter/visit and variables:
- USUBJID
Unique subject identifier.
- PARAMCD
Parameter code.
- PARAM
Parameter name.
- AVISIT
Analysis visit.
- TRT01P
Planned treatment for period 01.
- AVAL
Analysis value.
- BASE
Baseline value.
- CHG
Change from baseline.
- ABLFL
Baseline flag.
An object of class data.frame with 384 rows and 9 columns.
Example ADSL/ADLB lineage
Description
A td_lineage object linking treatment assignment, population flags and
laboratory analysis values to downstream summaries, an MMRM analysis and
outputs. Intended for use in examples and vignettes.
Usage
adsl_adlb_lineage
Format
A td_lineage object (see define_lineage()).
Synthetic ADSL data cuts
Description
Two synthetic subject-level analysis datasets (ADSL) representing two data
cuts of the same study. No proprietary or real patient data is used. The
later cut contains new subjects, a withdrawn subject, a treatment-assignment
correction, age corrections, missingness changes, a new variable (REGION)
and a label change for AGE.
Usage
adsl_cut1
adsl_cut2
Format
A data frame with one row per subject and variables:
- STUDYID
Study identifier.
- USUBJID
Unique subject identifier.
- SUBJID
Subject identifier within study.
- SITEID
Investigator site identifier.
- AGE
Age.
- SEX
Sex.
- RACE
Race.
- TRT01P
Planned treatment for period 01.
- TRT01A
Actual treatment for period 01.
- SAFFL
Safety population flag.
- ITTFL
Intention-to-treat population flag.
- REGION
Geographic region (later cut only).
An object of class data.frame with 31 rows and 12 columns.
Convert lineage to an igraph object
Description
Requires the igraph package.
Usage
as_igraph(x, ...)
Arguments
x |
A |
... |
Unused. |
Value
An igraph object.
Machine-readable report data
Description
Machine-readable report data
Usage
as_json(x, pretty = TRUE, ...)
Arguments
x |
A |
pretty |
Pretty-print JSON. |
... |
Unused. |
Value
For as_json(), a JSON string. For as_report_list(), a list.
Tidy register of every detected change
Description
Flattens a tdiff object into a single long table with one row per change
(added record, removed record, modified cell or schema change). This is the
machine-readable form intended for QC pipelines.
Usage
as_register(x, ...)
Arguments
x |
A |
... |
Unused. |
Value
A tibble.
Assess downstream impact of classified changes
Description
Combines a classified change set with a lineage graph to identify which
downstream variables, analyses and outputs could be affected. Impact is
graded as "definitely_affected", "potentially_affected" or "unlikely",
with an explicit rationale and the triggering change identifiers.
Usage
assess_impact(changes, lineage, policy = impact_policy(), ...)
Arguments
changes |
A classified |
lineage |
A |
policy |
An |
... |
Reserved for future extensions. |
Details
trialdiff never claims statistical impact. Every affected analysis or
output is reported as requiring programmer/statistician review and, where
appropriate, a rerun.
Value
An object of class td_impact with elements impacts, by_subject,
unlinked, summary, policy and meta.
Examples
old <- data.frame(USUBJID = c("S1", "S2"), TRT01P = c("Placebo", "Drug A"))
new <- data.frame(USUBJID = c("S1", "S2"), TRT01P = c("Drug A", "Drug A"))
diff <- compare_cut(old, new, by = "USUBJID", dataset = "ADSL") |>
classify_changes()
lineage <- define_lineage(
lineage_edge("ADSL.TRT01P", "ADLB.TRT01P", relationship = "groups_by"),
lineage_edge("ADLB.TRT01P", "MMRM", relationship = "models"),
lineage_edge("MMRM", "Table_14_2_1", relationship = "reports")
)
assess_impact(diff, lineage)
Classify detected changes
Description
classify_changes() turns the raw output of compare_cut() into a
clinically meaningful change register. Every change is assigned a
transparent, rule-based category (see td_default_rules()) together with a
human-readable explanation.
Usage
classify_changes(x, rules = td_default_rules(), context = list(), ...)
Arguments
x |
A |
rules |
A list of |
context |
Optional named list of extra context passed to rules, for
example |
... |
Unused. |
Value
When x is a tdiff, the same object with classification columns
added to $added, $removed, $modified and $schema, a $register
element, and class c("tdiff_classified", "tdiff"). When x is a
register (data frame), a classified tibble.
Examples
old <- data.frame(USUBJID = c("S1", "S2"), TRT01P = c("Placebo", "Drug A"))
new <- data.frame(USUBJID = c("S1", "S2"), TRT01P = c("Drug A", "Drug A"))
compare_cut(old, new, by = "USUBJID", dataset = "ADSL") |>
classify_changes()
Compare two data cuts of a clinical dataset
Description
compare_cut() performs a deterministic, key-based comparison of two
versions of the same clinical dataset (for example ADSL at two data cuts).
It reports observations added, removed and modified, and separately reports
schema-level changes (variables added/removed, type and label changes).
Usage
compare_cut(
old,
new,
by,
compare_labels = TRUE,
compare_types = TRUE,
tolerance = 1e-09,
ignore_vars = NULL,
name_old = "old",
name_new = "new",
dataset = NULL,
subject_var = NULL,
backend = c("trialdiff", "waldo", "diffdf"),
...
)
Arguments
old, new |
Data frames (or tibbles) representing the earlier and later
data cut. |
by |
Character vector of key variables that uniquely identify an
observation within each dataset. Common clinical keys include
|
compare_labels |
Logical. If |
compare_types |
Logical. If |
tolerance |
Numeric tolerance used when comparing numeric values. Set to
|
ignore_vars |
Character vector of variables to exclude from the value comparison (for example technical or audit columns). |
name_old, name_new |
Character labels used in reports. |
dataset |
Optional dataset name (for example |
subject_var |
Optional subject identifier variable. When |
backend |
Low-level comparison backend. |
... |
Reserved for future extensions. |
Details
The function is deliberately a low-level comparison engine. Clinical
meaning is added by classify_changes(), lineage by define_lineage() and
impact by assess_impact().
Value
An object of class tdiff: a list with elements meta, added,
removed, modified, schema, summary and register. See
tdiff-object for details.
Examples
old <- data.frame(
USUBJID = c("S1", "S2", "S3"),
TRT01P = c("Placebo", "Drug A", "Placebo"),
AGE = c(54, 61, 47),
stringsAsFactors = FALSE
)
new <- old
new$TRT01P[1] <- "Drug A"
new$AGE[2] <- 62
diff <- compare_cut(old, new, by = "USUBJID", dataset = "ADSL")
diff
Define a data lineage graph
Description
define_lineage() collects lineage edges into an auditable graph that can be
queried with trace_dependencies() and used by assess_impact(). The graph
is stored as a tidy edge list plus a node table; no external graph library is
required, although as_igraph() is provided for users who prefer one.
Usage
define_lineage(
...,
edges = NULL,
nodes = NULL,
dataset = NULL,
variable = NULL,
downstream = NULL,
relationship = "derives"
)
Arguments
... |
One or more edge tibbles created by |
edges |
Optional data frame of edges with columns |
nodes |
Optional node table with columns |
dataset, variable, downstream |
Convenience arguments matching the common
workflow: declare that |
relationship |
Relationship used with the convenience arguments. |
Value
An object of class td_lineage.
Examples
lineage <- define_lineage(
lineage_edge("ADSL.TRT01P", "ADLB.TRT01P", relationship = "derives"),
lineage_edge("ADLB.AVAL", "MMRM", relationship = "models"),
lineage_edge("MMRM", "Table_14_2_1", relationship = "reports")
)
trace_dependencies(lineage, from = "ADSL.TRT01P")
Impact-assessment policy
Description
Controls how aggressively changes are mapped onto downstream analyses. The policy is explicit and auditable: no statistical significance is ever claimed. Analyses and outputs are flagged as requiring review/rerun, never as "statistically affected".
Usage
impact_policy(
value_direct = "definitely_affected",
value_indirect = "potentially_affected",
record = "potentially_affected",
schema = "potentially_affected",
label = "unlikely",
max_depth = Inf
)
Arguments
value_direct |
Level for a variable derived directly from a changed variable. |
value_indirect |
Level for more distant value changes. |
record |
Level for added/removed observations. |
schema |
Level for schema changes other than label changes. |
label |
Level for label-only changes. |
max_depth |
Maximum lineage depth to traverse. |
Value
An object of class td_policy.
Define a lineage edge
Description
A lineage edge states that to depends on from. Node identifiers use a
simple convention:
Usage
lineage_edge(
from,
to,
from_type = NULL,
to_type = NULL,
relationship = "derives",
condition = NA_character_,
label = NA_character_
)
Arguments
from, to |
Character node identifiers. |
from_type, to_type |
Optional node types ( |
relationship |
Edge semantics. One of |
condition |
Optional condition restricting when the edge applies (for
example |
label |
Optional human-readable label. |
Details
a dataset is written as
"ADSL";a dataset variable is written as
"ADSL.TRT01P";an analysis is written as
"MMRM";an output (TLF) is written as
"Table_14_2_1".
Value
A tibble with one row and class td_edge.
Derive lineage from study metadata
Description
lineage_from_metadata() builds a define_lineage() graph from a
metacore object (or any list with the same tables). Dataset and
variable nodes come from ds_vars/ds_spec, and dependency edges are parsed
from the derivations and where columns of value_spec.
Usage
lineage_from_metadata(
metadata,
include_where = TRUE,
include_sdtm = TRUE,
aliases = NULL,
overrides = NULL,
...
)
Arguments
metadata |
A |
include_where |
Logical. If |
include_sdtm |
Logical. If |
aliases |
Optional data frame with columns |
overrides |
Optional edge table (for example from |
... |
Reserved for future extensions. |
Details
Parsing is deterministic and conservative: only tokens that resolve to known
variables (or explicit DATASET.VARIABLE references) become edges. Every
edge records its provenance, and references that cannot be resolved uniquely
are returned in a review table rather than guessed. See lineage_review().
Value
A td_lineage object with additional elements review (references
needing manual attention) and provenance.
Examples
metadata <- list(
ds_spec = data.frame(dataset = c("ADSL", "ADLB"), stringsAsFactors = FALSE),
ds_vars = data.frame(
dataset = c("ADSL", "ADSL", "ADLB", "ADLB", "ADLB"),
variable = c("USUBJID", "TRT01P", "USUBJID", "AVAL", "CHG"),
stringsAsFactors = FALSE
),
value_spec = data.frame(
dataset = c("ADSL", "ADLB", "ADLB"),
variable = c("TRT01P", "AVAL", "CHG"),
derivation_id = c("MT.ADSL.TRT01P", "MT.ADLB.AVAL", "MT.ADLB.CHG"),
where = c(NA, "PARAMCD == 'ALT'", NA),
stringsAsFactors = FALSE
),
derivations = data.frame(
derivation_id = c("MT.ADSL.TRT01P", "MT.ADLB.AVAL", "MT.ADLB.CHG"),
derivation = c("DM.ARM", "ADSL.TRT01P", "AVAL - BASE"),
stringsAsFactors = FALSE
)
)
lineage_from_metadata(metadata)
Provenance of metadata-driven lineage edges
Description
Provenance of metadata-driven lineage edges
Usage
lineage_provenance(x)
Arguments
x |
A |
Value
A tibble with the metadata source of each edge.
References needing review after metadata-driven lineage
Description
References needing review after metadata-driven lineage
Usage
lineage_review(x)
Arguments
x |
A |
Value
A tibble of unresolved or undetected references.
Build an analysis/output registry
Description
Collects one or more td_output() declarations into a single registry table
that can be grafted onto a lineage graph with add_outputs().
Usage
output_registry(...)
Arguments
... |
One or more |
Value
A tibble of registry edges.
Examples
registry <- output_registry(
td_output("MMRM", depends_on = c("ADLB.AVAL", "ADLB.CHG")),
td_output("Table_14_2_1", depends_on = "MMRM", type = "output")
)
registry
Remove edges from a lineage graph
Description
Removes edges matching any supplied filter. Useful for overriding or replacing automatically generated lineage.
Usage
remove_edges(x, from = NULL, to = NULL, relationship = NULL, source = NULL)
Arguments
x |
A |
from, to, relationship, source |
Optional character vectors of values to
match. |
Value
A td_lineage object.
Produce a change-review report
Description
report_diff() renders a structured review report from a comparison, its
classification and (optionally) an impact assessment. Human-readable HTML or
Quarto output is supported alongside machine-readable JSON and list output
for automated QC pipelines.
Usage
report_diff(
diff,
impact = NULL,
classified = NULL,
output = "html",
format = c("html", "quarto", "json", "list"),
title = NULL,
max_rows = 25L,
include_disclaimer = TRUE,
...
)
Arguments
diff |
A |
impact |
Optional |
classified |
Optional classified |
output |
Either a format ( |
format |
Report format. Ignored when |
title |
Report title. |
max_rows |
Maximum number of rows shown per table in HTML output. |
include_disclaimer |
Include the regulatory/statistical disclaimer. |
... |
Reserved for future extensions. |
Value
An object of class td_report. When a file path is supplied the
report is written and the path is stored in $path.
Category label lookup
Description
Category label lookup
Usage
td_categories()
Value
A tibble with category and label columns.
Default rule set
Description
Default rule set
Usage
td_default_rules()
Value
A list of td_rule() objects ordered by priority.
Declare an analysis or output and its inputs
Description
td_output() is the building block of an analysis/output registry. It states
that an analysis or output depends on one or more upstream nodes (typically
dataset.variable identifiers, but any node is allowed).
Usage
td_output(
id,
depends_on,
type = c("analysis", "output"),
relationship = NULL,
condition = NA_character_,
label = id
)
Arguments
id |
Node identifier for the analysis or output (for example |
depends_on |
Character vector of upstream node identifiers. |
type |
Either |
relationship |
Edge relationship. Defaults to |
condition |
Optional applicability condition (recorded, not evaluated). |
label |
Human-readable label. |
Value
A tibble of registry edges.
Change classification rules
Description
Classification in trialdiff is transparent and rule-based. A rule is a
named predicate evaluated against the long change register produced by
as_register(). Rules are applied in priority order; the first matching rule
determines the primary category, while every matched rule is retained in the
category_all column so that nothing is hidden.
Usage
td_rule(name, label, test, priority = 100L, reason = NULL, description = NULL)
Arguments
name |
Stable machine-readable category name (snake_case). |
label |
Human-readable category label. |
test |
A function |
priority |
Integer priority; lower values are evaluated first. |
reason |
Optional function |
description |
Optional longer description. |
Value
td_rule() returns an object of class td_rule.
The tdiff object
Description
compare_cut() returns an object of class tdiff. It is a list with the
following elements:
Details
-
meta: comparison metadata (dataset, cut names, keys, row counts, time). -
added: observations present in the new cut but not the old cut, with.keyand.subjecthelper columns. -
removed: observations present in the old cut but not the new cut. -
modified: one row per changed cell, withvariable,old_value,new_valueandchange("value","missing_to_value"or"value_to_missing"). -
schema: variable-level changes (variable_added,variable_removed,type_change,label_change). -
summary: a tidy metric/value table.
Trace downstream dependencies
Description
Performs a breadth-first traversal of the lineage graph starting from from
and returns every reachable node, its depth and the path taken.
Usage
trace_dependencies(
x,
from,
direction = c("downstream", "upstream"),
max_depth = Inf,
include_self = FALSE,
...
)
Arguments
x |
A |
from |
Character vector of starting node identifiers. |
direction |
|
max_depth |
Maximum traversal depth. |
include_self |
Include the starting node(s) in the result. |
... |
Unused. |
Value
A tibble with node, node_type, depth, path and
edge_relationship.
Trialdiff conditions
Description
Structured conditions used across the package so that callers (and automated QC pipelines) can handle errors and warnings programmatically.
Usage
td_abort(message, class = NULL, ..., call = rlang::caller_env())
td_warn(message, class = NULL, ..., call = rlang::caller_env())
td_inform(message, ...)
Arguments
message |
A character scalar. |
class |
Additional condition subclasses. |
... |
Additional data stored on the condition. |
call |
The calling environment. |
Internal helpers
Description
Small, dependency-light utilities used across trialdiff.
Arguments
x |
An object. |
nm |
A character scalar. |