Attaching package: 'dplyr'
The following objects are masked from 'package:stats':
filter, lag
The following objects are masked from 'package:base':
intersect, setdiff, setequal, union
library (readr)
library (tidyr)
library (lubridate)
Attaching package: 'lubridate'
The following objects are masked from 'package:base':
date, intersect, setdiff, union
library (ggplot2)
library (gtsummary)
Attaching package: 'flextable'
The following objects are masked from 'package:gtsummary':
as_flextable, continuous_summary
โ
Final Project: From SDTM to TLFs (Solution)
This walkthrough shows a full mock clinical reporting workflow using R.
๐งช Step 1: Load Raw Data
# Generate mock DM data
dm <- tibble:: tibble (
USUBJID = paste0 ("01-701-" , 1001 : 1005 ),
DOB = as.character (seq (as.Date ("1970-01-01" ), by = "10 years" , length.out = 5 )),
RFSTDTC = rep ("2023-01-01" , 5 ),
ARM = c ("Placebo" , "Drug A" , "Drug B" , "Placebo" , "Drug A" ),
SEX = c ("F" , "M" , "F" , "M" , "F" )
)
# Generate mock AE data
ae <- tibble:: tibble (
USUBJID = rep (dm$ USUBJID, each = 2 ),
AETERM = rep (c ("Headache" , "Nausea" ), times = 5 ),
AESEV = rep (c ("MILD" , "SEVERE" ), times = 5 ),
AESTDTC = rep ("2023-01-10" , 10 ),
AEENDTC = rep ("2023-01-12" , 10 ),
RFSTDTC = rep ("2023-01-01" , 10 )
)
# Generate mock EX data
ex <- tibble:: tibble (
USUBJID = dm$ USUBJID,
EXTRT = dm$ ARM,
EXSTDTC = rep ("2023-01-01" , 5 ),
EXENDTC = rep ("2023-01-14" , 5 ),
EXDOSE = c ("0" , "50" , "75" , "0" , "50" )
)
library (dplyr)
library (readr)
library (tidyr)
library (lubridate)
library (ggplot2)
library (gtsummary)
library (flextable)
library (tibble)
๐งฑ Step 2: SDTM Domains
DM
dm_sdtm <- dm %>%
mutate (
BRTHDTC = ymd (DOB),
AGE = floor ((ymd (RFSTDTC) - BRTHDTC) / 365.25 ),
ARMCD = substr (ARM, 1 , 3 ),
DOMAIN = "DM"
)
AE
ae_sdtm <- ae %>%
mutate (
AESTDTC = ymd (AESTDTC),
AEENDTC = ymd (AEENDTC),
AESTDY = as.integer (AESTDTC - ymd (RFSTDTC)),
AEENDY = as.integer (AEENDTC - ymd (RFSTDTC)),
DOMAIN = "AE" ,
AESEQ = row_number ()
)
EX
ex_sdtm <- ex %>%
mutate (
EXSTDTC = ymd (EXSTDTC),
EXENDTC = ymd (EXENDTC),
EXDOSE = as.numeric (EXDOSE),
DOMAIN = "EX"
)
๐ Step 3: ADaM Datasets
ADSL
adsl <- dm_sdtm %>%
select (USUBJID, AGE, SEX, ARM, ARMCD) %>%
mutate (TRT01P = ARM)
ADAE
adae <- ae_sdtm %>%
left_join (adsl, by = "USUBJID" ) %>%
mutate (
ASEVFL = ifelse (AESEV == "SEVERE" , "Y" , "N" ),
AVALC = AESEV
)
library (dplyr)
library (readr)
library (tidyr)
library (lubridate)
library (ggplot2)
library (gtsummary)
library (flextable)
library (tibble)
๐ Step 4: TLFs
1. AE Listing
ae_listing <- adae %>%
select (USUBJID, AETERM, AESEV, AESTDTC, AEENDTC) %>%
flextable () %>%
autofit ()
ae_listing
USUBJID
AETERM
AESEV
AESTDTC
AEENDTC
01-701-1001
Headache
MILD
2023-01-10
2023-01-12
01-701-1001
Nausea
SEVERE
2023-01-10
2023-01-12
01-701-1002
Headache
MILD
2023-01-10
2023-01-12
01-701-1002
Nausea
SEVERE
2023-01-10
2023-01-12
01-701-1003
Headache
MILD
2023-01-10
2023-01-12
01-701-1003
Nausea
SEVERE
2023-01-10
2023-01-12
01-701-1004
Headache
MILD
2023-01-10
2023-01-12
01-701-1004
Nausea
SEVERE
2023-01-10
2023-01-12
01-701-1005
Headache
MILD
2023-01-10
2023-01-12
01-701-1005
Nausea
SEVERE
2023-01-10
2023-01-12
2. Demographic Summary Table
adsl %>%
select (AGE, SEX, ARM) %>%
tbl_summary (by = ARM, missing = "no" )
AGE
12
1 (50%)
0 (0%)
0 (0%)
23
0 (0%)
0 (0%)
1 (50%)
32
0 (0%)
1 (100%)
0 (0%)
43
1 (50%)
0 (0%)
0 (0%)
52
0 (0%)
0 (0%)
1 (50%)
SEX
F
1 (50%)
1 (100%)
1 (50%)
M
1 (50%)
0 (0%)
1 (50%)
3. AE Severity Bar Plot
ae_fig <- adae %>%
count (ARM, AESEV) %>%
ggplot (aes (x = AESEV, y = n, fill = ARM)) +
geom_col (position = "dodge" ) +
labs (title = "Adverse Events by Severity" , y = "Count" ) +
theme_minimal ()
ae_fig
๐ค BONUS: Custom Export Function (Copilot Suggestion)
export_xpt <- function (df, name) {
path <- paste0 ("output/" , name, ".xpt" )
haven:: write_xpt (df, path)
}
You can use:
export_xpt (adsl, "adsl" )
export_xpt (adae, "adae" )