1 Data import and cleaning

Raw data are read directly from the tab-delimited export (row 1 = QA comments, row 2 = variable labels, data from row 3 onward).

## Edit this path to wherever you've saved the file
raw <- read_delim(
  "~/Documents/R & Stats/Analisis/GI_Ethnicity/Robert_abstract.txt", 
  delim = "\t", skip = 2, col_names = FALSE,
  quote = '"', escape_double = TRUE, trim_ws = TRUE,
  col_types = cols(.default = "c")   
  # read everything as text first; convert below
)

var_names <- c(
  "id", "hospital_number", "dob", "date_death", "deceased", "date_last_fup",
  "event_os", "ethnicity_code", "ethnicity_detail", "sex", "date_diag",
  "age_at_diag", "age_at_met_diag", "date_met_diag", "comment",
  "disease_stage", "tumour_location", "sidedness", "synchronicity",
  "age_at_met", "date_last_fup2", "kras_detail", "kras_mut", "kras_specify",
  "nras_mut", "nras_specify", "braf_mut", "braf_specify", "tp53_mut",
  "tp53_specify", "pik3ca_mut", "pik3ca_specify", "mmr_text", "mmrd"
)
raw <- raw[, 1:34]
colnames(raw) <- var_names
parse_mixed_date <- function(x) {
  x <- str_trim(x)
  if (is.na(x) || x == "" || x %in% c("?", "unknown", "Unknown")) return(as.Date(NA))
  d <- suppressWarnings(dmy(x))
  if (is.na(d)) d <- suppressWarnings(as.Date(as.numeric(x), origin = "1899-12-30"))
  d
}
parse_date_col <- function(col) as.Date(vapply(col, parse_mixed_date, numeric(1)), origin = "1970-01-01")

clean_binary_mut <- function(x) {
  x <- str_trim(tolower(x))
  case_when(
    is.na(x) | x %in% c("", "na", "?", "???", "not tested", "not performed", "not done") ~ NA_real_,
    str_detect(x, "and") ~ as.numeric(str_detect(x, "1")),
    x == "1" ~ 1,
    x == "0" ~ 0,
    TRUE ~ NA_real_
  )
}

clean_side <- function(x) {
  x <- str_trim(tolower(x))
  case_when(
    x == "l" ~ "Left",
    x == "r" ~ "Right",
    x %in% c("r and l", "l and r") ~ "Bilateral/both",
    TRUE ~ NA_character_
  )
}
df <- raw %>%
  mutate(
    dob            = parse_date_col(dob),
    date_death     = parse_date_col(date_death),
    date_last_fup  = parse_date_col(date_last_fup),
    date_diag      = parse_date_col(date_diag),
    date_met_diag  = parse_date_col(date_met_diag),
    date_last_fup2 = parse_date_col(date_last_fup2),

    sex = str_trim(tolower(sex)) %>%
      recode("female" = "Female", "male" = "Male", .default = NA_character_) %>%
      factor(levels = c("Female", "Male")),

    ethnicity_code = factor(
      as.integer(ethnicity_code), levels = 1:6,
      labels = c("Asian", "Black", "White", "Other", "Mixed", "No data")
    ),

    disease_stage = factor(as.integer(disease_stage), levels = c(1, 2),
                            labels = c("Early", "Metastatic")),

    sidedness     = clean_side(sidedness),
    synchronicity = str_trim(tolower(synchronicity)) %>%
      na_if("?") %>%
      str_to_sentence() %>%
      factor(levels = c("Synchronous", "Metachronous")),

    deceased = as.numeric(deceased),
    event_os = as.numeric(event_os),
    age_at_diag     = as.numeric(age_at_diag),
    age_at_met_diag = as.numeric(age_at_met_diag),

    kras_mut   = clean_binary_mut(kras_mut),
    nras_mut   = clean_binary_mut(nras_mut),
    braf_mut   = clean_binary_mut(braf_mut),
    tp53_mut   = clean_binary_mut(tp53_mut),
    pik3ca_mut = clean_binary_mut(pik3ca_mut),
    mmrd       = clean_binary_mut(mmrd),

    ## BRAF V600E specifically (not "any BRAF mutation"). braf_specify holds
    ## free-text/HGVS variant detail, e.g. "c.1799T>A p.(Val600Glu)" for true
    ## V600E, vs other variant names (e.g. "p.(Asp594Asn)", "p.(Gly469Ala)")
    ## for non-V600E BRAF mutations, which are reclassified as V600E-negative
    ## here (see braf_status below if you want to keep them as a separate
    ## group instead of folding them into "not V600E").
    braf_v600e = case_when(
      is.na(braf_mut) ~ NA_real_,
      braf_mut == 1 & str_detect(str_to_lower(braf_specify), "val600glu|v600e") ~ 1,
      TRUE ~ 0
    ),
    ## Three-way status, kept for transparency/QC — shows how many BRAF
    ## mutations were non-V600E and got reclassified above.
    braf_status = case_when(
      is.na(braf_mut) ~ NA_character_,
      braf_mut == 0 ~ "Wild-type",
      braf_mut == 1 & str_detect(str_to_lower(braf_specify), "val600glu|v600e") ~ "V600E",
      braf_mut == 1 ~ "Non-V600E mutant",
      TRUE ~ NA_character_
    ) %>% factor(levels = c("Wild-type", "Non-V600E mutant", "V600E")),

    mmr_status = case_when(
      str_detect(str_to_lower(mmr_text), "mmrd") ~ "dMMR",
      str_detect(str_to_lower(mmr_text), "mmrp") ~ "pMMR",
      TRUE ~ NA_character_
    ) %>% factor(levels = c("pMMR", "dMMR")),

    os_time_months = as.numeric(
      difftime(if_else(deceased == 1, date_death, date_last_fup), date_diag, units = "days")
    ) / 30.4375,

    ## For metastatic patients, OS is more appropriately measured from the
    ## date of metastatic diagnosis rather than initial diagnosis. This is
    ## NA for early-stage patients (no metastatic diagnosis date) and is
    ## only used in the "metastatic disease only" analyses below — the
    ## early-stage and all-stages-combined analyses continue to use
    ## os_time_months (from date_diag).
    os_time_months_metdx = if_else(
      disease_stage == "Metastatic",
      as.numeric(
        difftime(if_else(deceased == 1, date_death, date_last_fup), date_met_diag, units = "days")
      ) / 30.4375,
      NA_real_
    )
  ) %>%
  mutate(across(c(kras_mut, nras_mut, braf_mut, braf_v600e, tp53_mut, pik3ca_mut, mmrd),
                ~ factor(.x, levels = c(0, 1), labels = c("Wild-type/negative", "Mutant/positive")),
                .names = "{.col}_f")) %>%
  relocate(id, hospital_number, .before = 1)

## Flag and exclude implausible values (garbled dates in source)
bad_age    <- !between(df$age_at_diag, 18, 100) & !is.na(df$age_at_diag)
bad_os     <- df$os_time_months < 0 & !is.na(df$os_time_months)
bad_os_met <- df$os_time_months_metdx < 0 & !is.na(df$os_time_months_metdx)
df$age_at_diag[bad_age]               <- NA
df$os_time_months[bad_os]             <- NA
df$os_time_months_metdx[bad_os_met]   <- NA

df <- df %>%
  mutate(
    age_group = case_when(
      is.na(age_at_diag) ~ NA_character_,
      age_at_diag < 50   ~ "<50",
      age_at_diag >= 50  ~ "≥50"
    ) %>%
      factor(levels = c("<50", "≥50"))
  )

df <- df %>%
  mutate(
    age_group2 = case_when(
      is.na(age_at_diag) ~ NA_character_,
      age_at_diag < 50   ~ "<50",
      age_at_diag >= 50 & age_at_diag < 70 ~ "50-70",
      age_at_diag >= 70  ~ "≥70"
    ) %>%
      factor(levels = c("<50", "50-70", "≥70"))
  )

eth_groups <- c("Asian", "Black", "White")

8 rows with implausible age at diagnosis, 3 rows with negative survival time (from initial diagnosis), and 3 rows with negative survival time from metastatic diagnosis were excluded from the relevant analyses below (likely data-entry errors in the source — worth a query if these patients matter to your final numbers).

1.1 BRAF mutation detail: V600E vs non-V600E

BRAF analyses throughout this report use V600E specifically, not “any BRAF mutation”. The table below shows how many BRAF-mutant patients had a non-V600E variant (e.g. class II/III mutations) and were therefore reclassified as V600E-negative for all downstream tables and models.

df %>%
  filter(!is.na(braf_status)) %>%
  count(braf_status) %>%
  mutate(pct = round(100 * n / sum(n), 1)) %>%
  gt::gt() %>%
  gt::tab_header(title = "BRAF status: wild-type vs non-V600E mutant vs V600E") %>%
  gt::cols_label(braf_status = "BRAF status", n = "N", pct = "%")
BRAF status: wild-type vs non-V600E mutant vs V600E
BRAF status N %
Wild-type 547 90.6
Non-V600E mutant 16 2.6
V600E 41 6.8

2 Mutation and dMMR frequency by ethnicity

2.1 All stages combined

sub_eth <- df %>%
  filter(
    ethnicity_code %in% eth_groups
  ) %>%
  mutate(
    ethnicity_code = fct_drop(ethnicity_code)
  )

tbl_eth <- sub_eth %>%
  select(
    ethnicity_code,
    kras_mut_f,
    braf_v600e_f,
    tp53_mut_f,
    pik3ca_mut_f,
    mmr_status,
    sidedness
  ) %>%
  tbl_summary(
    by = ethnicity_code,
    missing = "no",
    statistic = all_categorical() ~ "{n} ({p}%)",
    label = list(
      kras_mut_f ~ "KRAS mutation",
      braf_v600e_f ~ "BRAF V600E mutation",
      tp53_mut_f ~ "TP53 mutation",
      pik3ca_mut_f ~ "PIK3CA mutation",
      mmr_status ~ "MMR status",
      sidedness ~ "Tumour sidedness"
    )
  ) %>%
  add_overall(last = TRUE) %>%
  add_p() %>%
  bold_labels() %>%
  modify_caption(
    "**Table 1. Molecular characteristics by ethnicity**"
  )

tbl_eth
Table 1. Molecular characteristics by ethnicity
Characteristic Asian
N = 135
1
Black
N = 99
1
White
N = 341
1
Overall
N = 575
1
p-value2
KRAS mutation



0.025
    Wild-type/negative 77 (60%) 41 (43%) 177 (56%) 295 (55%)
    Mutant/positive 51 (40%) 55 (57%) 138 (44%) 244 (45%)
BRAF V600E mutation



<0.001
    Wild-type/negative 127 (98%) 95 (100%) 278 (89%) 500 (93%)
    Mutant/positive 2 (1.6%) 0 (0%) 33 (11%) 35 (6.5%)
TP53 mutation



0.5
    Wild-type/negative 37 (30%) 29 (31%) 80 (26%) 146 (28%)
    Mutant/positive 86 (70%) 64 (69%) 226 (74%) 376 (72%)
PIK3CA mutation



0.5
    Wild-type/negative 107 (86%) 74 (80%) 252 (82%) 433 (82%)
    Mutant/positive 18 (14%) 18 (20%) 56 (18%) 92 (18%)
MMR status



>0.9
    pMMR 117 (91%) 90 (92%) 305 (91%) 512 (91%)
    dMMR 12 (9.3%) 8 (8.2%) 29 (8.7%) 49 (8.7%)
Tumour sidedness



0.021
    Bilateral/both 1 (0.8%) 0 (0%) 3 (0.9%) 4 (0.7%)
    Left 93 (70%) 56 (57%) 243 (73%) 392 (70%)
    Right 38 (29%) 42 (43%) 86 (26%) 166 (30%)
1 n (%)
2 Pearson’s Chi-squared test; Fisher’s exact test

2.2 Early-stage disease only

sub_early <- df %>%
  filter(
    ethnicity_code %in% eth_groups,
    disease_stage == "Early"
  ) %>%
  mutate(
    ethnicity_code = fct_drop(ethnicity_code)
  )

tbl_early <- sub_early %>%
  select(
    ethnicity_code,
    kras_mut_f,
    braf_v600e_f,
    tp53_mut_f,
    pik3ca_mut_f,
    mmr_status
  ) %>%
  tbl_summary(
    by = ethnicity_code,
    missing = "no",
    statistic = all_categorical() ~ "{n} ({p}%)",
    label = list(
      kras_mut_f ~ "KRAS mutation",
      braf_v600e_f ~ "BRAF V600E mutation",
      tp53_mut_f ~ "TP53 mutation",
      pik3ca_mut_f ~ "PIK3CA mutation",
      mmr_status ~ "MMR status"
    )
  ) %>%
  add_p() %>%
  bold_labels() %>%
  modify_caption(
    "**Table 2. Molecular characteristics by ethnicity in early disease**"
  )

tbl_early
Table 2. Molecular characteristics by ethnicity in early disease
Characteristic Asian
N = 51
1
Black
N = 35
1
White
N = 131
1
p-value2
KRAS mutation


0.8
    Wild-type/negative 29 (60%) 19 (54%) 65 (55%)
    Mutant/positive 19 (40%) 16 (46%) 53 (45%)
BRAF V600E mutation


0.003
    Wild-type/negative 49 (100%) 35 (100%) 101 (88%)
    Mutant/positive 0 (0%) 0 (0%) 14 (12%)
TP53 mutation


0.12
    Wild-type/negative 15 (31%) 16 (46%) 32 (27%)
    Mutant/positive 33 (69%) 19 (54%) 85 (73%)
PIK3CA mutation


0.14
    Wild-type/negative 42 (88%) 25 (71%) 98 (84%)
    Mutant/positive 6 (13%) 10 (29%) 19 (16%)
MMR status


0.3
    pMMR 45 (94%) 29 (83%) 117 (90%)
    dMMR 3 (6.3%) 6 (17%) 13 (10%)
1 n (%)
2 Pearson’s Chi-squared test; Fisher’s exact test

2.3 Metastatic disease only

sub_met <- df %>%
  filter(
    ethnicity_code %in% eth_groups,
    disease_stage == "Metastatic"
  ) %>%
  mutate(
    ethnicity_code = fct_drop(ethnicity_code)
  )

tbl_met <- sub_met %>%
  select(
    ethnicity_code,
    kras_mut_f,
    braf_v600e_f,
    tp53_mut_f,
    pik3ca_mut_f,
    mmr_status
  ) %>%
  tbl_summary(
    by = ethnicity_code,
    missing = "no",
    statistic = all_categorical() ~ "{n} ({p}%)",
    label = list(
      kras_mut_f ~ "KRAS mutation",
      braf_v600e_f ~ "BRAF V600E mutation",
      tp53_mut_f ~ "TP53 mutation",
      pik3ca_mut_f ~ "PIK3CA mutation",
      mmr_status ~ "MMR status"
    )
  ) %>%
  add_p() %>%
  bold_labels() %>%
  modify_caption(
    "**Table 3. Molecular characteristics by ethnicity in metastatic disease**"
  )

tbl_met
Table 3. Molecular characteristics by ethnicity in metastatic disease
Characteristic Asian
N = 82
1
Black
N = 64
1
White
N = 209
1
p-value2
KRAS mutation


0.008
    Wild-type/negative 48 (60%) 22 (36%) 112 (57%)
    Mutant/positive 32 (40%) 39 (64%) 85 (43%)
BRAF V600E mutation


0.005
    Wild-type/negative 78 (98%) 60 (100%) 177 (90%)
    Mutant/positive 2 (2.5%) 0 (0%) 19 (9.7%)
TP53 mutation


0.7
    Wild-type/negative 22 (29%) 13 (22%) 48 (25%)
    Mutant/positive 53 (71%) 45 (78%) 141 (75%)
PIK3CA mutation


0.6
    Wild-type/negative 65 (84%) 49 (86%) 154 (81%)
    Mutant/positive 12 (16%) 8 (14%) 37 (19%)
MMR status


0.2
    pMMR 72 (89%) 61 (97%) 188 (92%)
    dMMR 9 (11%) 2 (3.2%) 16 (7.8%)
1 n (%)
2 Pearson’s Chi-squared test; Fisher’s exact test

3 Mutation and dMMR frequency by age at diagnosis

3.1 All stages combined

sub_age_all <- df %>%
  filter(
    !is.na(age_group)
  )

tbl_age_all <- sub_age_all %>%
  select(
    age_group,
    kras_mut_f,
    braf_v600e_f,
    tp53_mut_f,
    pik3ca_mut_f,
    mmr_status,
    disease_stage,
    sidedness
  ) %>%
  tbl_summary(
    by = age_group,
    missing = "no",
    statistic = all_categorical() ~ "{n} ({p}%)",
    label = list(
      kras_mut_f ~ "KRAS mutation",
      braf_v600e_f ~ "BRAF V600E mutation",
      tp53_mut_f ~ "TP53 mutation",
      pik3ca_mut_f ~ "PIK3CA mutation",
      mmr_status ~ "MMR status",
      disease_stage ~ "Stage at diagnosis",
      sidedness ~ "Sidedness"
    )
  ) %>%
  add_p(pvalue_fun = ~style_pvalue(.x, digits = 3)) %>%
  bold_labels() %>%
  modify_caption(
    "**Table . Molecular characteristics by age group in all stages combined**"
  )

tbl_age_all
Table . Molecular characteristics by age group in all stages combined
Characteristic <50
N = 126
1
≥50
N = 514
1
p-value2
KRAS mutation

0.071
    Wild-type/negative 76 (62%) 254 (53%)
    Mutant/positive 47 (38%) 228 (47%)
BRAF V600E mutation

0.259
    Wild-type/negative 112 (91%) 447 (94%)
    Mutant/positive 11 (8.9%) 29 (6.1%)
TP53 mutation

0.260
    Wild-type/negative 28 (23%) 133 (28%)
    Mutant/positive 92 (77%) 334 (72%)
PIK3CA mutation

0.045
    Wild-type/negative 107 (88%) 378 (81%)
    Mutant/positive 14 (12%) 91 (19%)
MMR status

0.351
    pMMR 113 (90%) 469 (93%)
    dMMR 12 (9.6%) 36 (7.1%)
Stage at diagnosis

0.505
    Early 44 (35%) 196 (38%)
    Metastatic 82 (65%) 318 (62%)
Sidedness

0.123
    Bilateral/both 2 (1.6%) 2 (0.4%)
    Left 94 (75%) 356 (70%)
    Right 30 (24%) 149 (29%)
1 n (%)
2 Pearson’s Chi-squared test; Fisher’s exact test

3.2 Early-stage disease only

sub_age_early <- df %>%
  filter(
    !is.na(age_group),
    disease_stage == "Early"
  )

tbl_age_early <- sub_age_early %>%
  select(
    age_group,
    kras_mut_f,
    braf_v600e_f,
    tp53_mut_f,
    pik3ca_mut_f,
    mmr_status
  ) %>%
  tbl_summary(
    by = age_group,
    missing = "no",
    statistic = all_categorical() ~ "{n} ({p}%)",
    label = list(
      kras_mut_f ~ "KRAS mutation",
      braf_v600e_f ~ "BRAF V600E mutation",
      tp53_mut_f ~ "TP53 mutation",
      pik3ca_mut_f ~ "PIK3CA mutation",
      mmr_status ~ "MMR status"
    )
  ) %>%
  add_p() %>%
  bold_labels() %>%
  modify_caption(
    "**Table 4. Molecular characteristics by age group in early disease**"
  )

tbl_age_early
Table 4. Molecular characteristics by age group in early disease
Characteristic <50
N = 44
1
≥50
N = 196
1
p-value2
KRAS mutation

0.2
    Wild-type/negative 27 (64%) 97 (53%)
    Mutant/positive 15 (36%) 86 (47%)
BRAF V600E mutation

0.7
    Wild-type/negative 39 (93%) 169 (94%)
    Mutant/positive 3 (7.1%) 11 (6.1%)
TP53 mutation

0.3
    Wild-type/negative 10 (24%) 58 (32%)
    Mutant/positive 32 (76%) 124 (68%)
PIK3CA mutation

0.060
    Wild-type/negative 39 (93%) 147 (81%)
    Mutant/positive 3 (7.1%) 35 (19%)
MMR status

0.6
    pMMR 38 (88%) 177 (92%)
    dMMR 5 (12%) 16 (8.3%)
1 n (%)
2 Pearson’s Chi-squared test; Fisher’s exact test

3.3 Metastatic disease only

sub_age_met <- df %>%
  filter(
    !is.na(age_group),
    disease_stage == "Metastatic"
  )

tbl_age_met <- sub_age_met %>%
  select(
    age_group,
    kras_mut_f,
    braf_v600e_f,
    tp53_mut_f,
    pik3ca_mut_f,
    mmr_status
  ) %>%
  tbl_summary(
    by = age_group,
    missing = "no",
    statistic = all_categorical() ~ "{n} ({p}%)",
    label = list(
      kras_mut_f ~ "KRAS mutation",
      braf_v600e_f ~ "BRAF V600E mutation",
      tp53_mut_f ~ "TP53 mutation",
      pik3ca_mut_f ~ "PIK3CA mutation",
      mmr_status ~ "MMR status"
    )
  ) %>%
  add_p() %>%
  bold_labels() %>%
  modify_caption(
    "**Table 5. Molecular characteristics by age group in metastatic disease**"
  )

tbl_age_met
Table 5. Molecular characteristics by age group in metastatic disease
Characteristic <50
N = 82
1
≥50
N = 318
1
p-value2
KRAS mutation

0.2
    Wild-type/negative 49 (60%) 157 (53%)
    Mutant/positive 32 (40%) 142 (47%)
BRAF V600E mutation

0.2
    Wild-type/negative 73 (90%) 278 (94%)
    Mutant/positive 8 (9.9%) 18 (6.1%)
TP53 mutation

0.6
    Wild-type/negative 18 (23%) 75 (26%)
    Mutant/positive 60 (77%) 210 (74%)
PIK3CA mutation

0.3
    Wild-type/negative 68 (86%) 231 (80%)
    Mutant/positive 11 (14%) 56 (20%)
MMR status

0.5
    pMMR 75 (91%) 292 (94%)
    dMMR 7 (8.5%) 20 (6.4%)
1 n (%)
2 Pearson’s Chi-squared test

4 Overall survival by ethnicity

4.1 All stages combined

sub_eth_os <- df %>%
  filter(
    ethnicity_code %in% eth_groups,
    !is.na(os_time_months),
    !is.na(event_os)
  ) %>%
  mutate(
    ethnicity_code = fct_drop(ethnicity_code)
  )

fit_eth <- survfit(
  Surv(os_time_months, event_os) ~ ethnicity_code,
  data = sub_eth_os
)

ggsurvplot(
  fit_eth,
  data = sub_eth_os,
  pval = TRUE,
  risk.table = TRUE,
  conf.int = FALSE,
  xlab = "Time from diagnosis (months)",
  ylab = "Overall survival probability",
  legend.title = "Ethnicity",
  risk.table.title = "Number at risk",
  break.time.by = 12,
  ggtheme = theme_minimal(base_size = 12)
)

4.2 Early-stage disease only

sub_early_os <- df %>%
  filter(
    ethnicity_code %in% eth_groups,
    disease_stage == "Early",
    !is.na(os_time_months),
    !is.na(event_os)
  ) %>%
  mutate(
    ethnicity_code = fct_drop(ethnicity_code)
  )

fit_early_eth <- survfit(
  Surv(os_time_months, event_os) ~ ethnicity_code,
  data = sub_early_os
)

ggsurvplot(
  fit_early_eth,
  data = sub_early_os,
  pval = TRUE,
  risk.table = TRUE,
  conf.int = FALSE,
  xlab = "Time from diagnosis (months)",
  ylab = "Overall survival probability",
  legend.title = "Ethnicity",
  risk.table.title = "Number at risk",
  break.time.by = 12,
  ggtheme = theme_minimal(base_size = 12)
)

4.3 Metastatic disease only

sub_met_os <- df %>%
  filter(
    ethnicity_code %in% eth_groups,
    disease_stage == "Metastatic",
    !is.na(os_time_months_metdx),
    !is.na(event_os)
  ) %>%
  mutate(
    ethnicity_code = fct_drop(ethnicity_code)
  )

fit_met_eth <- survfit(
  Surv(os_time_months_metdx, event_os) ~ ethnicity_code,
  data = sub_met_os
)

ggsurvplot(
  fit_met_eth,
  data = sub_met_os,
  pval = TRUE,
  risk.table = TRUE,
  conf.int = FALSE,
  xlab = "Time from metastatic diagnosis (months)",
  ylab = "Overall survival probability",
  legend.title = "Ethnicity",
  risk.table.title = "Number at risk",
  break.time.by = 12,
  ggtheme = theme_minimal(base_size = 12)
)

5 Overall survival by age at diagnosis

5.1 All stages combined

sub_age_os <- df %>%
  filter(
    !is.na(age_group),
    !is.na(os_time_months),
    !is.na(event_os)
  )

fit_age_all <- survfit(
  Surv(os_time_months, event_os) ~ age_group,
  data = sub_age_os
)

ggsurvplot(
  fit_age_all,
  data = sub_age_os,
  pval = TRUE,
  risk.table = TRUE,
  conf.int = FALSE,
  xlab = "Time from diagnosis (months)",
  ylab = "Overall survival probability",
  legend.title = "Age group",
  risk.table.title = "Number at risk",
  break.time.by = 12,
  ggtheme = theme_minimal(base_size = 12)
)

5.2 Early-stage disease only

sub_age_early_os <- df %>%
  filter(
    !is.na(age_group),
    disease_stage == "Early",
    !is.na(os_time_months),
    !is.na(event_os)
  )

fit_age_early <- survfit(
  Surv(os_time_months, event_os) ~ age_group,
  data = sub_age_early_os
)

ggsurvplot(
  fit_age_early,
  data = sub_age_early_os,
  pval = TRUE,
  risk.table = TRUE,
  conf.int = FALSE,
  xlab = "Time from diagnosis (months)",
  ylab = "Overall survival probability",
  legend.title = "Age group",
  risk.table.title = "Number at risk",
  break.time.by = 12,
  ggtheme = theme_minimal(base_size = 12)
)

5.3 Metastatic disease only

sub_age_met_os <- df %>%
  filter(
    !is.na(age_group),
    disease_stage == "Metastatic",
    !is.na(os_time_months_metdx),
    !is.na(event_os)
  )

fit_age_met <- survfit(
  Surv(os_time_months_metdx, event_os) ~ age_group,
  data = sub_age_met_os
)

ggsurvplot(
  fit_age_met,
  data = sub_age_met_os,
  pval = TRUE,
  risk.table = TRUE,
  conf.int = FALSE,
  xlab = "Time from metastatic diagnosis (months)",
  ylab = "Overall survival probability",
  legend.title = "Age group",
  risk.table.title = "Number at risk",
  break.time.by = 12,
  ggtheme = theme_minimal(base_size = 12)
)

6 Multivariable Cox model — metastatic cohort

Adjusted for age group, sex, ethnicity and mutation/MMR status. Reference levels: age ≤50, ethnicity Asian, wild-type/negative for each marker, pMMR.

sub_met_cox <- df %>%
  filter(
    ethnicity_code %in% eth_groups,
    disease_stage == "Metastatic",
    !is.na(os_time_months_metdx),
    !is.na(event_os)
  ) %>%
  mutate(
    ethnicity_code = fct_drop(
      fct_relevel(ethnicity_code, "Asian")
    )
  )

cox_met <- coxph(
  Surv(os_time_months_metdx, event_os) ~
    age_group +
    sex +
    ethnicity_code +
    kras_mut_f +
    braf_v600e_f +
    tp53_mut_f +
    pik3ca_mut_f +
    mmr_status,
  data = sub_met_cox
)

tbl_cox <- tbl_regression(
  cox_met,
  exponentiate = TRUE,
  label = list(
    age_group ~ "Age group",
    sex ~ "Sex",
    ethnicity_code ~ "Ethnicity",
    kras_mut_f ~ "KRAS mutation",
    braf_v600e_f ~ "BRAF V600E mutation",
    tp53_mut_f ~ "TP53 mutation",
    pik3ca_mut_f ~ "PIK3CA mutation",
    mmr_status ~ "MMR status"
  )
) %>%
  bold_labels() %>%
  modify_caption(
    "**Table 6. Multivariable Cox regression for overall survival in metastatic disease**"
  )

tbl_cox
Table 6. Multivariable Cox regression for overall survival in metastatic disease
Characteristic HR 95% CI p-value
Age group


    <50 — —
    ≥50 0.96 0.61, 1.51 0.9
Sex


    Female — —
    Male 1.26 0.85, 1.88 0.2
Ethnicity


    Asian — —
    Black 1.12 0.59, 2.10 0.7
    White 1.03 0.62, 1.73 0.9
KRAS mutation


    Wild-type/negative — —
    Mutant/positive 1.48 0.95, 2.30 0.087
BRAF V600E mutation


    Wild-type/negative — —
    Mutant/positive 4.24 2.20, 8.18 <0.001
TP53 mutation


    Wild-type/negative — —
    Mutant/positive 1.12 0.71, 1.77 0.6
PIK3CA mutation


    Wild-type/negative — —
    Mutant/positive 0.66 0.37, 1.17 0.2
MMR status


    pMMR — —
    dMMR 0.48 0.20, 1.17 0.11
Abbreviations: CI = Confidence Interval, HR = Hazard Ratio

7 Supporting analyses for the age-based abstract 1

These tables provide the source figures for the claims made in the age-group conference abstract that are not already covered by the tables and survival curves above.

7.1 Stage at presentation by age group

sub_stage_age <- df %>%
  filter(!is.na(age_group), !is.na(disease_stage))

tbl_stage_age <- sub_stage_age %>%
  select(age_group, disease_stage) %>%
  tbl_summary(
    by = age_group,
    missing = "no",
    statistic = all_categorical() ~ "{n} ({p}%)",
    label = list(disease_stage ~ "Disease stage at presentation")
  ) %>%
  add_p(pvalue_fun = ~style_pvalue(.x, digits = 3)) %>%
  bold_labels() %>%
  modify_caption(
    "**Table 7. Disease stage at presentation by age group**"
  )

tbl_stage_age
Table 7. Disease stage at presentation by age group
Characteristic <50
N = 126
1
≥50
N = 514
1
p-value2
Disease stage at presentation

0.505
    Early 44 (35%) 196 (38%)
    Metastatic 82 (65%) 318 (62%)
1 n (%)
2 Pearson’s Chi-squared test

7.2 Sidedness by age group

sub_side_age <- df %>%
  filter(!is.na(age_group), sidedness %in% c("Left", "Right"))

tbl_side_age <- sub_side_age %>%
  select(age_group, sidedness) %>%
  tbl_summary(
    by = age_group,
    missing = "no",
    statistic = all_categorical() ~ "{n} ({p}%)",
    label = list(sidedness ~ "Tumour sidedness")
  ) %>%
  add_p(pvalue_fun = ~style_pvalue(.x, digits = 3)) %>%
  bold_labels() %>%
  modify_caption(
    "**Table 8. Tumour sidedness by age group**"
  )

tbl_side_age
Table 8. Tumour sidedness by age group
Characteristic <50
N = 124
1
≥50
N = 505
1
p-value2
Tumour sidedness

0.240
    Left 94 (76%) 356 (70%)
    Right 30 (24%) 149 (30%)
1 n (%)
2 Pearson’s Chi-squared test

7.3 Molecular testing completion by age group

tbl_testing_age <- df %>%
  filter(!is.na(age_group)) %>%
  mutate(tested = factor(!is.na(kras_mut), levels = c(FALSE, TRUE),
                          labels = c("Not tested", "Tested"))) %>%
  select(age_group, tested) %>%
  tbl_summary(
    by = age_group,
    missing = "no",
    statistic = all_categorical() ~ "{n} ({p}%)",
    label = list(tested ~ "Molecular testing completion")
  ) %>%
  bold_labels() %>%
  modify_caption(
    "**Table 9. Molecular testing completion by age group**"
  )

tbl_testing_age
Table 9. Molecular testing completion by age group
Characteristic <50
N = 126
1
≥50
N = 514
1
Molecular testing completion

    Not tested 3 (2.4%) 32 (6.2%)
    Tested 123 (98%) 482 (94%)
1 n (%)

7.4 RAS/BRAF wild-type rate by age group

sub_wt_age <- df %>%
  filter(!is.na(age_group), !is.na(kras_mut), !is.na(braf_v600e)) %>%
  mutate(
    ras_braf_wt = factor(kras_mut == 0 & braf_mut == 0,
                          levels = c(FALSE, TRUE),
                          labels = c("RAS and/or BRAF mutant", "RAS/BRAF wild-type"))
  )


tbl_wt_age <- sub_wt_age %>%
  select(age_group, ras_braf_wt) %>%
  tbl_summary(
    by = age_group,
    missing = "no",
    statistic = all_categorical() ~ "{n} ({p}%)",
    label = list(ras_braf_wt ~ "RAS/BRAF status")
  ) %>%
  add_p(pvalue_fun = ~style_pvalue(.x, digits = 3)) %>%
  bold_labels() %>%
  modify_caption(
    "**Table 10. RAS/BRAF wild-type rate by age group**"
  )

tbl_wt_age
Table 10. RAS/BRAF wild-type rate by age group
Characteristic <50
N = 123
1
≥50
N = 475
1
p-value2
RAS/BRAF status

0.241
    RAS and/or BRAF mutant 59 (48%) 256 (54%)
    RAS/BRAF wild-type 64 (52%) 219 (46%)
1 n (%)
2 Pearson’s Chi-squared test

7.5 Ethnic composition by age group

sub_eth_age <- df %>%
  filter(!is.na(age_group), ethnicity_code %in% eth_groups) %>%
  mutate(ethnicity_code = fct_drop(ethnicity_code))

tbl_eth_age <- sub_eth_age %>%
  select(age_group, ethnicity_code) %>%
  tbl_summary(
    by = age_group,
    missing = "no",
    statistic = all_categorical() ~ "{n} ({p}%)",
    label = list(ethnicity_code ~ "Ethnicity")
  ) %>%
  add_p() %>%
  bold_labels() %>%
  modify_caption(
    "**Table 11. Ethnic composition by age group**"
  )

tbl_eth_age
Table 11. Ethnic composition by age group
Characteristic <50
N = 104
1
≥50
N = 462
1
p-value2
Ethnicity

0.077
    Asian 33 (32%) 99 (21%)
    Black 17 (16%) 81 (18%)
    White 54 (52%) 282 (61%)
1 n (%)
2 Pearson’s Chi-squared test

Age distribution within each ethnic group, shown the other way round for reference (same underlying 2x3 table, same p-value):

tbl_age_by_eth <- sub_eth_age %>%
  select(ethnicity_code, age_group) %>%
  tbl_summary(
    by = ethnicity_code,
    missing = "no",
    statistic = all_categorical() ~ "{n} ({p}%)",
    label = list(age_group ~ "Age at diagnosis")
  ) %>%
  add_p() %>%
  bold_labels() %>%
  modify_caption(
    "**Table 12. Age at diagnosis by ethnicity**"
  )

tbl_age_by_eth
Table 12. Age at diagnosis by ethnicity
Characteristic Asian
N = 132
1
Black
N = 98
1
White
N = 336
1
p-value2
Age at diagnosis


0.077
    <50 33 (25%) 17 (17%) 54 (16%)
    ≥50 99 (75%) 81 (83%) 282 (84%)
1 n (%)
2 Pearson’s Chi-squared test