Code
library(DEP)
library(QFeatures)
library(SummarizedExperiment)
library(limma)
library(tidyverse)
data("UbiLength", package = "DEP") # real MaxQuant proteinGroups table
dim(UbiLength) # proteins × columnsEvery preceding chapter isolated one step. Here the steps become a study — twice, in two different fields, with the same toolchain.
This capstone runs two complete analyses from raw data to biological conclusion: a label-free proteomics experiment and an untargeted metabolomics experiment. Both use real, published datasets bundled in R packages, so you can reproduce every step without downloading anything.
The point of running them side by side is the thesis of this book made concrete: proteomics and metabolomics are different sciences that share one computational grammar in R. Watch how the same containers (SummarizedExperiment, QFeatures), the same normalization and missing-data logic, the same limma modelling, and the same reproducibility discipline serve both — you learn the toolchain once and apply it to either field.
This chapter uses small, bundled datasets (UbiLength, msdata) so every code block runs without external downloads. For production-scale versions of the same workflows — running on real PRIDE and MetaboLights data with parameter tuning, multi-file processing, and cluster execution — the companion project r4ms_book/ provides complete, documented pipelines:
| Case Study | Pedagogical Version (This Chapter) | Production Version (Companion) |
|---|---|---|
| LFQ proteomics | DEP::UbiLength (1,182 proteins) |
r4ms_book/analysis/proteomics_analysis.R — PXD004886 (4,517 proteins × 22 samples, 11-site benchmark) |
| Tissue proteomics | — | r4ms_book/analysis/tissue_atlas_analysis.R — PXD010154 (33,812 proteins × 12 organs) |
| Clinical DIA | — | r4ms_book/analysis/clinical_spectronaut_analysis.R — PXD000547 (paired patient samples) |
| Untargeted metabolomics | msdata::metabolomics() (4 files) |
r4ms_book/analysis/mtbls38_standards_analysis.R — MTBLS38 (51 compounds with known identity) |
The production scripts are the same logic scaled up — they run the same limma models, produce the same volcano plots, and follow the same reproducibility discipline. When you move from learning the toolchain to applying it to your own data, the companion scripts are your starting template.
Assuming your data behaves like the example. The workflow transfers; the parameters do not. Re-tune feature detection, normalization, and imputation to your own study before trusting a single number.
By the end of this chapter you will be able to:
QFeatures → filter → normalize → impute → differential abundance → interpretation → reportxcms feature detection → normalize → annotate → differential abundance → interpretation → reportEach case study is a guided tour with pointers back to the chapter where each step is taught in depth. If a step is unfamiliar, follow the reference; here we keep commentary short and let the pipeline flow.
| Stage | Proteomics tool | Metabolomics tool | Taught in |
|---|---|---|---|
| Import | readQFeatures() |
MsExperiment, Spectra |
Ch 4–5 |
| Feature generation | search-engine PSMs | xcms::findChromPeaks() |
Ch 7, 10 |
| Container | QFeatures |
SummarizedExperiment |
Ch 5, 12 |
| QC | detection rate, CV | TIC/BPC, QC-pool CV | Ch 6 |
| Normalize | median / VSN | log + median / TIC | Ch 17 |
| Missing data | filter + impute | filter + fill peaks | Ch 18 |
| Identity | protein inference | adduct + mass search | Ch 11, 8–9 |
| Model | limma |
limma |
Ch 20 |
| Interpret | enrichment | pathway / networks | Ch 24 |
| Report | Quarto + PRIDE | Quarto + MetaboLights | Ch 25 |
All code is shown with #| eval: false so the chapter renders without the full analysis stack installed. Each block runs as written against the bundled example data once the packages in the setup chunk are available.
The DEP package bundles the UbiLength dataset (Zhang et al.): a label-free experiment comparing HeLa cells in which the deubiquitinase Ubiquitin-specific protease pathway is perturbed (Ubi knockdown) against controls, quantified by MaxQuant LFQ. The biological question: which proteins change in abundance when the pathway is disrupted?
proteinGroups table (real output)library(DEP)
library(QFeatures)
library(SummarizedExperiment)
library(limma)
library(tidyverse)
data("UbiLength", package = "DEP") # real MaxQuant proteinGroups table
dim(UbiLength) # proteins × columnsFilter contaminants and reverse hits, resolve duplicate identifiers, and construct a SummarizedExperiment/QFeatures with a clean experimental design.
# Remove contaminant + reverse-database hits (standard MaxQuant filtering)
data <- UbiLength |>
dplyr::filter(Reverse != "+", Potential.contaminant != "+")
# Unique protein identifiers
data <- DEP::make_unique(data, names = "Gene.names", ids = "Protein.IDs")
# LFQ intensity columns and an explicit experimental design
lfq_cols <- grep("^LFQ.intensity", colnames(data))
experiment <- DEP::UbiLength_ExpDesign # label / condition / replicate
se <- DEP::make_se(data, lfq_cols, experiment)
seInspect protein counts per sample, the missing-value structure, and sample clustering before modelling.
DEP::plot_frequency(se) # proteins identified per sample
DEP::plot_numbers(se) # per-sample protein counts
DEP::plot_missval(se) # missing-value heatmap — is it structured (MNAR)?Variance-stabilise, then impute. The missingness here is largely left-censored (proteins below detection in one condition), so a minimum-based imputation is appropriate — exactly the MNAR reasoning from Chapter 18.
se_norm <- DEP::normalize_vsn(se)
DEP::plot_normalization(se, se_norm)
# Filter proteins missing in too many replicates, then impute the rest
se_filt <- DEP::filter_missval(se_norm, thr = 0)
se_imp <- DEP::impute(se_filt, fun = "MinProb", q = 0.01) # MNAR-appropriateFit the limma model and extract an FDR-controlled result. DEP wraps limma internally, so this is the empirical-Bayes moderation of Chapter 20 applied to the protein matrix.
diff <- DEP::test_diff(se_imp, type = "control", control = "Ctrl")
dep <- DEP::add_rejections(diff, alpha = 0.05, lfc = 1)
# Ranked results table
results <- DEP::get_results(dep)
results |>
dplyr::filter(significant) |>
dplyr::arrange(dplyr::desc(abs(Ubi_vs_Ctrl_ratio))) |>
dplyr::select(name, Ubi_vs_Ctrl_ratio, Ubi_vs_Ctrl_p.adj) |>
head(15)A volcano plot for the headline result, then pathway enrichment on the significant set to move from a protein list to biology.
DEP::plot_volcano(dep, contrast = "Ubi_vs_Ctrl", label_size = 3, add_names = TRUE)
# Enrichment on the significant proteins (Ch 24)
library(clusterProfiler)
library(org.Hs.eg.db)
sig_genes <- results |> dplyr::filter(significant) |> dplyr::pull(name)
ego <- clusterProfiler::enrichGO(
gene = sig_genes, OrgDb = org.Hs.eg.db,
keyType = "SYMBOL", ont = "BP", pAdjustMethod = "BH"
)
clusterProfiler::dotplot(ego, showCategory = 12)Export the figures and table, capture the environment, and prepare the ProteomeXchange deposit.
dir.create("results", showWarnings = FALSE)
readr::write_csv(results, "results/proteomics_DE.csv")
sessioninfo::session_info() |> capture.output() |>
writeLines("results/proteomics_session.txt")
# Deposit raw + mzIdentML + SDRF to PRIDE / ProteomeXchange (Ch 25)Result. A reproducible, FDR-controlled list of proteins responding to the perturbation, with enriched pathways and a deposit-ready bundle — built entirely from the containers and models of Parts I–VI.
The faahKO package bundles a classic real LC–MS dataset: spinal-cord extracts from fatty-acid amide hydrolase knockout (FAAH⁻/⁻) mice versus wild-type, in which the knockouts accumulate fatty-acid amides. The question: which metabolite features distinguish knockout from wild-type? This is the canonical xcms demonstration dataset — and a complete study in its own right.
.CDF/mzML fileslibrary(xcms)
library(MsExperiment)
library(SummarizedExperiment)
library(faahKO)
cdf_files <- dir(system.file("cdf", package = "faahKO"),
recursive = TRUE, full.names = TRUE)
pheno <- data.frame(
sample_name = sub("\\.CDF$", "", basename(cdf_files)),
sample_group = c(rep("KO", 6), rep("WT", 6))
)
mse <- MsExperiment::readMsExperiment(cdf_files, sampleData = pheno)
msexcms (Ch 7)The full CentWave → alignment → correspondence → gap-filling pipeline of Chapter 7, condensed.
# 1. Peak detection (CentWave)
cwp <- xcms::CentWaveParam(peakwidth = c(20, 80), ppm = 25, snthresh = 10)
xd <- xcms::findChromPeaks(mse, param = cwp)
# 2. Retention-time alignment (Obiwarp)
xd <- xcms::adjustRtime(xd, param = xcms::ObiwarpParam(binSize = 0.6))
# 3. Correspondence (group peaks into features across samples)
pdp <- xcms::PeakDensityParam(sampleGroups = pheno$sample_group,
minFraction = 0.5, bw = 30)
xd <- xcms::groupChromPeaks(xd, param = pdp)
# 4. Fill missing peaks so the matrix has no structural gaps
xd <- xcms::fillChromPeaks(xd)Turn the xcms result into the same SummarizedExperiment used on the proteomics side — the moment the two workflows converge.
se_metab <- xcms::quantify(xd, value = "into") # SummarizedExperiment
se_metab
# assay(): features × samples; rowData(): mz/rt; colData(): genotype# TIC per sample and log-transform + median normalization (Ch 17)
assay(se_metab) <- log2(assay(se_metab) + 1)
sample_medians <- matrixStats::colMedians(assay(se_metab), na.rm = TRUE)
assay(se_metab) <- sweep(assay(se_metab), 2, sample_medians - mean(sample_medians))
# PCA to confirm genotype separates and no batch structure dominates
pc <- prcomp(t(na.omit(assay(se_metab))), scale. = TRUE)limma (Ch 20)The identical modelling code from the proteomics study — the payoff of a shared container.
library(limma)
design <- model.matrix(~ 0 + factor(se_metab$sample_group))
colnames(design) <- c("KO", "WT")
fit <- limma::lmFit(assay(se_metab), design)
fit <- limma::contrasts.fit(fit, limma::makeContrasts(KO - WT, levels = design))
fit <- limma::eBayes(fit)
top <- limma::topTable(fit, number = Inf, adjust.method = "BH")
head(top, 15)Convert the significant features’ m/z to putative identities via exact-mass search — the annotation funnel of Chapter 8.
library(MetaboCoreUtils)
sig <- top |> tibble::rownames_to_column("feature") |>
dplyr::filter(adj.P.Val < 0.05)
sig_mz <- SummarizedExperiment::rowData(se_metab)[sig$feature, "mzmed"]
# Expected m/z for common adducts of candidate fatty-acid amides
adduct_mz <- MetaboCoreUtils::mass2mz(
MetaboCoreUtils::calculateMass("C18H35NO"), # e.g. oleamide
adduct = c("[M+H]+", "[M+Na]+")
)
# Match sig_mz against a reference library within tolerance (Ch 8–9)readr::write_csv(top, "results/metabolomics_DE.csv")
sessioninfo::session_info() |> capture.output() |>
writeLines("results/metabolomics_session.txt")
# Deposit raw mzML + ISA-Tab metadata to MetaboLights (Ch 25)Result. An aligned feature matrix, an FDR-controlled list of features separating knockout from wild-type (recovering the expected fatty-acid-amide accumulation), and a MetaboLights-ready deposit.
DEP::UbiLength) and a real untargeted metabolomics study (faahKO) were each carried from raw data to an interpreted, deposit-ready result.sessionInfo()