This file covers the sections that were not yet analysed, using the full 64-patient harmonized cohort (all PDAC, no controls — a BBP comparison is a separate data question, not covered here). It follows the reporting style used in the group’s own AP and GBC papers: an ordinal Spearman correlation against disease severity (here, pathology group), plus a Kruskal–Wallis test across the three groups, both reported with p-value and FDR side by side, as in those papers’ tables.
Pathology groups are ordered by clinical severity: RPC (resectable) = 1, LAPC (locally advanced) = 2, MPC (metastatic) = 3. This mirrors the AP paper’s HC/MAP/MSAP/SAP ordinal coding.
Not covered here, and why:
clusterProfiler / ReactomePA, protein-to-gene
mapping) not yet set up; scoped separately.harm <- readRDS("harmonized_multiomics_dataset.rds")
prot <- readRDS("harmonized_proteomics.rds")
metab <- readRDS("harmonized_metabolomics.rds")
clin <- readRDS("clinical_clean.rds")
standardize_id <- function(id) {
id <- str_trim(id)
prefix <- str_extract(id, "^[A-Za-z]+")
number <- str_extract(id, "\\d+")
paste0(toupper(prefix), sprintf("%03d", as.integer(number)))
}
# All 64 patients, with pathology, age and gender
clin64 <- clin %>%
mutate(id_std = standardize_id(linked_id)) %>%
filter(id_std %in% harm$id_std) %>%
distinct(id_std, .keep_all = TRUE) %>%
mutate(stage_ord = case_when(
pathology == "RPC" ~ 1L,
pathology == "LAPC" ~ 2L,
pathology == "MPC" ~ 3L,
TRUE ~ NA_integer_
)) %>%
select(id_std, pathology, stage_ord, age, gender)
stopifnot(nrow(clin64) == 64, sum(is.na(clin64$stage_ord)) == 0)
table(clin64$pathology)
##
## LAPC MPC RPC
## 10 4 50
prot_mat <- prot %>% column_to_rownames("id_std") %>% as.matrix()
metab_mat <- metab %>% column_to_rownames("id_std") %>% as.matrix()
# align row order to clin64
prot_mat <- prot_mat[clin64$id_std, ]
metab_mat <- metab_mat[clin64$id_std, ]
dim(prot_mat); dim(metab_mat)
## [1] 64 1221
## [1] 64 29
# Per-feature Kruskal-Wallis across 3 groups + Spearman correlation with
# ordinal severity, both with BH-adjusted FDR. Mirrors the AP paper's Table 2.
group_association_table <- function(X, group3, stage_ord) {
kw <- apply(X, 2, function(v) {
tryCatch(kruskal.test(v, group3)$p.value, error = function(e) NA_real_)
})
sp <- apply(X, 2, function(v) {
tryCatch({
r <- suppressWarnings(cor.test(v, stage_ord, method = "spearman"))
c(rho = unname(r$estimate), p = r$p.value)
}, error = function(e) c(rho = NA_real_, p = NA_real_))
})
tibble(
feature = colnames(X),
kw_p = kw,
kw_fdr = p.adjust(kw, method = "BH"),
spearman_rho = sp["rho", ],
spearman_p = sp["p", ],
spearman_fdr = p.adjust(sp["p", ], method = "BH")
) %>% arrange(spearman_p)
}
# Per-feature association with a continuous clinical variable (age)
clinical_cor_table <- function(X, clinvar) {
res <- apply(X, 2, function(v) {
tryCatch({
r <- suppressWarnings(cor.test(v, clinvar, method = "spearman"))
c(rho = unname(r$estimate), p = r$p.value)
}, error = function(e) c(rho = NA_real_, p = NA_real_))
})
tibble(feature = colnames(X), rho = res["rho", ], p = res["p", ]) %>%
mutate(fdr = p.adjust(p, method = "BH")) %>%
arrange(p)
}
# Per-feature association with a two-level clinical variable (gender)
clinical_wilcox_table <- function(X, clinvar) {
p <- apply(X, 2, function(v) {
tryCatch(wilcox.test(v ~ clinvar)$p.value, error = function(e) NA_real_)
})
tibble(feature = colnames(X), p = p) %>%
mutate(fdr = p.adjust(p, method = "BH")) %>%
arrange(p)
}
This uses the full 233-patient clinical file, not
the 64-patient omics cohort, because clinical_clean.rds
contains three groups with no omics data attached: BBP (benign biliary
pathology), CP (chronic pancreatitis) and HC (healthy control). This
answers a clinical baseline question — how routine lab
parameters differ across all six diagnostic groups — and is a different,
narrower thing than the proteomic/metabolomic PDAC-vs-BBP comparison
called for in 5.5.2, which still cannot be done because BBP patients
were never run through DIA-MS or NMR.
group_order <- c("HC", "BBP", "CP", "RPC", "LAPC", "MPC")
clin_all <- clin %>%
filter(pathology %in% group_order) %>%
mutate(pathology = factor(pathology, levels = group_order))
table(clin_all$pathology)
##
## HC BBP CP RPC LAPC MPC
## 42 83 6 79 13 10
lab_vars <- c(
white_cell_count = "WCC",
haemaglobin = "Haemoglobin",
creatinine_umol_l = "Creatinine",
c_reactive_protein_mg_l = "CRP",
total_protein_g_l = "Total Protein",
albumin_g_l = "Albumin",
total_bilirubin_tbil_umol_l = "Tbil",
conjugated_bilirubin_dbil_umol_l = "Dbil",
alanine_transaminase_alt_u_l = "ALT",
aspartate_transaminase_ast_u_l = "AST",
alkaline_phosphatase_alp_u_l = "ALP",
gamma_glutamyl_transferase_ggt_u_l = "GGT"
)
stopifnot(all(names(lab_vars) %in% names(clin_all)))
median_iqr <- function(x) {
x <- x[!is.na(x)]
if (length(x) == 0) return("-")
sprintf("%.1f [%.1f\u2013%.1f]", median(x), quantile(x, 0.25), quantile(x, 0.75))
}
table1_row <- function(var, label) {
x <- clin_all[[var]]
n_total <- sum(!is.na(x))
by_grp <- sapply(group_order, function(g) median_iqr(x[clin_all$pathology == g]))
kw_p <- tryCatch(kruskal.test(x ~ clin_all$pathology)$p.value, error = function(e) NA_real_)
as_tibble_row(c(list(Feature = label, N = n_total), as.list(by_grp),
list(p = kw_p)))
}
table1 <- bind_rows(lapply(names(lab_vars), function(v) table1_row(v, lab_vars[[v]])))
table1 <- table1 %>% mutate(p = ifelse(p < 0.01, "<0.01", sprintf("%.2f", p)))
knitr::kable(table1)
| Feature | N | HC | BBP | CP | RPC | LAPC | MPC | p |
|---|---|---|---|---|---|---|---|---|
| WCC | 163 | - | 8.1 [6.5–9.8] | 6.8 [6.2–8.2] | 8.9 [7.0–13.2] | 10.8 [7.1–11.4] | 13.4 [9.1–15.5] | 0.09 |
| Haemoglobin | 164 | - | 11.9 [10.7–13.7] | 12.7 [9.6–14.6] | 10.4 [9.2–12.2] | 9.9 [7.9–11.6] | 11.1 [9.7–12.2] | <0.01 |
| Creatinine | 162 | - | 69.5 [61.0–82.8] | 90.0 [64.2–130.0] | 72.0 [58.0–97.2] | 63.0 [56.0–82.0] | 71.0 [57.0–78.0] | 0.78 |
| CRP | 163 | - | 20.0 [8.0–73.0] | 70.0 [20.0–162.8] | 56.0 [19.0–135.0] | 94.0 [39.0–136.0] | 113.0 [50.8–222.8] | <0.01 |
| Total Protein | 146 | - | 72.5 [67.2–78.8] | 66.0 [61.2–71.5] | 64.0 [57.5–68.0] | 65.0 [55.5–67.5] | 72.0 [69.0–74.2] | <0.01 |
| Albumin | 155 | - | 39.5 [35.2–42.8] | 36.5 [35.2–38.5] | 31.0 [27.0–35.0] | 27.0 [25.0–30.0] | 36.0 [31.0–38.0] | <0.01 |
| Tbil | 176 | - | 30.0 [10.0–92.0] | 5.0 [5.0–8.8] | 160.0 [65.0–268.0] | 100.0 [43.0–185.0] | 60.0 [40.0–335.0] | <0.01 |
| Dbil | 176 | - | 23.0 [4.0–73.5] | 2.0 [2.0–2.8] | 141.0 [37.0–246.0] | 90.0 [31.0–151.0] | 51.0 [38.0–315.0] | <0.01 |
| ALT | 155 | - | 47.5 [17.0–107.5] | 18.0 [15.0–22.5] | 92.0 [41.0–151.0] | 64.0 [17.0–101.0] | 48.0 [30.0–362.0] | 0.01 |
| AST | 155 | - | 53.0 [28.0–90.0] | 28.5 [22.5–34.5] | 104.0 [59.0–162.0] | 106.0 [51.0–118.0] | 123.0 [57.0–268.0] | <0.01 |
| ALP | 176 | - | 200.0 [111.0–403.5] | 74.0 [70.5–104.5] | 615.0 [302.0–1045.0] | 386.0 [237.0–789.0] | 397.0 [302.0–1645.0] | <0.01 |
| GGT | 176 | - | 171.0 [80.5–500.5] | 61.5 [53.5–246.5] | 502.0 [198.0–1155.0] | 313.0 [151.0–683.0] | 839.0 [400.0–1021.0] | <0.01 |
Reading this table: any row with p < 0.05 differs significantly across at least one pair of the six groups (the test does not say which pair — follow up with pairwise Wilcoxon tests if a specific comparison, e.g. RPC vs BBP, is needed for the thesis text). Liver-function markers and inflammatory markers are expected to track disease group most closely, since biliary obstruction and systemic inflammation both increase with more advanced disease.
prot_stage <- group_association_table(prot_mat, clin64$pathology, clin64$stage_ord)
sig_prot_stage <- prot_stage %>% filter(kw_fdr < 0.10 | spearman_fdr < 0.10)
nrow(sig_prot_stage)
## [1] 0
knitr::kable(head(sig_prot_stage, 15), digits = 4)
| feature | kw_p | kw_fdr | spearman_rho | spearman_p | spearman_fdr |
|---|
if (nrow(sig_prot_stage) == 0) {
cat("**No proteins passed FDR < 0.10 across pathology groups in this cohort.**",
"This is a legitimate result to report as-is: with 50 RPC, 10 LAPC and",
"4 MPC patients, the MPC group is small and statistical power for a",
"3-group comparison is limited. Report the top nominal (unadjusted",
"p < 0.05) associations descriptively, with this caveat stated",
"explicitly, rather than implying a validated finding.\n")
}
No proteins passed FDR < 0.10 across pathology groups in this cohort. This is a legitimate result to report as-is: with 50 RPC, 10 LAPC and 4 MPC patients, the MPC group is small and statistical power for a 3-group comparison is limited. Report the top nominal (unadjusted p < 0.05) associations descriptively, with this caveat stated explicitly, rather than implying a validated finding.
prot_stage %>% filter(kw_p < 0.05 | spearman_p < 0.05) %>%
head(15) %>% knitr::kable(digits = 4)
| feature | kw_p | kw_fdr | spearman_rho | spearman_p | spearman_fdr |
|---|---|---|---|---|---|
| hspa1a | 0.0082 | 0.999 | 0.3746 | 0.0023 | 0.9964 |
| igf2 | 0.0230 | 0.999 | 0.3335 | 0.0071 | 0.9964 |
| sptb | 0.0332 | 0.999 | -0.3219 | 0.0095 | 0.9964 |
| ppp6c | 0.0365 | 0.999 | -0.3216 | 0.0096 | 0.9964 |
| galnt15 | 0.0595 | 0.999 | 0.2948 | 0.0181 | 0.9964 |
| prr4 | 0.0669 | 0.999 | 0.2926 | 0.0190 | 0.9964 |
| kpnb1 | 0.0734 | 0.999 | -0.2877 | 0.0212 | 0.9964 |
| fam114a2 | 0.0669 | 0.999 | -0.2854 | 0.0223 | 0.9964 |
| igfbp1 | 0.0846 | 0.999 | 0.2796 | 0.0253 | 0.9964 |
| septin7 | 0.0678 | 0.999 | 0.2747 | 0.0280 | 0.9964 |
| ppp6r3 | 0.0983 | 0.999 | -0.2711 | 0.0303 | 0.9964 |
| hspa4l | 0.0922 | 0.999 | 0.2669 | 0.0330 | 0.9964 |
| ahnak | 0.0967 | 0.999 | 0.2645 | 0.0347 | 0.9964 |
| coro1b | 0.0360 | 0.999 | 0.2641 | 0.0350 | 0.9964 |
| psmd2 | 0.1151 | 0.999 | -0.2609 | 0.0373 | 0.9964 |
metab_stage <- group_association_table(metab_mat, clin64$pathology, clin64$stage_ord)
sig_metab_stage <- metab_stage %>% filter(kw_fdr < 0.10 | spearman_fdr < 0.10)
nrow(sig_metab_stage)
## [1] 0
knitr::kable(metab_stage %>% head(15), digits = 4)
| feature | kw_p | kw_fdr | spearman_rho | spearman_p | spearman_fdr |
|---|---|---|---|---|---|
| glucose | 0.1839 | 0.8614 | 0.2279 | 0.0702 | 0.8213 |
| mannose | 0.2218 | 0.8614 | 0.2159 | 0.0866 | 0.8213 |
| pyruvate | 0.2519 | 0.8614 | 0.2090 | 0.0974 | 0.8213 |
| glycine | 0.2937 | 0.8614 | 0.1932 | 0.1261 | 0.8213 |
| isoleucine | 0.3564 | 0.8614 | 0.1805 | 0.1534 | 0.8213 |
| lactate | 0.3431 | 0.8614 | 0.1721 | 0.1739 | 0.8213 |
| acetoacetate | 0.2707 | 0.8614 | 0.1345 | 0.2895 | 0.8213 |
| leucine | 0.2272 | 0.8614 | 0.1343 | 0.2899 | 0.8213 |
| glyc_b | 0.2615 | 0.8614 | 0.1273 | 0.3162 | 0.8213 |
| cholesterol | 0.5688 | 0.9104 | 0.1241 | 0.3286 | 0.8213 |
| protein_nh | 0.3515 | 0.8614 | -0.1165 | 0.3591 | 0.8213 |
| lipid_beta_ch2 | 0.6088 | 0.9104 | 0.1115 | 0.3806 | 0.8213 |
| glyc_a | 0.5843 | 0.9104 | 0.1018 | 0.4235 | 0.8213 |
| glutamine | 0.7227 | 0.9104 | 0.0979 | 0.4417 | 0.8213 |
| tyrosine | 0.5970 | 0.9104 | 0.0923 | 0.4684 | 0.8213 |
Age (continuous, Spearman) and gender (two-level, Wilcoxon) are tested against every protein and metabolite. This uses what the clinical file actually has consistently for all 64 patients; if other variables (e.g. smoking, comorbidities) should be included, they can be added the same way.
prot_age <- clinical_cor_table(prot_mat, clin64$age)
metab_age <- clinical_cor_table(metab_mat, clin64$age)
cat("Proteins with age FDR < 0.10:", sum(prot_age$fdr < 0.10, na.rm = TRUE), "\n")
## Proteins with age FDR < 0.10: 0
cat("Metabolites with age FDR < 0.10:", sum(metab_age$fdr < 0.10, na.rm = TRUE), "\n")
## Metabolites with age FDR < 0.10: 0
knitr::kable(head(prot_age, 10), digits = 4)
| feature | rho | p | fdr |
|---|---|---|---|
| acly | 0.3816 | 0.0024 | 0.8952 |
| ighv5_51 | 0.3810 | 0.0025 | 0.8952 |
| hspb1 | -0.3777 | 0.0027 | 0.8952 |
| iglv3_19 | 0.3707 | 0.0033 | 0.8952 |
| tagln2 | -0.3638 | 0.0039 | 0.8952 |
| iglv3_25 | 0.3556 | 0.0049 | 0.8952 |
| ppia | -0.3540 | 0.0051 | 0.8952 |
| ilk | -0.3373 | 0.0079 | 0.9379 |
| iglv2_18 | 0.3359 | 0.0081 | 0.9379 |
| lrg1 | 0.3305 | 0.0093 | 0.9379 |
knitr::kable(head(metab_age, 10), digits = 4)
| feature | rho | p | fdr |
|---|---|---|---|
| creatinine | 0.3441 | 0.0066 | 0.1920 |
| glycorol_phospholipid | 0.2433 | 0.0588 | 0.7913 |
| glucose | -0.2246 | 0.0819 | 0.7913 |
| phenylalanine | 0.1983 | 0.1255 | 0.8064 |
| lipid_alpha_ch2 | 0.1916 | 0.1390 | 0.8064 |
| creatine | -0.1721 | 0.1847 | 0.8870 |
| glycine | -0.1481 | 0.2547 | 0.8870 |
| glutamate | -0.1367 | 0.2934 | 0.8870 |
| leucine | -0.1254 | 0.3357 | 0.8870 |
| glyc_b | -0.1220 | 0.3489 | 0.8870 |
gender_f <- factor(clin64$gender)
table(gender_f)
## gender_f
## female male
## 20 41
prot_gender <- clinical_wilcox_table(prot_mat, gender_f)
metab_gender <- clinical_wilcox_table(metab_mat, gender_f)
cat("Proteins with gender FDR < 0.10:", sum(prot_gender$fdr < 0.10, na.rm = TRUE), "\n")
## Proteins with gender FDR < 0.10: 0
cat("Metabolites with gender FDR < 0.10:", sum(metab_gender$fdr < 0.10, na.rm = TRUE), "\n")
## Metabolites with gender FDR < 0.10: 0
knitr::kable(head(prot_gender, 10), digits = 4)
| feature | p | fdr |
|---|---|---|
| mapk14 | 0.0013 | 0.9859 |
| psmd6 | 0.0029 | 0.9859 |
| ighv1_18 | 0.0046 | 0.9859 |
| iglv3_25 | 0.0054 | 0.9859 |
| pcdh12 | 0.0060 | 0.9859 |
| hpse | 0.0073 | 0.9859 |
| iglv3_9 | 0.0076 | 0.9859 |
| aco2 | 0.0122 | 0.9859 |
| eif3b | 0.0122 | 0.9859 |
| ppm1f | 0.0122 | 0.9859 |
knitr::kable(head(metab_gender, 10), digits = 4)
| feature | p | fdr |
|---|---|---|
| creatine | 0.0411 | 0.8302 |
| lipid_ch_ch2_ch | 0.0661 | 0.8302 |
| unsaturated_lipid_ch_ch | 0.0989 | 0.8302 |
| phenylalanine | 0.1233 | 0.8302 |
| formate | 0.1804 | 0.8302 |
| tyrosine | 0.2678 | 0.8302 |
| protein_nh | 0.3097 | 0.8302 |
| lipid_ch3 | 0.3097 | 0.8302 |
| valine | 0.3322 | 0.8302 |
| glucose | 0.3477 | 0.8302 |
Every protein is correlated (Spearman) against every metabolite: 1,221 × 29 = 35,409 pairs. FDR is applied across the full set of pairwise tests.
cor_mat <- cor(prot_mat, metab_mat, method = "spearman", use = "pairwise.complete.obs")
dim(cor_mat)
## [1] 1221 29
# p-values via cor.test, pairwise (slower but exact)
n <- nrow(prot_mat)
t_stat <- cor_mat * sqrt((n - 2) / (1 - cor_mat^2))
p_mat <- 2 * pt(-abs(t_stat), df = n - 2)
pm_long <- as_tibble(as.table(p_mat), .name_repair = "minimal")
colnames(pm_long) <- c("protein", "metabolite", "p")
rho_long <- as_tibble(as.table(cor_mat), .name_repair = "minimal")
colnames(rho_long) <- c("protein", "metabolite", "rho")
pm_results <- pm_long %>%
left_join(rho_long, by = c("protein", "metabolite")) %>%
mutate(fdr = p.adjust(p, method = "BH")) %>%
arrange(p)
cat("Pairs with FDR < 0.10:", sum(pm_results$fdr < 0.10, na.rm = TRUE), "\n")
## Pairs with FDR < 0.10: 232
knitr::kable(head(pm_results, 15), digits = 4)
| protein | metabolite | p | rho | fdr |
|---|---|---|---|---|
| serpine1 | lipid_alpha_ch2 | 0 | 0.6237 | 0.0013 |
| serpine1 | protein_nh | 0 | -0.5636 | 0.0166 |
| serpine1 | glycorol_phospholipid | 0 | 0.5612 | 0.0166 |
| serpine1 | lipid_ch2 | 0 | 0.5469 | 0.0225 |
| galnt15 | glutamine | 0 | 0.5452 | 0.0225 |
| itih4 | lipid_alpha_ch2 | 0 | 0.5400 | 0.0244 |
| gsn | lipid_alpha_ch2 | 0 | -0.5312 | 0.0318 |
| prdx4 | glyc_b | 0 | 0.5281 | 0.0323 |
| ctsd | protein_nh | 0 | -0.5200 | 0.0358 |
| itih4 | glycorol_phospholipid | 0 | 0.5191 | 0.0358 |
| prdx4 | glyc_a | 0 | 0.5190 | 0.0358 |
| acta1 | lipid_alpha_ch2 | 0 | -0.5148 | 0.0398 |
| iglv2_8 | glutamine | 0 | 0.5125 | 0.0406 |
| ywhag | lipid_alpha_ch2 | 0 | 0.5089 | 0.0443 |
| pcbp2 | lactate | 0 | -0.5036 | 0.0471 |
top_metab_by_hits <- pm_results %>% filter(fdr < 0.10) %>%
count(metabolite, sort = TRUE) %>% pull(metabolite)
if (length(top_metab_by_hits) >= 2) {
top_prot_by_hits <- pm_results %>% filter(fdr < 0.10) %>%
count(protein, sort = TRUE) %>% slice_head(n = 40) %>% pull(protein)
pheatmap(cor_mat[top_prot_by_hits, top_metab_by_hits, drop = FALSE],
main = "Spearman rho: top proteins x metabolites with FDR < 0.10",
fontsize_row = 6, fontsize_col = 8)
} else {
cat("Fewer than 2 metabolites had any FDR < 0.10 hit; heatmap skipped.",
"The strongest nominal pairs are shown in the table above instead.\n")
}
SERPINE1 appears in 4 of the top 15 protein-metabolite pairs. This checks whether it is a genuinely broad hub across metabolites, or driven by one or two outlying patients.
serpine1_cor <- pm_results %>% filter(protein == "serpine1") %>% arrange(p)
cat("SERPINE1 pairs with FDR < 0.10:", sum(serpine1_cor$fdr < 0.10, na.rm = TRUE),
"of 29 metabolites tested\n")
## SERPINE1 pairs with FDR < 0.10: 4 of 29 metabolites tested
knitr::kable(serpine1_cor, digits = 4)
| protein | metabolite | p | rho | fdr |
|---|---|---|---|---|
| serpine1 | lipid_alpha_ch2 | 0.0000 | 0.6237 | 0.0013 |
| serpine1 | protein_nh | 0.0000 | -0.5636 | 0.0166 |
| serpine1 | glycorol_phospholipid | 0.0000 | 0.5612 | 0.0166 |
| serpine1 | lipid_ch2 | 0.0000 | 0.5469 | 0.0225 |
| serpine1 | unsaturated_lipid_ch_ch | 0.0192 | 0.2921 | 0.2515 |
| serpine1 | creatinine | 0.0201 | 0.2899 | 0.2542 |
| serpine1 | alanine | 0.0300 | -0.2715 | 0.2867 |
| serpine1 | glycine | 0.0476 | -0.2486 | 0.3366 |
| serpine1 | cholesterol | 0.0590 | -0.2374 | 0.3649 |
| serpine1 | glutamate | 0.1149 | -0.1990 | 0.4680 |
| serpine1 | mannose | 0.1772 | 0.1708 | 0.5475 |
| serpine1 | glutamine | 0.2247 | 0.1539 | 0.5994 |
| serpine1 | glucose | 0.2275 | -0.1530 | 0.6024 |
| serpine1 | lipid_beta_ch2 | 0.2375 | 0.1498 | 0.6110 |
| serpine1 | pyruvate | 0.2437 | 0.1478 | 0.6167 |
| serpine1 | phenylalanine | 0.2604 | 0.1428 | 0.6316 |
| serpine1 | acetate | 0.2929 | -0.1335 | 0.6589 |
| serpine1 | lactate | 0.4364 | 0.0990 | 0.7622 |
| serpine1 | acetoacetate | 0.5293 | 0.0801 | 0.8156 |
| serpine1 | isoleucine | 0.6062 | 0.0657 | 0.8553 |
| serpine1 | lipid_ch_ch2_ch | 0.6424 | 0.0592 | 0.8715 |
| serpine1 | formate | 0.7068 | -0.0479 | 0.8987 |
| serpine1 | lipid_ch3 | 0.7127 | 0.0469 | 0.9007 |
| serpine1 | glyc_b | 0.7714 | -0.0370 | 0.9250 |
| serpine1 | leucine | 0.8235 | -0.0284 | 0.9435 |
| serpine1 | creatine | 0.8395 | -0.0258 | 0.9483 |
| serpine1 | glyc_a | 0.9230 | -0.0123 | 0.9762 |
| serpine1 | valine | 0.9305 | -0.0111 | 0.9783 |
| serpine1 | tyrosine | 0.9573 | 0.0068 | 0.9878 |
pal <- c(RPC = "#1D9E75", LAPC = "#EF9F27", MPC = "#D85A30")
top_pair <- serpine1_cor$metabolite[1]
plot(prot_mat[, "serpine1"], metab_mat[, top_pair],
xlab = "SERPINE1 (standardized)", ylab = paste0(top_pair, " (standardized)"),
pch = 19, col = pal[clin64$pathology],
main = sprintf("SERPINE1 vs %s (rho = %.2f, FDR = %.4f)",
top_pair, serpine1_cor$rho[1], serpine1_cor$fdr[1]))
legend("topleft", legend = names(pal), col = pal, pch = 19, bty = "n")
Reading this: if the strongest pair’s scatter looks like a smooth trend across most patients, that supports a real biological association. If it looks driven by one or two extreme points sitting apart from the rest, the correlation is fragile and should be reported as such, the same way SPTAN1’s influence from P009 was checked in Objective 2.
A joint PCA on the combined 1,250-feature harmonized matrix (all 64 patients), coloured by pathology group. This is the standard-methods equivalent of the papers’ KODAMA unsupervised analysis: it asks the same question — does the molecular profile separate by pathology group at all?
X_all <- harm %>% column_to_rownames("id_std") %>% as.matrix()
X_all <- X_all[clin64$id_std, ]
pca <- prcomp(X_all, scale. = FALSE) # data already scaled in Objective 1
var_exp <- summary(pca)$importance[2, 1:2] * 100
var_exp
## PC1 PC2
## 34.178 9.439
pal <- c(RPC = "#1D9E75", LAPC = "#EF9F27", MPC = "#D85A30")
plot(pca$x[, 1], pca$x[, 2], col = pal[clin64$pathology], pch = 19,
xlab = sprintf("PC1 (%.1f%%)", var_exp[1]),
ylab = sprintf("PC2 (%.1f%%)", var_exp[2]),
main = "Joint proteomic-metabolomic PCA, coloured by pathology")
legend("topright", legend = names(pal), col = pal, pch = 19, bty = "n")
ann <- data.frame(pathology = clin64$pathology, row.names = clin64$id_std)
ann_colors <- list(pathology = pal)
pheatmap(t(scale(X_all)), show_colnames = FALSE, show_rownames = FALSE,
annotation_col = ann, annotation_colors = ann_colors,
main = "Hierarchical clustering: all 64 patients x 1,250 features")
Reading this section: if patients do not visibly separate by pathology colour in either the PCA or the clustering, that is itself the honest result — it says the dominant variation in the omics profile is not primarily driven by resectability stage, which is worth stating plainly rather than searching for a cluster structure that isn’t there.
summary_tbl <- tibble(
Section = c("5.5.3 Proteins x stage", "5.6.2 Metabolites x stage",
"5.5.4 Proteins x age", "5.5.4 Proteins x gender",
"5.6.3 Metabolites x age", "5.6.3 Metabolites x gender",
"5.7.2 Protein-metabolite pairs", "5.7.3 Joint PCA (PC1+PC2 variance)"),
Result = c(
sprintf("%d of %d proteins FDR<0.10 (KW or Spearman)", nrow(sig_prot_stage), ncol(prot_mat)),
sprintf("%d of %d metabolites FDR<0.10", nrow(sig_metab_stage), ncol(metab_mat)),
sprintf("%d of %d FDR<0.10", sum(prot_age$fdr < 0.10, na.rm = TRUE), ncol(prot_mat)),
sprintf("%d of %d FDR<0.10", sum(prot_gender$fdr < 0.10, na.rm = TRUE), ncol(prot_mat)),
sprintf("%d of %d FDR<0.10", sum(metab_age$fdr < 0.10, na.rm = TRUE), ncol(metab_mat)),
sprintf("%d of %d FDR<0.10", sum(metab_gender$fdr < 0.10, na.rm = TRUE), ncol(metab_mat)),
sprintf("%d of %d pairs FDR<0.10", sum(pm_results$fdr < 0.10, na.rm = TRUE), nrow(pm_results)),
sprintf("%.1f%% + %.1f%%", var_exp[1], var_exp[2])
)
)
knitr::kable(summary_tbl)
| Section | Result |
|---|---|
| 5.5.3 Proteins x stage | 0 of 1221 proteins FDR<0.10 (KW or Spearman) |
| 5.6.2 Metabolites x stage | 0 of 29 metabolites FDR<0.10 |
| 5.5.4 Proteins x age | 0 of 1221 FDR<0.10 |
| 5.5.4 Proteins x gender | 0 of 1221 FDR<0.10 |
| 5.6.3 Metabolites x age | 0 of 29 FDR<0.10 |
| 5.6.3 Metabolites x gender | 0 of 29 FDR<0.10 |
| 5.7.2 Protein-metabolite pairs | 232 of 35409 pairs FDR<0.10 |
| 5.7.3 Joint PCA (PC1+PC2 variance) | 34.2% + 9.4% |
saveRDS(
list(prot_stage = prot_stage, metab_stage = metab_stage,
prot_age = prot_age, prot_gender = prot_gender,
metab_age = metab_age, metab_gender = metab_gender,
pm_results = pm_results, pca = pca, clin64 = clin64),
"extended_profiling_results.rds"
)
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] pheatmap_1.0.13 tibble_3.3.1 stringr_1.6.0 dplyr_1.2.1
##
## loaded via a namespace (and not attached):
## [1] vctrs_0.7.3 cli_3.6.6 knitr_1.51 rlang_1.2.0
## [5] xfun_0.59 stringi_1.8.7 otel_0.2.0 generics_0.1.4
## [9] jsonlite_2.0.0 glue_1.8.1 htmltools_0.5.9 sass_0.4.10
## [13] scales_1.4.0 rmarkdown_2.31 grid_4.6.1 evaluate_1.0.5
## [17] jquerylib_0.1.4 fastmap_1.2.0 yaml_2.3.12 lifecycle_1.0.5
## [21] compiler_4.6.1 codetools_0.2-20 RColorBrewer_1.1-3 pkgconfig_2.0.3
## [25] rstudioapi_0.19.0 farver_2.1.2 digest_0.6.39 R6_2.6.1
## [29] tidyselect_1.2.1 pillar_1.11.1 magrittr_2.0.5 bslib_0.11.0
## [33] withr_3.0.3 gtable_0.3.6 tools_4.6.1 cachem_1.1.0