Skip to Tutorial Content

Data Sets

First, let’s introduce our three data sets for today

The supraclavicular data set contains data on 103 subjects undergoing upper extremity procedures and receiving supraclavicular anesthesia. They were randomly assigned to a combination of ropivacaine and mepivacaine or sequential mepivacaine followed by ropivacaine. The primary outcome is time to onset of 4-nerve sensory block.

The licorice gargle data set contains data on 236 subjects undergoing thoracic surgery with intubation. They were randomly assigned to a gargle of sugar water vs. licorice before anesthesia. The primary outcome is throat pain at 30 minutest post procedure in the PACU.

The blood storage data set contains data on 316 subjects after radical prostatectomy who received blood transfusion within 30 days of their surgery and had available PSA follow-up data. The main exposure of interest was the RBC storage duration time. It is suspected that transfusion of older blood causes immunosuppression and could increase cancer recurrence.

Making Summary Tables

Let’s work on how to make summary tables with three different packages:

  1. {dplyr}
  2. {janitor}
  3. {gtsummary}.

Summarizing with Dplyr

Simple dyplyr grouping and counting

It is simple to get counts by groups with a familiar friend, the {dplyr) package.
The main functions you will use are

  • tally() counts all rows, like n()
  • count(catvars) groups by categorical variable arguments, then counts rows within groups, then ungroups. Essentially, count() combines group_by() and tally() and ungroup() into one function.
  • add_tally() combines mutate() and tally() to add an new column, named n, to your dataframe.
  • add_count combines mutate() and count() to add a new column to your dataframe.

Examine the code chunk below to see an example of counts from the blood dataset by African-American status, Gleason Score category, and Tumor Volume category. You can page through the results to see how many n are in each category. Adding a tally gives you the total n for your denominator.
You can edit the code to change the name of the tally column, with the argument name = "total". Give this a try. You can edit the code to sort the categories in descending order of n, with the argument sort = TRUE (after a comma) in the count function. Give this a try.

blood %>% 
  count(aa, gleason, tumor_vol) %>% 
  add_tally()
## Storing counts in `nn`, as `n` already present in input
## ℹ Use `name = "new_name"` to pick a new name.

Improvements

You can change the name of the tally column, with the argument name = "total". You can also sort the categories in descending order of n, with the argument sort = TRUE (after a comma) in the count function. See the example below

blood %>% 
  count(aa, gleason, tumor_vol, sort = TRUE) %>% 
  add_tally(name = "Total")

Your turn with licorice!

Use the licorice data to get counts across several categories: grouped by gender, preOp_asa, and smoking.
Edit the chunk below to get a table of counts.
If you want to get fancy, try sorting, and adding a named total column with tally().

licorice 
licorice %>% 
  count(gender)
licorice %>% 
  count(gender, preOp_asa, smoking)
licorice %>% 
  count(gender, preOp_asa, smoking, sort = TRUE) %>% 
  add_tally(name = "big_N")

Your turn with supra!

Use the supraclavicular dataset to get counts across several categories: grouped by gender and group and bmi_cat6,
Edit the chunk below to get a table of counts.
If you want to get fancy, try sorting, and adding a named total column with tally().

supra 
supra %>% 
  count(gender)
supra %>% 
  count(gender, group, bmi_cat6)
supra %>% 
  count(gender, group, bmi_cat6, sort = TRUE) %>% 
  add_tally(name = "Denominator")

Fancy formatting

If you want to control the format of your tables, you can transform them into formats used by the {gt} or {flextable} packages.
The gt package works well in HTML and web formats, and has hundreds of formatting options.
The flextable package is a bit more flexible, and includes the ability to output tables to HTML, PDF, MS Word, and Powerpoint.
For either of these packages, you just have to take your output from dplyr (a tibble) and convert it to either gt or flextable formats. This can be done with piping into:

  • gt(), or
  • flextable()

and then you can format to your heart’s content.

The formatting options for gt can be found at https://gt.rstudio.com.
Here is a starter example

supra %>% 
  count(gender, group, bmi_cat6, sort = TRUE) %>% 
  add_tally(name = "Denominator") %>% 
  gt() %>% 
   tab_header(
    title = "Counts by Group",
    subtitle = "From the Supraclavicular Dataset"
  ) %>% 
  tab_options(
   table.background.color = "darkorchid",
   heading.background.color = "yellow"
  ) 
Counts by Group
From the Supraclavicular Dataset
gender group bmi_cat6 n Denominator
male Sequential Normal weight 9 19
female Mixture Normal weight 8 19
female Mixture Overweight 8 19
female Sequential Normal weight 8 19
male Sequential Overweight 8 19
male Mixture Normal weight 7 19
female Mixture Obesity Class I 6 19
female Mixture Obestity Class II 6 19
male Mixture Overweight 6 19
male Sequential Obesity Class I 6 19
female Mixture Extreme Obesity Class III 5 19
female Sequential Obesity Class I 5 19
female Sequential Obestity Class II 5 19
female Sequential Overweight 4 19
male Mixture Obesity Class I 4 19
male Sequential Obestity Class II 4 19
male Mixture NA 2 19
female Sequential Extreme Obesity Class III 1 19
female Sequential NA 1 19


The formatting options for flextable can be found at https://davidgohel.github.io/flextable/.
Here is a starter example

supra %>% 
  count(gender, group, bmi_cat6, sort = TRUE) %>% 
  add_tally(name = "Denominator") %>% 
  flextable() %>% 
  autofit() %>% 
  bold(part = "header") %>% 
  fontsize(part = "header", size = 16) %>% 
  fontsize(part = "body", size = 14) %>% 
  align(align = "center", part = "all") %>% 
  #note conditional formatting in rows when n>7, turn column n purple and bold. Could do this for P <0.05, etc.
  color(color = "purple", i =~ n>7, j =~ n) %>% 
  bold(i =~ n>7, j =~ n)

gender

group

bmi_cat6

n

Denominator

male

Sequential

Normal weight

9

19

female

Mixture

Normal weight

8

19

female

Mixture

Overweight

8

19

female

Sequential

Normal weight

8

19

male

Sequential

Overweight

8

19

male

Mixture

Normal weight

7

19

female

Mixture

Obesity Class I

6

19

female

Mixture

Obestity Class II

6

19

male

Mixture

Overweight

6

19

male

Sequential

Obesity Class I

6

19

female

Mixture

Extreme Obesity Class III

5

19

female

Sequential

Obesity Class I

5

19

female

Sequential

Obestity Class II

5

19

female

Sequential

Overweight

4

19

male

Mixture

Obesity Class I

4

19

male

Sequential

Obestity Class II

4

19

male

Mixture

2

19

female

Sequential

Extreme Obesity Class III

1

19

female

Sequential

1

19

Counting in Tabyls with Janitor

Counts can also be obtained in 2 by 2 tables with the tabyl function from the {janitor} package.
The 3 main functions of {janitor} include

  • tabyl(catvars) which makes tables with 1,2, or 3 variables (when using 3 variables, you end up with a list of tables). Unlike the {base} table() function, tabyl() plays well with tidyverse pipes.
  • clean_names() which consistently formats variable names
  • get_dupes() which helps you find and deal with duplicate rows

Note that there a lot more handy functions in {janitor}, which can be found here:
https://sfirke.github.io/janitor/reference/index.html

In addition to the basic tabyl, you can adorn your table of counts with adorn functions. These include

  • adorn_percentages()
  • adorn_ns() if you also want Ns
  • adorn_pct_formatting() to give you nice looking percentages
  • adorn_totals(“rows”) or adorn_totals(“cols”) for totals
  • adorn_rounding()

Review the example below and its output to see how this works.

blood %>% 
  tabyl(gleason, tumor_vol) 

Simple janitor tabyl with percentages

We can make this fancier with percentages, rather than just counts. See the example below to see how this is done.

blood %>% 
  tabyl(aa, tumor_vol) %>% 
  adorn_percentages() %>% 
  adorn_pct_formatting() %>% 
  adorn_ns()

Your turn with licorice!

Now try this with the licorice dataset, making a table of
gender by smoking. Add nicely formatted percentages and Ns.
licorice
licorice %>% 
  tabyl(gender, smoking)
licorice %>% 
  tabyl(gender, smoking) %>% 
  adorn_percentages() %>% 
  adorn_pct_formatting() %>% 
  adorn_ns()

{janitor} tabyl with Totals and Titles

Here is an example of how to improve a simple 2 x 2 table with the {janitor} package function tabyl by adding totals to rows/columns and titles.

supra %>% 
  tabyl(gender, group) %>% 
  adorn_totals(where = c("row", "col")) %>% 
  adorn_title(row_name = "Gender", 
              col_name = "Group Assigned",
              placement = "combined")

Note that the adorn_title() placement argument can also be “top” - see what this looks like in the chunk below.

supra %>% 
  tabyl(gender, group) %>% 
  adorn_totals(where = c("row", "col")) %>% 
  adorn_title(row_name = "Gender", 
              col_name = "Group Assigned",
              placement = "top")

Your turn with blood!

Make a nicely titled tabyl with totals and titles from the blood datset, with the variables fam_hx and recur.

blood
blood %>% 
  tabyl(fam_hx, recur)
blood %>% 
  tabyl(fam_hx, recur) %>% 
  adorn_totals(where = c("row", "col")) %>% 
  adorn_title(row_name = "Family History", 
              col_name = "Tumor Recurrence",
              placement = "top")

{janitor} tabyl with pct, without NAs

Missing data, or NAs, are included as a category by default. This is a good thing, so that you don’t miss the presence of missing data. But it is problematic if you are trying to do a chi square or fisher exact test. You can just make the tabyl argument show_na false. See the code chunk below for an example.

blood %>% 
  tabyl(gleason, tumor_vol, show_na = F)

Adding chi squared test to a janitor tabyl

Once the NA values are removed, you can then do a chi squared test. Just add one line, as in the code chunk below.
If you have a lot of low-count (<5) cells, you can also use a fisher exact test with fisher.test()

blood %>% 
  tabyl(gleason, tumor_vol, show_na = F) %>% 
  chisq.test()
## 
##  Pearson's Chi-squared test
## 
## data:  .
## X-squared = 43.318, df = 4, p-value = 8.888e-09

Your turn with supra!

Now try this with the supraclavicular data, with a table of group and gender.
Add a chi-squared test.

supra 
supra %>% 
  tabyl(group, gender)
supra %>% 
  tabyl(group, gender) %>% 
  chisq.test()

A Formatting Note for Tabyls

You can format tabyls with {gt} by just piping into the gt() function, then adding formatting options.

supra %>% 
  tabyl(group, gender) %>% 
  gt() %>% 
  tab_header(
    title = "Counts by Group",
    subtitle = "From the Supraclavicular Dataset"
  ) %>% 
  tab_options(
   table.background.color = "goldenrod",
   heading.background.color = "orange"
  ) 
Counts by Group
From the Supraclavicular Dataset
group female male
Mixture 33 19
Sequential 24 27


To format a tabyl with {flextable}, you have to undo the tabyl formatting first, so you pipe into untabyl() and then into flextable() and then you can format with flextable options.

supra %>% 
  tabyl(group, gender) %>% 
  untabyl() %>% 
  flextable() %>% 
  autofit() %>% 
  bold(part = "header") %>% 
  fontsize(part = "header", size = 20) %>% 
  fontsize(part = "body", size = 18) %>% 
  align(align = "center", part = "all") %>% 
  # note conditional formatting in rows when n>7, turn column n purple and bold. Could do this for P <0.05, etc.
  # rows denoted by i, columns denoted by j (as in i,j)
  color(color = "green", i =~ male>20, j =~ male) %>% 
  bold(i =~ male>20, j =~ male)

group

female

male

Mixture

33

19

Sequential

24

27

Using {gtsummary} to Count Groups

gtsummary is a relatively new package based on the gt package, but it is optimized for clinical trial tables.

  • The tbl_summary() function takes the selected table variables and does an auto-summary.
  • The tbl_regression() function does the same for regression tables.

Look at this chunk of code using tbl_summary() and its output. What does each line of code do?

supra %>% 
  select(gender, bmi, age, group) %>% 
  tbl_summary(by = group) 
Characteristic Mixture, N = 521 Sequential, N = 511
gender

    female 33 (63%) 24 (47%)
    male 19 (37%) 27 (53%)
bmi 28.3 (24.5, 34.1) 28.4 (23.8, 32.5)
    Unknown 2 1
age 52 (41, 59) 50 (36, 56)
1 n (%); Median (IQR)

This is a pretty slick and easy summary. You can modify parts of the table by modifying the default arguments to tbl_summary(), including

  • label specify the variable labels printed in table
  • type specify the variable type (e.g. continuous, categorical, etc.)
  • statistic change the summary statistics presented
  • digits number of digits the summary statistics will be rounded to
  • missing whether to display a row with the number of missing observations
  • sort change the sorting of categorical levels by frequency
  • percent print column, row, or cell percentages

You can also add to the table by piping into add functions, including

  • add_p() add p-values to the output comparing values across groups
  • add_overall() add a column with overall summary statistics
  • add_n() add a column with N (or N missing) for each variable
  • add_stat_label() add a column showing a label for the summary statistics shown in each row
  • add_q() add a column of q values to control for multiple comparisons

There are also several functions to format the gtsummary() table including

  • modify_header() relabel columns in summary table
  • bold_labels() bold variable labels
  • bold_levels() bold variable levels
  • italicize_labels() italicize variable labels
  • italicize_levels() italicize variable levels
  • bold_p() bold significant p-values

Here is a fancier example:

licorice %>%
  select(gender, smoking, mallampati, preOp_calcBMI, preOp_age, treat) %>% 
  # build base summary table
  tbl_summary(
    # split table by treatment variable
    by = treat,
    # change variable labels
    label = list(preOp_calcBMI ~ "BMI",
                 preOp_age ~ "Age"),
    # change statistics printed in table
    statistic = list(all_continuous() ~ "{mean} ({sd})",
                     all_categorical() ~ "{n} / {N} ({p}%)"),
    digits = list("preOp_age" ~ c(1, 2))
  ) %>%
  # add p-values, report t-test, round large pvalues to two decimal place
  add_p(test = list(preOp_calcBMI ~ "t.test"),
                 pvalue_fun = function(x) style_pvalue(x, digits = 2)) %>%
  # add statistic labels
  add_stat_label() %>%
  # bold variable labels, italicize levels
  bold_labels() %>%
  italicize_levels() %>%
  # bold p-values under a given threshold (default is 0.05)
  bold_p(t = 0.52) %>%
  # include percent in headers
  modify_header(all_stat_cols(FALSE) ~"**{level}**, N = {n} ({style_percent(p, symbol = TRUE)})")
Characteristic Sugar, N = 117 (50%) Licorice, N = 118 (50%) p-value1
gender, n / N (%)

0.54
    Male 73 / 117 (62%) 69 / 118 (58%)
    Female 44 / 117 (38%) 49 / 118 (42%)
smoking, n / N (%)

>0.99
    Current 45 / 117 (38%) 45 / 118 (38%)
    Past 36 / 117 (31%) 36 / 118 (31%)
    Never 36 / 117 (31%) 37 / 118 (31%)
mallampati, n / N (%)

0.53
    1 31 / 117 (26%) 39 / 118 (33%)
    2 69 / 117 (59%) 66 / 118 (56%)
    3 16 / 117 (14%) 13 / 118 (11%)
    4 1 / 117 (0.9%) 0 / 118 (0%)
BMI, Mean (SD) 25.6 (4.2) 25.6 (4.3) 0.93
Age, Mean (SD) 58.0 (16.08) 56.7 (14.86) 0.50
1 Pearson’s Chi-squared test; Fisher’s exact test; Welch Two Sample t-test; Wilcoxon rank sum test

More Formatting

Note that if you want to use the formatting functions of gt or flextable, these are also options. Pipe your result into the functions

  • as_gt() OR
  • as_flextable()

Note - one possible gotcha - if you load the package flextable after the package gtsummary, you will need to specify which version of as_flextable() you want, as each has a function by that name. The easiest way to do this is to use gtsummary::as_flextable() so that R knows which version of the function that you want to use.

Your Turn with blood!

Now try gtsummary on the blood data set, using the variables age, aa, tumor_vol, and recur, divided by rbc_age categories, in the exercise below (Note - don’t eliminate rbc_age with your select statement)

blood 
blood %>% 
  select(Age, aa, tumor_vol, rbc_age, recur)
blood %>% 
  select(Age, aa, tumor_vol, rbc_age, recur) %>% 
  tbl_summary(by = rbc_age) %>% 
  bold_labels() %>%
  italicize_levels() %>%
  add_p() 

Adding p values for Comparisons with {gtsummary}

You can add comparative P values easily, see the chunk below (and its output) for an example.

supra %>% 
  select(gender, bmi, age, group) %>% 
  tbl_summary(by = group) %>% 
  add_p()
Characteristic Mixture, N = 521 Sequential, N = 511 p-value2
gender

0.094
    female 33 (63%) 24 (47%)
    male 19 (37%) 27 (53%)
bmi 28.3 (24.5, 34.1) 28.4 (23.8, 32.5) 0.5
    Unknown 2 1
age 52 (41, 59) 50 (36, 56) 0.2
1 n (%); Median (IQR)
2 Pearson’s Chi-squared test; Wilcoxon rank sum test

Your turn!

Now try this yourself on the blood dataset. Add p values for this comparison.
Which predictor variables do you think will be significant?

blood %>% 
  select(Age, aa, FamHx, recur, rbc_age) %>% 
  tbl_summary(by = recur,
  label = list(aa ~ "African-American",
                FamHx ~ "Family History of Prostate Ca",
               rbc_age ~ "Age of Transfused RBC"))
blood %>% 
  select(Age, aa, FamHx, recur, rbc_age) %>% 
  tbl_summary(by = recur,
  label = list(aa ~ "African-American",
                FamHx ~ "Family History of Prostate Ca",
               rbc_age ~ "Age of Transfused RBC")) %>% 
  add_p() %>% 
  bold_p(t = 0.10)

Your turn with licorice!

Now try this yourself on the licorice dataset. Add p values for this comparison.
Which predictor variables do you think will be significant?

licorice  %>% 
  select(gender, smoking, mallampati, treat, preOp_age) %>% 
  tbl_summary(by = treat)
licorice  %>% 
  select(gender, smoking, mallampati, treat, preOp_age) %>% 
  tbl_summary(by = treat) %>% 
  add_p() %>% 
  bold_p(t = 0.10)

Regression tables with gtsummary

You can easily summarize a linear regression with the {gtsummary} package function tbl_regression()

Review the chunk below for an example. Longer time to onset of anesthesia (more minutes) is a bad outcome. Which variables favor a worse outcome? Are any significant?

supra_model_tbl <- lm(onset_sensory ~ gender + group + midazolam + age + subject + bmi_cat6, 
                      data = supra) %>% 
  tbl_regression() 

supra_model_tbl

Your turn!

Try to summarize in a table this model (in the blood dataset) for recurrence of prostate cancer with tbl_summary from the {gtsummary} package. Be sure to add the exponentiate = TRUE option to get odds ratios when you use tbl_summary()

model_recur_tbl <- glm(Recurrence ~ aa + fam_hx + rbc_age + tumor_vol + gleason,
          data = blood)

model_recur_tbl
model_recur_tbl <- glm(Recurrence ~ aa + fam_hx + rbc_age + tumor_vol + gleason,
                   data = blood) %>% 
   tbl_regression() 


model_recur_tbl
model_recur_tbl <- glm(Recurrence ~ aa + fam_hx + rbc_age + tumor_vol + gleason,
                   data = blood) %>% 
   tbl_regression(exponentiate = TRUE) 


model_recur_tbl

Building up side by side tables

You can also combine tables with the tbl_merge function from {gtsummary} Here is an example of a summary table and a modeling table.

Quick Summary Table with gtsummary

First, this code chunk makes the summary table.

summ_tbl1 <- licorice %>% 
  select(gender, preOp_age, treat) %>% 
  tbl_summary(missing = "no") %>% 
  add_n()
summ_tbl1
Characteristic N N = 2351
gender 235
    Male
142 (60%)
    Female
93 (40%)
preOp_age 235 62 (47, 68)
treat 235
    Sugar
117 (50%)
    Licorice
118 (50%)
1 n (%); Median (IQR)

Regression table with gtsummary

Then this code chunk builds the model, mod_tbl2,
and the tbl_regression() function makes the regression table

mod_tbl2 <- lm(pacu30min_throatPain ~ gender + smoking + mallampati + treat, data = licorice) %>% 
  tbl_regression()
mod_tbl2
Characteristic Beta 95% CI1 p-value
gender


    Male — —
    Female -0.23 -0.55, 0.09 0.2
smoking


    Current — —
    Past 0.50 0.13, 0.87 0.009
    Never 0.07 -0.30, 0.45 0.7
mallampati


    1 — —
    2 0.23 -0.12, 0.57 0.2
    3 0.10 -0.42, 0.61 0.7
    4 -1.1 -3.4, 1.3 0.4
treat


    Sugar — —
    Licorice -0.74 -1.0, -0.44 <0.001
1 CI = Confidence Interval

Now merge these tables

Then you can merge these and add two tab spanners.

tbl_merge(
  list(summ_tbl1, mod_tbl2),
  tab_spanner = c("**Summary Statistics**",
                  "**30 Min Cough Outcome**")
)
Characteristic Summary Statistics 30 Min Cough Outcome
N N = 2351 Beta 95% CI2 p-value
gender 235



    Male
142 (60%) — —
    Female
93 (40%) -0.23 -0.55, 0.09 0.2
preOp_age 235 62 (47, 68)


treat 235



    Sugar
117 (50%) — —
    Licorice
118 (50%) -0.74 -1.0, -0.44 <0.001
smoking




    Current

— —
    Past

0.50 0.13, 0.87 0.009
    Never

0.07 -0.30, 0.45 0.7
mallampati




    1

— —
    2

0.23 -0.12, 0.57 0.2
    3

0.10 -0.42, 0.61 0.7
    4

-1.1 -3.4, 1.3 0.4
1 n (%); Median (IQR)
2 CI = Confidence Interval

Your turn!

  1. Make a summary table from the supra dataset - group, gender, bmi, and age
  2. Make a regression summary model from the blood dataset with the outcome of onset_motor, and the predictors group, gender, bmi, age, and fentanyl
  3. use tbl_merge and some appropriate spanner text to merge these tables.
summ_tbl_supra


regress_tbl_supra


merged_tbl_blood
summ_tbl_supra <- supra %>% 
  select(group, gender, bmi, age)

regress_tbl_supra <- lm(onset_motor ~ group + gender + bmi + age +fentanyl, data = supra)


merged_tbl_blood <-  tbl_merge(list(summ_tbl_supra, regress_tbl_supra))
summ_tbl_supra <- supra %>% 
  select(group, gender, bmi, age) %>% 
  tbl_summary() %>% 
  add_n()
summ_tbl_supra


regress_tbl_supra <- lm(onset_motor ~ group + gender + bmi + age +fentanyl, data = supra) %>% 
  tbl_regression()
regress_tbl_supra

merged_tbl_blood <-  tbl_merge(list(summ_tbl_supra, regress_tbl_supra),
            tab_spanner = c("**Summary Statistics**",
                  "**Onset Motor Outcome**"))
merged_tbl_blood

Final Quizzes

Dplyr Quiz
Janitor Tabyl Quiz
gtsummary Quiz

Table Exercises