How to make nice publishable adverse event tables using tidyverse
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("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
## `summarise()` has grouped output by 'trt'. You can override using the `.groups`
## argument.
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)
## `summarise()` has grouped output by 'subjectid'. You can override using the
## `.groups` argument.
## `summarise()` has grouped output by 'trt'. You can override using the `.groups`
## argument.
## `summarise()` has grouped output by 'subjectid', 'trt'. You can override using
## the `.groups` argument.
## `summarise()` has grouped output by 'trt'. You can override using the `.groups`
## argument.
## `summarise()` has grouped output by 'subjectid', 'trt'. You can override using
## the `.groups` argument.
## `summarise()` has grouped output by 'trt'. You can override using the `.groups`
## argument.
## `summarise()` has grouped output by 'subjectid', 'trt'. You can override using
## the `.groups` argument.
## `summarise()` has grouped output by 'trt'. You can override using the `.groups`
## argument.
## `summarise()` has grouped output by 'subjectid', 'trt'. You can override using
## the `.groups` argument.
## `summarise()` has grouped output by 'trt'. You can override using the `.groups`
## argument.
## `summarise()` has grouped output by 'subjectid'. You can override using the
## `.groups` argument.
## `summarise()` has grouped output by 'trt'. You can override using the `.groups`
## argument.
## `summarise()` has grouped output by 'subjectid', 'trt'. You can override using
## the `.groups` argument.
## `summarise()` has grouped output by 'trt'. You can override using the `.groups`
## argument.
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") %>%
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)
## `summarise()` has grouped output by 'subjectid', 'trt', 'N_pat', 'soc'. You can
## override using the `.groups` argument.
## `summarise()` has grouped output by 'trt', 'N_pat', 'soc'. You can override
## using the `.groups` argument.
System Organ Class | Preferred Term | Active (N=81) | Control (N=80) |
---|---|---|---|
Blood and lymphatic system disorders | #Total | [1] 1 (1.2%) | [2] 2 (2.5%) |
Increased tendency to bruise | [1] 1 (1.2%) | ||
Neutropenia | [1] 1 (1.2%) | ||
Thrombocytopenia | [1] 1 (1.2%) | ||
Cardiac disorders | #Total | [2] 2 (2.5%) | [2] 2 (2.5%) |
Palpitations | [2] 2 (2.5%) | [2] 2 (2.5%) | |
Eye disorders | #Total | [2] 2 (2.5%) | [5] 5 (6.2%) |
Dry eye | [2] 2 (2.5%) | ||
Eye irritation | [2] 2 (2.5%) | [1] 1 (1.2%) | |
Vision blurred | [2] 2 (2.5%) | ||
Gastrointestinal disorders | #Total | [42] 32 (39.5%) | [25] 19 (23.8%) |
Abdominal discomfort | [2] 2 (2.5%) | [1] 1 (1.2%) | |
Abdominal pain | [1] 1 (1.2%) | ||
Abdominal pain upper | [1] 1 (1.2%) | [1] 1 (1.2%) | |
Angular cheilitis | [1] 1 (1.2%) | ||
Constipation | [1] 1 (1.2%) | ||
Diarrhoea | [3] 3 (3.7%) | ||
Diverticulum intestinal | [1] 1 (1.2%) | ||
Dyspepsia | [1] 1 (1.2%) | ||
Flatulence | [1] 1 (1.2%) | ||
Gastritis | [2] 2 (2.5%) | ||
Gastrooesophageal reflux disease | [2] 2 (2.5%) | ||
Glossodynia | [1] 1 (1.2%) | ||
Lip ulceration | [1] 1 (1.2%) | ||
Mouth ulceration | [1] 1 (1.2%) | [2] 2 (2.5%) | |
Nausea | [25] 22 (27.2%) | [11] 10 (12.5%) | |
Oral mucosal blistering | [1] 1 (1.2%) | [1] 1 (1.2%) | |
Paraesthesia oral | [1] 1 (1.2%) | ||
Tooth loss | [1] 1 (1.2%) | ||
Vomiting | [4] 4 (4.9%) | ||
General disorders and administration site conditions | #Total | [12] 12 (14.8%) | [12] 11 (13.8%) |
Asthenia | [1] 1 (1.2%) | ||
Fatigue | [4] 4 (4.9%) | [5] 5 (6.2%) | |
Impaired healing | [1] 1 (1.2%) | ||
Influenza like illness | [1] 1 (1.2%) | ||
Infusion site swelling | [1] 1 (1.2%) | ||
Injection site bruising | [1] 1 (1.2%) | ||
Injection site reaction | [1] 1 (1.2%) | ||
Malaise | [1] 1 (1.2%) | ||
Nodule | [1] 1 (1.2%) | ||
Pain | [1] 1 (1.2%) | ||
Pyrexia | [4] 4 (4.9%) | [2] 2 (2.5%) | |
Immune system disorders | #Total | [1] 1 (1.2%) | [2] 2 (2.5%) |
Hypersensitivity | [1] 1 (1.2%) | [2] 2 (2.5%) | |
Infections and infestations | #Total | [19] 18 (22.2%) | [38] 31 (38.8%) |
Borrelia infection | [1] 1 (1.2%) | ||
Bronchitis | [1] 1 (1.2%) | [1] 1 (1.2%) | |
Conjunctivitis | [1] 1 (1.2%) | ||
Conjunctivitis bacterial | [1] 1 (1.2%) | ||
Diverticulitis | [1] 1 (1.2%) | ||
Epididymitis | [1] 1 (1.2%) | ||
Furuncle | [1] 1 (1.2%) | ||
Gastroenteritis | [2] 2 (2.5%) | ||
Gastroenteritis viral | [1] 1 (1.2%) | ||
Gingival abscess | [1] 1 (1.2%) | ||
Herpes virus infection | [1] 1 (1.2%) | ||
Infected skin ulcer | [1] 1 (1.2%) | ||
Influenza | [1] 1 (1.2%) | [2] 2 (2.5%) | |
Localised infection | [1] 1 (1.2%) | [1] 1 (1.2%) | |
Nail bed infection | [1] 1 (1.2%) | ||
Nasopharyngitis | [5] 5 (6.2%) | [11] 10 (12.5%) | |
Oral herpes | [1] 1 (1.2%) | ||
Otitis media | [1] 1 (1.2%) | ||
Respiratory tract infection | [1] 1 (1.2%) | ||
Rhinitis | [1] 1 (1.2%) | ||
Sinusitis | [1] 1 (1.2%) | ||
Tinea versicolour | [1] 1 (1.2%) | ||
Upper respiratory tract infection | [4] 4 (4.9%) | [8] 8 (10%) | |
Urinary tract infection | [1] 1 (1.2%) | [1] 1 (1.2%) | |
Urinary tract infection bacterial | [1] 1 (1.2%) | ||
Injury, poisoning and procedural complications | #Total | [3] 3 (3.7%) | [7] 7 (8.8%) |
Arthropod bite | [2] 2 (2.5%) | ||
Arthropod sting | [1] 1 (1.2%) | ||
Contusion | [1] 1 (1.2%) | ||
Incorrect dose administered | [1] 1 (1.2%) | ||
Joint dislocation | [1] 1 (1.2%) | ||
Limb injury | [1] 1 (1.2%) | ||
Road traffic accident | [1] 1 (1.2%) | ||
Skin wound | [1] 1 (1.2%) | [1] 1 (1.2%) | |
Investigations | #Total | [21] 17 (21%) | [15] 13 (16.2%) |
Alanine aminotransferase increased | [13] 11 (13.6%) | [9] 9 (11.2%) | |
Biopsy prostate | [1] 1 (1.2%) | ||
Blood bilirubin increased | [2] 2 (2.5%) | ||
Blood pressure decreased | [1] 1 (1.2%) | ||
Blood pressure increased | [1] 1 (1.2%) | ||
Blood triglycerides increased | [1] 1 (1.2%) | ||
Chest X-ray abnormal | [1] 1 (1.2%) | ||
Hepatic enzyme increased | [3] 3 (3.7%) | ||
Platelet count decreased | [2] 2 (2.5%) | ||
Transaminases increased | [1] 1 (1.2%) | ||
Weight increased | [1] 1 (1.2%) | ||
Metabolism and nutrition disorders | #Total | [3] 3 (3.8%) | |
Decreased appetite | [1] 1 (1.2%) | ||
Hyperlipidaemia | [1] 1 (1.2%) | ||
Hypertriglyceridaemia | [1] 1 (1.2%) | ||
Musculoskeletal and connective tissue disorders | #Total | [7] 7 (8.6%) | [6] 6 (7.5%) |
Arthralgia | [1] 1 (1.2%) | ||
Back pain | [1] 1 (1.2%) | ||
Groin pain | [1] 1 (1.2%) | ||
Intervertebral disc protrusion | [1] 1 (1.2%) | ||
Musculoskeletal pain | [1] 1 (1.2%) | [1] 1 (1.2%) | |
Neck pain | [2] 2 (2.5%) | ||
Pain in extremity | [2] 2 (2.5%) | [1] 1 (1.2%) | |
Rheumatoid arthritis | [1] 1 (1.2%) | ||
Rotator cuff syndrome | [1] 1 (1.2%) | ||
Neoplasms benign, malignant and unspecified (incl cysts and polyps) | #Total | [1] 1 (1.2%) | |
Malignant melanoma | [1] 1 (1.2%) | ||
Nervous system disorders | #Total | [8] 8 (9.9%) | [17] 15 (18.8%) |
Anosmia | [1] 1 (1.2%) | ||
Dementia | [1] 1 (1.2%) | ||
Dizziness | [2] 2 (2.5%) | [6] 5 (6.2%) | |
Dysgeusia | [1] 1 (1.2%) | [1] 1 (1.2%) | |
Headache | [2] 2 (2.5%) | [4] 4 (5%) | |
Hypoaesthesia | [2] 2 (2.5%) | ||
Muscle contractions involuntary | [1] 1 (1.2%) | ||
Paraesthesia | [2] 2 (2.5%) | ||
Taste disorder | [1] 1 (1.2%) | ||
Tension headache | [1] 1 (1.2%) | ||
Psychiatric disorders | #Total | [2] 2 (2.5%) | [2] 2 (2.5%) |
Anxiety | [1] 1 (1.2%) | ||
Insomnia | [2] 2 (2.5%) | ||
Terminal insomnia | [1] 1 (1.2%) | ||
Renal and urinary disorders | #Total | [1] 1 (1.2%) | [1] 1 (1.2%) |
Dysuria | [1] 1 (1.2%) | ||
Renal mass | [1] 1 (1.2%) | ||
Reproductive system and breast disorders | #Total | [1] 1 (1.2%) | [1] 1 (1.2%) |
Hypomenorrhoea | [1] 1 (1.2%) | ||
Uterine polyp | [1] 1 (1.2%) | ||
Respiratory, thoracic and mediastinal disorders | #Total | [8] 8 (9.9%) | [7] 7 (8.8%) |
Cough | [3] 3 (3.7%) | [2] 2 (2.5%) | |
Dysphonia | [1] 1 (1.2%) | ||
Dyspnoea | [1] 1 (1.2%) | [1] 1 (1.2%) | |
Epistaxis | [2] 2 (2.5%) | ||
Interstitial lung disease | [1] 1 (1.2%) | ||
Nasal discomfort | [1] 1 (1.2%) | ||
Oropharyngeal pain | [2] 2 (2.5%) | ||
Throat tightness | [1] 1 (1.2%) | ||
Skin and subcutaneous tissue disorders | #Total | [14] 12 (14.8%) | [17] 17 (21.2%) |
Acne | [1] 1 (1.2%) | ||
Alopecia | [4] 4 (4.9%) | [2] 2 (2.5%) | |
Blister | [4] 3 (3.7%) | [1] 1 (1.2%) | |
Erythema | [1] 1 (1.2%) | ||
Hyperhidrosis | [1] 1 (1.2%) | ||
Night sweats | [1] 1 (1.2%) | ||
Pain of skin | [1] 1 (1.2%) | ||
Pruritus | [2] 2 (2.5%) | ||
Purpura | [1] 1 (1.2%) | ||
Rash | [2] 2 (2.5%) | [6] 6 (7.5%) | |
Rash erythematous | [1] 1 (1.2%) | ||
Urticaria | [1] 1 (1.2%) | [2] 2 (2.5%) | |
Social circumstances | #Total | [1] 1 (1.2%) | |
Stress at work | [1] 1 (1.2%) | ||
Surgical and medical procedures | #Total | [2] 2 (2.5%) | [1] 1 (1.2%) |
Parathyroidectomy | [1] 1 (1.2%) | ||
Rehabilitation therapy | [1] 1 (1.2%) | ||
Rheumatoid nodule removal | [1] 1 (1.2%) | ||
Vascular disorders | #Total | [2] 2 (2.5%) | [1] 1 (1.2%) |
Hypertension | [2] 2 (2.5%) | [1] 1 (1.2%) | |
NA | #Total | [12] 12 (14.8%) | [7] 6 (7.5%) |
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") %>%
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)
## `summarise()` has grouped output by 'subjectid', 'trt', 'N_pat', 'soc'. You can
## override using the `.groups` argument.
## `summarise()` has grouped output by 'trt', 'N_pat', 'soc'. You can override
## using the `.groups` argument.
System Organ Class | Preferred Term | Active (N=81) | Control (N=80) |
---|---|---|---|
General disorders and administration site conditions | #Total | [1] 1 (1.2%) | |
Pyrexia | [1] 1 (1.2%) | ||
Infections and infestations | #Total | [1] 1 (1.2%) | |
Epididymitis | [1] 1 (1.2%) | ||
Neoplasms benign, malignant and unspecified (incl cysts and polyps) | #Total | [1] 1 (1.2%) | |
Malignant melanoma | [1] 1 (1.2%) | ||
Reproductive system and breast disorders | #Total | [1] 1 (1.2%) | |
Uterine polyp | [1] 1 (1.2%) | ||
Surgical and medical procedures | #Total | [2] 2 (2.5%) | |
Parathyroidectomy | [1] 1 (1.2%) | ||
Rehabilitation therapy | [1] 1 (1.2%) |
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 %>%
group_by(trt) %>%
mutate(N_pat = n_distinct(subjectid)) %>%
group_by(subjectid, trt, N_pat, soc, pt) %>%
summarise(n_ae = n()) %>%
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)
## `summarise()` has grouped output by 'subjectid', 'trt', 'N_pat', 'soc'. You can
## override using the `.groups` argument.
## `summarise()` has grouped output by 'trt', 'N_pat', 'soc'. You can override
## using the `.groups` argument.
Preferred Term | Active (N=81) | Control (N=80) |
---|---|---|
Nausea | [25] 22 (27.2%) | [11] 10 (12.5%) |
Alanine aminotransferase increased | [13] 11 (13.6%) | [9] 9 (11.2%) |
NA | [12] 12 (14.8%) | [7] 6 (7.5%) |
Nasopharyngitis | [5] 5 (6.2%) | [11] 10 (12.5%) |
Upper respiratory tract infection | [4] 4 (4.9%) | [8] 8 (10%) |
Fatigue | [4] 4 (4.9%) | [5] 5 (6.2%) |
Rash | [2] 2 (2.5%) | [6] 6 (7.5%) |
Dizziness | [2] 2 (2.5%) | [6] 5 (6.2%) |
Pyrexia | [4] 4 (4.9%) | [2] 2 (2.5%) |
Headache | [2] 2 (2.5%) | [4] 4 (5%) |
Alopecia | [4] 4 (4.9%) | [2] 2 (2.5%) |