Module 7 โ€” Final Project: Solution

library(dplyr)

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)
#StandWithUkraine
library(flextable)

Attaching package: 'flextable'
The following objects are masked from 'package:gtsummary':

    as_flextable, continuous_summary
library(tibble)

โœ… 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")
Characteristic Drug A, N = 21 Drug B, N = 11 Placebo, N = 21
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%)
1 n (%)

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")