Real-world effectiveness and safety of risankizumab in patients with moderate-to-severe multi-refractory Crohn’s disease: a Belgian multi-centric cohort study

Author

Dahham Alsoud

Published

August 20, 2023

Modified

March 10, 2024

The aim of this analysis notebook is to reproduce the reported analysis in the paper Real-world effectiveness and safety of risankizumab in patients with moderate-to-severe multi-refractory Crohn's disease: a Belgian multi-centric cohort study.

To run the analysis, we assume that you already have the raw data file, named realworld-risankizumab-belgium.xlsx, in the input folder, within the home directory of your project.

This raw data file can be obtained upon reasonable request to the corresponding author, Marc Ferrante, via marc.ferrante@uzleuven.be.

1. Install and load needed packages, and write an S3 method for printing tables

if (!require(pacman)) install.packages("pacman")
pacman::p_load(here, rio, magrittr, janitor, tidyverse, rstatix, gtsummary, huxtable,flextable, ggsurvfit, survival)

devtools::install_github("DahhamAlsoud/phdcocktail")
devtools::install_github("MSKCC-Epi-Bio/bstfun")

pacman::p_load(bstfun, phdcocktail)
print.data.frame <- function(df) {
  df %>% flextable::flextable() %>% flextable::align(part = "all", align = "center") %>% flextable::autofit()
}

print.tabyl <- function(df) {
  df %>% flextable::flextable() %>% flextable::align(part = "all", align = "center") %>% flextable::autofit()
}

print.tbl <- function(df) {
  df %>% flextable::flextable() %>% flextable::align(part = "all", align = "center") %>% flextable::autofit()
}

2. Import raw data

We will now import the raw data. There are 3 rows for each patient: baseline, week 24 and week 52. We will filter the dataframe to only the “baseline” rows as these contain the baseline characteristics, and the outcomes. We will also filter out excluded patients.

input_file <- import(here("input", "realworld-risankizumab-belgium.xlsx")) %>% clean_names() 
input_file_baseline <- input_file[input_file$visit_timepoint == "baseline" & input_file$excluded == 0 ,]

3. Pre-process raw data and get general info about the included cohort

Pre-process variables

We will transform some variables and generate some new ones which we will need later to construct our Table 1, or to be reported in the results/abstract.

continuous_vrs <- c("age_yrs", "age_at_diagnosis_yrs", "disease_duration_yrs", "bmi_kg_m2", "crp_mg_l", "calprotectin_ug_g", "ses_cd")

input_file_baseline %<>% mutate(across(all_of(continuous_vrs), ~ round(as.numeric(.), 1)))

input_file_baseline %<>% mutate(female_gender = if_else(gender == "F", 1, 0), .after = gender)

input_file_baseline %<>% mutate(current_smoker = if_else(smoking == 1, 1, 0), .after = smoking)

input_file_baseline %<>% mutate(age_montreal = case_when(
  age_at_diagnosis_yrs < 17 ~ "A1",
  age_at_diagnosis_yrs > 40 ~ "A3",
  .default = "A2"
), .after = age_at_diagnosis_yrs)

input_file_baseline %<>% mutate(concomitant_cs = case_when(
  concomitant_cs == "0" ~ "no",
  str_detect(concomitant_cs, regex("medrol", ignore_case = TRUE)) ~ "sys",
  .default = "top"
))

input_file_baseline %<>% mutate(concomitant_imm = if_else(concomitant_imm == "0", 0, 1))

input_file_baseline %<>% mutate(concomitant_5asa = if_else(concomitant_5asa == "0", 0, 1))

input_file_baseline %<>% mutate(concomitant_advanced = if_else(concomitant_advanced == "0", 0, 1))

input_file_baseline %<>% mutate(three_advanced = if_else(amount_previous_advanced > 2, 1, 0), .after = amount_previous_advanced)

input_file_baseline %<>% mutate(four_advanced = if_else(amount_previous_advanced > 3, 1, 0), .after = amount_previous_advanced)

input_file_baseline %<>% mutate(five_advanced = if_else(amount_previous_advanced > 4, 1, 0), .after = amount_previous_advanced)

input_file_baseline %<>% mutate(any_resection_before = if_else(previous_ileocaecal_resection == 1 | other_surgery_before == 1, 1, 0), .after = previous_ileocaecal_resection)

Get general info about the included cohort

calculate number of included/excluded patients

input_file %>% 
  filter(visit_timepoint == "baseline") %>%
  tabyl(excluded) %>%
  adorn_pct_formatting(digits = 2) %>% print()

excluded

n

percent

0

69

93.24%

1

5

6.76%

calculate number of included patients with/without ostomy

input_file_baseline %>% 
  tabyl(stoma_at_start) %>%
  adorn_pct_formatting(digits = 2) %>% print()

stoma_at_start

n

percent

0

55

79.71%

1

14

20.29%

calculate % female in the included cohort

input_file_baseline %>% 
  tabyl(gender) %>%
  adorn_pct_formatting(digits = 2) %>% print()

gender

n

percent

F

39

56.52%

M

30

43.48%

calculate median age for the included cohort

print(report_quantiles(input_file_baseline, summary_vrs = "age_yrs"))
median (IQR) age_yrs was 37.2 (29.5-49)

4. Present baseline characteristics (i.e. make Table 1)

We will define which variables we would like to include in our Table 1, recode them, and finally present them.

tableone_df <- input_file_baseline %>%
  select(stoma_at_start, female_gender, age_yrs, disease_duration_yrs, current_smoker, bmi_kg_m2, crp_mg_l, calprotectin_ug_g, ses_cd, age_montreal,
         disease_location, upper_gi_disease, disease_behaviour, peri_anal_disease, current_fibrostenosing, current_penetrating, any_resection_before,
         concomitant_cs, concomitant_imm, concomitant_5asa, concomitant_advanced, three_advanced, four_advanced, five_advanced, previous_ustekinumab, reason_stop_ust)

tableone_df$stoma_at_start <- if_else(tableone_df$stoma_at_start == 0, "Without an ostomy", "With an ostomy") %>% factor(., levels = c("Without an ostomy", "With an ostomy"))

categorical_vrs = c("age_montreal", "disease_location", "disease_behaviour", "concomitant_cs", "reason_stop_ust")

tableone_df_recoded <- recode_vrs(tableone_df, data_dictionary = ibd_data_dict, vrs = categorical_vrs, factor = TRUE)

dichotomous_vrs <- base::setdiff(names(tableone_df), c(categorical_vrs, continuous_vrs))[-1]


theme_gtsummary_journal(journal = "jama")
theme_gtsummary_compact()

tableone <- tableone_df_recoded %>% 
  tbl_summary(by = stoma_at_start,
              type = list(any_of(continuous_vrs) ~ "continuous",
                          any_of(categorical_vrs) ~ "categorical",
                          any_of(dichotomous_vrs) ~ "dichotomous"),
              missing_text = "missing, n",
              digits = list(all_categorical() ~ c(0, 1))) %>%
  modify_header(label = "**Baseline characteristic**",
                stat_1 = "**Without an ostomy<br />(n = 55)**",
                stat_2 = "**With an ostomy<br />(n = 14)**") 

tableone <- tableone %>%
  add_variable_grouping(
    "Previous number of advanced therapies, n (%)" = c("three_advanced", "four_advanced", "five_advanced")
  )

labels_to_levels <- c("upper_gi_disease", "peri_anal_disease", "three_advanced", "four_advanced", "five_advanced")

tableone_body <- tableone[["table_body"]]

tableone_body$row_type[tableone_body$variable %in% labels_to_levels] <- "level"

tableone_body$stat_label[tableone_body$variable %in% labels_to_levels] <- NA

tableone_body <- tableone_body[tableone_body$label != "None",]

tableone_body <- tableone_body[-which(tableone_body$variable == "reason_stop_ust" & tableone_body$row_type == "missing"),]

tableone[["table_body"]]  <- tableone_body

tableone <- tableone %>%
  bold_labels() %>%
  italicize_levels() 

tableone
Baseline characteristic Without an ostomy
(n = 55)
With an ostomy
(n = 14)
Female, n (%) 32 (58.2) 7 (50.0)
Age (year), Median (IQR) 39 (30 – 50) 34 (26 – 43)
Disease duration (year), Median (IQR) 17 (10 – 24) 16 (11 – 21)
Current smoking, n (%) 8 (14.8) 2 (14.3)
    missing, n 1 0
Body mass index (kg/m²), Median (IQR) 23.6 (19.7 – 27.0) 21.7 (18.9 – 24.1)
C-reactive protein (mg/L), Median (IQR) 10 (4 – 25) 21 (8 – 52)
    missing, n 6 1
Faecal calprotectin (ug/g), Median (IQR) 955 (484 – 1,800) 1,038 (656 – 1,419)
    missing, n 30 12
Simple endoscopic score for Crohn's disease, Median (IQR) 10 (7 – 16) 11 (8 – 19)
    missing, n 19 5
Age at diagnosis (year), n (%)
    < 17 years 18 (32.7) 8 (57.1)
    17-40 years 30 (54.5) 6 (42.9)
    > 40 years 7 (12.7) 0 (0.0)
Disease location, n (%)
    Ileal 7 (12.7) 1 (7.1)
    Colonic 9 (16.4) 2 (14.3)
    Ileocolonic 39 (70.9) 11 (78.6)
    Upper GI modifier 11 (20.0) 3 (21.4)
Disease behaviour, n (%)
    Inflammatory 18 (32.7) 3 (21.4)
    Fibrostenotic 25 (45.5) 7 (50.0)
    Penetrating 12 (21.8) 4 (28.6)
    Perianal modifier 24 (43.6) 10 (71.4)
Current fibrostenosing complications, n (%) 10 (18.2) 4 (28.6)
Current penetrating complications, n (%) 1 (1.8) 1 (7.1)
Previous CD-related intestinal resection, n (%) 33 (60.0) 8 (57.1)
Concomitant corticosteroids, n (%)
    Topical 8 (14.5) 1 (7.1)
    Systemic 10 (18.2) 1 (7.1)
Concomitant immunomodulators, n (%) 6 (10.9) 3 (21.4)
Concomitant 5-aminosalicylates, n (%) 2 (3.6) 0 (0.0)
Concomitant advanced therapies, n (%) 3 (5.5) 2 (14.3)
Previous number of advanced therapies, n (%)
        ≥ 3 52 (94.5) 14 (100.0)
        ≥ 4 46 (83.6) 13 (92.9)
        ≥ 5 16 (29.1) 5 (35.7)
Previous ustekinumab, n (%) 54 (98.2) 14 (100.0)
Reason for ustekinumab discontinuation, n (%)
    Loss of response 34 (63.0) 8 (57.1)
    Primary non-response 20 (37.0) 3 (21.4)
    Adverse events 0 (0.0) 3 (21.4)

5. Survival analysis & dose optimizations

generate few “time” variables which we will need for further calculations

input_file_baseline %<>% mutate(persistence_duration = round(as.numeric(date_stop_or_surgery - visit_date, "weeks"), 1), .after = date_stop_or_surgery)

input_file_baseline %<>% mutate(time_to_optimize = round(as.numeric(date_dose_optimization - visit_date, "weeks"), 1), .after = date_dose_optimization)

input_file_baseline %<>% mutate(followup_duration = round(as.numeric(date_last_infusion - visit_date, "weeks"), 1), .after = date_last_infusion)

calculate follow-up duration

print(report_quantiles(input_file_baseline, summary_vrs = "followup_duration"))
median (IQR) followup_duration was 68.3 (51.9-98.4)

check how many patients stopped risankizumab and why

input_file_baseline %>% 
  tabyl(stop_current_treatment, reason_stop) %>% print()

stop_current_treatment

LOF

LOR

PNR

NA_

0

0

0

0

54

1

2

7

6

0

calculate time to stop in patients with PNR/LOR

print(report_quantiles(input_file_baseline[input_file_baseline$reason_stop %in% c("LOR", "PNR"),], summary_vrs = "persistence_duration"))
median (IQR) persistence_duration was 43.1 (29.6-55.9)

check how many of these stops were during the first 52 weeks of follow-up

input_file_baseline %>% 
  filter(stop_current_treatment == 1) %>%
  count(reason_stop, during_w52 = followup_duration <=52) %>% print()

reason_stop

during_w52

n

LOF

TRUE

2

LOR

FALSE

4

LOR

TRUE

3

PNR

TRUE

6

Survival analysis

construct KM curve

persistence_km <- survfit2(Surv(persistence_duration, stop_or_surgery) ~ 1, data = input_file_baseline) %>%
  ggsurvfit(linewidth = 1) +
  add_risktable() +
  add_censor_mark() +
  coord_cartesian(xlim = c(0,54), ylim = c(0.45, 1)) +
  labs(x = "Follow-up time (weeks)", y = "Surgery-free survival percentage") +
  scale_y_continuous(label = scales::percent, 
                     breaks = seq(0, 1, by = 0.1),
                     expand = c(0.01, 0)) +
  scale_x_continuous(breaks = seq(0, 54, by = 4), 
                     expand = c(0.012, 0)) +
  add_quantile(linetype = 2, x_value = 24) +
  add_quantile(linetype = 2, x_value = 52) +
  theme(panel.grid = element_blank(),
        axis.title = element_text(face = "bold"))

persistence_km

calculate survival probabilities

tbl_survfit <- survfit(Surv(persistence_duration, stop_or_surgery) ~ 1, input_file_baseline) %>%
  tbl_survfit(
  times = c(24, 52),
  label_header = "**Week {time}**",
  estimate_fun = function(x) style_percent(x, symbol = TRUE, digits = 1)
)

tbl_survfit
Characteristic Week 24 Week 52
Overall 92.8% (86.8% to 99.1%) 75.2% (65.6% to 86.1%)

Dose optimizations

check how many patients needed optimizations

input_file_baseline %>%
  tabyl(dose_optimization) %>%
  adorn_pct_formatting(digits = 2) %>% print()

dose_optimization

n

percent

0

50

72.46%

1

19

27.54%

calculate time to optimizations

print(report_quantiles(input_file_baseline, summary_vrs = "time_to_optimize"))
median (IQR) time_to_optimize was 48.7 (33.5-61.5)

check how many of these optimizations were during the first 52 weeks of follow-up

input_file_baseline %>% 
  filter(dose_optimization == 1) %>%
  count(during_w52 = time_to_optimize <= 52) %>% print()

during_w52

n

FALSE

7

TRUE

12

6. Calculate and plot outcome data

in patients without ostomy

for clinical outcomes

calculate…

clinical_outcomes_without <- input_file_baseline %>%
  filter(stoma_at_start == 0) %>%
  select(clinical_outome_w24_itt, steroid_dependent_w24, clinical_outome_w52_itt, steroid_dependent_w52) %>%
  mutate(clinical_response_w24 = if_else(clinical_outome_w24_itt != "pnr", 1, 0),
         clinical_remission_w24 = if_else(clinical_outome_w24_itt == "rem", 1, 0),
         clinical_response_w52 = if_else(clinical_outome_w52_itt != "pnr", 1, 0),
         clinical_remission_w52 = if_else(clinical_outome_w52_itt == "rem", 1, 0), .keep = "unused")

clinical_outcomes_without %<>%
  mutate(across(all_of(c("clinical_response_w24", "clinical_remission_w24")), ~ if_else(steroid_dependent_w24 == 1, 0, .), .names = "csfree_{.col}"),
          .after = clinical_remission_w24) %>%
  mutate(across(all_of(c("clinical_response_w52", "clinical_remission_w52")), ~ if_else(steroid_dependent_w52 == 1, 0, .), .names = "csfree_{.col}"),
          .after = clinical_remission_w52) %>%
  select(matches("clinical")) 

clinical_outcomes_without_long <- clinical_outcomes_without %>%
  summarise(across(matches("clinical"), ~ sum(. == 1, na.rm = T))) %>%
  pivot_longer(cols = everything(),names_to = "outcome", values_to = "achieved") %>%
  mutate(total= nrow(clinical_outcomes_without)) %>%
  mutate(timepoint = if_else(str_detect(outcome, "24"), "Week 24", "Week 52"), .after = outcome) %>%
  mutate(outcome = str_remove(outcome, "_w(\\d*)") %>%
                   str_replace_all(.,"_", " ") %>%
                   str_replace(., "csfree", "steroid-free") %>%
                   str_to_sentence()
         ) %>%
  mutate(proportion = round_half_up(achieved/total, 3),
         percentage = proportion*100,
         percentage_labelled = scales::percent(proportion))


clinical_outcomes_without_long %<>% mutate(timepoint = factor(timepoint, levels = c("Week 24", "Week 52"))) %>%
  mutate(outcome = factor(outcome, levels = c("Clinical response", "Steroid-free clinical response", "Clinical remission", "Steroid-free clinical remission")))

clinical_outcomes_without_long %>% print()

outcome

timepoint

achieved

total

proportion

percentage

percentage_labelled

Clinical response

Week 24

36

55

0.655

65.5

65.5%

Clinical remission

Week 24

10

55

0.182

18.2

18.2%

Steroid-free clinical response

Week 24

34

55

0.618

61.8

61.8%

Steroid-free clinical remission

Week 24

10

55

0.182

18.2

18.2%

Clinical response

Week 52

33

55

0.600

60.0

60.0%

Clinical remission

Week 52

15

55

0.273

27.3

27.3%

Steroid-free clinical response

Week 52

32

55

0.582

58.2

58.2%

Steroid-free clinical remission

Week 52

15

55

0.273

27.3

27.3%

then plot!

clinical_bars <- plot_bars(clinical_outcomes_without_long,outcome = outcome, proportion = proportion, percentage_labelled = percentage_labelled,
           achieved = achieved, total = total, grouping = timepoint)

for endoscopic outcomes

get the number of patients who had both baseline and follow-up endoscopy

input_file_baseline %<>%
  mutate(baseANDfollowup_endoscopy = if_else(endoscopy_baseline == 1 & endoscopy_w24orw52 == 1, 1, 0), .after = endoscopy_w24orw52)


input_file_baseline %>%
  group_by(stoma_at_start) %>%
  summarise(baseline_endoscopy = sum(endoscopy_baseline == 1),
            baseANDfollowup_endoscopy = sum(baseANDfollowup_endoscopy == 1)) %>% print()

stoma_at_start

baseline_endoscopy

baseANDfollowup_endoscopy

0

42

32

1

12

6

calculate…

endoscopic_outcomes_without <- input_file_baseline %>%
  filter(stoma_at_start == 0, baseANDfollowup_endoscopy == 1) %>%
  select(endoscopic_outcome_w24orw52) %>%
  mutate(endoscopic_response = if_else(endoscopic_outcome_w24orw52 != "pnr", 1, 0),
         endoscopic_remission = if_else(endoscopic_outcome_w24orw52 == "rem", 1, 0), .keep = "unused")


endoscopic_outcomes_without_long <- endoscopic_outcomes_without %>%
  summarise(across(matches("endoscopic"), ~ sum(. == 1, na.rm = T))) %>%
  pivot_longer(cols = everything(),names_to = "outcome", values_to = "achieved") %>%
  mutate(total= nrow(endoscopic_outcomes_without)) %>%
  mutate(outcome = str_replace_all(outcome,"_", " ") %>%
           str_to_sentence()
  ) %>%
  mutate(proportion = round_half_up(achieved/total, 3),
         percentage = proportion*100,
         percentage_labelled = scales::percent(proportion))


endoscopic_outcomes_without_long$outcome <- factor(endoscopic_outcomes_without_long$outcome, levels = c("Endoscopic response", "Endoscopic remission")) 

endoscopic_outcomes_without_long %>% print()

outcome

achieved

total

proportion

percentage

percentage_labelled

Endoscopic response

16

32

0.500

50.0

50%

Endoscopic remission

2

32

0.063

6.3

6%

in patients with ostomy

for clinical outcomes

calculate…

clinical_outcomes_with<- input_file_baseline %>%
  filter(stoma_at_start == 1) %>%
  select(clinical_outome_w24_itt, steroid_dependent_w24, clinical_outome_w52_itt, steroid_dependent_w52) %>%
  mutate(clinical_response_w24 = if_else(clinical_outome_w24_itt != "pnr", 1, 0),
         clinical_remission_w24 = if_else(clinical_outome_w24_itt == "rem", 1, 0),
         clinical_response_w52 = if_else(clinical_outome_w52_itt != "pnr", 1, 0),
         clinical_remission_w52 = if_else(clinical_outome_w52_itt == "rem", 1, 0), .keep = "unused")

clinical_outcomes_with%<>%
  mutate(across(all_of(c("clinical_response_w24", "clinical_remission_w24")), ~ if_else(steroid_dependent_w24 == 1, 0, .), .names = "csfree_{.col}"),
         .after = clinical_remission_w24) %>%
  mutate(across(all_of(c("clinical_response_w52", "clinical_remission_w52")), ~ if_else(steroid_dependent_w52 == 1, 0, .), .names = "csfree_{.col}"),
         .after = clinical_remission_w52) %>%
  select(matches("clinical")) 

clinical_outcomes_with_long <- clinical_outcomes_with%>%
  summarise(across(matches("clinical"), ~ sum(. == 1, na.rm = T))) %>%
  pivot_longer(cols = everything(),names_to = "outcome", values_to = "achieved") %>%
  mutate(total= nrow(clinical_outcomes_with)) %>%
  mutate(timepoint = if_else(str_detect(outcome, "24"), "Week 24", "Week 52"), .after = outcome) %>%
  mutate(outcome = str_remove(outcome, "_w(\\d*)") %>%
           str_replace_all(.,"_", " ") %>%
           str_replace(., "csfree", "steroid-free") %>%
           str_to_sentence()
  ) %>%
  mutate(proportion = round_half_up(achieved/total, 3),
         percentage = proportion*100,
         percentage_labelled = scales::percent(proportion))


clinical_outcomes_with_long %<>% mutate(timepoint = factor(timepoint, levels = c("Week 24", "Week 52"))) %>%
  mutate(outcome = factor(outcome, levels = c("Clinical response", "Steroid-free clinical response", "Clinical remission", "Steroid-free clinical remission")))

clinical_outcomes_with_long %>% print()

outcome

timepoint

achieved

total

proportion

percentage

percentage_labelled

Clinical response

Week 24

7

14

0.500

50.0

50.0%

Clinical remission

Week 24

1

14

0.071

7.1

7.1%

Steroid-free clinical response

Week 24

7

14

0.500

50.0

50.0%

Steroid-free clinical remission

Week 24

1

14

0.071

7.1

7.1%

Clinical response

Week 52

6

14

0.429

42.9

42.9%

Clinical remission

Week 52

2

14

0.143

14.3

14.3%

Steroid-free clinical response

Week 52

6

14

0.429

42.9

42.9%

Steroid-free clinical remission

Week 52

2

14

0.143

14.3

14.3%

for endoscopic outcomes

get the number of patients who had both baseline and follow-up endoscopy

input_file_baseline %>%
  group_by(stoma_at_start) %>%
  summarise(baseline_endoscopy = sum(endoscopy_baseline == 1),
            baseANDfollowup_endoscopy = sum(baseANDfollowup_endoscopy == 1)) %>% print()

stoma_at_start

baseline_endoscopy

baseANDfollowup_endoscopy

0

42

32

1

12

6

calculate…

endoscopic_outcomes_with <- input_file_baseline %>%
  filter(stoma_at_start == 1, baseANDfollowup_endoscopy == 1) %>%
  select(endoscopic_outcome_w24orw52) %>%
  mutate(endoscopic_response = if_else(endoscopic_outcome_w24orw52 != "pnr", 1, 0),
         endoscopic_remission = if_else(endoscopic_outcome_w24orw52 == "rem", 1, 0), .keep = "unused")


endoscopic_outcomes_with_long <- endoscopic_outcomes_with %>%
  summarise(across(matches("endoscopic"), ~ sum(. == 1, na.rm = T))) %>%
  pivot_longer(cols = everything(),names_to = "outcome", values_to = "achieved") %>%
  mutate(total= nrow(endoscopic_outcomes_with)) %>%
  mutate(outcome = str_replace_all(outcome,"_", " ") %>%
           str_to_sentence()
  ) %>%
  mutate(proportion = round_half_up(achieved/total, 3),
         percentage = proportion*100,
         percentage_labelled = scales::percent(proportion))


endoscopic_outcomes_with_long$outcome <- factor(endoscopic_outcomes_with_long$outcome, levels = c("Endoscopic response", "Endoscopic remission"))

endoscopic_outcomes_with_long %>% print()

outcome

achieved

total

proportion

percentage

percentage_labelled

Endoscopic response

4

6

0.667

66.7

67%

Endoscopic remission

4

6

0.667

66.7

67%