Demographic Tables with flextable


Clinical studies often begin with a summary table of the demographic characteristics of the patients ncluded in the study to demonstrate a balance between treatments and other subgroups. This table is called “Demographic Tables”.

The format is standard and can be realized with flextable in 3 operations.

The three steps in creating these tables are:

  1. summarize the information with the flextable::summarizor() function. It computes a set of statistics for each variable by groups. It returns a data.frame ready to be consumed by flextable::tabulator().
  2. Call flextable::tabulator() to specify how to arrange the rows and columns and how to format the contents.
  3. Finally create the flextable with the as_flextable() function and customize it with the ‘flextable’ functions.

Example data



  border.color = "#AAAAAA", = "Arial",
  font.size = 11, padding = 2, line_spacing = 1.3
adsl <- select(adam_adsl, AGE, SEX, BMIBLGR1, DURDIS, ARM)


First of all, we count the number of rows per treatment group in order to display them in the table later.

arm_cpt <- count(adsl, ARM)

We will use the function flextable::summarizor(). It produces an aggregated data.frame structured for flextable::tabulator().

dat <- summarizor(adsl, by = "ARM") %>% 
  mutate(variable = factor(variable, levels = c("AGE", "SEX", "BMIBLGR1", "DURDIS")))

flextable creation

We want a visual where the treatments are distributed in columns and where the content of the paragraphs is flexible enough to allow the creation of the table.

tab_object <- dat %>% 
    rows = c("variable", "stat"),
    columns = "ARM",
    content_cell = as_paragraph(
        stat = stat, 
        num1 = value1, num2 = value2, 
        cts = cts, pcts = percent 

Name content_cell is not useful in our example, it can only be seen when there are more than one name to display.

Function fmt_2stats() is specially written to work with the output of summarizor().

We get a first table with as_flextable().



This part will allow us to retrieve a set of labels that we will use to replace some texts displayed in the flextable.

The function to use is labelizor(), it takes a simple argument as a named vector, the names are the values to replace, the values are the replacement values.

First we need to prepare the headers of the columns where we want to add the contingency contained in arm_cpt.

arm_cpt <- mutate(arm_cpt, label = paste0(ARM, "\n", "(N=", n, ")"))
arm_labels <- set_names(arm_cpt$label, arm_cpt$ARM)
##                        Placebo           Xanomeline High Dose 
##              "Placebo\n(N=86)" "Xanomeline High Dose\n(N=84)" 
##            Xanomeline Low Dose 
##  "Xanomeline Low Dose\n(N=84)"

We will also retrieve the labels of the columns stored in the original table.

col_labels <- map_chr(adsl, function(x) attr(x, "label"))
##                            AGE                            SEX 
##                          "Age"                          "Sex" 
##                       BMIBLGR1                         DURDIS 
##  "Pooled Baseline BMI Group 1" "Duration of Disease (Months)" 
##                            ARM 
##   "Description of Planned Arm"

We will stack them in a single vector with some other labels manually defined.

labs <- c(
    stat = "",
    mean_sd = "Mean (SD)", median_iqr = "Median (IQR)",
    range = "Range", missing = "Missing",
    AGE = "Age (Years)",
    SEX = "Sex, n (%)"
##                        Placebo           Xanomeline High Dose 
##              "Placebo\n(N=86)" "Xanomeline High Dose\n(N=84)" 
##            Xanomeline Low Dose                            AGE 
##  "Xanomeline Low Dose\n(N=84)"                          "Age" 
##                            SEX                       BMIBLGR1 
##                          "Sex"  "Pooled Baseline BMI Group 1" 
##                         DURDIS                            ARM 
## "Duration of Disease (Months)"   "Description of Planned Arm" 
##                           stat                        mean_sd 
##                             ""                    "Mean (SD)" 
##                     median_iqr                          range 
##                 "Median (IQR)"                        "Range" 
##                        missing                            AGE 
##                      "Missing"                  "Age (Years)" 
##                            SEX 
##                   "Sex, n (%)"
ft <- as_flextable(
    x = tab_object,
    sep_w = 0,
    separate_with = "variable",
    spread_first_col = TRUE) %>%
  labelizor(j = "stat", labels = labs, part = "all") %>% 
    j = tabulator_colnames(
      columns = "content_cell", 
      type = "columns"), 
    labels = labs, part = "all")

We also add a caption and a note at the bottom of the table and some additional settings.

ft <- ft %>%
    caption = as_paragraph(
          "Demographic Characteristics",
          "\nx.x: Study Subject Data"
  ) %>% 
  add_footer_lines("Source: ADaM adsl data frame from r package 'safetyData'") %>% 
  fix_border_issues() %>% 
  set_table_properties(layout = "autofit")


In a Word document

We will prepare the table for exportation in a Word document by adding a header line with the page number. The caption will be auto-numbered and left aligned in the document.

ft %>%
  add_header_lines("Page ") %>%
  append_chunks(i = 1, part = "header", j = 1, as_word_field(x = "Page")) %>% 
    autonum = officer::run_autonum(seq_id = "tab", bkm = "demo_tab", bkm_all = FALSE),
    caption = as_paragraph(
      "Demographic Characteristics",
      "\nx.x: Study Subject Data"
    fp_p = fp_par(text.align = "left", line_spacing = 2),
    align_with_table = FALSE) %>% 

  save_as_docx(path = "adsl.docx")

The resulting Word document can be downloaded here: adsl.docx. A miniature below show the expected document.

miniatures du document Word produit