--- title: "Populations, variable specifications, and reporting" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Populations, variable specifications, and reporting} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") library(lineager) ``` After tagging, filtering, and deriving, two tasks remain before the provenance record is complete: 1. **Documenting populations** — the definitions and criteria for each analysis set or cohort (`lg_population()`) 2. **Documenting variable derivations** — linking each output variable back to its source (`lg_spec()`) Both feed directly into `lg_report()`, which compiles everything into a structured HTML provenance report. ## Setup: a complete pipeline We build a session with tagging, derivation, filtering, and multiple analysis populations to demonstrate the full reporting workflow. ```{r setup-pipeline} lg_start(study_id = "TRIAL-001", analysis_id = "primary-efficacy") # Source data — patients and lab measurements patients <- data.frame( USUBJID = sprintf("PT-%03d", 1:15), age = c( 22L, 45L, 38L, 61L, 29L, 55L, 43L, 17L, 52L, 34L, 48L, 27L, 66L, 39L, 51L ), sex = rep(c("M", "F", "M"), 5L), arm = c( "TRT", "TRT", "CTL", "TRT", "CTL", "CTL", "TRT", "TRT", "CTL", "TRT", "CTL", "TRT", "CTL", "CTL", "TRT" ), enrolled = c(rep(TRUE, 13), FALSE, FALSE), dosed = c(rep(TRUE, 10), FALSE, TRUE, TRUE, FALSE, FALSE), stringsAsFactors = FALSE ) labs <- data.frame( USUBJID = sprintf("PT-%03d", c(1:10, 12:13)), baseline = round(c( 24.1, 31.8, 28.4, 22.9, 35.2, 26.7, 29.1, 33.4, 27.8, 25.5, 30.2, 28.9 ), 1), endpoint = round(c( 18.4, 27.1, 24.6, 19.8, NA, 22.3, 25.4, 28.1, NA, 21.7, 26.4, 24.1 ), 1), stringsAsFactors = FALSE ) # Tag pts <- lg_tag(patients, dataset_id = "PATIENTS", label = "Patient registry") labs_tagged <- lg_tag(labs, dataset_id = "LABS", label = "Laboratory measurements") cat("Patients:", nrow(pts), "\n") cat("Labs: ", nrow(labs_tagged), "\n") ``` ### Derive analysis variables ```{r derive} # Adult flag pts <- lg_derive(pts, adult = age >= 18L, description = "adult: TRUE if age >= 18 years" ) # Derive population flags pts <- lg_derive(pts, ENRLFL = ifelse(enrolled, "Y", "N"), DOSEFL = ifelse(enrolled & dosed, "Y", "N"), description = "ENRLFL: enrolled; DOSEFL: enrolled AND received study treatment" ) # Join lab measurements pts_labs <- lg_join(pts, labs_tagged, by = "USUBJID", type = "left", description = "Merge baseline and endpoint lab measurements from LABS" ) # Derive analysis variables analysis_ds <- lg_derive(pts_labs, CHG = endpoint - baseline, PCHG = round((endpoint - baseline) / baseline * 100, 2), description = paste( "CHG: absolute change from baseline (endpoint - baseline);", "PCHG: percent change from baseline" ) ) # Filter to enrolled, adult, dosed patients with complete endpoint final <- analysis_ds |> lg_filter(enrolled == TRUE, adult == TRUE, reason = "Not enrolled or under 18", reason_code = "NOT_ENROLLED" ) |> lg_filter(dosed == TRUE, reason = "Did not receive study treatment", reason_code = "NOT_DOSED", population = "SAFETY_SET" ) |> lg_filter(!is.na(endpoint), reason = "Missing primary endpoint measurement", reason_code = "MISSING_EP", population = "ANALYSIS_SET" ) cat("Final analysis set:", nrow(final), "patients\n") ``` ## 1. lg_population(): documenting analysis sets `lg_population()` registers the formal definition of a population flag. It requires that the flag variable already exists in the dataset — compute it first with `lg_derive()`, then register the definition. ```{r population-basic} lg_population(pts, flag_var = "ENRLFL", label = "Enrolled Set", definition = "All patients who met eligibility criteria and were enrolled in the study", incl_criteria = "enrolled == TRUE AND age >= 18" ) ``` ### Multiple populations ```{r population-multiple} lg_population(pts, flag_var = "DOSEFL", label = "Safety Set", definition = "All enrolled patients who received at least one dose of study treatment", incl_criteria = c("enrolled == TRUE", "dosed == TRUE"), excl_criteria = "Enrolled but did not receive study treatment (dosed == FALSE)" ) ``` ### What lg_population() records ```{r population-inspect} env <- getFromNamespace(".lg", "lineager") pop <- env$populations[["DOSEFL"]] cat("Flag: ", pop$flag_var, "\n") cat("Label: ", pop$label, "\n") cat("N included: ", pop$n_included, "\n") cat("N excluded: ", pop$n_excluded, "\n") cat("N total: ", pop$n_total, "\n") ``` The `n_included` and `n_excluded` counts come from the `flag_var` column in the dataset at the time `lg_population()` is called. Pass the most complete dataset (before any population-specific filtering) so the counts reflect the full enrolled cohort. ### print method ```{r population-print} print(pop) ``` ## 2. lg_spec(): documenting variable derivations `lg_spec()` registers a structured derivation specification linking an output variable back to its source. Think of it as the "Variable Derivations" section of an analysis plan, expressed as structured R objects rather than prose. ```{r spec-basic} lg_spec( adam_dataset = "ANALYSIS", adam_var = "CHG", label = "Change from Baseline", source_domain = "LABS", source_var = "endpoint / baseline", derivation = "endpoint - baseline; NA when endpoint is missing" ) lg_spec( adam_dataset = "ANALYSIS", adam_var = "PCHG", label = "Percent Change from Baseline", source_domain = "LABS", source_var = "endpoint / baseline", derivation = "(endpoint - baseline) / baseline * 100, rounded to 2 decimal places", conditions = "Only computed when baseline is non-missing and non-zero" ) ``` ### Documenting flag derivations ```{r spec-flags} lg_spec( adam_dataset = "PATIENTS", adam_var = "ENRLFL", label = "Enrolled Flag", source_domain = "PATIENTS", source_var = "enrolled", derivation = "Y if enrolled == TRUE; N otherwise" ) lg_spec( adam_dataset = "PATIENTS", adam_var = "DOSEFL", label = "Safety Flag", source_domain = "PATIENTS", source_var = "enrolled / dosed", derivation = "Y if enrolled == TRUE AND dosed == TRUE; N otherwise" ) ``` ### Overwriting a spec If you call `lg_spec()` with the same `adam_dataset` and `adam_var`, the prior spec is replaced — useful during iterative analysis. `lg_spec()` emits a warning when this happens, so an accidental duplicate key doesn't silently overwrite an unrelated spec; the warning below is expected here, since we are deliberately re-registering `ANALYSIS.CHG`: ```{r spec-overwrite} # Refine the CHG derivation description lg_spec( adam_dataset = "ANALYSIS", adam_var = "CHG", label = "Change from Baseline", source_domain = "LABS", source_var = "endpoint / baseline", derivation = "endpoint - baseline. LOCF applied when endpoint missing at Week 12 only.", conditions = "LOCF from Week 8 visit applied per SAP section 7.2" ) ``` ## 3. lg_report(): generating the provenance report `lg_report()` compiles the full session into a self-contained HTML document. It draws from everything accumulated during the session: | Report section | Source | |---|---| | Dataset Inventory | All `lg_tag()` calls | | Subject Disposition | All `lg_filter()` exclusions | | Population Definitions | All `lg_population()` registrations | | Variable Derivations | All `lg_spec()` registrations | | Operation Log | All `lg_derive()`, `lg_join()`, `lg_filter()` calls | | Exclusion Listing | Full exclusion registry | ### In-memory report (inspect without writing to disk) ```{r report-memory} html <- lg_report( output = NULL, title = "Data Provenance Report", study_id = "TRIAL-001", author = "Ndoh Penn" ) cat("Report size:", nchar(html), "characters\n") cat("Sections found:\n") sections <- c( "Dataset Inventory", "Subject Disposition", "Population Flag", "Variable Derivation", "Operation Log", "Exclusion Listing" ) for (s in sections) { cat( " ", if (grepl(s, html, ignore.case = TRUE)) "[YES]" else "[NO]", s, "\n" ) } ``` ### Write to file ```{r report-file, eval = FALSE} lg_report( output = "outputs/provenance_TRIAL001_primary.html", title = "Data Provenance Report — TRIAL-001 Primary Analysis", study_id = "TRIAL-001", sponsor = "Example Pharma Ltd", author = "Ndoh Penn, Biostatistician", date = as.Date("2026-06-23") ) ``` The report is entirely self-contained HTML — no external CSS, no JavaScript, no internet connection required at render time. It opens in any browser and can be attached to a regulatory submission package. ### Report arguments | Argument | Default | Purpose | |---|---|---| | `format` | `"html"` | Output format (currently only HTML) | | `output` | `NULL` | File path; `NULL` returns the HTML string | | `title` | `"Data Provenance Report"` | Report title | | `study_id` | Session `study_id` | Study identifier for header | | `sponsor` | `NULL` | Sponsor name | | `author` | `NULL` | Analyst name | | `date` | `Sys.Date()` | Report date | ## 4. A note on CDISC-specific usage All functions in this vignette work identically for CDISC clinical data. For pharmaceutical users: - Pass `domain = "DM"` etc. in `lg_tag()` — domain appears in dataset inventory and distinguishes SDTM domains from ADaM datasets - `USUBJID` column is detected automatically and embedded in `lineage_id` values for human readability - `lg_population()` naturally documents SAFFL, ITTFL, PPROTFL — the labels and definitions become the Reviewer's Guide population section - `lg_spec()` documents SDTM-to-ADaM variable mappings — the report output aligns with the "Variable Derivations" section of the Reviewer's Guide For non-CDISC users, these arguments and features are entirely optional. The core workflow — tag, filter, trace, report — works without any CDISC-specific configuration. ## 5. Visualise the pipeline lineage `lg_lineage()` builds a graph of the complete pipeline — source datasets, all derive/join/filter operations, and exclusion branches — as a Graphviz DOT string. `lg_plot()` renders it inline or writes it to a file. ```{r lineage} lin <- lg_lineage() print(lin) ``` ```{r lineage-plot, eval = FALSE} # Render inline (requires DiagrammeR) lg_plot(lin) # Export DOT for Graphviz or https://dreampuf.github.io/GraphvizOnline/ lg_plot(lin, output = "outputs/pipeline.dot") ``` The lineage graph for this pipeline shows the two source datasets (PATIENTS and LABS) flowing through four derive steps, a left join, and three filter steps with exclusion branches at each stage. ## 6. End the session ```{r end} lg_end() ```