SDTM DM/SUPPDM Mini DEMO¶

Install and Load Package

In [ ]:
install.packages('pacman')
In [ ]:
pacman::p_load(haven, dplyr,tidyr,tibble, purrr,readr, glue, lubridate,stringr, EDCimport, logger, validate)

Upload the following raw datasets/exports needed to program SDTM.DM:

In [5]:
# path <- "C:/Users/Waraba/Desktop/R in CDISC _ Jupyter Lab/rawdata"

# db <- read_all_xpt(path, format_file = NULL)  # XPT
# db <- read_all_sas(path)                    # SAS7BDAT
# db <- read_all_csv(path)                    # CSV
# load_database(db)   # puts each table into your environment

demog        <-   read_xpt("DEMOG.xpt")
ipadmin      <-   read_xpt("IPADMIN.xpt")
eos          <-   read_xpt("EOS.xpt")
enrlment     <-   read_xpt("ENRLMENT.xpt")
rand         <-   read_xpt("RAND.xpt")
box          <-   read_xpt("BOX.xpt")
adverse      <-   read_xpt("ADVERSE.xpt")
conmeds      <-   read_xpt("CONMEDS.xpt")
ecg          <-   read_xpt("ECG.xpt")
eoip         <-   read_xpt("EOIP.xpt")
eq5d3l       <-   read_xpt("EQ5D3L.xpt")
hosp         <-   read_xpt("HOSP.xpt")
lab_chem     <-   read_xpt("LAB_CHEM.xpt")
lab_hema     <-   read_xpt("LAB_HEMA.xpt")
physmeas     <-   read_xpt("PHYSMEAS.xpt")
surg         <-   read_xpt("SURG.xpt")
vitals       <-   read_xpt("VITALS.xpt")

Map: Domain, studyid, subjid, siteid, usubjid, country, ethnic, race using raw.demog

In [6]:
demog
A tibble: 8 × 13
studyptsexethnicracerace2race3race4racespage_rawage_rawubrthdt_rawcountry
<chr><chr><chr><chr><chr><chr><chr><chr><chr><chr><chr><chr><chr>
STU0011001Male Hispanic or Latino White 35YearsUSA
STU0011002FemaleNot Hispanic or LatinoAsian American Indian or Alaska Native 40YearsUSA
STU0011003Male Hispanic or Latino Other BRAZILIAN40YearsUSA
STU0011004Male Hispanic or Latino White 38YearsUSA
STU0011005Male Not Hispanic or LatinoAmerican Indian or Alaska Native 64YearsUSA
STU0011006FemaleNot Hispanic or LatinoNative Hawaiian or Other Pacific Islander 75YearsUSA
STU0011007Male Not Hispanic or LatinoUnknown 32YearsUSA
STU0011008FemaleNot Hispanic or LatinoNot Reported 83YearsUSA
In [7]:
demo1 <- demog %>%
        rename(race1=race) %>%
        mutate(
        nmiss_count = rowSums(across(c(race1, race2, race3, race4), ~ !is.na(.) & . != "")),   # To count how races were selected
        race = ifelse(nmiss_count > 1, "MULTIPLE", toupper(coalesce(race1, race2, race3, race4))),
        racesp = racesp,
        race1 = ifelse(nmiss_count > 1,toupper(race1),""),
        race2 = ifelse(nmiss_count > 1,toupper(race2),""),
        race3 = ifelse(nmiss_count > 1,toupper(race3),""),
        race4 = ifelse(nmiss_count > 1,toupper(race4),""),   
    
        age = ifelse(!is.na(age_raw), as.integer(age_raw), NA_integer_),
        ageu = toupper(age_rawu),
            
        sex = ifelse(sex == "Female", "F", ifelse(sex == "Male", "M", sex)),
        siteid = substr(pt, 1, 2),
        usubjid = paste(study, pt, sep = "-"),
       
        domain = "DM",
        studyid = study,
        subjid = pt,
        country = country,
        ethnic = toupper(ethnic)
        )
demo1
A tibble: 8 × 22
studyptsexethnicrace1race2race3race4racespage_raw⋯countrynmiss_countraceageageusiteidusubjiddomainstudyidsubjid
<chr><chr><chr><chr><chr><chr><chr><chr><chr><chr>⋯<chr><dbl><chr><int><chr><chr><chr><chr><chr><chr>
STU0011001MHISPANIC OR LATINO 35⋯USA1WHITE 35YEARS10STU001-1001DMSTU0011001
STU0011002FNOT HISPANIC OR LATINOASIANAMERICAN INDIAN OR ALASKA NATIVE 40⋯USA2MULTIPLE 40YEARS10STU001-1002DMSTU0011002
STU0011003MHISPANIC OR LATINO BRAZILIAN40⋯USA1OTHER 40YEARS10STU001-1003DMSTU0011003
STU0011004MHISPANIC OR LATINO 38⋯USA1WHITE 38YEARS10STU001-1004DMSTU0011004
STU0011005MNOT HISPANIC OR LATINO 64⋯USA1AMERICAN INDIAN OR ALASKA NATIVE 64YEARS10STU001-1005DMSTU0011005
STU0011006FNOT HISPANIC OR LATINO 75⋯USA1NATIVE HAWAIIAN OR OTHER PACIFIC ISLANDER75YEARS10STU001-1006DMSTU0011006
STU0011007MNOT HISPANIC OR LATINO 32⋯USA1UNKNOWN 32YEARS10STU001-1007DMSTU0011007
STU0011008FNOT HISPANIC OR LATINO 83⋯USA1NOT REPORTED 83YEARS10STU001-1008DMSTU0011008

Derive disposition related variables rficdtc, rfendtc, dthdtc

In [8]:
rficdtc <- enrlment %>%
 mutate(
 rficdtc = ifelse(!is.na(icdt_raw), format(as.Date(icdt_raw, format = "%d/%b/%Y"),"%Y-%m-%d"), NA),
 enrldtc = ifelse(!is.na(enrldt_raw), format(as.Date(enrldt_raw, format = "%d/%b/%Y"),"%Y-%m-%d"), NA),
 randdtc = ifelse(!is.na(randdt_raw), format(as.Date(randdt_raw, format = "%d/%b/%Y"),"%Y-%m-%d"), NA)
 ) %>%
 select(study, pt, rficdtc, enrldtc, randdtc)
 
rfendtc <- eos %>%
 filter(eoscat == "End of Study") %>%
 mutate(rfendtc = ifelse(!is.na(eostdt_raw), format(as.Date(eostdt_raw, format = "%d/%b/%Y"),"%Y-%m-%d"), NA)) %>%
 select(study, pt, rfendtc)
 
dthdtc <- eos %>%
 filter(eoscat == "End of Study" & eoterm == "Death") %>%
 mutate(dthdtc = ifelse(!is.na(eostdt_raw), format(as.Date(eostdt_raw, format = "%d/%b/%Y"),"%Y-%m-%d"), NA), dthfl = "Y") %>%
 select(study, pt, dthdtc, dthfl)

Map Exposure Related Variables: rfxstdtc, rfxendtc

In [9]:
ipadmin
A tibble: 16 × 10
studyptfolderipconcipstdt_rawipsttm_rawipqty_rawipqtyuipadjipboxid
<chr><chr><chr><chr><chr><chr><chr><chr><chr><chr>
STU0011004WEEK 150005/JAN/20108:352 mL 13434371
STU0011004WEEK 250012/JAN/20108:352 mL 52970539
STU0011004WEEK 350018/JAN/20109:301 mLAdverse Event52120567
STU0011004WEEK 450025/JAN/20108:452 mL 59305202
STU0011005WEEK 150005/FEB/20108:462 mL 13787377
STU0011005WEEK 250012/FEB/20108:302 mL 65580239
STU0011005WEEK 350019/FEB/20108:150 mLAdverse Event45377264
STU0011006WEEK 15002/MAR/2010 8:301.9mL 39024101
STU0011006WEEK 250010/MAR/20108:302 mL 65845489
STU0011007WEEK 150015/APR/20108:232 mL 66223983
STU0011007WEEK 250022/APR/20109:002 mL 71763169
STU0011007WEEK 350029/APR/20109:032 mL 60038358
STU0011007WEEK 45006/MAY/2010 8:122 mL 68706162
STU0011008WEEK 150027/JUN/20108:452 mL 68891589
STU0011008WEEK 25004/JUL/2010 8:172 mL 2311359
STU0011008WEEK 350011/JUL/20109:202 mL 3199027
In [10]:
expodate1 <- ipadmin %>%
    filter(as.integer(ipqty_raw) > 0) %>%

    mutate(
    ipstdtc = as.Date(ipstdt_raw, format = "%d/%b/%Y"),
    ipsttm = format(as.POSIXct(ipsttm_raw, format = "%H:%M", tz = ""),"%H:%M"),
    infudtc = paste(ipstdtc, ipsttm, sep = "T")
 ) %>%

select(study, pt, infudtc, ipboxid)

expodate1
A tibble: 15 × 4
studyptinfudtcipboxid
<chr><chr><chr><chr>
STU00110042010-01-05T08:3513434371
STU00110042010-01-12T08:3552970539
STU00110042010-01-18T09:3052120567
STU00110042010-01-25T08:4559305202
STU00110052010-02-05T08:4613787377
STU00110052010-02-12T08:3065580239
STU00110062010-03-02T08:3039024101
STU00110062010-03-10T08:3065845489
STU00110072010-04-15T08:2366223983
STU00110072010-04-22T09:0071763169
STU00110072010-04-29T09:0360038358
STU00110072010-05-06T08:1268706162
STU00110082010-06-27T08:4568891589
STU00110082010-07-04T08:172311359
STU00110082010-07-11T09:203199027
In [11]:
#Earliest treatment date
rfxstdtc <- expodate1 %>%
    arrange(study,pt,infudtc) %>%
    group_by(study, pt) %>%
    slice(1) %>%
    mutate(rfxstdtc = infudtc)
rfxstdtc
A grouped_df: 5 × 5
studyptinfudtcipboxidrfxstdtc
<chr><chr><chr><chr><chr>
STU00110042010-01-05T08:35134343712010-01-05T08:35
STU00110052010-02-05T08:46137873772010-02-05T08:46
STU00110062010-03-02T08:30390241012010-03-02T08:30
STU00110072010-04-15T08:23662239832010-04-15T08:23
STU00110082010-06-27T08:45688915892010-06-27T08:45
In [12]:
#Late treatment date
rfxendtc <- expodate1 %>%
    arrange(study,pt,infudtc) %>%
    group_by(study, pt) %>%
    slice(n()) %>%
    mutate(rfxendtc = infudtc)
rfxendtc
A grouped_df: 5 × 5
studyptinfudtcipboxidrfxendtc
<chr><chr><chr><chr><chr>
STU00110042010-01-25T08:45593052022010-01-25T08:45
STU00110052010-02-12T08:30655802392010-02-12T08:30
STU00110062010-03-10T08:30658454892010-03-10T08:30
STU00110072010-05-06T08:12687061622010-05-06T08:12
STU00110082010-07-11T09:203199027 2010-07-11T09:20

Derive Planned and Actual Arm related variables

In [13]:
enrlment
A tibble: 8 × 9
studyptfoldericdt_rawicversprtversenrldt_rawranddt_rawrandno
<chr><chr><chr><chr><chr><chr><chr><chr><chr>
STU0011001SCR1/JAN/2010 11
STU0011002SCR1/JAN/2010 114/JAN/2010
STU0011003SCR1/JAN/2010 113/JAN/2010 3/JAN/2010 514876
STU0011004SCR1/JAN/2010 114/JAN/2010 5/JAN/2010 101415
STU0011005SCR15/JAN/2010111/FEB/2010 5/FEB/2010 306185
STU0011006SCR18/FEB/2010111/MAR/2010 1/MAR/2010 987435
STU0011007SCR4/APR/2010 2214/APR/201014/APR/2010098745
STU0011008SCR20/JUN/20102326/JUN/201027/JUN/2010123098
In [14]:
randno <- enrlment %>%
    filter(!is.na(randno) & randno!="") %>%
    select(study, pt, randno)

randno
A tibble: 6 × 3
studyptrandno
<chr><chr><chr>
STU0011003514876
STU0011004101415
STU0011005306185
STU0011006987435
STU0011007098745
STU0011008123098
In [15]:
rand
A tibble: 6 × 4
rand_idtx_cdcohortstrata
<chr><chr><chr><chr>
514876PBO 1Dummy strata1
101415ACTIVE1Dummy strata1
306185ACTIVE1Dummy strata1
987435PBO 2Dummy strata2
098745PBO 2Dummy strata2
123098ACTIVE2Dummy strata2
In [16]:
randtrt <- rand %>%
    mutate(
    armcd = tx_cd,
    arm = ifelse(armcd == "ACTIVE", "Active", ifelse(armcd == "PBO", "Placebo", NA_character_))
) %>%
select(armcd, arm, randno=rand_id)
randtrt
A tibble: 6 × 3
armcdarmrandno
<chr><chr><chr>
PBO Placebo514876
ACTIVEActive 101415
ACTIVEActive 306185
PBO Placebo987435
PBO Placebo098745
ACTIVEActive 123098

Merge

In [17]:
armdata <- randno %>%
left_join(randtrt, by = "randno")

armdata
A tibble: 6 × 5
studyptrandnoarmcdarm
<chr><chr><chr><chr><chr>
STU0011003514876PBO Placebo
STU0011004101415ACTIVEActive
STU0011005306185ACTIVEActive
STU0011006987435PBO Placebo
STU0011007098745PBO Placebo
STU0011008123098ACTIVEActive

Derive actual ARM related variable

In [18]:
actarmcode <- rfxstdtc
actarmcode
A grouped_df: 5 × 5
studyptinfudtcipboxidrfxstdtc
<chr><chr><chr><chr><chr>
STU00110042010-01-05T08:35134343712010-01-05T08:35
STU00110052010-02-05T08:46137873772010-02-05T08:46
STU00110062010-03-02T08:30390241012010-03-02T08:30
STU00110072010-04-15T08:23662239832010-04-15T08:23
STU00110082010-06-27T08:45688915892010-06-27T08:45

Box data for mapping actual arm

In [19]:
box
A tibble: 16 × 2
kitidcontent
<chr><chr>
13434371ACTIVE
52970539ACTIVE
52120567ACTIVE
59305202ACTIVE
13787377PBO
65580239ACTIVE
45377264ACTIVE
39024101PBO
65845489PBO
66223983PBO
71763169PBO
60038358PBO
68706162PBO
68891589ACTIVE
2311359 ACTIVE
3199027 ACTIVE
In [20]:
boxdata <- box %>%
    mutate(
    ipboxid = kitid,
    actarmcd = case_when(
        content == "ACTIVE" ~ "ACTIVE",
        content == "PBO" ~ "PBO",
        TRUE ~ NA_character_
     ),

    actarm = case_when(
        content == "ACTIVE" ~ "Active",
        content == "PBO" ~ "Placebo",
        TRUE ~ NA_character_)
)
boxdata
A tibble: 16 × 5
kitidcontentipboxidactarmcdactarm
<chr><chr><chr><chr><chr>
13434371ACTIVE13434371ACTIVEActive
52970539ACTIVE52970539ACTIVEActive
52120567ACTIVE52120567ACTIVEActive
59305202ACTIVE59305202ACTIVEActive
13787377PBO 13787377PBO Placebo
65580239ACTIVE65580239ACTIVEActive
45377264ACTIVE45377264ACTIVEActive
39024101PBO 39024101PBO Placebo
65845489PBO 65845489PBO Placebo
66223983PBO 66223983PBO Placebo
71763169PBO 71763169PBO Placebo
60038358PBO 60038358PBO Placebo
68706162PBO 68706162PBO Placebo
68891589ACTIVE68891589ACTIVEActive
2311359 ACTIVE2311359 ACTIVEActive
3199027 ACTIVE3199027 ACTIVEActive

Merge 'actarmcode' and 'boxdata' data frames by 'ipboxid'

In [21]:
actarmdata <- left_join(
                    actarmcode, boxdata, by = "ipboxid") %>%
              filter(!is.na(actarmcd)) %>%
              select(study, pt, actarmcd, actarm)

actarmdata
A grouped_df: 5 × 4
studyptactarmcdactarm
<chr><chr><chr><chr>
STU0011004ACTIVEActive
STU0011005PBO Placebo
STU0011006PBO Placebo
STU0011007PBO Placebo
STU0011008ACTIVEActive

Reference End of participation
Combine the raw date variables into 'combdate' data frame

In [22]:
hospc <-hosp %>% mutate(
    study = as.character(study))
In [23]:
combdate <- bind_rows(
 adverse %>% select(study, pt, date = aestdt_raw),
 adverse %>% select(study, pt, date = aeendt_raw),
 adverse %>% select(study, pt, date = hadmtdt_raw),
 adverse %>% select(study, pt, date = hdsdt_raw),
 conmeds %>% select(study, pt, date = cmstdt_raw),
 conmeds %>% select(study, pt, date = cmendt_raw),
 ecg %>% select(study, pt, date = egdt_raw),
 enrlment %>% select(study, pt, date = icdt_raw),
 enrlment %>% select(study, pt, date = enrldt_raw),
 enrlment %>% select(study, pt, date = randdt_raw),

 eos %>% select(study, pt, date = eostdt_raw),
 eoip %>% select(study, pt, date = eostdt_raw),
 eq5d3l %>% select(study, pt, date = dt_raw),


 ipadmin %>% select(study, pt, date = ipstdt_raw),
 lab_chem %>% select(study, pt, date = lbdt_raw),
 lab_hema %>% select(study, pt, date = lbdt_raw),
 physmeas %>% select(study, pt, date = pmdt_raw),
 surg %>% select(study, pt, date = surgdt_raw),
 vitals %>% select(study, pt, date = vsdt_raw)
)
combdate
A tibble: 953 × 3
studyptdate
<chr><chr><chr>
STU001100101/JAN/2010
STU001100305/JAN/2010
STU001100401/JAN/2010
STU001100403/JAN/2010
STU001100408/JAN/2010
STU001100410/JAN/2010
STU001100518/FEB/2010
STU0011006UN/MAR/2010
STU00110079/MAY/2010
STU001100101/JAN/2010
STU001100305/JAN/2010
STU001100401/JAN/2010
STU001100407/JAN/2010
STU001100409/JAN/2010
STU0011004
STU001100521/FEB/2010
STU001100625/MAR/2010
STU001100712/MAY/2010
STU0011001
STU00110035/JAN/2010
STU0011004
STU0011004
STU0011004
STU0011004
STU001100520/FEB/2020
STU0011006
STU0011007
STU0011001
STU00110035/JAN/2010
STU0011004
⋮⋮⋮
STU001100811/Jul/2010
STU001100811/Jul/2010
STU001100811/Jul/2010
STU001100811/Jul/2010
STU001100811/Jul/2010
STU001100811/Jul/2010
STU001100811/Jul/2010
STU001100811/Jul/2010
STU001100811/Jul/2010
STU001100811/Jul/2010
STU001100811/Jul/2010
STU001100811/Jul/2010
STU001100811/Jul/2010
STU001100811/Jul/2010
STU001100811/Jul/2010
STU001100811/Jul/2010
STU00110044/JAN/2010
STU00110055/FEB/2010
STU00110061/MAR/2010
STU001100713/APR/2010
STU001100520/FEB/2010
STU001100402/JAN/2010
STU001100412/JAN/2010
STU00110055/FEB/2010
STU001100512/FEB/2010
STU001100522/FEB/2010
STU001100601/MAR/2010
STU001100611/MAR/2010
STU001100715/APR/2010
STU001100729/APR/2010

Process the date variables to create date in ISO format in 'alldates02' data frame

In [24]:
combdate01 <- combdate %>%
         mutate(
             dayn = suppressWarnings(as.integer(stringr::word(date, 1, sep = "/"))),
             day = if_else(!is.na(dayn), sprintf("%02d", dayn), "-"),
             monthc = toupper(word(date, 2, sep='/')),
             month = case_when(
             monthc == "JAN" ~ "01",
             monthc == "FEB" ~ "02",
             monthc == "MAR" ~ "03",
             monthc == "APR" ~ "04",
             monthc == "MAY" ~ "05",
             monthc == "JUN" ~ "06",
             monthc == "JUL" ~ "07",
             monthc == "AUG" ~ "08",
             monthc == "SEP" ~ "09",
             monthc == "OCT" ~ "10",
             monthc == "NOV" ~ "11",
             monthc == "DEC" ~ "12",
             TRUE ~ "-"
         ),
             
         year = word(date,3,sep='/'),
         year = if_else(toupper(year) == "UNK", "-", year),
             
         datec = str_c(year, month, day, sep = "-"),
         datec = ifelse(str_sub(datec, -5) == "-----", str_sub(datec, end = -6), datec),
         datec = ifelse(str_sub(datec, -4) == "----",  str_sub(datec, end = -5), datec),
         datec = ifelse(str_sub(datec, -2) == "--",    str_sub(datec, end = -3), datec)
)
 
combdate01
A tibble: 953 × 9
studyptdatedayndaymonthcmonthyeardatec
<chr><chr><chr><int><chr><chr><chr><chr><chr>
STU001100101/JAN/2010 101JAN0120102010-01-01
STU001100305/JAN/2010 505JAN0120102010-01-05
STU001100401/JAN/2010 101JAN0120102010-01-01
STU001100403/JAN/2010 303JAN0120102010-01-03
STU001100408/JAN/2010 808JAN0120102010-01-08
STU001100410/JAN/20101010JAN0120102010-01-10
STU001100518/FEB/20101818FEB0220102010-02-18
STU0011006UN/MAR/2010NA- MAR0320102010-03
STU00110079/MAY/2010 909MAY0520102010-05-09
STU001100101/JAN/2010 101JAN0120102010-01-01
STU001100305/JAN/2010 505JAN0120102010-01-05
STU001100401/JAN/2010 101JAN0120102010-01-01
STU001100407/JAN/2010 707JAN0120102010-01-07
STU001100409/JAN/2010 909JAN0120102010-01-09
STU0011004 NA- NA - NA NA
STU001100521/FEB/20102121FEB0220102010-02-21
STU001100625/MAR/20102525MAR0320102010-03-25
STU001100712/MAY/20101212MAY0520102010-05-12
STU0011001 NA- NA - NA NA
STU00110035/JAN/2010 505JAN0120102010-01-05
STU0011004 NA- NA - NA NA
STU0011004 NA- NA - NA NA
STU0011004 NA- NA - NA NA
STU0011004 NA- NA - NA NA
STU001100520/FEB/20202020FEB0220202020-02-20
STU0011006 NA- NA - NA NA
STU0011007 NA- NA - NA NA
STU0011001 NA- NA - NA NA
STU00110035/JAN/2010 505JAN0120102010-01-05
STU0011004 NA- NA - NA NA
⋮⋮⋮⋮⋮⋮⋮⋮⋮
STU001100811/Jul/20101111JUL0720102010-07-11
STU001100811/Jul/20101111JUL0720102010-07-11
STU001100811/Jul/20101111JUL0720102010-07-11
STU001100811/Jul/20101111JUL0720102010-07-11
STU001100811/Jul/20101111JUL0720102010-07-11
STU001100811/Jul/20101111JUL0720102010-07-11
STU001100811/Jul/20101111JUL0720102010-07-11
STU001100811/Jul/20101111JUL0720102010-07-11
STU001100811/Jul/20101111JUL0720102010-07-11
STU001100811/Jul/20101111JUL0720102010-07-11
STU001100811/Jul/20101111JUL0720102010-07-11
STU001100811/Jul/20101111JUL0720102010-07-11
STU001100811/Jul/20101111JUL0720102010-07-11
STU001100811/Jul/20101111JUL0720102010-07-11
STU001100811/Jul/20101111JUL0720102010-07-11
STU001100811/Jul/20101111JUL0720102010-07-11
STU00110044/JAN/2010 404JAN0120102010-01-04
STU00110055/FEB/2010 505FEB0220102010-02-05
STU00110061/MAR/2010 101MAR0320102010-03-01
STU001100713/APR/20101313APR0420102010-04-13
STU001100520/FEB/20102020FEB0220102010-02-20
STU001100402/JAN/2010 202JAN0120102010-01-02
STU001100412/JAN/20101212JAN0120102010-01-12
STU00110055/FEB/2010 505FEB0220102010-02-05
STU001100512/FEB/20101212FEB0220102010-02-12
STU001100522/FEB/20102222FEB0220102010-02-22
STU001100601/MAR/2010 101MAR0320102010-03-01
STU001100611/MAR/20101111MAR0320102010-03-11
STU001100715/APR/20101515APR0420102010-04-15
STU001100729/APR/20102929APR0420102010-04-29

Pick the latest non-missing date for each subject

In [25]:
rfpendtc <- combdate01 %>%
         filter(!is.na(datec) & datec != "") %>%
         arrange(study, pt, datec) %>%
         group_by(study, pt) %>%
         slice(n()) %>%
         ungroup() %>%

         select(study, pt, rfpendtc = datec)
rfpendtc
A tibble: 8 × 3
studyptrfpendtc
<chr><chr><chr>
STU00110012010-01-01
STU00110022010-01-05
STU00110032010-01-05
STU00110042010-02-28
STU00110052020-02-20
STU00110062010-03-25
STU00110072010-06-12
STU00110082010-08-18
In [ ]:

Merge all datasets together

In [26]:
demo2 <- demo1 %>%
             left_join(rficdtc, by = c("study", "pt")) %>%
             left_join(dthdtc, by = c("study", "pt")) %>%
             left_join(rfendtc, by = c("study", "pt")) %>%
             left_join(rfxstdtc, by = c("study", "pt")) %>%
             left_join(rfxendtc, by = c("study", "pt")) %>%
             left_join(actarmdata, by = c("study", "pt")) %>%
             left_join(armdata, by = c("study", "pt")) %>%
             left_join(rfpendtc, by = c("study", "pt"))

demo2
A tibble: 8 × 40
studyptsexethnicrace1race2race3race4racespage_raw⋯rfxstdtcinfudtc.yipboxid.yrfxendtcactarmcdactarmrandnoarmcdarmrfpendtc
<chr><chr><chr><chr><chr><chr><chr><chr><chr><chr>⋯<chr><chr><chr><chr><chr><chr><chr><chr><chr><chr>
STU0011001MHISPANIC OR LATINO 35⋯NA NA NA NA NA NA NA NA NA 2010-01-01
STU0011002FNOT HISPANIC OR LATINOASIANAMERICAN INDIAN OR ALASKA NATIVE 40⋯NA NA NA NA NA NA NA NA NA 2010-01-05
STU0011003MHISPANIC OR LATINO BRAZILIAN40⋯NA NA NA NA NA NA 514876PBO Placebo2010-01-05
STU0011004MHISPANIC OR LATINO 38⋯2010-01-05T08:352010-01-25T08:45593052022010-01-25T08:45ACTIVEActive 101415ACTIVEActive 2010-02-28
STU0011005MNOT HISPANIC OR LATINO 64⋯2010-02-05T08:462010-02-12T08:30655802392010-02-12T08:30PBO Placebo306185ACTIVEActive 2020-02-20
STU0011006FNOT HISPANIC OR LATINO 75⋯2010-03-02T08:302010-03-10T08:30658454892010-03-10T08:30PBO Placebo987435PBO Placebo2010-03-25
STU0011007MNOT HISPANIC OR LATINO 32⋯2010-04-15T08:232010-05-06T08:12687061622010-05-06T08:12PBO Placebo098745PBO Placebo2010-06-12
STU0011008FNOT HISPANIC OR LATINO 83⋯2010-06-27T08:452010-07-11T09:203199027 2010-07-11T09:20ACTIVEActive 123098ACTIVEActive 2010-08-18

Derive additional variables that depend on previously derived variables.

In [27]:
demo3 <- demo2 %>%
         mutate(
             rfstdtc = substr(rfxstdtc, 1, 10),
             rfstdtc = ifelse(is.na(rfstdtc) & !is.na(randdtc), randdtc, rfstdtc),
             rfstdtc = ifelse(is.na(rfstdtc) & !is.na(rficdtc), rficdtc, rfstdtc),
 
             armcd = case_when(
                         is.na(enrldtc) ~ "SCRNFAIL",
                         is.na(randdtc) ~ "NOTASSGN",
                         TRUE ~ armcd),
             arm =   case_when(
                         armcd =="SCRNFAIL" ~ "Screen Failure",
                         armcd == "NOTASSGN" ~ "Not Assigned",
                         TRUE ~ arm),
             actarmcd = case_when(
                         is.na(enrldtc) ~ "SCRNFAIL",
                         is.na(randdtc) ~ "NOTASSGN",
                         is.na(rfxstdtc) ~ "NOTTRT",
                         TRUE ~ actarmcd),
 
             actarm = case_when(
                     actarmcd =="SCRNFAIL" ~ "Screen Failure",
                     actarmcd == "NOTASSGN" ~ "Not Assigned",
                     actarmcd == "NOTTRT" ~ "Not Treated",
                     TRUE ~ actarm)) %>%
 rename_all(toupper)

demo3
A tibble: 8 × 41
STUDYPTSEXETHNICRACE1RACE2RACE3RACE4RACESPAGE_RAW⋯INFUDTC.YIPBOXID.YRFXENDTCACTARMCDACTARMRANDNOARMCDARMRFPENDTCRFSTDTC
<chr><chr><chr><chr><chr><chr><chr><chr><chr><chr>⋯<chr><chr><chr><chr><chr><chr><chr><chr><chr><chr>
STU0011001MHISPANIC OR LATINO 35⋯NA NA NA SCRNFAILScreen FailureNA SCRNFAILScreen Failure2010-01-012010-01-01
STU0011002FNOT HISPANIC OR LATINOASIANAMERICAN INDIAN OR ALASKA NATIVE 40⋯NA NA NA NOTASSGNNot Assigned NA NOTASSGNNot Assigned 2010-01-052010-01-01
STU0011003MHISPANIC OR LATINO BRAZILIAN40⋯NA NA NA NOTTRT Not Treated 514876PBO Placebo 2010-01-052010-01-03
STU0011004MHISPANIC OR LATINO 38⋯2010-01-25T08:45593052022010-01-25T08:45ACTIVE Active 101415ACTIVE Active 2010-02-282010-01-05
STU0011005MNOT HISPANIC OR LATINO 64⋯2010-02-12T08:30655802392010-02-12T08:30PBO Placebo 306185ACTIVE Active 2020-02-202010-02-05
STU0011006FNOT HISPANIC OR LATINO 75⋯2010-03-10T08:30658454892010-03-10T08:30PBO Placebo 987435PBO Placebo 2010-03-252010-03-02
STU0011007MNOT HISPANIC OR LATINO 32⋯2010-05-06T08:12687061622010-05-06T08:12PBO Placebo 098745PBO Placebo 2010-06-122010-04-15
STU0011008FNOT HISPANIC OR LATINO 83⋯2010-07-11T09:203199027 2010-07-11T09:20ACTIVE Active 123098ACTIVE Active 2010-08-182010-06-27

Write attributes and keep only required variables and in the required order

In [28]:
varlist <- c(
 'STUDYID', 'DOMAIN', 'USUBJID', 'SUBJID', 'RFSTDTC', 'RFENDTC', 'RFXSTDTC', 'RFXENDTC',
 'RFICDTC', 'RFPENDTC', 'DTHDTC', 'DTHFL', 'SITEID', 'AGE', 'AGEU', 'SEX', 'RACE', 'ETHNIC',
 'ARMCD', 'ARM', 'ACTARMCD', 'ACTARM', 'COUNTRY', 'RACE1', 'RACE2', 'RACE3', 'RACE4', 'RACESP'
)
In [29]:
dm <- demo3 %>%
 select(all_of(varlist))
 
dm
A tibble: 8 × 28
STUDYIDDOMAINUSUBJIDSUBJIDRFSTDTCRFENDTCRFXSTDTCRFXENDTCRFICDTCRFPENDTC⋯ARMCDARMACTARMCDACTARMCOUNTRYRACE1RACE2RACE3RACE4RACESP
<chr><chr><chr><chr><chr><chr><chr><chr><chr><chr>⋯<chr><chr><chr><chr><chr><chr><chr><chr><chr><chr>
STU001DMSTU001-100110012010-01-01NA NA NA 2010-01-012010-01-01⋯SCRNFAILScreen FailureSCRNFAILScreen FailureUSA
STU001DMSTU001-100210022010-01-012010-01-05NA NA 2010-01-012010-01-05⋯NOTASSGNNot Assigned NOTASSGNNot Assigned USAASIANAMERICAN INDIAN OR ALASKA NATIVE
STU001DMSTU001-100310032010-01-032010-01-05NA NA 2010-01-012010-01-05⋯PBO Placebo NOTTRT Not Treated USA BRAZILIAN
STU001DMSTU001-100410042010-01-052010-02-282010-01-05T08:352010-01-25T08:452010-01-012010-02-28⋯ACTIVE Active ACTIVE Active USA
STU001DMSTU001-100510052010-02-05NA 2010-02-05T08:462010-02-12T08:302010-01-152020-02-20⋯ACTIVE Active PBO Placebo USA
STU001DMSTU001-100610062010-03-022010-03-252010-03-02T08:302010-03-10T08:302010-02-182010-03-25⋯PBO Placebo PBO Placebo USA
STU001DMSTU001-100710072010-04-152010-06-122010-04-15T08:232010-05-06T08:122010-04-042010-06-12⋯PBO Placebo PBO Placebo USA
STU001DMSTU001-100810082010-06-272010-08-182010-06-27T08:452010-07-11T09:202010-06-202010-08-18⋯ACTIVE Active ACTIVE Active USA
In [30]:
write_xpt(dm, "dm.xpt")
In [31]:
colnames(dm)
  1. 'STUDYID'
  2. 'DOMAIN'
  3. 'USUBJID'
  4. 'SUBJID'
  5. 'RFSTDTC'
  6. 'RFENDTC'
  7. 'RFXSTDTC'
  8. 'RFXENDTC'
  9. 'RFICDTC'
  10. 'RFPENDTC'
  11. 'DTHDTC'
  12. 'DTHFL'
  13. 'SITEID'
  14. 'AGE'
  15. 'AGEU'
  16. 'SEX'
  17. 'RACE'
  18. 'ETHNIC'
  19. 'ARMCD'
  20. 'ARM'
  21. 'ACTARMCD'
  22. 'ACTARM'
  23. 'COUNTRY'
  24. 'RACE1'
  25. 'RACE2'
  26. 'RACE3'
  27. 'RACE4'
  28. 'RACESP'
In [ ]:

In [32]:
suppdm <- dm %>%
  select(STUDYID, USUBJID, RACE1, RACE2, RACESP) %>%
  pivot_longer(
    cols = c(RACE1, RACE2, RACESP),
    names_to = "QNAM",
    values_to = "QVAL"
  ) %>%

  filter(!is.na(QVAL) & QVAL != "") %>%
  mutate(
    RDOMAIN   = "DM",
    IDVAR     = "",
    IDVARVAL  = "",
    QNAM      = str_to_upper(str_sub(QNAM, 1, 8)),
    QLABEL    = case_when(
      QNAM == "RACE1"   ~ "Race Component 1",
      QNAM == "RACE2"   ~ "Race Component 2",
      QNAM == "RACESP" ~ "Race, Specify",
      TRUE              ~ QNAM
    ),
    QVAL   = as.character(QVAL),
    QORIG  = case_when(
      QNAM %in% c("RACE1","RACE2","RACESP") ~ "CRF",
      TRUE ~ "DERIVED"
    ),
    QEVAL = ""
  ) %>%
  select(STUDYID, RDOMAIN, USUBJID, IDVAR, IDVARVAL, QNAM, QLABEL, QVAL, QORIG, QEVAL) %>%
  arrange(STUDYID, USUBJID, QNAM)

suppdm
A tibble: 3 × 10
STUDYIDRDOMAINUSUBJIDIDVARIDVARVALQNAMQLABELQVALQORIGQEVAL
<chr><chr><chr><chr><chr><chr><chr><chr><chr><chr>
STU001DMSTU001-1002RACE1 Race Component 1ASIAN CRF
STU001DMSTU001-1002RACE2 Race Component 2AMERICAN INDIAN OR ALASKA NATIVECRF
STU001DMSTU001-1003RACESPRace, Specify BRAZILIAN CRF

Generated QC logs and check summaries for SDTM DM in R (keys, required variables, CT, ISO8601 dates), producing reviewer-ready PASS/WARN/FAIL
outputs and exportable summaries.

In [41]:
# ---- 1) Configure logging (writes to console + file) ----
log_file <- "qc_run.log"
log_appender(appender_tee(log_file))     # tee: console + file
log_layout(layout_glue_generator("{time} [{level}] {msg}"))

# Helper: standard result row
qc_result <- function(check_id, dataset, severity, status, message, n_issue = NA_integer_) {
  tibble(
    check_id = check_id,
    dataset  = dataset,
    severity = severity,      # INFO/WARN/ERROR
    status   = status,        # PASS/WARN/FAIL
    n_issue  = n_issue,
    message  = message
  )
}

# ---- 2) Example checks (write to log + return summary rows) ----
check_unique <- function(df, keys, dataset = "ADSL", check_id = "UNIQUE_USUBJID") {
  log_info("Running {check_id} on {dataset} (keys: {paste(keys, collapse=', ')})")

  dup <- df %>%
    count(across(all_of(keys))) %>%
    filter(n > 1)

  if (nrow(dup) == 0) {
    log_info("{check_id}: PASS")
    qc_result(check_id, dataset, "INFO", "PASS", "No duplicate keys.")
  } else {
    log_error("{check_id}: FAIL - {nrow(dup)} duplicate key combinations found.")
    qc_result(check_id, dataset, "ERROR", "FAIL",
              glue("{nrow(dup)} duplicate key combinations found."),
              n_issue = nrow(dup))
  }
}

check_missing <- function(df, vars, dataset = "ADSL", check_id = "MISSING_CORE") {
  log_info("Running {check_id} on {dataset} (vars: {paste(vars, collapse=', ')})")

  miss_counts <- sapply(vars, function(v) sum(is.na(df[[v]]) | df[[v]] == ""))
  n_bad_vars  <- sum(miss_counts > 0)

  if (n_bad_vars == 0) {
    log_info("{check_id}: PASS")
    qc_result(check_id, dataset, "INFO", "PASS", "No missing values in required variables.")
  } else {
    bad <- paste(names(miss_counts)[miss_counts > 0], collapse = ", ")
    log_warn("{check_id}: WARN - Missing values found in: {bad}")
    qc_result(check_id, dataset, "WARN", "WARN",
              glue("Missing values found in: {bad}"),
              n_issue = sum(miss_counts))
  }
}

# ---- 3) Run checks & build a reviewer summary ----
run_qc <- function(df, dataset = "ADSL") {
  log_info("===== QC START: {dataset} =====")

  results <- bind_rows(
    check_unique(df, keys = c("USUBJID"), dataset = dataset),
    check_missing(df, vars = c("USUBJID","AGE","SEX"), dataset = dataset)
  )

  # Quick reviewer view: counts by status
  reviewer_summary <- results %>%
    count(status, severity, name = "n_checks") %>%
    arrange(match(status, c("FAIL","WARN","PASS")))

  log_info("===== QC END: {dataset} =====")
  list(results = results, reviewer_summary = reviewer_summary)
}

Basics QC Check: But Use Pinnacle 21

In [42]:
out <- run_qc(dm, "DM")

# ---- 5) Export summaries ----
write_csv(out$results, "qc_check_results.csv")
write_csv(out$reviewer_summary, "qc_reviewer_summary.csv")

out$results
out$reviewer_summary
A tibble: 2 × 6
check_iddatasetseveritystatusn_issuemessage
<chr><chr><chr><chr><int><chr>
UNIQUE_USUBJIDDMINFOPASSNANo duplicate keys.
MISSING_CORE DMINFOPASSNANo missing values in required variables.
A tibble: 1 × 3
statusseverityn_checks
<chr><chr><int>
PASSINFO2