30 Days of Pharmaverse
  • Week 1: SDTM Fundamentals
  • Week 2: Production SDTM
  • Week 3: ADaM Deep Dive
  • Week 4: Tables, Listings and Figures
  1. Day 21: ADTTE - Time-to-Event Analysis Dataset
  • Day 15: ADaM Architecture & Admiral Core Engine
  • Day 16: ADSL Part 1 - Treatment Variables & Dates
  • Day 17: ADSL Part 2 - Population Flags & Demographics
  • Day 18: ADAE - Adverse Events Analysis Dataset
  • Day 19: ADLB - Lab Analysis Dataset (BDS)
  • Day 20: ADVS - Vitals Analysis Dataset (BDS)
  • Day 21: ADTTE - Time-to-Event Analysis Dataset

On this page

  • 1 Welcome to ADTTE
  • 2 Setup
  • 3 Step 1: Start with ADSL
  • 4 Step 2: Prepare AE Data
  • 5 Step 3: Merge AE Dates
  • 6 Step 4: Create TTAE Parameter
  • 7 Step 5: Add Event Description
  • 8 Step 6: Create Serious AE Parameter
  • 9 Step 7: Combine Parameters
  • 10 Step 8: Derive Analysis Sequence
  • 11 Step 9: Derive Study Day
  • 12 Validation
  • 13 Summary
  • 14 Export
  • 15 Key Takeaways
  • 16 Next Steps
  • 17 Resources

Day 21: ADTTE - Time-to-Event Analysis Dataset

Week 3, Day 21: Survival analysis structure with events and censoring

Back to Roadmap

1 Welcome to ADTTE

ADTTE is the dataset for survival analysis - time-to-event endpoints like overall survival, progression-free survival, and time to first AE.

Today’s focus: Build ADTTE with event/censor indicators and time-to-event calculations.


2 Setup

library(admiral)
library(pharmaversesdtm)
library(pharmaverseadam)
library(dplyr)
library(lubridate)
library(stringr)
library(xportr)

# Load SDTM and ADaM
dm <- pharmaversesdtm::dm
ae <- pharmaversesdtm::ae
adsl <- pharmaverseadam::adsl

cat("Loaded DM:", nrow(dm), "subjects\n")
Loaded DM: 306 subjects
cat("Loaded AE:", nrow(ae), "AE records\n")
Loaded AE: 1191 AE records
cat("Loaded ADSL:", nrow(adsl), "subjects\n")
Loaded ADSL: 306 subjects

3 Step 1: Start with ADSL

# ADTTE starts with one row per subject from ADSL
adtte <- adsl %>%
  select(STUDYID, USUBJID, TRTSDT, TRT01A, AGE, SEX)

cat("\nADTTE initialized:", nrow(adtte), "subjects\n")

ADTTE initialized: 306 subjects
# Check
adtte %>%
  select(USUBJID, TRTSDT, TRT01A) %>%
  head(5)
# A tibble: 5 × 3
  USUBJID     TRTSDT     TRT01A              
  <chr>       <date>     <chr>               
1 01-701-1015 2014-01-02 Placebo             
2 01-701-1023 2012-08-05 Placebo             
3 01-701-1028 2013-07-19 Xanomeline High Dose
4 01-701-1033 2014-03-18 Xanomeline Low Dose 
5 01-701-1034 2014-07-01 Xanomeline High Dose

ADTTE Structure:

  • One row per subject per parameter
  • PARAMCD identifies the endpoint (OS, TTAE, etc.)
  • Each parameter has event/censor logic

4 Step 2: Prepare AE Data

# Get first AE date per subject
ae_first <- ae %>%
  admiral::derive_vars_dt(
    new_vars_prefix = "AE",
    dtc = AESTDTC
  ) %>%
  filter(!is.na(AEDT)) %>%
  group_by(STUDYID, USUBJID) %>%
  summarise(
    FIRST_AEDT = min(AEDT),
    .groups = "drop"
  )

cat("\nFirst AE dates prepared\n")

First AE dates prepared
# Check
ae_first %>%
  head(5)
# A tibble: 5 × 3
  STUDYID      USUBJID     FIRST_AEDT
  <chr>        <chr>       <date>    
1 CDISCPILOT01 01-701-1015 2014-01-03
2 CDISCPILOT01 01-701-1023 2012-08-07
3 CDISCPILOT01 01-701-1028 2013-07-21
4 CDISCPILOT01 01-701-1034 2014-08-27
5 CDISCPILOT01 01-701-1047 2013-02-12

5 Step 3: Merge AE Dates

# Merge first AE date to ADTTE
adtte <- adtte %>%
  left_join(
    ae_first,
    by = c("STUDYID", "USUBJID")
  )

cat("\nAE dates merged\n")

AE dates merged
# Check
adtte %>%
  select(USUBJID, TRTSDT, FIRST_AEDT) %>%
  head(10)
# A tibble: 10 × 3
   USUBJID     TRTSDT     FIRST_AEDT
   <chr>       <date>     <date>    
 1 01-701-1015 2014-01-02 2014-01-03
 2 01-701-1023 2012-08-05 2012-08-07
 3 01-701-1028 2013-07-19 2013-07-21
 4 01-701-1033 2014-03-18 NA        
 5 01-701-1034 2014-07-01 2014-08-27
 6 01-701-1047 2013-02-12 2013-02-12
 7 01-701-1057 NA         NA        
 8 01-701-1097 2014-01-01 2014-01-03
 9 01-701-1111 2012-09-07 2012-07-08
10 01-701-1115 2012-11-30 2012-12-02

6 Step 4: Create TTAE Parameter

# Time to First AE parameter
adtte_ttae <- adtte %>%
  mutate(
    PARAMCD = "TTAE",
    PARAM = "Time to First Adverse Event",
    # Event if AE occurred after treatment start
    CNSR = case_when(
      !is.na(FIRST_AEDT) & FIRST_AEDT >= TRTSDT ~ 0,  # Event
      TRUE ~ 1  # Censored
    ),
    # Analysis date
    ADT = case_when(
      CNSR == 0 ~ FIRST_AEDT,  # Event date
      TRUE ~ TRTSDT + 365  # Censor at 1 year (example)
    ),
    # Time to event in days
    AVAL = case_when(
      !is.na(ADT) & !is.na(TRTSDT) ~ as.numeric(ADT - TRTSDT) + 1,
      TRUE ~ NA_real_
    ),
    AVALU = "DAYS"
  )

cat("\nTTAE parameter created\n")

TTAE parameter created
# Summary
adtte_ttae %>%
  count(CNSR) %>%
  mutate(
    Label = case_when(
      CNSR == 0 ~ "Event",
      CNSR == 1 ~ "Censored"
    )
  )
# A tibble: 2 × 3
   CNSR     n Label   
  <dbl> <int> <chr>   
1     0   201 Event   
2     1   105 Censored

CNSR Convention:

  • CNSR = 0: Event occurred
  • CNSR = 1: Censored (no event)

7 Step 5: Add Event Description

# Add event description
adtte_ttae <- adtte_ttae %>%
  mutate(
    EVNTDESC = case_when(
      CNSR == 0 ~ "Adverse Event",
      TRUE ~ NA_character_
    ),
    CNSDTDSC = case_when(
      CNSR == 1 ~ "No Event by Study End",
      TRUE ~ NA_character_
    )
  )

cat("\nEvent descriptions added\n")

Event descriptions added
# Check
adtte_ttae %>%
  select(USUBJID, PARAMCD, AVAL, CNSR, EVNTDESC, CNSDTDSC) %>%
  head(10)
# A tibble: 10 × 6
   USUBJID     PARAMCD  AVAL  CNSR EVNTDESC      CNSDTDSC             
   <chr>       <chr>   <dbl> <dbl> <chr>         <chr>                
 1 01-701-1015 TTAE        2     0 Adverse Event <NA>                 
 2 01-701-1023 TTAE        3     0 Adverse Event <NA>                 
 3 01-701-1028 TTAE        3     0 Adverse Event <NA>                 
 4 01-701-1033 TTAE      366     1 <NA>          No Event by Study End
 5 01-701-1034 TTAE       58     0 Adverse Event <NA>                 
 6 01-701-1047 TTAE        1     0 Adverse Event <NA>                 
 7 01-701-1057 TTAE       NA     1 <NA>          No Event by Study End
 8 01-701-1097 TTAE        3     0 Adverse Event <NA>                 
 9 01-701-1111 TTAE      366     1 <NA>          No Event by Study End
10 01-701-1115 TTAE        3     0 Adverse Event <NA>                 

8 Step 6: Create Serious AE Parameter

# Get first serious AE
ae_serious <- ae %>%
  admiral::derive_vars_dt(
    new_vars_prefix = "AE",
    dtc = AESTDTC
  ) %>%
  filter(AESER == "Y", !is.na(AEDT)) %>%
  group_by(STUDYID, USUBJID) %>%
  summarise(
    FIRST_SERAEDT = min(AEDT),
    .groups = "drop"
  )

# Merge to base
adtte_base <- adtte %>%
  left_join(ae_serious, by = c("STUDYID", "USUBJID"))

# Create parameter
adtte_ttserae <- adtte_base %>%
  mutate(
    PARAMCD = "TTSERAE",
    PARAM = "Time to First Serious Adverse Event",
    CNSR = case_when(
      !is.na(FIRST_SERAEDT) & FIRST_SERAEDT >= TRTSDT ~ 0,
      TRUE ~ 1
    ),
    ADT = case_when(
      CNSR == 0 ~ FIRST_SERAEDT,
      TRUE ~ TRTSDT + 365
    ),
    AVAL = case_when(
      !is.na(ADT) & !is.na(TRTSDT) ~ as.numeric(ADT - TRTSDT) + 1,
      TRUE ~ NA_real_
    ),
    AVALU = "DAYS",
    EVNTDESC = case_when(
      CNSR == 0 ~ "Serious Adverse Event",
      TRUE ~ NA_character_
    ),
    CNSDTDSC = case_when(
      CNSR == 1 ~ "No Serious Event by Study End",
      TRUE ~ NA_character_
    )
  )

cat("\nTTSERAE parameter created\n")

TTSERAE parameter created
# Summary
adtte_ttserae %>%
  count(CNSR) %>%
  mutate(
    Label = case_when(
      CNSR == 0 ~ "Event",
      CNSR == 1 ~ "Censored"
    )
  )
# A tibble: 2 × 3
   CNSR     n Label   
  <dbl> <int> <chr>   
1     0     3 Event   
2     1   303 Censored

9 Step 7: Combine Parameters

# Combine TTAE and TTSERAE
adtte_final <- bind_rows(adtte_ttae, adtte_ttserae)

cat("\nParameters combined\n")

Parameters combined
cat("Total records:", nrow(adtte_final), "\n")
Total records: 612 
# Distribution
adtte_final %>%
  count(PARAMCD, PARAM)
# A tibble: 2 × 3
  PARAMCD PARAM                                   n
  <chr>   <chr>                               <int>
1 TTAE    Time to First Adverse Event           306
2 TTSERAE Time to First Serious Adverse Event   306

10 Step 8: Derive Analysis Sequence

# Create sequence number
adtte_final <- adtte_final %>%
  arrange(USUBJID, PARAMCD) %>%
  group_by(USUBJID) %>%
  mutate(ASEQ = row_number()) %>%
  ungroup()

cat("\nAnalysis sequence derived\n")

Analysis sequence derived

11 Step 9: Derive Study Day

# Calculate study day for analysis date
adtte_final <- adtte_final %>%
  mutate(
    ADY = case_when(
      !is.na(ADT) & !is.na(TRTSDT) ~ as.numeric(ADT - TRTSDT) + 1,
      TRUE ~ NA_real_
    )
  )

cat("\nStudy day derived\n")

Study day derived
# Check
adtte_final %>%
  select(USUBJID, PARAMCD, ADT, TRTSDT, ADY, AVAL) %>%
  head(10)
# A tibble: 10 × 6
   USUBJID     PARAMCD ADT        TRTSDT       ADY  AVAL
   <chr>       <chr>   <date>     <date>     <dbl> <dbl>
 1 01-701-1015 TTAE    2014-01-03 2014-01-02     2     2
 2 01-701-1015 TTSERAE 2015-01-02 2014-01-02   366   366
 3 01-701-1023 TTAE    2012-08-07 2012-08-05     3     3
 4 01-701-1023 TTSERAE 2013-08-05 2012-08-05   366   366
 5 01-701-1028 TTAE    2013-07-21 2013-07-19     3     3
 6 01-701-1028 TTSERAE 2014-07-19 2013-07-19   366   366
 7 01-701-1033 TTAE    2015-03-18 2014-03-18   366   366
 8 01-701-1033 TTSERAE 2015-03-18 2014-03-18   366   366
 9 01-701-1034 TTAE    2014-08-27 2014-07-01    58    58
10 01-701-1034 TTSERAE 2015-07-01 2014-07-01   366   366

12 Validation

cat("\n=== ADTTE Validation ===\n\n")

=== ADTTE Validation ===
# Check 1: CNSR values
check1 <- adtte_final %>%
  filter(!CNSR %in% c(0, 1))
cat("Check 1 - Invalid CNSR values:", nrow(check1), "\n")
Check 1 - Invalid CNSR values: 0 
# Check 2: AVAL consistency
check2 <- adtte_final %>%
  filter(!is.na(AVAL) & !is.na(ADY) & abs(AVAL - ADY) > 0.1)
cat("Check 2 - AVAL != ADY:", nrow(check2), "\n")
Check 2 - AVAL != ADY: 0 
# Check 3: Event has EVNTDESC
check3 <- adtte_final %>%
  filter(CNSR == 0, is.na(EVNTDESC))
cat("Check 3 - Event missing EVNTDESC:", nrow(check3), "\n")
Check 3 - Event missing EVNTDESC: 0 
# Check 4: Censored has CNSDTDSC
check4 <- adtte_final %>%
  filter(CNSR == 1, is.na(CNSDTDSC))
cat("Check 4 - Censored missing CNSDTDSC:", nrow(check4), "\n")
Check 4 - Censored missing CNSDTDSC: 0 
cat("\n✓ Validation complete\n")

✓ Validation complete

13 Summary

cat("\n=== ADTTE Summary ===\n\n")

=== ADTTE Summary ===
cat("Total records:", nrow(adtte_final), "\n")
Total records: 612 
cat("Parameters:", n_distinct(adtte_final$PARAMCD), "\n")
Parameters: 2 
cat("Subjects:", n_distinct(adtte_final$USUBJID), "\n\n")
Subjects: 306 
# By parameter
adtte_final %>%
  group_by(PARAMCD, PARAM) %>%
  summarise(
    N_Subjects = n(),
    N_Events = sum(CNSR == 0),
    N_Censored = sum(CNSR == 1),
    Median_AVAL = median(AVAL, na.rm = TRUE),
    .groups = "drop"
  )
# A tibble: 2 × 6
  PARAMCD PARAM                       N_Subjects N_Events N_Censored Median_AVAL
  <chr>   <chr>                            <int>    <int>      <int>       <dbl>
1 TTAE    Time to First Adverse Event        306      201        105          25
2 TTSERAE Time to First Serious Adve…        306        3        303         366
# AVAL distribution
adtte_final %>%
  group_by(PARAMCD) %>%
  summarise(
    N = sum(!is.na(AVAL)),
    Mean = round(mean(AVAL, na.rm = TRUE), 1),
    Median = round(median(AVAL, na.rm = TRUE), 1),
    Min = min(AVAL, na.rm = TRUE),
    Max = max(AVAL, na.rm = TRUE),
    .groups = "drop"
  )
# A tibble: 2 × 6
  PARAMCD     N  Mean Median   Min   Max
  <chr>   <int> <dbl>  <dbl> <dbl> <dbl>
1 TTAE      254  99.1     25     1   366
2 TTSERAE   254 362      366     5   366

14 Export

# Add labels
attr(adtte_final$PARAMCD, "label") <- "Parameter Code"
attr(adtte_final$PARAM, "label") <- "Parameter"
attr(adtte_final$AVAL, "label") <- "Analysis Value"
attr(adtte_final$AVALU, "label") <- "Analysis Value Unit"
attr(adtte_final$ADT, "label") <- "Analysis Date"
attr(adtte_final$ADY, "label") <- "Analysis Relative Day"
attr(adtte_final$CNSR, "label") <- "Censor"
attr(adtte_final$EVNTDESC, "label") <- "Event Description"
attr(adtte_final$CNSDTDSC, "label") <- "Censored Description"
attr(adtte_final$ASEQ, "label") <- "Analysis Sequence Number"

# Export
xportr_write(adtte_final, path = "adtte.xpt", domain = "ADTTE")

cat("\n✓ ADTTE exported to: adtte.xpt\n")

✓ ADTTE exported to: adtte.xpt

15 Key Takeaways

  1. ADTTE has one row per subject per parameter
  2. CNSR = 0 (event), CNSR = 1 (censored)
  3. AVAL = time to event in days (ADT - TRTSDT + 1)
  4. Events need EVNTDESC, censored need CNSDTDSC
  5. Multiple parameters: TTAE, TTSERAE, etc.
  6. Simple dplyr approach without complex admiral TTE functions

16 Next Steps

Day 22: Tables and Listings with gt
Day 23: Figures with ggplot2
Day 24: Complete TLF Package


17 Resources

  • ADTTE Structure: https://www.cdisc.org/standards/foundational/adam
  • Admiral TTE Vignette: https://pharmaverse.github.io/admiral/articles/bds_tte.html
  • Survival Analysis in R: https://www.emilyzabor.com/tutorials/survival_analysis_in_r_tutorial.html

End of Day 21

Week 3 Complete! You’ve built: - ADSL (Days 16-17) - ADAE (Day 18) - ADLB (Day 19) - ADVS (Day 20) - ADTTE (Day 21)

Ready for TLF production in Week 4!


 

30 Days of Pharmaverse  ·  Disclaimer  ·  Indraneel Chakraborty  ·  © 2026