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.