This script builds Figure 1 (“Preliminary clinical and molecular characteristics of the PDAC cohort”), using the demographic, clinical, and pathology variables available in this dataset:
clinical_clean <- readRDS("clinical_clean.rds")
nms <- names(clinical_clean)
cat("CA19-9 column present:", any(grepl("ca19|ca_19", nms, ignore.case = TRUE)), "\n")
## CA19-9 column present: FALSE
cat("Stage/grade/resection/margin/TNM column present:",
any(grepl("stage|grade|resect|margin|tnm", nms, ignore.case = TRUE)), "\n")
## Stage/grade/resection/margin/TNM column present: FALSE
library(dplyr)
library(ggplot2)
library(patchwork)
library(survival)
theme_set(theme_minimal(base_size = 10))
cc1 <- clinical_clean %>%
mutate(group2 = case_when(
pathology == "BBP" ~ "BBP",
pathology %in% c("RPC", "LAPC", "MPC") ~ "PDAC",
TRUE ~ NA_character_
)) %>%
filter(!is.na(group2))
table(cc1$group2)
##
## BBP PDAC
## 83 102
Bilirubin and ALP are shown on a log10 scale given their wide range; CRP has a minimum of 0 in this dataset, so it is left on a linear scale rather than log-transformed (log of zero is undefined).
make_box <- function(varlab, varname, logscale = FALSE) {
d <- cc1 %>% filter(!is.na(.data[[varname]]))
p <- ggplot(d, aes(x = group2, y = .data[[varname]], fill = group2)) +
geom_boxplot(outlier.size = 0.8, width = 0.6) +
scale_fill_manual(values = c(BBP = "#4C72B0", PDAC = "#DD8452"), guide = "none") +
labs(x = NULL, y = NULL, title = varlab) +
theme(plot.title = element_text(size = 8, face = "bold"))
if (logscale) p <- p + scale_y_log10()
p
}
pA1 <- make_box("Total bilirubin (umol/L)", "total_bilirubin_tbil_umol_l", logscale = TRUE)
pA2 <- make_box("ALP (U/L)", "alkaline_phosphatase_alp_u_l", logscale = TRUE)
pA3 <- make_box("Albumin (g/L)", "albumin_g_l")
pA4 <- make_box("CRP (mg/L)", "c_reactive_protein_mg_l")
rowA <- (pA1 | pA2 | pA3 | pA4)
rowA
sex_d <- cc1 %>%
filter(!is.na(gender)) %>%
count(group2, gender) %>%
group_by(group2) %>%
mutate(pct = 100 * n / sum(n))
pB <- ggplot(sex_d, aes(x = group2, y = pct, fill = gender)) +
geom_col(width = 0.6) +
scale_fill_manual(values = c(female = "#C44E52", male = "#55A868")) +
labs(x = NULL, y = "Percent", title = "Sex distribution (BBP vs PDAC)", fill = NULL) +
theme(plot.title = element_text(size = 8, face = "bold"), legend.position = "bottom")
pB
cat_d <- cc1 %>%
filter(group2 == "PDAC") %>%
count(pathology) %>%
mutate(pathology = factor(pathology, levels = c("RPC", "LAPC", "MPC")))
pC <- ggplot(cat_d, aes(x = pathology, y = n, fill = pathology)) +
geom_col(width = 0.6) +
geom_text(aes(label = n), vjust = -0.4, size = 3) +
scale_fill_manual(values = c(RPC = "#4C72B0", LAPC = "#DD8452", MPC = "#C44E52"), guide = "none") +
labs(x = NULL, y = "n patients", title = "PDAC disease category distribution") +
theme(plot.title = element_text(size = 8, face = "bold")) +
ylim(0, max(cat_d$n) * 1.15)
pC
Caveat, stated plainly before the result: only 33 of 102 PDAC patients (21 RPC, 8 LAPC, 4 MPC) have both a recorded follow-up time and vital status. This is a small, non-random subset (patients with complete follow-up capture are unlikely to be representative of the full PDAC cohort), and the LAPC/MPC arms in particular are too small for a reliable survival estimate. This panel is preliminary and hypothesis-generating only, not a definitive survival analysis.
dead_alive is coded 0 = Dead, 1 = Alive in the source
data (verified against date_of_death in
00_Clinical_Data_Import_QC.Rmd); this is converted to the
standard event = 1 (death observed) /
event = 0 (censored) coding expected by the
survival package.
pdac_surv <- clinical_clean %>%
filter(pathology %in% c("RPC", "LAPC", "MPC")) %>%
filter(!is.na(followup), !is.na(dead_alive)) %>%
mutate(
event = ifelse(dead_alive == 0, 1, 0),
pathology = factor(pathology, levels = c("RPC", "LAPC", "MPC"))
)
table(pdac_surv$pathology)
##
## RPC LAPC MPC
## 21 8 4
fit <- survfit(Surv(followup, event) ~ pathology, data = pdac_surv)
fit
## Call: survfit(formula = Surv(followup, event) ~ pathology, data = pdac_surv)
##
## n events median 0.95LCL 0.95UCL
## pathology=RPC 21 12 356 126 NA
## pathology=LAPC 8 5 159 85 NA
## pathology=MPC 4 4 100 14 NA
km_df <- data.frame(time = fit$time, surv = fit$surv,
strata = rep(names(fit$strata), fit$strata))
km_df$strata <- gsub("pathology=", "", km_df$strata)
logrank <- survdiff(Surv(followup, event) ~ pathology, data = pdac_surv)
p_logrank <- 1 - pchisq(logrank$chisq, length(logrank$n) - 1)
cat("Log-rank p-value:", round(p_logrank, 4), "\n")
## Log-rank p-value: 0.1495
pD <- ggplot(km_df, aes(x = time, y = surv, color = strata)) +
geom_step(linewidth = 0.8) +
scale_color_manual(values = c(RPC = "#4C72B0", LAPC = "#DD8452", MPC = "#C44E52")) +
labs(x = "Follow-up (days)", y = "Survival probability",
title = "Preliminary survival by disease category",
subtitle = paste0("n=33 complete cases; log-rank p=", round(p_logrank, 3)),
color = NULL) +
theme(plot.title = element_text(size = 8, face = "bold"),
plot.subtitle = element_text(size = 6.5, color = "grey40"),
legend.position = "bottom") +
ylim(0, 1)
pD
The log-rank test does not reach significance (p = 0.15), consistent with the very small LAPC and MPC sample sizes in the complete-case subset; the curves separate in the expected direction (MPC declining fastest, RPC most favourable), but this should be read as directionally suggestive only, not confirmatory, given the sample size.
rowBCD <- (pB | pC | pD)
figure1 <- rowA / rowBCD +
plot_annotation(
tag_levels = list(c("A", "", "", "", "B", "C", "D")),
title = "Figure 1. Preliminary clinical and molecular characteristics of the PDAC cohort"
) &
theme(plot.tag = element_text(face = "bold"))
figure1
ggsave("Figure1_Preliminary_Clinical.png", figure1, width = 12, height = 7, dpi = 200)
PDAC and BBP were first compared with respect to demographic and clinical variables (Table 1; Figure 1A-B). PDAC patients were significantly older and more frequently male than BBP patients, and showed significantly elevated total bilirubin, ALP, and CRP alongside significantly reduced albumin, consistent with biliary obstruction, hepatobiliary compromise, and a systemic inflammatory response, respectively (Figure 1A). Resectability-based disease category (RPC/LAPC/MPC) is reported in Table 2 and Figure 1C, with the cohort predominantly resectable at presentation (79/102, 77.5%).
A preliminary, hypothesis-generating survival comparison across disease categories (Figure 1D) did not reach statistical significance (log-rank p = 0.15), reflecting the small subset of patients (n = 33/102) with complete follow-up data rather than necessarily the absence of a true survival difference; this is revisited with the full available survival data in Objective 2.
sessionInfo()
## R version 4.6.1 (2026-06-24 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
##
## Matrix products: default
## LAPACK version 3.12.1
##
## locale:
## [1] LC_COLLATE=English_United States.utf8
## [2] LC_CTYPE=English_United States.utf8
## [3] LC_MONETARY=English_United States.utf8
## [4] LC_NUMERIC=C
## [5] LC_TIME=English_United States.utf8
##
## time zone: Africa/Johannesburg
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] survival_3.8-6 patchwork_1.3.2 ggplot2_4.0.3 dplyr_1.2.1
##
## loaded via a namespace (and not attached):
## [1] Matrix_1.7-5 gtable_0.3.6 jsonlite_2.0.0 compiler_4.6.1
## [5] tidyselect_1.2.1 jquerylib_0.1.4 textshaping_1.0.5 systemfonts_1.3.2
## [9] splines_4.6.1 scales_1.4.0 yaml_2.3.12 fastmap_1.2.0
## [13] lattice_0.22-9 R6_2.6.1 labeling_0.4.3 generics_0.1.4
## [17] knitr_1.51 tibble_3.3.1 bslib_0.11.0 pillar_1.11.1
## [21] RColorBrewer_1.1-3 rlang_1.2.0 cachem_1.1.0 xfun_0.59
## [25] sass_0.4.10 S7_0.2.2 otel_0.2.0 cli_3.6.6
## [29] withr_3.0.3 magrittr_2.0.5 digest_0.6.39 grid_4.6.1
## [33] rstudioapi_0.19.0 lifecycle_1.0.5 vctrs_0.7.3 evaluate_1.0.5
## [37] glue_1.8.1 farver_2.1.2 ragg_1.5.2 rmarkdown_2.31
## [41] tools_4.6.1 pkgconfig_2.0.3 htmltools_0.5.9