## ----include = FALSE----------------------------------------------------------
# Skip this vignette when the optional packages are missing, or when the
# ducklake DuckDB extension is not already installed. The probe never
# downloads anything, so building this vignette needs no network access.
has_deps <- requireNamespace("pharmaversesdtm", quietly = TRUE) &&
requireNamespace("admiral", quietly = TRUE)
ducklake_available <- ducklake::ducklake_extension_available()
knitr::opts_chunk$set(
collapse = TRUE,
comment = "#>",
eval = has_deps && ducklake_available
)
# Use a unique temp directory for this vignette to avoid conflicts during R CMD check
vignette_temp_dir <- file.path(tempdir(), "clinical_trial_vignette")
dir.create(vignette_temp_dir, showWarnings = FALSE, recursive = TRUE)
knitr::opts_knit$set(root.dir = vignette_temp_dir)
## ----setup, message=FALSE-----------------------------------------------------
library(ducklake)
library(dplyr)
library(tidyr)
library(pharmaversesdtm)
library(admiral)
library(lubridate)
library(stringr)
## ----create-datalake----------------------------------------------------------
# Install the ducklake extension to duckdb (required once per system)
# The ducklake extension only needs installing once per machine:
# install_ducklake()
# Define where to create a new data lake or access an existing one
# For this vignette, we use vignette_temp_dir; in practice, use a permanent location
# lake_path <- "/path/to/your/project/data_lake"
trial_lake_path <- vignette_temp_dir
# attach_ducklake creates or attaches (if it already exists) a data lake
attach_ducklake(
ducklake_name = "clinical_trial_lake",
lake_path = trial_lake_path
)
# Verify the lake was created
list.files(trial_lake_path, pattern = "clinical_trial_lake")
## ----architecture-diagram, echo=FALSE, message=FALSE, warning=FALSE-----------
if (requireNamespace("DiagrammeR", quietly = TRUE)) {
DiagrammeR::mermaid("
graph LR
A[Bronze Layer
Raw Data
dm_raw, ae_raw, ...] --> B[Silver Layer
Cleaned Data
dm, ae, ...]
B --> C[Gold Layer
Analysis Data
adsl, adae, ...]
style A fill:#cd7f32,stroke:#8b4513,stroke-width:2px,color:#fff
style B fill:#c0c0c0,stroke:#808080,stroke-width:2px,color:#000
style C fill:#ffd700,stroke:#daa520,stroke-width:2px,color:#000
", width = "100%", height = 200)
}
## ----load-dm------------------------------------------------------------------
# Bronze layer: Load raw SDTM Demographics exactly as received
with_transaction(
create_table(pharmaversesdtm::dm, "dm_raw"),
author = "T Gerke",
commit_message = "Add raw demographics"
)
# Silver layer: Apply cleaning transformations
with_transaction(
get_ducklake_table("dm_raw") |>
admiral::convert_blanks_to_na() |>
create_table("dm"),
author = "T Gerke",
commit_message = "Clean demographics data"
)
# Verify the cleaned table
get_ducklake_table("dm") |>
select(USUBJID, AGE, SEX, RACE, ARM) |>
head()
## ----dm-labels----------------------------------------------------------------
get_table_comments("dm_raw") |> head()
## ----dm-labels-collect--------------------------------------------------------
dm_check <- get_ducklake_table("dm_raw") |>
select(USUBJID, AGE, SEX) |>
collect()
attr(dm_check$AGE, "label")
## ----load-suppdm--------------------------------------------------------------
# Bronze layer: Raw data
with_transaction(
create_table(pharmaversesdtm::suppdm, "suppdm_raw"),
author = "T Gerke",
commit_message = "Add raw supplemental demographics"
)
# Silver layer: Cleaned data
with_transaction(
get_ducklake_table("suppdm_raw") |>
admiral::convert_blanks_to_na() |>
create_table("suppdm"),
author = "T Gerke",
commit_message = "Clean supplemental demographics"
)
## ----load-ds------------------------------------------------------------------
# Bronze layer
with_transaction(
create_table(pharmaversesdtm::ds, "ds_raw"),
author = "T Gerke",
commit_message = "Add raw disposition"
)
# Silver layer
with_transaction(
ds <- get_ducklake_table("ds_raw") |>
admiral::convert_blanks_to_na() |>
create_table("ds"),
author = "T Gerke",
commit_message = "Clean disposition data"
)
## ----load-ex------------------------------------------------------------------
# Bronze layer
with_transaction(
create_table(pharmaversesdtm::ex, "ex_raw"),
author = "T Gerke",
commit_message = "Add raw exposure"
)
# Silver layer
with_transaction(
ex <- get_ducklake_table("ex_raw") |>
admiral::convert_blanks_to_na() |>
create_table("ex"),
author = "T Gerke",
commit_message = "Clean exposure data"
)
## ----load-ae------------------------------------------------------------------
# Bronze layer
with_transaction(
create_table(pharmaversesdtm::ae, "ae_raw"),
author = "T Gerke",
commit_message = "Add raw adverse events"
)
# Silver layer
with_transaction(
ae <- get_ducklake_table("ae_raw") |>
admiral::convert_blanks_to_na() |>
create_table("ae"),
author = "T Gerke",
commit_message = "Clean adverse events"
)
## ----load-vs------------------------------------------------------------------
# Bronze layer
with_transaction(
create_table(pharmaversesdtm::vs, "vs_raw"),
author = "T Gerke",
commit_message = "Add raw vital signs"
)
# Silver layer
with_transaction(
vs <- get_ducklake_table("vs_raw") |>
admiral::convert_blanks_to_na() |>
create_table("vs"),
author = "T Gerke",
commit_message = "Clean vital signs"
)
## ----load-pc------------------------------------------------------------------
# Bronze layer
with_transaction(
create_table(pharmaversesdtm::pc, "pc_raw"),
author = "T Gerke",
commit_message = "Add raw PK concentrations"
)
# Silver layer
with_transaction(
pc <- get_ducklake_table("pc_raw") |>
admiral::convert_blanks_to_na() |>
create_table("pc"),
author = "T Gerke",
commit_message = "Clean PK concentrations"
)
## ----verify-sdtm-versions-----------------------------------------------------
# View the first 5 of all snapshots in the data lake
list_table_snapshots() |>
head(5)
# Filter snapshots for specific tables
list_table_snapshots("dm_raw")
list_table_snapshots("dm")
## ----build-adsl---------------------------------------------------------------
# Read SDTM data from the lake and collect into memory
# Admiral functions require tibbles/data.frames, not lazy database connections
dm <- get_ducklake_table("dm") |> collect()
suppdm <- get_ducklake_table("suppdm") |> collect()
ds <- get_ducklake_table("ds") |> collect()
ex <- get_ducklake_table("ex") |> collect()
ae <- get_ducklake_table("ae") |> collect()
vs <- get_ducklake_table("vs") |> collect()
# Combine DM and SUPPDM
dm_suppdm <- dm |>
left_join(
suppdm |>
filter(QNAM %in% c("EDUCLVL", "DISCONFL", "DSRAEFL")) |>
pivot_wider(
id_cols = c(STUDYID, USUBJID),
names_from = QNAM,
values_from = QVAL
),
by = c("STUDYID", "USUBJID")
)
# Derive treatment dates and durations
ex_ext <- ex |>
derive_vars_dtm(
dtc = EXSTDTC,
new_vars_prefix = "EXST"
) |>
derive_vars_dtm(
dtc = EXENDTC,
new_vars_prefix = "EXEN",
time_imputation = "last"
)
# Derive disposition variables first (needed for later derivations)
ds_ext <- ds |>
derive_vars_dt(
dtc = DSSTDTC,
new_vars_prefix = "DSST"
)
# Build ADSL with all derivations in a single pipeline
adsl <- dm_suppdm |>
# Treatment Start Datetime
derive_vars_merged(
dataset_add = ex_ext,
filter_add = (EXDOSE > 0 | (EXDOSE == 0 & str_detect(EXTRT, "PLACEBO"))) &
!is.na(EXSTDTM),
new_vars = exprs(TRTSDTM = EXSTDTM),
order = exprs(EXSTDTM, EXSEQ),
mode = "first",
by_vars = exprs(STUDYID, USUBJID)
) |>
# Treatment End Datetime
derive_vars_merged(
dataset_add = ex_ext,
filter_add = (EXDOSE > 0 | (EXDOSE == 0 & str_detect(EXTRT, "PLACEBO"))) &
!is.na(EXENDTM),
new_vars = exprs(TRTEDTM = EXENDTM),
order = exprs(EXENDTM, EXSEQ),
mode = "last",
by_vars = exprs(STUDYID, USUBJID)
) |>
# Convert to dates
derive_vars_dtm_to_dt(source_vars = exprs(TRTSDTM, TRTEDTM)) |>
# Treatment duration
derive_var_trtdurd() |>
# Safety population flag
derive_var_merged_exist_flag(
dataset_add = ex,
by_vars = exprs(STUDYID, USUBJID),
new_var = SAFFL,
condition = (EXDOSE > 0 | (EXDOSE == 0 & str_detect(EXTRT, "PLACEBO")))
) |>
# Treatment variables
mutate(
TRT01P = ARM,
TRT01A = ACTARM
) |>
# Age groups
mutate(
AGEGR1 = case_when(
AGE < 18 ~ "<18",
between(AGE, 18, 64) ~ "18-64",
AGE > 64 ~ ">64",
TRUE ~ "Missing"
),
AGEGR1N = case_when(
AGE < 18 ~ 1,
between(AGE, 18, 64) ~ 2,
AGE > 64 ~ 3,
TRUE ~ 4
)
) |>
# Randomization date
derive_vars_merged(
dataset_add = ds_ext,
by_vars = exprs(STUDYID, USUBJID),
new_vars = exprs(RANDDT = DSSTDT),
filter_add = DSDECOD == "RANDOMIZED"
) |>
# End of study date
derive_vars_merged(
dataset_add = ds_ext,
by_vars = exprs(STUDYID, USUBJID),
new_vars = exprs(EOSDT = DSSTDT),
filter_add = DSCAT == "DISPOSITION EVENT" & DSDECOD != "SCREEN FAILURE"
) |>
# End of study status
mutate(
EOSSTT = case_when(
is.na(EOSDT) ~ "ONGOING",
TRUE ~ "COMPLETED"
)
)
# Store ADSL in the data lake
with_transaction(
create_table(adsl, "adsl"),
author = "T Gerke",
commit_message = "Create ADSL dataset",
commit_extra_info = "Derived from DM, SUPPDM, DS, EX; includes treatment dates, safety flags, age groups"
)
# Variables carried over from DM kept their SDTM labels automatically.
# Derived variables are new, so label them now -- the ADaM way, but stored
# in the catalog rather than a spec sidecar
set_column_comments(
"adsl",
TRT01P = "Planned Treatment for Period 01",
TRT01A = "Actual Treatment for Period 01",
AGEGR1 = "Pooled Age Group 1",
AGEGR1N = "Pooled Age Group 1 (N)",
SAFFL = "Safety Population Flag"
)
# Preview ADSL
get_ducklake_table("adsl") |>
select(USUBJID, AGE, AGEGR1, TRT01P, TRTSDT, TRTEDT, SAFFL) |>
head(10)
## ----build-adae---------------------------------------------------------------
# Read ADSL for merging
adsl <- get_ducklake_table("adsl") |> collect()
ae <- get_ducklake_table("ae") |> collect()
# Build ADAE
adae <- ae |>
# Merge ADSL variables
derive_vars_merged(
dataset_add = adsl,
new_vars = exprs(TRTSDT, TRTEDT, TRT01A, TRT01P),
by_vars = exprs(STUDYID, USUBJID)
) |>
# Derive analysis dates
derive_vars_dt(
dtc = AESTDTC,
new_vars_prefix = "AST"
) |>
derive_vars_dt(
dtc = AEENDTC,
new_vars_prefix = "AEN",
date_imputation = "last"
) |>
# Derive treatment-emergent flag
mutate(
TRTEMFL = if_else(
!is.na(ASTDT) & !is.na(TRTSDT) & ASTDT >= TRTSDT,
"Y",
NA_character_
)
) |>
# Derive analysis variables
mutate(
AOCCPFL = if_else(AESEQ == min(AESEQ), "Y", NA_character_),
AOCC01FL = AOCCPFL
) |>
group_by(USUBJID, AEDECOD) |>
mutate(
AOCC01FL = if_else(row_number() == 1, "Y", NA_character_)
) |>
ungroup()
# Store ADAE in the data lake
with_transaction(
create_table(adae, "adae"),
author = "T Gerke",
commit_message = "Create ADAE dataset",
commit_extra_info = "Includes treatment-emergent flags and occurrence flags"
)
# Preview ADAE
get_ducklake_table("adae") |>
filter(TRTEMFL == "Y") |>
select(USUBJID, AEDECOD, ASTDT, AESEV, TRTEMFL) |>
head(10)
## ----build-adpc---------------------------------------------------------------
# Read required datasets
adsl <- get_ducklake_table("adsl") |> collect()
pc <- get_ducklake_table("pc") |> collect()
ex <- get_ducklake_table("ex") |> collect()
vs <- get_ducklake_table("vs") |> collect()
# Get ADSL variables needed
adsl_vars <- exprs(TRTSDT, TRTSDTM, TRT01P, TRT01A)
# Derive PC dates and times
pc_dates <- pc |>
derive_vars_merged(
dataset_add = adsl,
new_vars = adsl_vars,
by_vars = exprs(STUDYID, USUBJID)
) |>
derive_vars_dtm(
new_vars_prefix = "A",
dtc = PCDTC,
time_imputation = "00:00:00",
ignore_seconds_flag = FALSE
) |>
derive_vars_dtm_to_dt(exprs(ADTM)) |>
derive_vars_dtm_to_tm(exprs(ADTM)) |>
derive_vars_dy(reference_date = TRTSDT, source_vars = exprs(ADT)) |>
mutate(
EVID = 0,
NFRLT = if_else(PCTPTNUM < 0, 0, PCTPTNUM)
)
# Process exposure records
ex_dates <- ex |>
derive_vars_merged(
dataset_add = adsl,
new_vars = adsl_vars,
by_vars = exprs(STUDYID, USUBJID)
) |>
filter(EXDOSE > 0) |>
derive_vars_dtm(
new_vars_prefix = "AST",
dtc = EXSTDTC,
time_imputation = "00:00:00"
) |>
mutate(
EVID = 1,
NFRLT = 24 * VISITDY
) |>
derive_vars_dtm_to_dt(exprs(ASTDTM))
# Combine PC and EX
adpc <- bind_rows(pc_dates, ex_dates) |>
arrange(STUDYID, USUBJID, ADTM) |>
mutate(
PARAMCD = coalesce(PCTESTCD, "DOSE"),
AVAL = case_when(
EVID == 1 ~ EXDOSE,
PCSTRESC == " 0 ~ 0.5 * PCLLOQ,
TRUE ~ PCSTRESN
),
PARAM = case_when(
PARAMCD == "XAN" ~ "Xanomeline Concentration",
PARAMCD == "DOSE" ~ "Xanomeline Dose"
)
)
# Store ADPC in the data lake
with_transaction(
create_table(adpc, "adpc"),
author = "T Gerke",
commit_message = "Create ADPC dataset",
commit_extra_info = "PK concentrations with dosing records for NCA"
)
# Preview ADPC
get_ducklake_table("adpc") |>
filter(PARAMCD == "XAN") |>
select(USUBJID, ADT, PCTPT, AVAL, PARAM) |>
head(10)
## ----store-define-------------------------------------------------------------
# Example: Store define.xml content
# In practice, you might read this from a file generated by your metadata system
define_xml <- '
'
# Create a table for regulatory documents
regulatory_docs <- tibble(
doc_type = "define.xml",
doc_version = "1.0",
content = define_xml,
created_date = Sys.Date(),
description = "Dataset and variable metadata for regulatory submission"
)
with_transaction(
create_table(regulatory_docs, "regulatory_documents"),
author = "T Gerke",
commit_message = "Add define.xml metadata"
)
get_ducklake_table("regulatory_documents")
## ----store-arm----------------------------------------------------------------
# Example: Store Analysis Results Metadata (ARM)
arm_content <- tibble(
analysis_id = "DEMO01",
analysis_name = "Demographics Table",
dataset_used = "ADSL",
program_name = "t_demographics.R",
output_file = "t_demographics.rtf",
analysis_date = Sys.Date()
)
# Add to or update regulatory documents table
with_transaction(
get_ducklake_table("regulatory_documents") |>
collect() |>
mutate(content = as.character(content)) |>
bind_rows(
tibble(
doc_type = "ARM",
doc_version = "1.0",
content = as.character(jsonlite::toJSON(arm_content)),
created_date = Sys.Date(),
description = "Analysis Results Metadata"
)
) |>
distinct(doc_type, doc_version, .keep_all = TRUE) |>
replace_table("regulatory_documents"),
author = "T Gerke",
commit_message = "Add ARM metadata"
)
# Example: Store Analysis Results Data (ARD)
ard_content <- tibble(
analysis_id = "DEMO01",
row_type = "header",
row_label = "Age (years)",
treatment = c("Placebo", "Xanomeline Low", "Xanomeline High"),
n = c(86, 84, 84),
mean = c(75.2, 75.7, 74.4),
sd = c(8.59, 7.89, 7.89)
)
with_transaction(
get_ducklake_table("regulatory_documents") |>
collect() |>
mutate(content = as.character(content)) |>
bind_rows(
tibble(
doc_type = "ARD",
doc_version = "1.0",
content = as.character(jsonlite::toJSON(ard_content)),
created_date = Sys.Date(),
description = "Analysis Results Data for Demographics Table"
)
) |>
distinct(doc_type, doc_version, .keep_all = TRUE) |>
replace_table("regulatory_documents"),
author = "T Gerke",
commit_message = "Add demographics ARD"
)
## ----store-specs, eval=FALSE--------------------------------------------------
# # Example: Store ADSL specifications
# adsl_spec <- tibble(
# dataset = "ADSL",
# variable = c("USUBJID", "AGE", "AGEGR1", "TRT01P", "SAFFL"),
# label = c(
# "Unique Subject Identifier",
# "Age",
# "Age Group 1",
# "Planned Treatment",
# "Safety Population Flag"
# ),
# type = c("text", "num", "text", "text", "text"),
# length = c(20, 8, 10, 40, 1),
# derivation = c("DM.USUBJID", "DM.AGE", "Derived from AGE", "DM.ARM", "Derived")
# )
#
# with_transaction(
# create_table(adsl_spec, "dataset_specifications"),
# author = "T Gerke",
# commit_message = "Add ADSL specifications"
# )
#
# # Query specifications when needed
# get_ducklake_table("dataset_specifications") |>
# filter(dataset == "ADSL")
## ----demonstrate-relational---------------------------------------------------
# Traditional approach: Load multiple files, manually join
# adsl <- read_xpt("adsl.xpt")
# ex <- read_xpt("ex.xpt")
# ae <- read_xpt("ae.xpt")
# result <- adsl |> left_join(ex, ...) |> left_join(ae, ...)
# DuckLake approach: Query across related tables directly using dplyr
# Complex cross-domain query without loading all data
adsl_tbl <- get_ducklake_table("adsl")
ex_tbl <- get_ducklake_table("ex")
ae_tbl <- get_ducklake_table("ae")
adsl_tbl |>
filter(SAFFL == "Y") |>
left_join(ex_tbl, by = "USUBJID") |>
left_join(ae_tbl, by = "USUBJID") |>
group_by(USUBJID, AGE, TRT01P) |>
summarise(
n_aes = n_distinct(AESEQ, na.rm = TRUE),
total_dose = sum(EXDOSE, na.rm = TRUE),
.groups = "drop"
) |>
arrange(desc(n_aes)) |>
head(10) |>
collect()
## ----warehouse-benefits-------------------------------------------------------
# 1. Single source of truth - all datasets in one repository
# List all tables in the data lake
DBI::dbListTables(get_ducklake_connection())
# 2. Efficient filtering before loading into R
# Only load subjects with adverse events
get_ducklake_table("ae") |>
filter(AESEV == "SEVERE") |>
distinct(USUBJID)
# 3. Aggregations performed at database level
get_ducklake_table("adae") |>
filter(TRTEMFL == "Y") |>
group_by(TRT01A, AESEV) |>
summarise(
n_events = n(),
n_subjects = n_distinct(USUBJID),
.groups = "drop"
)
# 4. Joins across SDTM and ADaM layers
# Example: Find date discrepancies between SDTM and ADaM
ae_sdtm <- get_ducklake_table("ae") |>
select(USUBJID, AESEQ, ae_date = AESTDTC, ae_term = AEDECOD)
adae_adam <- get_ducklake_table("adae") |>
select(USUBJID, AESEQ, adae_date = ASTDT, adae_term = AEDECOD)
ae_sdtm |>
inner_join(adae_adam, by = c("USUBJID", "AESEQ")) |>
# Convert SDTM character date to comparable format for filtering
mutate(ae_date_comparable = substr(ae_date, 1, 10)) |>
filter(ae_date_comparable != as.character(adae_date)) |>
select(
USUBJID,
sdtm_start_date = ae_date,
adam_start_date = adae_date,
sdtm_term = ae_term,
adam_term = adae_term
)
# Note: This returns 0 rows with clean pharmaversesdtm data,
# but demonstrates how to check for data quality issues across layers
## ----explore-structure--------------------------------------------------------
# List all tables in the data lake
DBI::dbListTables(get_ducklake_connection())
# View snapshot history for key tables
metadata_tables <- c("dm", "ex", "ae", "pc",
"adsl", "adae", "adpc")
# Collect snapshots for all tables
purrr::map_dfr(metadata_tables, ~{
list_table_snapshots(.x) |>
mutate(table = .x, .before = 1)
}) |>
select(table, snapshot_id, snapshot_time, changes)
# Check all ADAE subjects exist in ADSL
adae_tbl <- get_ducklake_table("adae")
adsl_tbl <- get_ducklake_table("adsl")
integrity_check <- adae_tbl |>
anti_join(adsl_tbl, by = "USUBJID") |>
summarise(orphaned_records = n()) |>
collect()
# ADAE records without ADSL subject:
integrity_check |> pull(orphaned_records)
## ----demonstrate-versioning---------------------------------------------------
# Add new derived columns using dplyr syntax
# replace_table() handles the DROP/CREATE cycle internally
with_transaction(
get_ducklake_table("adsl") |>
mutate(
AGE65FL = if_else(AGE >= 65, "Y", "N"),
AGECAT = case_when(
AGE < 65 ~ "<65",
AGE >= 65 & AGE < 75 ~ "65-74",
AGE >= 75 ~ ">=75",
TRUE ~ NA_character_
)
) |>
replace_table("adsl"),
author = "T Gerke",
commit_message = "Add age categorization vars"
)
# View version history - should now show 2 snapshots
list_table_snapshots("adsl")
# Verify new columns exist
get_ducklake_table("adsl") |>
select(USUBJID, AGE, AGE65FL, AGECAT) |>
head(5)
## ----iterative-workflow-------------------------------------------------------
# Iteration 1: First attempt (creates snapshot v2)
with_transaction(
get_ducklake_table("adsl") |>
mutate(AGECAT_TEST = case_when(
AGE < 50 ~ "Young",
AGE >= 50 ~ "Older"
)) |>
replace_table("adsl"),
author = "T Gerke",
commit_message = "Test age categories v1"
)
# Iteration 2: Refinement (creates snapshot v3)
with_transaction(
get_ducklake_table("adsl") |>
mutate(AGECAT_TEST = case_when(
AGE < 40 ~ "18-39",
AGE < 65 ~ "40-64",
AGE >= 65 ~ "65+"
)) |>
replace_table("adsl"),
author = "T Gerke",
commit_message = "Refine age categories v2"
)
# Iteration 3: Final version (creates snapshot v4)
with_transaction(
get_ducklake_table("adsl") |>
mutate(
AGECAT_TEST = NULL,
AGECAT2 = case_when(
AGE < 40 ~ "18-39",
AGE < 65 ~ "40-64",
AGE >= 65 ~ "65+",
TRUE ~ "Missing"
)
) |>
replace_table("adsl"),
author = "T Gerke",
commit_message = "Finalize age categories"
)
# Complete audit trail available
snapshots <- list_table_snapshots("adsl")
snapshots # Shows all iterations with snapshot metadata
# Each row represents a point in time you can restore to
# - snapshot_id: Unique identifier for this version
# - snapshot_time: When this version was created
# - changes: What operations created this snapshot
# Time-travel to specific snapshots using snapshot_id
# Use actual snapshot IDs from the list (first, second, and last)
snapshot_ids <- snapshots$snapshot_id
adsl_v1 <- get_ducklake_table_version("adsl", snapshot_ids[1])
adsl_v2 <- get_ducklake_table_version("adsl", snapshot_ids[2])
adsl_final <- get_ducklake_table_version("adsl", snapshot_ids[length(snapshot_ids)])
# Or use snapshot times for time-travel
# Note: Add 1 second to ensure we query AFTER the snapshot was created
adsl_asof <- get_ducklake_table_asof("adsl", snapshots$snapshot_time[2] + 1)
# Compare columns across iterations
colnames(adsl_v1 |> collect()) # Initial version
colnames(adsl_final |> collect()) # Final version with all derivations
## ----demonstrate-time-travel--------------------------------------------------
# Get the current version
adsl_current <- get_ducklake_table("adsl")
# Get the version history for adsl
versions <- list_table_snapshots("adsl")
print(versions)
# Get data from the first snapshot version
first_snapshot_id <- versions |>
slice(1) |>
pull(snapshot_id)
adsl_v1 <- get_ducklake_table_version(
table_name = "adsl",
version = first_snapshot_id
)
# Compare versions - earlier version shouldn't have derived variables added later
adsl_v1 |> collect()
adsl_current |> collect()
## ----demonstrate-transactions-------------------------------------------------
# Add ANALYSISFL to both ADSL and ADAE in a single atomic operation
# with_transaction() automatically handles rollback on error
with_transaction({
# First, add the flag to ADSL
get_ducklake_table("adsl") |>
mutate(ANALYSISFL = if_else(SAFFL == "Y" & !is.na(TRTSDT), "Y", "N")) |>
replace_table("adsl") # Creates versioned snapshot
# Then propagate to ADAE by joining
adsl_flags <- get_ducklake_table("adsl") |>
select(USUBJID, ANALYSISFL)
get_ducklake_table("adae") |>
select(-any_of("ANALYSISFL")) |> # Remove if exists
left_join(adsl_flags, by = "USUBJID") |>
replace_table("adae") # Creates versioned snapshot
# Both updates succeed together
cat("Both tables updated successfully\n")
}, author = "T Gerke", commit_message = "Add analysis flag")
## ----update-records-----------------------------------------------------------
# Update a specific record with versioning
with_transaction(
get_ducklake_table("adae") |>
mutate(
AESEV = if_else(
USUBJID == "01-701-1015" & AESEQ == 1,
"SEVERE",
AESEV
)
) |>
replace_table("adae"), # Creates versioned snapshot
author = "T Gerke",
commit_message = "Correct AE severity"
)
# Verify the update
get_ducklake_table("adae") |>
filter(USUBJID == "01-701-1015", AESEQ == 1) |>
select(USUBJID, AEDECOD, AESEV)
## ----analysis-examples--------------------------------------------------------
# Example 1: Subject disposition summary
get_ducklake_table("adsl") |>
count(EOSSTT, TRT01P) |>
arrange(TRT01P, EOSSTT)
# Example 2: Treatment-emergent AE summary by severity
get_ducklake_table("adae") |>
filter(TRTEMFL == "Y") |>
count(TRT01A, AESEV) |>
arrange(TRT01A, AESEV)
# Example 3: PK concentration profile
get_ducklake_table("adpc") |>
filter(PARAMCD == "XAN", EVID == 0) |>
group_by(NFRLT) |>
summarise(
n = n(),
mean_conc = mean(AVAL, na.rm = TRUE),
sd_conc = sd(AVAL, na.rm = TRUE)
) |>
arrange(NFRLT)
# Example 4: Cross-domain analysis: AEs by age group
get_ducklake_table("adae") |>
filter(TRTEMFL == "Y") |>
left_join(
get_ducklake_table("adsl") |>
select(USUBJID, AGEGR1, TRT01A),
by = "USUBJID"
) |>
count(AGEGR1, TRT01A.x) |>
arrange(AGEGR1, TRT01A.x)
## ----audit-trail--------------------------------------------------------------
# Generate audit report for ADSL
audit_report <- list_table_snapshots("adsl")
audit_report
# Get table metadata from DuckLake system tables
adsl_table_meta <- get_metadata_table("ducklake_table") |>
filter(table_name == "adsl") |>
collect()
adsl_table_meta
# Export audit information
audit_export <- audit_report |>
mutate(
table_name = "adsl",
dataset_label = "Subject-Level Analysis Dataset"
)
audit_export
## ----pre-archival-checkpoint, eval = FALSE------------------------------------
# # Materialise any inlined data to Parquet before archival
# checkpoint_ducklake()
## ----cleanup------------------------------------------------------------------
detach_ducklake()