Program

“Complete Example 1” showed how to create a simple demographics table using the fmtr package and Tidyverse. Here is the same table created with only Base R and the sassy system of packages.

The data for this example has been included in the fmtr package as an external data file. It may be accessed using the system.file() function as shown below, or downloaded directly from the fmtr GitHub site here

library(sassy)

# Prepare Log -------------------------------------------------------------


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

# Get temp location for log and report output
tmp <- tempdir()

# Open log
lf <- log_open(file.path(tmp, "example2.log"))


# Load and Prepare Data ---------------------------------------------------

sep("Prepare Data")

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

# Define data library
libname(sdtm, pkg, "csv") 

# Prepare data
put("Subset DM dataset")
dm_mod <- subset(sdtm$DM, ARM != "SCREEN FAILURE", 
                 v(USUBJID, SEX, AGE, ARM)) |> put()

put("Get ARM population counts")
arm_pop <- proc_freq(dm_mod, tables = ARM, 
                     output = long,
                     options = v(nocum, nopercent, nonobs))
  

# Create Format Catalog --------------------------------------------------
sep("Create format catalog")

fmts <- fcat(AGECAT = value(condition(x >= 18 & x <= 24, "18 to 24"),
                            condition(x >= 25 & x <= 44, "25 to 44"),
                            condition(x >= 45 & x <= 64, "45 to 64"),
                            condition(x >= 65, ">= 65")),
             SEX = value(condition(is.na(x), "Missing", order = 3),
                         condition(x == "M", "Male", order = 1),
                         condition(x == "F", "Female", order = 2)),
             VAR = c("AGE" = "Age", 
                     "AGECAT" = "Age Group", 
                     "SEX" = "Sex"))

numfmts <- fcat(N = "%d", MEAN = "%.1f", STD = "(%04.2f)", MEDIAN = "%d", Q1 = "%.1f",
                Q3 = "%.1f", MIN = "%d", MAX = "%d", CNT = "%d", PCT = "(%4.1f%%)")

numlbls <- c(N = "n", MEANSD = "Mean (SD)", MEDIAN = "Median", Q1Q3 = "Q1 - Q3",
             MINMAX = "Min - Max")

# Age Summary Block -------------------------------------------------------

sep("Create summary statistics for age")

age_block <- proc_means(dm_mod, stats = v(n, mean, std, median, q1, q3, min, max),
                        class = ARM, options = v(nway, notype, nofreq)) |> 
  datastep(format = numfmts,
           keep = v(CLASS, VAR, N, MEANSD, MEDIAN, Q1Q3, MINMAX),
           {
             
             MEANSD <- fapply2(MEAN, STD)
             Q1Q3 <- fapply2(Q1, Q3, sep = " - ")
             MINMAX <- fapply2(MIN, MAX, sep = " - ")
           }) |> 
  proc_transpose(id = CLASS, copy = VAR, 
                 var = v(N, MEANSD, MEDIAN, Q1Q3, MINMAX),
                 name = "LABEL") |> 
  datastep({LABEL <- fapply(LABEL, numlbls)})
    
  
# Age Group Block ----------------------------------------------------------

sep("Create frequency counts for Age Group")

put("Create age group frequency counts")
ageg_block <- dm_mod |> 
  datastep({AGECAT <- fapply(AGE, fmts$AGECAT)}) |> 
  proc_freq(tables = AGECAT, by = ARM,
            options = nonobs) |> 
  datastep(format = numfmts,
           keep = v(VAR, BY, LABEL, CNTPCT),
           { 
             LABEL <- CAT
             CNTPCT <- fapply2(CNT, PCT)
           }) |> 
  proc_transpose(var = CNTPCT, by = LABEL, copy = VAR, id = BY, options = noname)

put("Sort age groups as desired")
ageg_block$LABEL <- factor(ageg_block$LABEL, levels = levels(fmts$AGECAT))
ageg_block <- proc_sort(ageg_block, by = LABEL, as.character = TRUE)


# Sex Block ---------------------------------------------------------------

sep("Create frequency counts for SEX")

put("Create sex frequency counts")
sex_block <- dm_mod |> 
  datastep({SEX <- fapply(SEX, fmts$SEX)}) |> 
  proc_freq(tables = SEX, by = ARM,
            options = nonobs) |> 
  datastep(format = numfmts,
           keep = v(VAR, BY, LABEL, CNTPCT),
           { 
             LABEL <- CAT
             CNTPCT <- fapply2(CNT, PCT)
           }) |> 
  proc_transpose(var = CNTPCT, by = LABEL, copy = VAR, id = BY, options = noname)

put("Sort age groups as desired")
sex_block$LABEL <- factor(sex_block$LABEL, levels = levels(fmts$SEX))
sex_block <- proc_sort(sex_block, by = LABEL, as.character = TRUE)


put("Combine blocks into final data frame")
final <- bind_rows(age_block, ageg_block, sex_block) |> put()

# Report ------------------------------------------------------------------


sep("Create and print report")

# Create Table
tbl <- create_table(final, first_row_blank = TRUE, borders = c("top", "bottom")) |> 
  column_defaults(from = `ARM A`, to = `ARM D`, align = "center", width = 1.25) |> 
  stub(vars = v(VAR, LABEL), "Variable", width = 2.5) |> 
  define(VAR, blank_after = TRUE, dedupe = TRUE, label = "Variable",
         format = fmts$VAR,label_row = TRUE) |> 
  define(LABEL, indent = .25, label = "Demographic Category") |> 
  define(`ARM A`,  label = "Treatment Group 1", n = arm_pop["ARM A"]) |> 
  define(`ARM B`,  label = "Treatment Group 2", n = arm_pop["ARM B"]) |> 
  define(`ARM C`,  label = "Treatment Group 3", n = arm_pop["ARM C"]) |> 
  define(`ARM D`,  label = "Treatment Group 4", n = arm_pop["ARM D"]) 

rpt <- create_report(file.path(tmp, "output/example2.rtf"), 
                     output_type = "RTF", font = "Arial") |> 
  set_margins(top = 1, bottom = 1) |> 
  page_header("Sponsor: Company", "Study: ABC") |> 
  titles("Table 1.0", bold = TRUE, blank_row = "none") |> 
  titles("Analysis of Demographic Characteristics", 
         "Safety Population") |> 
  add_content(tbl) |> 
  footnotes("Program: DM_Table.R",
            "NOTE: Denominator based on number of non-missing responses.") |> 
  page_footer(paste0("Date Produced: ", fapply(Sys.time(), "%d%b%y %H:%M")), 
              right = "Page [pg] of [tpg]")

res <- write_report(rpt)


# Clean Up ----------------------------------------------------------------
sep("Clean Up")

# Close log
log_close()

# View report
# file.show(res$modified_path)

# View log
# file.show(lf)

Output

Here is the report produced by the above sample program:

Log

Here is the log produced by the above sample program:

=========================================================================
Log Path: C:/Users/dbosa/AppData/Local/Temp/RtmpKc23Rz/log/example2.log
Program Path: C:/Users/dbosa/Documents/.active-rstudio-document
Working Directory: C:/Projects/Archytas/Sassy/Code
User Name: dbosa
R Version: 4.3.2 (2023-10-31 ucrt)
Machine: SOCRATES x86-64
Operating System: Windows 10 x64 build 22621
Base Packages: stats graphics grDevices utils datasets methods base Other
Packages: tidylog_1.0.2 reporter_1.4.4 logr_1.3.5 sassy_1.2.1 lubridate_1.9.2
forcats_1.0.0 stringr_1.5.0 purrr_1.0.1 readr_2.1.4 tidyr_1.3.0 tibble_3.2.1
ggplot2_3.4.4 tidyverse_2.0.0 libr_1.2.8 dplyr_1.1.3 fmtr_1.6.2 procs_1.0.4
common_1.1.1
Log Start Time: 2024-01-07 18:26:25.636328
=========================================================================

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

# library 'sdtm': 1 items
- attributes: csv not loaded
- path: C:/Users/dbosa/AppData/Local/R/win-library/4.3/fmtr/extdata
- items:
  Name Extension Rows Cols    Size        LastModified
1   DM       csv   87   24 45.5 Kb 2023-12-16 23:10:51

Subset DM dataset

# A tibble: 85 × 4
   USUBJID    SEX     AGE ARM  
   <chr>      <chr> <dbl> <chr>
 1 ABC-01-049 M        39 ARM D
 2 ABC-01-050 M        47 ARM B
 3 ABC-01-051 M        34 ARM A
 4 ABC-01-052 F        45 ARM C
 5 ABC-01-053 F        26 ARM B
 6 ABC-01-054 M        44 ARM D
 7 ABC-01-055 F        47 ARM C
 8 ABC-01-056 M        31 ARM A
 9 ABC-01-113 M        74 ARM D
10 ABC-01-114 F        72 ARM B
# ℹ 75 more rows
# ℹ Use `print(n = ...)` to see more rows

Get ARM population counts

proc_freq: input data set 85 rows and 4 columns
           tables: ARM
           output: long
           view: TRUE
           output: 1 datasets

# A tibble: 1 × 6
  VAR   STAT  `ARM A` `ARM B` `ARM C` `ARM D`
  <chr> <chr>   <dbl>   <dbl>   <dbl>   <dbl>
1 ARM   CNT        20      21      21      23

=========================================================================
Create format catalog
=========================================================================

# A user-defined format: 4 conditions
  Name Type        Expression    Label Order
1  obj    U x >= 18 & x <= 24 18 to 24    NA
2  obj    U x >= 25 & x <= 44 25 to 44    NA
3  obj    U x >= 45 & x <= 64 45 to 64    NA
4  obj    U           x >= 65    >= 65    NA

# A user-defined format: 3 conditions
  Name Type Expression   Label Order
1  obj    U   is.na(x) Missing     3
2  obj    U   x == "M"    Male     1
3  obj    U   x == "F"  Female     2

# A format catalog: 3 formats
- $AGECAT: type U, 4 conditions
- $SEX: type U, 3 conditions
- $VAR: type V, 3 elements

# A format catalog: 10 formats
- $N: type S, "%d"
- $MEAN: type S, "%.1f"
- $STD: type S, "(%04.2f)"
- $MEDIAN: type S, "%d"
- $Q1: type S, "%.1f"
- $Q3: type S, "%.1f"
- $MIN: type S, "%d"
- $MAX: type S, "%d"
- $CNT: type S, "%d"
- $PCT: type S, "(%4.1f%%)"

=========================================================================
Create summary statistics for age
=========================================================================

proc_means: input data set 85 rows and 4 columns
            class: ARM
            var: AGE
            stats: n mean std median q1 q3 min max
            view: TRUE
            output: 1 datasets

  CLASS VAR  N     MEAN      STD MEDIAN   Q1 Q3 MIN MAX
1 ARM A AGE 20 53.15000 11.89991   52.5 47.5 60  31  73
2 ARM B AGE 21 47.38095 16.25877   46.0 35.0 61  22  73
3 ARM C AGE 21 45.71429 14.41923   46.0 38.0 53  19  71
4 ARM D AGE 23 49.73913 14.32486   48.0 39.0 62  21  75

datastep: columns decreased from 10 to 7

  CLASS VAR  N       MEANSD MEDIAN        Q1Q3  MINMAX
1 ARM A AGE 20 53.1 (11.90)   52.5 47.5 - 60.0 31 - 73
2 ARM B AGE 21 47.4 (16.26)   46.0 35.0 - 61.0 22 - 73
3 ARM C AGE 21 45.7 (14.42)   46.0 38.0 - 53.0 19 - 71
4 ARM D AGE 23 49.7 (14.32)   48.0 39.0 - 62.0 21 - 75

proc_transpose: input data set 4 rows and 7 columns
                var: N MEANSD MEDIAN Q1Q3 MINMAX
                id: CLASS
                copy: VAR
                name: LABEL
                output dataset 5 rows and 6 columns

  VAR  LABEL        ARM A        ARM B        ARM C        ARM D
1 AGE      N           20           21           21           23
2 AGE MEANSD 53.1 (11.90) 47.4 (16.26) 45.7 (14.42) 49.7 (14.32)
3 AGE MEDIAN         52.5         46.0         46.0         48.0
4 AGE   Q1Q3  47.5 - 60.0  35.0 - 61.0  38.0 - 53.0  39.0 - 62.0
5 AGE MINMAX      31 - 73      22 - 73      19 - 71      21 - 75

datastep: columns started with 6 and ended with 6

  VAR     LABEL        ARM A        ARM B        ARM C        ARM D
1 AGE         n           20           21           21           23
2 AGE Mean (SD) 53.1 (11.90) 47.4 (16.26) 45.7 (14.42) 49.7 (14.32)
3 AGE    Median         52.5         46.0         46.0         48.0
4 AGE   Q1 - Q3  47.5 - 60.0  35.0 - 61.0  38.0 - 53.0  39.0 - 62.0
5 AGE Min - Max      31 - 73      22 - 73      19 - 71      21 - 75

=========================================================================
Create frequency counts for Age Group
=========================================================================

Create age group frequency counts

datastep: columns increased from 4 to 5

# A tibble: 85 × 5
   USUBJID    SEX     AGE ARM   AGECAT  
   <chr>      <chr> <dbl> <chr> <chr>   
 1 ABC-01-049 M        39 ARM D 25 to 44
 2 ABC-01-050 M        47 ARM B 45 to 64
 3 ABC-01-051 M        34 ARM A 25 to 44
 4 ABC-01-052 F        45 ARM C 45 to 64
 5 ABC-01-053 F        26 ARM B 25 to 44
 6 ABC-01-054 M        44 ARM D 25 to 44
 7 ABC-01-055 F        47 ARM C 45 to 64
 8 ABC-01-056 M        31 ARM A 25 to 44
 9 ABC-01-113 M        74 ARM D >= 65   
10 ABC-01-114 F        72 ARM B >= 65   
# ℹ 75 more rows
# ℹ Use `print(n = ...)` to see more rows

proc_freq: input data set 85 rows and 5 columns
           tables: AGECAT
           by: ARM
           view: TRUE
           output: 1 datasets

# A tibble: 16 × 5
   BY    VAR    CAT        CNT   PCT
   <chr> <chr>  <chr>    <dbl> <dbl>
 1 ARM A AGECAT >= 65        3 15   
 2 ARM A AGECAT 18 to 24     0  0   
 3 ARM A AGECAT 25 to 44     4 20   
 4 ARM A AGECAT 45 to 64    13 65   
 5 ARM B AGECAT >= 65        5 23.8 
 6 ARM B AGECAT 18 to 24     1  4.76
 7 ARM B AGECAT 25 to 44     8 38.1 
 8 ARM B AGECAT 45 to 64     7 33.3 
 9 ARM C AGECAT >= 65        2  9.52
10 ARM C AGECAT 18 to 24     3 14.3 
11 ARM C AGECAT 25 to 44     4 19.0 
12 ARM C AGECAT 45 to 64    12 57.1 
13 ARM D AGECAT >= 65        3 13.0 
14 ARM D AGECAT 18 to 24     1  4.35
15 ARM D AGECAT 25 to 44     7 30.4 
16 ARM D AGECAT 45 to 64    12 52.2 

datastep: columns decreased from 5 to 4

# A tibble: 16 × 4
   VAR    BY    LABEL    CNTPCT    
   <chr>  <chr> <chr>    <chr>     
 1 AGECAT ARM A >= 65    3 (15.0%) 
 2 AGECAT ARM A 18 to 24 0 ( 0.0%) 
 3 AGECAT ARM A 25 to 44 4 (20.0%) 
 4 AGECAT ARM A 45 to 64 13 (65.0%)
 5 AGECAT ARM B >= 65    5 (23.8%) 
 6 AGECAT ARM B 18 to 24 1 ( 4.8%) 
 7 AGECAT ARM B 25 to 44 8 (38.1%) 
 8 AGECAT ARM B 45 to 64 7 (33.3%) 
 9 AGECAT ARM C >= 65    2 ( 9.5%) 
10 AGECAT ARM C 18 to 24 3 (14.3%) 
11 AGECAT ARM C 25 to 44 4 (19.0%) 
12 AGECAT ARM C 45 to 64 12 (57.1%)
13 AGECAT ARM D >= 65    3 (13.0%) 
14 AGECAT ARM D 18 to 24 1 ( 4.3%) 
15 AGECAT ARM D 25 to 44 7 (30.4%) 
16 AGECAT ARM D 45 to 64 12 (52.2%)

proc_transpose: input data set 16 rows and 4 columns
                by: LABEL
                var: CNTPCT
                id: BY
                copy: VAR
                name: NAME
                output dataset 4 rows and 6 columns

# A tibble: 4 × 6
  VAR    LABEL    `ARM A`    `ARM B`   `ARM C`    `ARM D`   
  <chr>  <chr>    <chr>      <chr>     <chr>      <chr>     
1 AGECAT >= 65    3 (15.0%)  5 (23.8%) 2 ( 9.5%)  3 (13.0%) 
2 AGECAT 18 to 24 0 ( 0.0%)  1 ( 4.8%) 3 (14.3%)  1 ( 4.3%) 
3 AGECAT 25 to 44 4 (20.0%)  8 (38.1%) 4 (19.0%)  7 (30.4%) 
4 AGECAT 45 to 64 13 (65.0%) 7 (33.3%) 12 (57.1%) 12 (52.2%)

Sort age groups as desired

proc_sort: input data set 4 rows and 6 columns
           by: LABEL
           keep: VAR LABEL ARM A ARM B ARM C ARM D
           order: a
           output data set 4 rows and 6 columns

# A tibble: 4 × 6
  VAR    LABEL    `ARM A`    `ARM B`   `ARM C`    `ARM D`   
  <chr>  <chr>    <chr>      <chr>     <chr>      <chr>     
1 AGECAT 18 to 24 0 ( 0.0%)  1 ( 4.8%) 3 (14.3%)  1 ( 4.3%) 
2 AGECAT 25 to 44 4 (20.0%)  8 (38.1%) 4 (19.0%)  7 (30.4%) 
3 AGECAT 45 to 64 13 (65.0%) 7 (33.3%) 12 (57.1%) 12 (52.2%)
4 AGECAT >= 65    3 (15.0%)  5 (23.8%) 2 ( 9.5%)  3 (13.0%) 

=========================================================================
Create frequency counts for SEX
=========================================================================

Create sex frequency counts

datastep: columns started with 4 and ended with 4

# A tibble: 85 × 4
   USUBJID    SEX      AGE ARM  
   <chr>      <chr>  <dbl> <chr>
 1 ABC-01-049 Male      39 ARM D
 2 ABC-01-050 Male      47 ARM B
 3 ABC-01-051 Male      34 ARM A
 4 ABC-01-052 Female    45 ARM C
 5 ABC-01-053 Female    26 ARM B
 6 ABC-01-054 Male      44 ARM D
 7 ABC-01-055 Female    47 ARM C
 8 ABC-01-056 Male      31 ARM A
 9 ABC-01-113 Male      74 ARM D
10 ABC-01-114 Female    72 ARM B
# ℹ 75 more rows
# ℹ Use `print(n = ...)` to see more rows

proc_freq: input data set 85 rows and 4 columns
           tables: SEX
           by: ARM
           view: TRUE
           output: 1 datasets

# A tibble: 8 × 5
  BY    VAR   CAT      CNT   PCT
  <chr> <chr> <chr>  <dbl> <dbl>
1 ARM A SEX   Female     5  25  
2 ARM A SEX   Male      15  75  
3 ARM B SEX   Female    11  52.4
4 ARM B SEX   Male      10  47.6
5 ARM C SEX   Female     9  42.9
6 ARM C SEX   Male      12  57.1
7 ARM D SEX   Female     7  30.4
8 ARM D SEX   Male      16  69.6

datastep: columns decreased from 5 to 4

# A tibble: 8 × 4
  VAR   BY    LABEL  CNTPCT    
  <chr> <chr> <chr>  <chr>     
1 SEX   ARM A Female 5 (25.0%) 
2 SEX   ARM A Male   15 (75.0%)
3 SEX   ARM B Female 11 (52.4%)
4 SEX   ARM B Male   10 (47.6%)
5 SEX   ARM C Female 9 (42.9%) 
6 SEX   ARM C Male   12 (57.1%)
7 SEX   ARM D Female 7 (30.4%) 
8 SEX   ARM D Male   16 (69.6%)

proc_transpose: input data set 8 rows and 4 columns
                by: LABEL
                var: CNTPCT
                id: BY
                copy: VAR
                name: NAME
                output dataset 2 rows and 6 columns

# A tibble: 2 × 6
  VAR   LABEL  `ARM A`    `ARM B`    `ARM C`    `ARM D`   
  <chr> <chr>  <chr>      <chr>      <chr>      <chr>     
1 SEX   Female 5 (25.0%)  11 (52.4%) 9 (42.9%)  7 (30.4%) 
2 SEX   Male   15 (75.0%) 10 (47.6%) 12 (57.1%) 16 (69.6%)

Sort age groups as desired

proc_sort: input data set 2 rows and 6 columns
           by: LABEL
           keep: VAR LABEL ARM A ARM B ARM C ARM D
           order: a
           output data set 2 rows and 6 columns

# A tibble: 2 × 6
  VAR   LABEL  `ARM A`    `ARM B`    `ARM C`    `ARM D`   
  <chr> <chr>  <chr>      <chr>      <chr>      <chr>     
1 SEX   Male   15 (75.0%) 10 (47.6%) 12 (57.1%) 16 (69.6%)
2 SEX   Female 5 (25.0%)  11 (52.4%) 9 (42.9%)  7 (30.4%) 

Combine blocks into final data frame

      VAR     LABEL        ARM A        ARM B        ARM C        ARM D
1     AGE         n           20           21           21           23
2     AGE Mean (SD) 53.1 (11.90) 47.4 (16.26) 45.7 (14.42) 49.7 (14.32)
3     AGE    Median         52.5         46.0         46.0         48.0
4     AGE   Q1 - Q3  47.5 - 60.0  35.0 - 61.0  38.0 - 53.0  39.0 - 62.0
5     AGE Min - Max      31 - 73      22 - 73      19 - 71      21 - 75
6  AGECAT  18 to 24    0 ( 0.0%)    1 ( 4.8%)    3 (14.3%)    1 ( 4.3%)
7  AGECAT  25 to 44    4 (20.0%)    8 (38.1%)    4 (19.0%)    7 (30.4%)
8  AGECAT  45 to 64   13 (65.0%)    7 (33.3%)   12 (57.1%)   12 (52.2%)
9  AGECAT     >= 65    3 (15.0%)    5 (23.8%)    2 ( 9.5%)    3 (13.0%)
10    SEX      Male   15 (75.0%)   10 (47.6%)   12 (57.1%)   16 (69.6%)
11    SEX    Female    5 (25.0%)   11 (52.4%)    9 (42.9%)    7 (30.4%)

=========================================================================
Create and print report
=========================================================================

# A report specification: 1 pages
- file_path: 'C:\Users\dbosa\AppData\Local\Temp\RtmpKc23Rz/output/example2.rtf'
- output_type: RTF
- units: inches
- orientation: landscape
- margins: top 1 bottom 1 left 1 right 1
- line size/count: 9/36
- page_header: left=Sponsor: Company right=Study: ABC
- title 1: 'Table 1.0'
- title 2: 'Analysis of Demographic Characteristics'
- title 3: 'Safety Population'
- footnote 1: 'Program: DM_Table.R'
- footnote 2: 'NOTE: Denominator based on number of non-missing responses.'
- page_footer: left=Date Produced: 07Jan24 18:26 center= right=Page [pg] of [tpg]
- content: 
# A table specification:
- data: data.frame 'final' 11 rows 6 cols
- show_cols: all
- use_attributes: all
- stub: VAR LABEL 'Variable' width=2.5 align='left' 
- define: VAR 'Variable' dedupe='TRUE' 
- define: LABEL 'Demographic Category' 
- define: ARM A 'Treatment Group 1' 
- define: ARM B 'Treatment Group 2' 
- define: ARM C 'Treatment Group 3' 
- define: ARM D 'Treatment Group 4' 

=========================================================================
Clean Up
=========================================================================

=========================================================================
Log End Time: 2024-01-07 18:26:27.426253
Log Elapsed Time: 0 00:00:01
=========================================================================