Automatic slide generation with autoslider.core

{autoslider.core} is a newly open-sourced package for automatically creating standard study outputs and adding them to PowerPoint slides.
TLG
Author

Stefan P. Thoma, Joe Zhu

Published

March 14, 2025

We are excited to announce that the {autoslider.core} package is now a part of pharmaverse!

The normal process of creating clinical study slides is that a statistician manually types in numbers from outputs and a separate statistician then double checks the typed-in numbers. This process is time consuming, resource intensive, and error prone. This package lets users create slides with study-specific outputs in an automated and reproducible way – without the need to copy-paste. It reduces the amount of work and the required time when creating slides, and reduces the risk of errors from manually typing or copying numbers from the output to slides. It also helps users to avoid unnecessary stress when creating large amounts of slide decks in a short time window. It is particularly helpful for slides that need to be continuously created with updated data, such as slides needed for dose-escalation meetings.

The autoslider development work started as a Roche internal initiative. As it grew over the years, it was well received and we decided to open-source this package while at the same time not causing any downstream breaking changes to our internal users. Therefore, the autoslider package became two layers, the outer layer exposed to internal users remains as autoslider. It now mostly contains Roche-specific formats, layouts and designs. The inner layer, aka {autoslider.core} , which controls the rendering and workflow, is open source and fully functional and user-facing on its own. We consider autoslider as a framework, and would like to invite users to build their own autoslider.* packages. You can find t0he developer guidelines at the package homepage.

The value proposition of the {autoslider.core} package is two-fold:

  1. functions to create standard outputs with (some) inbuilt customization capability
  1. back-end machinery to manage the workflow. This includes:

In this blog post, we will guide you through the {autoslider.core} package and demonstrate how it can be used to streamline your data analysis and output slide generation.

Requirements

To follow along with this tutorial, you need to have the {autoslider.core} package installed. You will also need data available in ADaM format (or SDTM, depending on the template used), which should be stored in a named list where the names correspond to ADaM datasets.

library(autoslider.core)
Registered S3 method overwritten by 'tern':
  method   from 
  tidy.glm broom
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

Workflow

To get started, the folder structure for your project could look something like this:

Copy code
├── programs
│   ├── run_script.R
│   ├── R
│   │   ├── helping_functions.R
│   │   ├── output_functions.R
├── outputs
├── specs.yml
├── filters.yml

The {autoslider.core} workflow would involve implementing the key aspects in the run_script.R file. This script guides through the workflow without needing the files in R/. However, custom output-creating functions can be stored in the R/ folder.

Specifications (specs.yml)

This file contains the specifications of all outputs you want to create. Each output includes details such as the program name, footnotes & titles, paper orientation and font size, suffix, and additional arguments.

The following example specs.yml content would create two outputs based off of two template functions, t_ds_slide() and t_dm_slide():

- program: t_ds_slide
  titles: Patient Disposition ({filter_titles("adsl")})
  footnotes: 't_ds footnotes'
  paper: L6
  suffix: ITT
- program: t_dm_slide
  titles: Patient Demographics and Baseline Characteristics
  footnotes: 't_dm_slide footnote'
  paper: L6
  suffix: ITT
  args:
    arm: "TRT01A"
    vars: ["SEX", "AGE", "RACE", "ETHNIC", "COUNTRY"]

Filters (filters.yml)

In the filters.yml file, the names of the filters used across outputs are specified. Each filter includes a name, title, filtering condition, target dataset, and type.

The following example filters.yml file specifies four commonly used filters:

ITT:
  title: Intent to Treat Population
  condition: ITTFL =='Y'
  target: adsl
  type: slref
SAS:
  title: Secondary Analysis Set
  condition: SASFL == 'Y'
  target: adsl
  type: slref
SE:
  title: Safety Evaluable Population
  condition: SAFFL=='Y'
  target: adsl
  type: slref
SER:
  title: Serious Adverse Events
  condition: AESER == 'Y'
  target: adae
  type: anl

Functions

An overview of all {autoslider.core} functions can be found here. Custom functions can be created if the built-in functions do not meet your needs.

Backend Machinery

A typical workflow involves defining paths to the YAML files, loading the filters, reading the data, creating the outputs based on the specifications, and decorating the outputs with titles and footnotes. Example code for setting up the workflow:

# define path to the yml files
spec_file <- "spec.yml"
filters <- "filters.yml"
library("dplyr")
# load all filters
filters::load_filters(filters, overwrite = TRUE)
# read data
data <- list(
  "adsl" = eg_adsl %>%
    mutate(
      FASFL = SAFFL, # add FASFL for illustrative purpose for t_pop_slide
      # DISTRTFL is needed for t_ds_slide but is missing in example data
      DISTRTFL = sample(c("Y", "N"), size = length(TRT01A), replace = TRUE, prob = c(.1, .9))
    ) %>%
    preprocess_t_ds(), # this preproccessing is required by one of the autoslider.core functions
  "adae" = eg_adae,
  "adtte" = eg_adtte,
  "adrs" = eg_adrs,
  "adlb" = eg_adlb
)

# create outputs based on the specs and the functions
outputs <- spec_file %>%
  read_spec() %>%
  filter_spec(., program %in% c("t_ds_slide", "t_dm_slide")) %>%
  generate_outputs(datasets = data) %>%
  decorate_outputs(version_label = NULL)
✔ 2/3 outputs matched the filter condition `program %in% c("t_ds_slide", "t_dm_slide")`.
❯ Running program `t_ds_slide` with suffix 'ITT'.
Filter 'ITT' matched target ADSL.
400/400 records matched the filter condition `ITTFL == 'Y'`.
❯ Running program `t_dm_slide` with suffix 'ITT'.
Filter 'ITT' matched target ADSL.
400/400 records matched the filter condition `ITTFL == 'Y'`.

Example of saving outputs to a slide:

# Output to slides with template and color theme
outputs %>%
  generate_slides(
    outfile = tempfile(fileext = ".pptx"),
    template = file.path(system.file(package = "autoslider.core"), "/theme/basic.pptx"),
    table_format = autoslider_format
  )
[1] " Patient Disposition (Intent to Treat Population)"
[1] " Patient Disposition (Intent to Treat Population) (cont.)"
[1] " Patient Demographics and Baseline Characteristics, Intent to Treat Population"
[1] " Patient Demographics and Baseline Characteristics, Intent to Treat Population (cont.)"
[1] " Patient Demographics and Baseline Characteristics, Intent to Treat Population (cont.)"
[1] " Patient Demographics and Baseline Characteristics, Intent to Treat Population (cont.)"

Writing Custom Functions

For study-specific outputs not covered by {autoslider.core} functions, you can create custom functions. Custom functions need to return either a {ggplot2} (for plots), {rtables} (for tables), or {rlistings} or data.frame objects (for listings).

Example custom function:

lbt06 <- function(datasets) {
  adsl <- datasets$adsl %>% tern::df_explicit_na()
  adlb <- datasets$adlb %>% tern::df_explicit_na()

  adlb_f <- adlb %>%
    dplyr::filter(ABLFL != "Y") %>%
    dplyr::filter(!(AVISIT %in% c("SCREENING", "BASELINE"))) %>%
    dplyr::mutate(AVISIT = droplevels(AVISIT)) %>%
    formatters::var_relabel(AVISIT = "Visit")

  adlb_f_crp <- adlb_f %>% dplyr::filter(PARAMCD == "CRP")

  split_fun <- rtables::drop_split_levels

  lyt <- rtables::basic_table(show_colcounts = TRUE) %>%
    rtables::split_cols_by("ARM") %>%
    rtables::split_rows_by("AVISIT", split_fun = split_fun, label_pos = "topleft", split_label = formatters::obj_label(adlb_f_crp$AVISIT)) %>%
    tern::count_abnormal_by_baseline("ANRIND", abnormal = c(Low = "LOW", High = "HIGH"), .indent_mods = 4L) %>%
    tern::append_varlabels(adlb_f_crp, "ANRIND", indent = 1L) %>%
    rtables::append_topleft("    Baseline Status")

  result <- rtables::build_table(lyt = lyt, df = adlb_f_crp, alt_counts_df = adsl) %>%
    rtables::trim_rows()

  result
}

Testing the custom function:

lbt06(data)
Visit                                                                                  
  Analysis Reference Range Indicator     A: Drug X        B: Placebo     C: Combination
    Baseline Status                       (N=134)          (N=134)          (N=132)    
———————————————————————————————————————————————————————————————————————————————————————
WEEK 1 DAY 8                                                                           
  Low                                                                                  
            Not low                    16/119 (13.4%)   22/113 (19.5%)   24/112 (21.4%)
            Low                         2/15 (13.3%)     2/21 (9.5%)       7/20 (35%)  
            Total                      18/134 (13.4%)   24/134 (17.9%)   31/132 (23.5%)
  High                                                                                 
            Not high                   21/114 (18.4%)   20/112 (17.9%)   17/115 (14.8%)
            High                         2/20 (10%)      4/22 (18.2%)     3/17 (17.6%) 
            Total                      23/134 (17.2%)   24/134 (17.9%)   20/132 (15.2%)
WEEK 2 DAY 15                                                                          
  Low                                                                                  
            Not low                    26/119 (21.8%)   20/113 (17.7%)   12/112 (10.7%)
            Low                         2/15 (13.3%)     3/21 (14.3%)      4/20 (20%)  
            Total                      28/134 (20.9%)   23/134 (17.2%)   16/132 (12.1%)
  High                                                                                 
            Not high                   15/114 (13.2%)   17/112 (15.2%)    15/115 (13%) 
            High                         2/20 (10%)      4/22 (18.2%)     4/17 (23.5%) 
            Total                      17/134 (12.7%)   21/134 (15.7%)   19/132 (14.4%)
WEEK 3 DAY 22                                                                          
  Low                                                                                  
            Not low                    15/119 (12.6%)   21/113 (18.6%)   18/112 (16.1%)
            Low                             0/15         3/21 (14.3%)         0/20     
            Total                      15/134 (11.2%)   24/134 (17.9%)   18/132 (13.6%)
  High                                                                                 
            Not high                   22/114 (19.3%)   18/112 (16.1%)   17/115 (14.8%)
            High                         2/20 (10%)      5/22 (22.7%)     1/17 (5.9%)  
            Total                      24/134 (17.9%)   23/134 (17.2%)   18/132 (13.6%)
WEEK 4 DAY 29                                                                          
  Low                                                                                  
            Not low                    30/119 (25.2%)   13/113 (11.5%)   16/112 (14.3%)
            Low                          3/15 (20%)      2/21 (9.5%)       5/20 (25%)  
            Total                      33/134 (24.6%)   15/134 (11.2%)   21/132 (15.9%)
  High                                                                                 
            Not high                   17/114 (14.9%)   11/112 (9.8%)    16/115 (13.9%)
            High                         2/20 (10%)      6/22 (27.3%)     3/17 (17.6%) 
            Total                      19/134 (14.2%)   17/134 (12.7%)   19/132 (14.4%)
WEEK 5 DAY 36                                                                          
  Low                                                                                  
            Not low                    17/119 (14.3%)   19/113 (16.8%)   16/112 (14.3%)
            Low                         2/15 (13.3%)     3/21 (14.3%)      5/20 (25%)  
            Total                      19/134 (14.2%)   22/134 (16.4%)   21/132 (15.9%)
  High                                                                                 
            Not high                   19/114 (16.7%)   17/112 (15.2%)   11/115 (9.6%) 
            High                         4/20 (20%)      6/22 (27.3%)     2/17 (11.8%) 
            Total                      23/134 (17.2%)   23/134 (17.2%)   13/132 (9.8%) 

To use the custom function within the {autoslider.core} workflow, ensure it is available in the global environment:

source("programs/output_functions.R")

With the correct specs.yml and filters.yml, integrate the custom function into the general workflow:

filters <- "filters.yml"
spec_file <- "specs.yml"
filters::load_filters(filters, overwrite = TRUE)
outputs <- spec_file %>%
  read_spec() %>%
  generate_outputs(data) %>%
  decorate_outputs()

outputs$lbt06_ITT_LBCRP_LBNOBAS
outputs <- spec_file %>%
  read_spec() %>%
  generate_outputs(data) %>%
  decorate_outputs()
❯ Running program `t_ds_slide` with suffix 'ITT'.
Filter 'ITT' matched target ADSL.
400/400 records matched the filter condition `ITTFL == 'Y'`.
❯ Running program `t_dm_slide` with suffix 'ITT'.
Filter 'ITT' matched target ADSL.
400/400 records matched the filter condition `ITTFL == 'Y'`.
❯ Running program `lbt06` with suffix 'ITT_LBCRP_LBNOBAS'.
Filter 'ITT' matched target ADSL.
400/400 records matched the filter condition `ITTFL == 'Y'`.
Filters 'LBCRP', 'LBNOBAS' matched target ADLB.
2000/8400 records matched the filter condition `PARAMCD == 'CRP' & (ABLFL != 'Y' & !(AVISIT %in% c('SCREENING', 'BASELINE')))`.
outputs$lbt06_ITT_LBCRP_LBNOBAS
An object of class "dVTableTree"
Slot "tbl":
 Patient Disposition (Intent to Treat Population)

———————————————————————————————————————————————————————————————————————————————————————
Visit                                                                                  
  Analysis Reference Range Indicator     A: Drug X        B: Placebo     C: Combination
    Baseline Status                       (N=134)          (N=134)          (N=132)    
———————————————————————————————————————————————————————————————————————————————————————
WEEK 1 DAY 8                                                                           
  Low                                                                                  
            Not low                    16/119 (13.4%)   22/113 (19.5%)   24/112 (21.4%)
            Low                         2/15 (13.3%)     2/21 (9.5%)       7/20 (35%)  
            Total                      18/134 (13.4%)   24/134 (17.9%)   31/132 (23.5%)
  High                                                                                 
            Not high                   21/114 (18.4%)   20/112 (17.9%)   17/115 (14.8%)
            High                         2/20 (10%)      4/22 (18.2%)     3/17 (17.6%) 
            Total                      23/134 (17.2%)   24/134 (17.9%)   20/132 (15.2%)
WEEK 2 DAY 15                                                                          
  Low                                                                                  
            Not low                    26/119 (21.8%)   20/113 (17.7%)   12/112 (10.7%)
            Low                         2/15 (13.3%)     3/21 (14.3%)      4/20 (20%)  
            Total                      28/134 (20.9%)   23/134 (17.2%)   16/132 (12.1%)
  High                                                                                 
            Not high                   15/114 (13.2%)   17/112 (15.2%)    15/115 (13%) 
            High                         2/20 (10%)      4/22 (18.2%)     4/17 (23.5%) 
            Total                      17/134 (12.7%)   21/134 (15.7%)   19/132 (14.4%)
WEEK 3 DAY 22                                                                          
  Low                                                                                  
            Not low                    15/119 (12.6%)   21/113 (18.6%)   18/112 (16.1%)
            Low                             0/15         3/21 (14.3%)         0/20     
            Total                      15/134 (11.2%)   24/134 (17.9%)   18/132 (13.6%)
  High                                                                                 
            Not high                   22/114 (19.3%)   18/112 (16.1%)   17/115 (14.8%)
            High                         2/20 (10%)      5/22 (22.7%)     1/17 (5.9%)  
            Total                      24/134 (17.9%)   23/134 (17.2%)   18/132 (13.6%)
WEEK 4 DAY 29                                                                          
  Low                                                                                  
            Not low                    30/119 (25.2%)   13/113 (11.5%)   16/112 (14.3%)
            Low                          3/15 (20%)      2/21 (9.5%)       5/20 (25%)  
            Total                      33/134 (24.6%)   15/134 (11.2%)   21/132 (15.9%)
  High                                                                                 
            Not high                   17/114 (14.9%)   11/112 (9.8%)    16/115 (13.9%)
            High                         2/20 (10%)      6/22 (27.3%)     3/17 (17.6%) 
            Total                      19/134 (14.2%)   17/134 (12.7%)   19/132 (14.4%)
WEEK 5 DAY 36                                                                          
  Low                                                                                  
            Not low                    17/119 (14.3%)   19/113 (16.8%)   16/112 (14.3%)
            Low                         2/15 (13.3%)     3/21 (14.3%)      5/20 (25%)  
            Total                      19/134 (14.2%)   22/134 (16.4%)   21/132 (15.9%)
  High                                                                                 
            Not high                   19/114 (16.7%)   17/112 (15.2%)   11/115 (9.6%) 
            High                         4/20 (20%)      6/22 (27.3%)     2/17 (11.8%) 
            Total                      23/134 (17.2%)   23/134 (17.2%)   13/132 (9.8%) 
———————————————————————————————————————————————————————————————————————————————————————

t_ds footnotes
Confidential and for internal use only
GitHub repository: NA
Git hash: d90795d2a97abda810205d7fd0f0fc335200e9fc

Slot "titles":
 Patient Disposition (Intent to Treat Population)

Slot "footnotes":
[1] "t_ds footnotes"                        
[2] "Confidential and for internal use only"

Slot "paper":
[1] "L6"

Slot "width":
[1] 36 14 14 14

Generate the slides:

filepath <- tempfile(fileext = ".pptx")
generate_slides(outputs, outfile = filepath)
[1] " Patient Disposition (Intent to Treat Population)"
[1] " Patient Disposition (Intent to Treat Population) (cont.)"
[1] " Patient Demographics and Baseline Characteristics, Intent to Treat Population"
[1] " Patient Demographics and Baseline Characteristics, Intent to Treat Population (cont.)"
[1] " Patient Demographics and Baseline Characteristics, Intent to Treat Population (cont.)"
[1] " Patient Demographics and Baseline Characteristics, Intent to Treat Population (cont.)"
[1] " Patient Disposition (Intent to Treat Population)"
[1] " Patient Disposition (Intent to Treat Population) (cont.)"
[1] " Patient Disposition (Intent to Treat Population) (cont.)"
[1] " Patient Disposition (Intent to Treat Population) (cont.)"
[1] " Patient Disposition (Intent to Treat Population) (cont.)"
[1] " Patient Disposition (Intent to Treat Population) (cont.)"
[1] " Patient Disposition (Intent to Treat Population) (cont.)"
[1] " Patient Disposition (Intent to Treat Population) (cont.)"
[1] " Patient Disposition (Intent to Treat Population) (cont.)"
[1] " Patient Disposition (Intent to Treat Population) (cont.)"

We hope this guide helps you get started with {autoslider.core} in pharmaverse. Happy coding!

Last updated

2025-03-17 09:30:58.01543

Details

Reuse

Citation

BibTeX citation:
@online{p._thoma,_joe_zhu2025,
  author = {P. Thoma, Joe Zhu, Stefan},
  title = {Automatic Slide Generation with Autoslider.core},
  date = {2025-03-14},
  url = {https://pharmaverse.github.io/blog/posts/2025-03-14_automatic_s.../automatic_slide_generation_with_{autoslide_r}.html},
  langid = {en}
}
For attribution, please cite this work as:
P. Thoma, Joe Zhu, Stefan. 2025. “Automatic Slide Generation with Autoslider.core.” March 14, 2025. https://pharmaverse.github.io/blog/posts/2025-03-14_automatic_s.../automatic_slide_generation_with_{autoslide_r}.html.