How to make nice publishable adverse event tables using tidyverse
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
This blog post is just an answer to a colleague to provide R code for the generation of Adverse Event tables. And it is also nice to have the code available when I need it in the future. Probably I will pull my hair at the horrible code, but this gives room to enhance it later.
Functions
First I define all functions to be used. I reuse some of the ideas in the post where I show how to make publishable tables using purrr here.
ae_n_pct <- function(data, var, group, level = 1) {
var <- ensym(var)
group <- ensym(group)
data %>%
group_by(subjectid, !!group, !!var) %>%
summarise(n = sum(!!var)) %>%
group_by(!!group, !!var) %>%
summarise(n_ae = sum(n),
n_pat = n()) %>%
group_by(!!group) %>%
mutate(N_pat = sum(n_pat),
pct = round(n_pat/N_pat*100,digits = 1),
txt = paste0(n_pat, " (", pct, "%)")) %>%
filter(!!var %in% !!level) %>%
ungroup %>%
select(!!group, txt) %>%
deframe
}
ae_N_n_pct <- function(data, var, group, level = 1) {
var <- ensym(var)
group <- ensym(group)
data %>%
group_by(subjectid, !!group) %>%
summarise(n = sum(!!var)) %>%
mutate(!!var := if_else(n==0, 0, 1)) %>%
group_by(!!group, !!var) %>%
summarise(n_ae = sum(n),
n_pat = n()) %>%
group_by(!!group) %>%
mutate(N_pat = sum(n_pat),
pct = round(n_pat/N_pat*100,digits = 1),
txt = paste0("[", n_ae,"] ", n_pat, " (", pct, "%)")) %>%
mutate(txt = if_else(n_ae == 0, "[0] 0 (0%)", txt)) %>%
filter(!!var %in% !!level) %>%
ungroup %>%
select(!!group, txt) %>%
deframe
}
stats_exec <- function(f, data, var, group, ...){
exec(f, data, var, group, !!!(...))
}
Then I do a bit of data wrangling. The mock-up data can be downloaded in .rds format here
adae <- readRDS("static/datasets/adae.rds") %>%
mutate(anyae = if_else(is.na(pt), 0, 1),
sae = if_else(is.na(pt), 0, sae)
) %>%
group_by(subjectid) %>%
mutate(n_ae = sum(anyae),
one_ae = n_ae == 1,
two_ae = n_ae == 2,
three_plus_ae = n_ae > 2,
anysae = max(sae)) %>%
ungroup
Summary of Adverse Events
The summary of Adverse Events is a nice table just summing up the adverse events in the trial. Note the “[N] n (%)”-format which is the number of events, number of patients with events and percentage of patients with event.
arms <- c("Active", "Control")
total_n <- n_distinct(adae$subjectid)
header_ae <- adae %>%
group_by(trt, subjectid) %>%
summarise(n=n()) %>%
group_by(trt) %>%
summarise(n = n()) %>%
ungroup() %>%
mutate(armtxt = arms) %>%
mutate(txt = paste0(armtxt, " (N=", n, ")")) %>%
select(txt) %>%
deframe
ae_summary_table <- tribble(
~text, ~var, ~f,
"Number of AEs", "anyae", "ae_N_n_pct",
"Number of patients with any AEs?", "anyae", "ae_n_pct",
"Number of patients with one AE", "one_ae", "ae_n_pct",
"Number of patients with two AE", "two_ae", "ae_n_pct",
"Number of patients with three or more AEs", "three_plus_ae", "ae_n_pct",
"Number of SAEs", "sae", "ae_N_n_pct",
"Number of patients with any SAEs?", "anysae","ae_n_pct"
)
ae_summary_table %>%
mutate(data = list(adae),
group = "trt",
param = list(level = 1)) %>%
mutate(res = pmap(list(f, data, var, group, param), stats_exec)) %>%
mutate(id = map(res,names)) %>%
unnest(c(res, id)) %>%
mutate(id = paste0("txt", id)) %>%
pivot_wider(values_from = res, names_from = id) %>%
select(text, starts_with("txt")) %>%
kable(col.names = c("Parameter", header_ae),
caption = "Summary of Adverse Events",
booktabs = TRUE)
| Parameter | Active (N=81) | Control (N=80) |
|---|---|---|
| Number of AEs | [149] 74 (91.4%) | [165] 79 (98.8%) |
| Number of patients with any AEs? | 74 (86%) | 79 (92.9%) |
| Number of patients with one AE | 14 (17.3%) | 12 (15%) |
| Number of patients with two AE | 13 (16%) | 16 (20%) |
| Number of patients with three or more AEs | 49 (60.5%) | 52 (65%) |
| Number of SAEs | [4] 4 (4.9%) | [2] 2 (2.5%) |
| Number of patients with any SAEs? | 6 (7.4%) | 6 (7.5%) |
Adverse Events by System Organ Class and Preferred term
This table is almost a listing, but it gives a nice overview of all Adverse Events in the trial. First we need to make function which does most of the work.
ae_table_fns <- function(data, filtervar){
filtervar = ensym(filtervar)
data %>%
group_by(trt) %>%
mutate(N_pat = n_distinct(subjectid)) %>%
filter(!!filtervar == 1) %>%
group_by(subjectid, trt, N_pat, soc, pt) %>%
summarise(n_ae = n()) %>%
filter(!is.na(pt)) %>%
group_by(trt, N_pat, soc, pt) %>%
summarise(n_pat = n(),
n_ae = sum(n_ae)) %>%
mutate(pct = round(n_pat/N_pat*100,digits = 1),
txt = paste0("[", n_ae,"] ", n_pat, " (", pct, "%)"),
arm = paste0("arm", trt)) %>%
ungroup %>% select(arm, soc, pt, txt) %>%
pivot_wider(values_from = txt, names_from = arm) %>%
mutate_at(vars(starts_with("arm")), ~if_else(is.na(.), "", .)) %>%
arrange(soc, pt) %>% group_by(soc2 = soc) %>%
mutate(soc = if_else(row_number() != 1, "", soc)) %>% ungroup() %>% select(-soc2)
}
adae %>%
bind_rows(adae, .id="added") %>%
filter(!is.na(pt)) %>%
mutate(pt = if_else(added == 2, "#Total", pt)) %>%
mutate(all = 1) %>%
ae_table_fns("all") %>%
knitr::kable(col.names = c("System Organ Class", "Preferred Term", header_ae),
caption = " Adverse Events by System Organ Class and Preferred term*",
booktabs = TRUE,
longtable = TRUE)
| System Organ Class | Preferred Term | Active (N=81) | Control (N=80) |
|---|---|---|---|
| Blood and lymphatic system disorders | #Total | [1] 1 (1.4%) | [2] 2 (2.5%) |
| Increased tendency to bruise | [1] 1 (1.3%) | ||
| Neutropenia | [1] 1 (1.4%) | ||
| Thrombocytopenia | [1] 1 (1.3%) | ||
| Cardiac disorders | #Total | [2] 2 (2.7%) | [2] 2 (2.5%) |
| Palpitations | [2] 2 (2.7%) | [2] 2 (2.5%) | |
| Eye disorders | #Total | [2] 2 (2.7%) | [5] 5 (6.3%) |
| Dry eye | [2] 2 (2.5%) | ||
| Eye irritation | [2] 2 (2.7%) | [1] 1 (1.3%) | |
| Vision blurred | [2] 2 (2.5%) | ||
| Gastrointestinal disorders | #Total | [42] 32 (43.2%) | [25] 19 (24.1%) |
| Abdominal discomfort | [2] 2 (2.7%) | [1] 1 (1.3%) | |
| Abdominal pain | [1] 1 (1.4%) | ||
| Abdominal pain upper | [1] 1 (1.4%) | [1] 1 (1.3%) | |
| Angular cheilitis | [1] 1 (1.4%) | ||
| Constipation | [1] 1 (1.3%) | ||
| Diarrhoea | [3] 3 (4.1%) | ||
| Diverticulum intestinal | [1] 1 (1.4%) | ||
| Dyspepsia | [1] 1 (1.3%) | ||
| Flatulence | [1] 1 (1.3%) | ||
| Gastritis | [2] 2 (2.5%) | ||
| Gastrooesophageal reflux disease | [2] 2 (2.5%) | ||
| Glossodynia | [1] 1 (1.3%) | ||
| Lip ulceration | [1] 1 (1.3%) | ||
| Mouth ulceration | [1] 1 (1.4%) | [2] 2 (2.5%) | |
| Nausea | [25] 22 (29.7%) | [11] 10 (12.7%) | |
| Oral mucosal blistering | [1] 1 (1.4%) | [1] 1 (1.3%) | |
| Paraesthesia oral | [1] 1 (1.4%) | ||
| Tooth loss | [1] 1 (1.4%) | ||
| Vomiting | [4] 4 (5.4%) | ||
| General disorders and administration site conditions | #Total | [12] 12 (16.2%) | [12] 11 (13.9%) |
| Asthenia | [1] 1 (1.3%) | ||
| Fatigue | [4] 4 (5.4%) | [5] 5 (6.3%) | |
| Impaired healing | [1] 1 (1.4%) | ||
| Influenza like illness | [1] 1 (1.3%) | ||
| Infusion site swelling | [1] 1 (1.4%) | ||
| Injection site bruising | [1] 1 (1.3%) | ||
| Injection site reaction | [1] 1 (1.3%) | ||
| Malaise | [1] 1 (1.3%) | ||
| Nodule | [1] 1 (1.4%) | ||
| Pain | [1] 1 (1.4%) | ||
| Pyrexia | [4] 4 (5.4%) | [2] 2 (2.5%) | |
| Immune system disorders | #Total | [1] 1 (1.4%) | [2] 2 (2.5%) |
| Hypersensitivity | [1] 1 (1.4%) | [2] 2 (2.5%) | |
| Infections and infestations | #Total | [19] 18 (24.3%) | [38] 31 (39.2%) |
| Borrelia infection | [1] 1 (1.3%) | ||
| Bronchitis | [1] 1 (1.4%) | [1] 1 (1.3%) | |
| Conjunctivitis | [1] 1 (1.3%) | ||
| Conjunctivitis bacterial | [1] 1 (1.3%) | ||
| Diverticulitis | [1] 1 (1.3%) | ||
| Epididymitis | [1] 1 (1.3%) | ||
| Furuncle | [1] 1 (1.4%) | ||
| Gastroenteritis | [2] 2 (2.7%) | ||
| Gastroenteritis viral | [1] 1 (1.3%) | ||
| Gingival abscess | [1] 1 (1.3%) | ||
| Herpes virus infection | [1] 1 (1.3%) | ||
| Infected skin ulcer | [1] 1 (1.4%) | ||
| Influenza | [1] 1 (1.4%) | [2] 2 (2.5%) | |
| Localised infection | [1] 1 (1.4%) | [1] 1 (1.3%) | |
| Nail bed infection | [1] 1 (1.3%) | ||
| Nasopharyngitis | [5] 5 (6.8%) | [11] 10 (12.7%) | |
| Oral herpes | [1] 1 (1.3%) | ||
| Otitis media | [1] 1 (1.4%) | ||
| Respiratory tract infection | [1] 1 (1.3%) | ||
| Rhinitis | [1] 1 (1.3%) | ||
| Sinusitis | [1] 1 (1.4%) | ||
| Tinea versicolour | [1] 1 (1.3%) | ||
| Upper respiratory tract infection | [4] 4 (5.4%) | [8] 8 (10.1%) | |
| Urinary tract infection | [1] 1 (1.4%) | [1] 1 (1.3%) | |
| Urinary tract infection bacterial | [1] 1 (1.3%) | ||
| Injury, poisoning and procedural complications | #Total | [3] 3 (4.1%) | [7] 7 (8.9%) |
| Arthropod bite | [2] 2 (2.5%) | ||
| Arthropod sting | [1] 1 (1.3%) | ||
| Contusion | [1] 1 (1.3%) | ||
| Incorrect dose administered | [1] 1 (1.4%) | ||
| Joint dislocation | [1] 1 (1.4%) | ||
| Limb injury | [1] 1 (1.3%) | ||
| Road traffic accident | [1] 1 (1.3%) | ||
| Skin wound | [1] 1 (1.4%) | [1] 1 (1.3%) | |
| Investigations | #Total | [21] 17 (23%) | [15] 13 (16.5%) |
| Alanine aminotransferase increased | [13] 11 (14.9%) | [9] 9 (11.4%) | |
| Biopsy prostate | [1] 1 (1.4%) | ||
| Blood bilirubin increased | [2] 2 (2.5%) | ||
| Blood pressure decreased | [1] 1 (1.4%) | ||
| Blood pressure increased | [1] 1 (1.4%) | ||
| Blood triglycerides increased | [1] 1 (1.3%) | ||
| Chest X-ray abnormal | [1] 1 (1.3%) | ||
| Hepatic enzyme increased | [3] 3 (4.1%) | ||
| Platelet count decreased | [2] 2 (2.5%) | ||
| Transaminases increased | [1] 1 (1.4%) | ||
| Weight increased | [1] 1 (1.4%) | ||
| Metabolism and nutrition disorders | #Total | [3] 3 (3.8%) | |
| Decreased appetite | [1] 1 (1.3%) | ||
| Hyperlipidaemia | [1] 1 (1.3%) | ||
| Hypertriglyceridaemia | [1] 1 (1.3%) | ||
| Musculoskeletal and connective tissue disorders | #Total | [7] 7 (9.5%) | [6] 6 (7.6%) |
| Arthralgia | [1] 1 (1.4%) | ||
| Back pain | [1] 1 (1.4%) | ||
| Groin pain | [1] 1 (1.3%) | ||
| Intervertebral disc protrusion | [1] 1 (1.4%) | ||
| Musculoskeletal pain | [1] 1 (1.4%) | [1] 1 (1.3%) | |
| Neck pain | [2] 2 (2.5%) | ||
| Pain in extremity | [2] 2 (2.7%) | [1] 1 (1.3%) | |
| Rheumatoid arthritis | [1] 1 (1.4%) | ||
| Rotator cuff syndrome | [1] 1 (1.3%) | ||
| Neoplasms benign, malignant and unspecified (incl cysts and polyps) | #Total | [1] 1 (1.4%) | |
| Malignant melanoma | [1] 1 (1.4%) | ||
| Nervous system disorders | #Total | [8] 8 (10.8%) | [17] 15 (19%) |
| Anosmia | [1] 1 (1.4%) | ||
| Dementia | [1] 1 (1.4%) | ||
| Dizziness | [2] 2 (2.7%) | [6] 5 (6.3%) | |
| Dysgeusia | [1] 1 (1.4%) | [1] 1 (1.3%) | |
| Headache | [2] 2 (2.7%) | [4] 4 (5.1%) | |
| Hypoaesthesia | [2] 2 (2.5%) | ||
| Muscle contractions involuntary | [1] 1 (1.4%) | ||
| Paraesthesia | [2] 2 (2.5%) | ||
| Taste disorder | [1] 1 (1.3%) | ||
| Tension headache | [1] 1 (1.3%) | ||
| Psychiatric disorders | #Total | [2] 2 (2.7%) | [2] 2 (2.5%) |
| Anxiety | [1] 1 (1.4%) | ||
| Insomnia | [2] 2 (2.5%) | ||
| Terminal insomnia | [1] 1 (1.4%) | ||
| Renal and urinary disorders | #Total | [1] 1 (1.4%) | [1] 1 (1.3%) |
| Dysuria | [1] 1 (1.3%) | ||
| Renal mass | [1] 1 (1.4%) | ||
| Reproductive system and breast disorders | #Total | [1] 1 (1.4%) | [1] 1 (1.3%) |
| Hypomenorrhoea | [1] 1 (1.4%) | ||
| Uterine polyp | [1] 1 (1.3%) | ||
| Respiratory, thoracic and mediastinal disorders | #Total | [8] 8 (10.8%) | [7] 7 (8.9%) |
| Cough | [3] 3 (4.1%) | [2] 2 (2.5%) | |
| Dysphonia | [1] 1 (1.3%) | ||
| Dyspnoea | [1] 1 (1.4%) | [1] 1 (1.3%) | |
| Epistaxis | [2] 2 (2.7%) | ||
| Interstitial lung disease | [1] 1 (1.3%) | ||
| Nasal discomfort | [1] 1 (1.3%) | ||
| Oropharyngeal pain | [2] 2 (2.7%) | ||
| Throat tightness | [1] 1 (1.3%) | ||
| Skin and subcutaneous tissue disorders | #Total | [14] 12 (16.2%) | [17] 17 (21.5%) |
| Acne | [1] 1 (1.4%) | ||
| Alopecia | [4] 4 (5.4%) | [2] 2 (2.5%) | |
| Blister | [4] 3 (4.1%) | [1] 1 (1.3%) | |
| Erythema | [1] 1 (1.4%) | ||
| Hyperhidrosis | [1] 1 (1.4%) | ||
| Night sweats | [1] 1 (1.3%) | ||
| Pain of skin | [1] 1 (1.3%) | ||
| Pruritus | [2] 2 (2.5%) | ||
| Purpura | [1] 1 (1.3%) | ||
| Rash | [2] 2 (2.7%) | [6] 6 (7.6%) | |
| Rash erythematous | [1] 1 (1.3%) | ||
| Urticaria | [1] 1 (1.4%) | [2] 2 (2.5%) | |
| Social circumstances | #Total | [1] 1 (1.3%) | |
| Stress at work | [1] 1 (1.3%) | ||
| Surgical and medical procedures | #Total | [2] 2 (2.7%) | [1] 1 (1.3%) |
| Parathyroidectomy | [1] 1 (1.4%) | ||
| Rehabilitation therapy | [1] 1 (1.4%) | ||
| Rheumatoid nodule removal | [1] 1 (1.3%) | ||
| Vascular disorders | #Total | [2] 2 (2.7%) | [1] 1 (1.3%) |
| Hypertension | [2] 2 (2.7%) | [1] 1 (1.3%) |
Serious Adverse Events by System Organ Class and Preferred term
Then the sub-table with only the serious adverse events.
adae %>%
bind_rows(adae, .id="added") %>%
filter(!is.na(pt)) %>%
mutate(pt = if_else(added == 2, "#Total", pt)) %>%
ae_table_fns("sae") %>%
knitr::kable( col.names = c("System Organ Class", "Preferred Term", header_ae),
caption = "Serious Adverse Events by System Organ Class and Preferred term*",
booktabs = TRUE,
longtable = TRUE)
| System Organ Class | Preferred Term | Active (N=81) | Control (N=80) |
|---|---|---|---|
| General disorders and administration site conditions | #Total | [1] 1 (1.4%) | |
| Pyrexia | [1] 1 (1.4%) | ||
| Infections and infestations | #Total | [1] 1 (1.3%) | |
| Epididymitis | [1] 1 (1.3%) | ||
| Neoplasms benign, malignant and unspecified (incl cysts and polyps) | #Total | [1] 1 (1.4%) | |
| Malignant melanoma | [1] 1 (1.4%) | ||
| Reproductive system and breast disorders | #Total | [1] 1 (1.3%) | |
| Uterine polyp | [1] 1 (1.3%) | ||
| Surgical and medical procedures | #Total | [2] 2 (2.7%) | |
| Parathyroidectomy | [1] 1 (1.4%) | ||
| Rehabilitation therapy | [1] 1 (1.4%) |
Usually there is also a table of probable/possible study treatment related AE/SAEs, and maybe also a AE/SAE of special interest table. They are made similarly to the SAE table.
Most common Adverse events
Lastly a table of the most common Adverse events. It is easy to change the treshold.
adae %>%
filter(!is.na(pt)) %>%
group_by(trt) %>%
mutate(N_pat = n_distinct(subjectid)) %>%
group_by(subjectid, trt, N_pat, soc, pt) %>%
summarise(n_ae = n()) %>%
filter(!is.na(pt)) %>%
group_by(trt, N_pat, soc, pt) %>%
summarise(n_pat = n(),
n_ae = sum(n_ae)) %>%
mutate(pct = round(n_pat/N_pat*100,digits = 1),
txt = paste0("[", n_ae,"] ", n_pat, " (", pct, "%)"),
arm = paste0("arm", trt)) %>%
group_by(pt) %>%
mutate(N_pat = sum(n_pat),
pct_tot = N_pat /total_n) %>%
filter(pct_tot>0.05) %>%
ungroup %>%
select(arm, soc, pt, txt, pct_tot) %>%
pivot_wider(values_from = txt, names_from = arm) %>%
mutate_at(vars(starts_with("arm")), ~if_else(is.na(.), "", .)) %>%
arrange(desc(pct_tot)) %>% select( pt, everything()) %>% select(-pct_tot, -soc) %>%
knitr::kable( col.names = c( "Preferred Term", header_ae),
caption = "Most common Adverse Events (more than 5 percent) by Preferred Term",
booktabs = TRUE,
longtable = TRUE)
| Preferred Term | Active (N=81) | Control (N=80) |
|---|---|---|
| Nausea | [25] 22 (29.7%) | [11] 10 (12.7%) |
| Alanine aminotransferase increased | [13] 11 (14.9%) | [9] 9 (11.4%) |
| Nasopharyngitis | [5] 5 (6.8%) | [11] 10 (12.7%) |
| Upper respiratory tract infection | [4] 4 (5.4%) | [8] 8 (10.1%) |
| Fatigue | [4] 4 (5.4%) | [5] 5 (6.3%) |
| Rash | [2] 2 (2.7%) | [6] 6 (7.6%) |
| Dizziness | [2] 2 (2.7%) | [6] 5 (6.3%) |
| Pyrexia | [4] 4 (5.4%) | [2] 2 (2.5%) |
| Headache | [2] 2 (2.7%) | [4] 4 (5.1%) |
| Alopecia | [4] 4 (5.4%) | [2] 2 (2.5%) |
R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.