Example 5: Vital Signs Table

The fifth example produces a summary table of selected Vital Signs for Placebo vs. Treated groups. The report shows statistics for both baseline and after-treatment time points. This example also demonstrates how to use tidyverse functions for data preparation instead of procs.

Program

Note the following about this example:

  • The %eq% operator from the common package allows comparing of a variable that contains many NA values, without error.
  • The fmtr package provides capabilities to create a user-defined format, similar to proc format.
  • The reporter package supports spanning headers in the report header.
  • Notice that sassy functions integrate nicely with dplyr. You can put a datastep() right in the middle of a dplyr pipeline and get the best of both worlds!
library(tidyverse)
library(sassy)

options("logr.autolog" = TRUE,
        "logr.notes" = FALSE)

# Get path to temp directory
tmp <- tempdir() 

# Get path to sample data
pkg <- system.file("extdata", package = "sassy")

# Open log
lgpth <- log_open(file.path(tmp, "example5.log"))

sep("Prepare Data")

# Create libname for csv data
libname(sdtm, pkg, "csv") 

put("Join and prepare data")
prep <- sdtm$DM |> 
  left_join(sdtm$VS, by = c("USUBJID" = "USUBJID")) |> 
  select(USUBJID, VSTESTCD, VISIT, VISITNUM, VSSTRESN, ARM, VSBLFL) |> 
  filter(VSTESTCD %in% c("PULSE", "RESP", "TEMP", "DIABP", "SYSBP"), 
         !(VISIT == "SCREENING" & VSBLFL != "Y")) |> 
  arrange(USUBJID, VSTESTCD, VISITNUM) |> 
  group_by(USUBJID, VSTESTCD) |> 
  datastep(retain = list(BSTRESN = 0), {
    
    # Combine treatment groups
    # And distingish baseline time points
    if (ARM == "ARM A") {
      if (VSBLFL %eq% "Y") {
        GRP <- "A_BASE"
      } else {
        GRP <- "A_TRT"
      }
    } else {
      if (VSBLFL %eq% "Y") {
        GRP <- "O_BASE"
      } else {
        GRP <- "O_TRT"
      }
    }
    
    # Populate baseline value
    if (first.)
      BSTRESN = VSSTRESN
    
  }) |> 
  ungroup() 

put("Get population counts")
pop_A <- prep |> select(USUBJID, GRP) |> filter(GRP == "A_BASE") |> 
  distinct() |> count() |> deframe() |> put()
pop_O <- prep |> select(USUBJID, GRP) |> filter(GRP == "O_BASE") |> 
  distinct() |> count() |> deframe() |> put()

put("Prepare final data frame")
final <- prep |> 
  select(VSTESTCD, GRP, VSSTRESN, BSTRESN) |> 
  group_by(VSTESTCD, GRP) |> 
  summarize(Mean = fmt_mean_sd(VSSTRESN),
            Median = fmt_median(VSSTRESN),
            Quantiles = fmt_quantile_range(VSSTRESN),
            Range = fmt_range(VSSTRESN)) |> 
  ungroup() |> 
  pivot_longer(cols = c(Mean, Median, Quantiles, Range),
               names_to = "stats",
               values_to = "values") |> 
  pivot_wider(names_from = GRP,
              values_from = values) |> 
  put()


sep("Create formats")

# Vital sign lookup format 
vs_fmt <- c(PULSE = "Pulse", 
            TEMP = "Temperature °C", 
            RESP = "Respirations/min", 
            SYSBP = "Systolic Blood Pressure", 
            DIABP = "Diastolic Blood Pressure") |> 
  put()

# Statistics user-defined format                
stat_fmt <- value(condition(x == "Mean", "Mean (SD)"),
                  condition(x == "Quantiles", "Q1 - Q3")) |> 
  put()

sep("Create Report")


# Apply sort
final <- final |> 
  mutate(VSTESTCD = factor(VSTESTCD, levels = names(vs_fmt))) |> 
  arrange(VSTESTCD)

# Define table object
tbl <- create_table(final, borders = "bottom") |> 
  spanning_header(A_BASE, A_TRT, "Placebo", n = pop_A) |> 
  spanning_header(O_BASE, O_TRT, "Treated", n = pop_O) |> 
  column_defaults(width = 1.25, align = "center") |> 
  stub(c(VSTESTCD, stats), width = 2.5) |> 
  define(VSTESTCD, "Vital Sign", format = vs_fmt, 
         blank_after = TRUE, dedupe = TRUE, label_row = TRUE) |> 
  define(stats, indent = .25, format = stat_fmt) |> 
  define(A_BASE, "Baseline") |> 
  define(A_TRT, "After Treatment") |> 
  define(O_BASE, "Baseline") |> 
  define(O_TRT, "After Treatment")

# Construct output path
pth <- file.path(tmp, "output/t_vs.pdf")

# Define report object
rpt <- create_report(pth, output_type = "PDF", font = "Times", 
                     font_size = 11) |> 
  page_header("Sponsor: Company", "Study: ABC") |> 
  titles("Table 4.0", "Selected Vital Signs", bold = TRUE, font_size = 12) |> 
  add_content(tbl, align = "center") |> 
  page_footer(Sys.time(), "CONFIDENTIAL", "Page [pg] of [tpg]")

# Write report to file system  
write_report(rpt) 

# Close log
log_close()

# View files
# file.show(pth)
# file.show(lgpth)

Output

Here is the output report:

Log

Here is the log from the above example:

========================================================================= 
Log Path: C:/Users/User/AppData/Local/Temp/Rtmpum5T6o/log/example5.log 
Working Directory: C:/packages/Testing 
User Name: User 
R Version: 4.0.3 (2020-10-10) 
Machine: DESKTOP-1F27OR8 x86-64 
Operating System: Windows 10 x64 build 18363 
Log Start Time: 2021-01-05 08:13:13 
========================================================================= 

========================================================================= 
Prepare Data 
========================================================================= 

# library 'sdtm': 8 items
- attributes: csv not loaded
- path: C:/Users/User/Documents/R/win-library/4.0/sassy/extdata
- items:
  Name Extension Rows Cols     Size        LastModified
1   AE       csv  150   27  88.1 Kb 2020-12-27 23:21:55
2   DA       csv 3587   18 527.8 Kb 2020-12-27 23:21:55
3   DM       csv   87   24  45.2 Kb 2020-12-27 23:21:55
4   DS       csv  174    9  33.7 Kb 2020-12-27 23:21:55
5   EX       csv   84   11    26 Kb 2020-12-27 23:21:55
6   IE       csv    2   14    13 Kb 2020-12-27 23:21:55
7   SV       csv  685   10  69.9 Kb 2020-12-27 23:21:55
8   VS       csv 3358   17   467 Kb 2020-12-27 23:21:55

Join and prepare data 

left_join: added 18 columns (STUDYID.x, DOMAIN.x, STUDYID.y, DOMAIN.y, VSSEQ, …)

           > rows only in x       0

           > rows only in y  (    0)

           > matched rows     3,358    (includes duplicates)

           >                 =======

           > rows total       3,358

select: dropped 33 variables (STUDYID.x, DOMAIN.x, SUBJID, RFSTDTC, RFENDTC, …)

filter: removed 590 rows (18%), 2,768 rows remaining

group_by: 2 grouping variables (USUBJID, VSTESTCD)

datastep: columns decreased from 7 to 9 

ungroup: no grouping variables

# A tibble: 2,768 x 9
   USUBJID    VSTESTCD VISIT   VISITNUM VSSTRESN ARM   VSBLFL BSTRESN GRP   
   <chr>      <chr>    <chr>      <dbl>    <dbl> <chr> <chr>    <dbl> <chr> 
 1 ABC-01-049 DIABP    DAY 1          1       76 ARM D Y           76 O_BASE
 2 ABC-01-049 DIABP    WEEK 2         2       66 ARM D <NA>        76 O_TRT 
 3 ABC-01-049 DIABP    WEEK 4         4       84 ARM D <NA>        76 O_TRT 
 4 ABC-01-049 DIABP    WEEK 6         6       68 ARM D <NA>        76 O_TRT 
 5 ABC-01-049 DIABP    WEEK 8         8       80 ARM D <NA>        76 O_TRT 
 6 ABC-01-049 DIABP    WEEK 12       12       70 ARM D <NA>        76 O_TRT 
 7 ABC-01-049 DIABP    WEEK 16       16       70 ARM D <NA>        76 O_TRT 
 8 ABC-01-049 PULSE    DAY 1          1       84 ARM D Y           84 O_BASE
 9 ABC-01-049 PULSE    WEEK 2         2       84 ARM D <NA>        84 O_TRT 
10 ABC-01-049 PULSE    WEEK 4         4       76 ARM D <NA>        84 O_TRT 
# ... with 2,758 more rows

Get population counts 

select: dropped 7 variables (VSTESTCD, VISIT, VISITNUM, VSSTRESN, ARM, …)

filter: removed 2,669 rows (96%), 99 rows remaining

distinct: removed 79 rows (80%), 20 rows remaining

count: now one row and one column, ungrouped

[1] 20

select: dropped 7 variables (VSTESTCD, VISIT, VISITNUM, VSSTRESN, ARM, …)

filter: removed 2,435 rows (88%), 333 rows remaining

distinct: removed 266 rows (80%), 67 rows remaining

count: now one row and one column, ungrouped

[1] 67

Prepare final data frame 

select: dropped 5 variables (USUBJID, VISIT, VISITNUM, ARM, VSBLFL)

group_by: 2 grouping variables (VSTESTCD, GRP)

summarize: now 20 rows and 6 columns, one group variable remaining (VSTESTCD)

ungroup: no grouping variables

pivot_longer: reorganized (Mean, Median, Quantiles, Range) into (stats, values) [was 20x6, now 80x4]

pivot_wider: reorganized (GRP, values) into (A_BASE, A_TRT, O_BASE, O_TRT) [was 80x4, now 20x6]

# A tibble: 20 x 6
   VSTESTCD stats     A_BASE        A_TRT         O_BASE        O_TRT        
   <chr>    <chr>     <chr>         <chr>         <chr>         <chr>        
 1 DIABP    Mean      77.2 (10.7)   77.1 (8.1)    77.5 (8.1)    76.9 (9.2)   
 2 DIABP    Median    78.5          78.0          78.0          78.0         
 3 DIABP    Quantiles 70.0 - 82.5   72.0 - 82.0   70.2 - 82.0   70.0 - 83.8  
 4 DIABP    Range     54 - 96       50 - 98       61 - 95       50 - 104     
 5 PULSE    Mean      72.8 (10.1)   74.6 (10.2)   73.6 (9.7)    74.0 (9.9)   
 6 PULSE    Median    72.0          74.0          72.0          72.0         
 7 PULSE    Quantiles 66.0 - 76.0   67.0 - 80.0   67.5 - 79.5   66.0 - 80.0  
 8 PULSE    Range     60 - 103      54 - 102      52 - 100      50 - 109     
 9 RESP     Mean      16.5 (2.5)    15.8 (3.4)    16.0 (3.1)    15.4 (3.4)   
10 RESP     Median    16.0          16.0          16.0          16.0         
11 RESP     Quantiles 16.0 - 18.0   16.0 - 18.0   15.5 - 18.0   12.0 - 18.0  
12 RESP     Range     12 - 20       8 - 24        8 - 22        8 - 24       
13 SYSBP    Mean      128.2 (16.7)  130.4 (17.6)  126.7 (15.5)  125.9 (15.2) 
14 SYSBP    Median    125.5         126.0         123.0         124.0        
15 SYSBP    Quantiles 117.5 - 140.0 118.0 - 140.0 116.0 - 138.0 115.2 - 135.0
16 SYSBP    Range     98 - 161      95 - 184      100 - 164     82 - 180     
17 TEMP     Mean      36.5 (0.4)    36.5 (0.3)    36.4 (0.5)    36.3 (0.4)   
18 TEMP     Median    36.4          36.5          36.4          36.3         
19 TEMP     Quantiles 36.2 - 36.9   36.2 - 36.6   36.2 - 36.6   36.1 - 36.5  
20 TEMP     Range     35.9 - 37.4   35.8 - 37.3   35.3 - 39.8   34.4 - 38.2  

========================================================================= 
Create formats 
========================================================================= 

PULSE                       TEMP                       RESP                      SYSBP 
"Pulse"           "Temperature °C"         "Respirations/min"  "Systolic Blood Pressure" 
DIABP 
"Diastolic Blood Pressure" 

# A user-defined format: 2 conditions
  Name Type       Expression     Label Order
1    x    U      x == "Mean" Mean (SD)    NA
2    x    U x == "Quantiles"   Q1 - Q3    NA

========================================================================= 
Create Report 
========================================================================= 

mutate: converted 'VSTESTCD' from character to factor (0 new NA)

# A report specification: 1 pages
- file_path: 'C:\Users\User\AppData\Local\Temp\Rtmpum5T6o/output/t_vs.rtf'
- output_type: RTF
- units: inches
- orientation: landscape
- line size/count: 108/48
- page_header: left=Sponsor: Company right=Study: ABC
- title 1: 'Table 4.0'
- title 2: 'Selected Vital Signs'
- page_footer: left=2021-01-05 08:13:45 center=CONFIDENTIAL right=Page [pg] of [tpg]
- content: 
# A table specification:
- data: tibble 'final' 20 rows 6 cols
- show_cols: all
- use_attributes: all
- spanning_header: from='A_BASE' to='A_TRT' 'Placebo' level=1 
- spanning_header: from='O_BASE' to='O_TRT' 'Treated' level=1 
- stub: VSTESTCD stats width=2.5 align='left' 
- define: VSTESTCD 'Vital Sign' dedupe='TRUE' 
- define: stats 
- define: A_BASE 'Baseline' 
- define: A_TRT 'After Treatment' 
- define: O_BASE 'Baseline' 
- define: O_TRT 'After Treatment' 

========================================================================= 
Log End Time: 2021-01-05 08:13:49 
Log Elapsed Time: 0 00:00:36 
========================================================================= 

Next: Example 6: Figure with By Group