Skip to content

How to Build a Proteomics Pipeline for Your Own Plasma Samples

Oak
An open mass-spectrometry instrument splits a plasma aerosol into a fan of light, fed by a carousel of hundreds of amber vials in graded colors.

By the end of this guide you will have a protein-by-sample matrix for your own plasma, on a log2 scale, with per-protein quality flags, a normalization step you can defend, and a model that tells you which proteins are moving over time beyond what the assay’s own noise explains. The inputs are either vendor mass spectrometry files (Thermo .raw, Bruker .d, Sciex .wiff) or an affinity panel export (Olink NPX in long CSV, SomaScan in an .adat), plus a machine with 32 GB of RAM and a recent R or Python installation. If you are running mass spectrometry searches, budget 64 GB and a fast NVMe scratch disk: DIA-NN writes large intermediate .quant files and will thrash a spinning disk. Everything below assumes plasma or serum, which is the hardest human matrix to quantify well and therefore the one where pipeline choices matter most.

1. Establish which assay produced your data

Proteomics is not one measurement. The three branches people usually mean are expression proteomics (how much of each protein is present), structural proteomics (where proteins sit and what shape they take), and interaction proteomics (what binds what). A personal molecular profile is almost entirely expression proteomics, measured either by mass spectrometry or by affinity reagents, and the two produce data with different failure modes.

Mass spectrometry gives you untargeted coverage with absolute peptide evidence, but plasma spans more than ten orders of magnitude in protein abundance and albumin alone accounts for roughly half the protein mass. Neat plasma on a short gradient typically yields 400 to 600 protein groups. Depletion of the top 14 abundant proteins, or nanoparticle-based enrichment, pushes that into the low thousands at the cost of added variance in the depletion step itself. Affinity panels (Olink’s proximity extension assay, SomaScan’s aptamers) sidestep dynamic range by measuring a fixed list of targets with high sensitivity, and report relative units that are not comparable across platforms or even across panel versions.

Write down which you have before doing anything else, because the normalization and the interpretation differ completely. If you have both, keep them in separate matrices and compare them at the end rather than merging them early.

2. Convert vendor files to open formats

DIA-NN reads Thermo .raw and Bruker .d directly on Windows, but on Linux the dependable path is conversion to mzML first. Use ProteoWizard’s msconvert with vendor peak picking, which applies the instrument’s own centroiding algorithm rather than a generic one:

docker run -v $PWD:/data chambm/pwiz-skyline-i-agree-to-the-vendor-licenses \
  wine msconvert /data/raw/*.raw \
  --mzML --64 --zlib \
  --filter "peakPicking vendor msLevel=1-2" \
  --outdir /data/mzml

For Thermo data specifically, ThermoRawFileParser is faster and avoids Wine:

ThermoRawFileParser -d=raw/ -o=mzml/ -f=2 -m=1

-f=2 writes indexed mzML, -m=1 writes a metadata text file per run that contains the instrument model, gradient length, and total ion current. Keep those metadata files. When a run looks like an outlier six months later, the first thing you will want is the acquisition date and the column used.

Archive the original vendor files. Conversion is lossy in practice even when it is lossless in principle, because you will want to reprocess with a newer search engine in two years and the vendor format is what the next tool will support.

3. Search DIA data with DIA-NN

For data-independent acquisition, which is what nearly all modern plasma proteomics uses, we recommend DIA-NN in library-free mode with a predicted spectral library. It is faster than the alternatives on single-machine hardware and its match-between-runs implementation is well suited to a longitudinal design where the same proteins recur across dozens of runs.

diann \
  --dir /data/mzml \
  --fasta UP000005640_9606.fasta \
  --fasta-search --predictor \
  --cut K*,R* --missed-cleavages 1 \
  --min-pep-len 7 --max-pep-len 30 \
  --min-pr-charge 2 --max-pr-charge 4 \
  --var-mods 1 --var-mod "UniMod:35,15.994915,M" \
  --met-excision --unimod4 \
  --qvalue 0.01 \
  --mass-acc 15 --mass-acc-ms1 15 \
  --reanalyse --smart-profiling \
  --matrices --threads 32 \
  --out /data/out/report.parquet

A few of these deserve explanation. The --fasta-search --predictor flags build an in-silico library from the UniProt human reference proteome using DIA-NN’s deep learning retention time and fragmentation models, which removes the need for a separately acquired DDA library. The --reanalyse flag performs match-between-runs by refining the library against your own data, which materially raises identifications when you have many similar runs. The --unimod4 flag fixes carbamidomethylation on cysteine, correct if your sample prep used iodoacetamide. The --mass-acc 15 flag sets a 15 ppm tolerance. Leave it off entirely on the first pass and let DIA-NN infer it, then fix the inferred value for subsequent runs so results stay comparable.

The output you want is report.pg_matrix.tsv, which contains MaxLFQ-normalized protein group intensities. Filter the precursor-level report.parquet yourself rather than trusting defaults, keeping rows with Q.Value <= 0.01, PG.Q.Value <= 0.01, and Lib.Q.Value <= 0.01. If you plan to compare across projects, add --relaxed-prot-inf off and be explicit about protein inference, because protein group membership changes when the search space changes.

Alternatives are worth knowing. FragPipe with MSFragger and DIA-NN as its quantification back end is the most complete GUI-driven option and is what we use when a search needs open modification discovery. The Trans-Proteomic Pipeline remains the reference implementation for standardized, reproducible processing at scale and is the right choice if you need statistical validation steps like PeptideProphet and ProteinProphet with well-characterized error models.1 OpenMS exposes the same operations as composable command-line tools that slot into Nextflow or Snakemake, which is what you want if the pipeline has to run unattended on new samples every quarter.2

4. Quality-control the runs before looking at any biology

Resist the urge to plot proteins of interest. Compute run-level metrics first and decide which runs to drop, because a single bad injection will dominate any downstream clustering.

For each run, pull the number of precursors and protein groups at 1% FDR, the median peak width, the total ion current, and the fraction of missing values relative to the cohort median. A run with 30% fewer protein groups than its neighbors is usually a column or spray problem. Biology is not the usual cause. Plot retention time of a set of stable peptides across runs to catch chromatographic drift.

Then compute coefficients of variation on technical replicates if you have them. A working threshold for plasma DIA is a median CV under 20% across protein groups. Affinity panels should be tighter, and Olink reports per-assay intra- and inter-plate CVs you can compare against. Proteins whose technical CV exceeds their biological variation across your timepoints carry no information for you and should be flagged now rather than explained later.

library(tidyverse)
pg <- read_tsv("out/report.pg_matrix.tsv")
mat <- pg %>% column_to_rownames("Protein.Group") %>%
  select(where(is.numeric)) %>% as.matrix()
log2mat <- log2(mat)
detect_rate <- rowMeans(!is.na(log2mat))
qc <- tibble(run = colnames(log2mat),
             n_proteins = colSums(!is.na(log2mat)),
             median_int = apply(log2mat, 2, median, na.rm = TRUE))

5. Normalize and handle missing values deliberately

MaxLFQ already applies a delayed normalization within DIA-NN, but you will usually still see a systematic offset between runs acquired weeks apart. Median-centering on the log2 scale is the least assumption-heavy correction and is what we default to:

log2mat <- sweep(log2mat, 2, apply(log2mat, 2, median, na.rm = TRUE), "-")

Quantile normalization via limma::normalizeBetweenArrays(log2mat, method = "quantile") is stronger and appropriate when you expect the bulk of the proteome to be unchanged, which is a reasonable assumption for repeated sampling of one healthy person and a bad one if a sample was collected during an acute illness.

Missingness is the decision that most affects results. Mass spectrometry missing values are a mixture of missing-not-at-random (below detection) and missing-at-random (stochastic fragmentation), and the common practice of imputing everything from a downshifted normal distribution manufactures differences at exactly the proteins you are most curious about. Our approach is to filter first and impute rarely: keep proteins detected in at least 70% of runs, then leave the remaining gaps as NA and use models that tolerate them. If a downstream method demands a complete matrix, use a k-nearest-neighbor imputation restricted to proteins with fewer than 20% missing, and never impute a protein that is missing in an entire timepoint.

Affinity data has a different convention. Olink values below the limit of detection are reported anyway, and the OlinkAnalyze package exposes both the LOD and a per-sample QC_Warning. Drop assays where more than half your samples fall below LOD rather than treating those numbers as measurements.

library(OlinkAnalyze)
npx <- read_NPX("olink_explore_export.parquet")
npx_clean <- npx %>% filter(QC_Warning == "PASS", SampleQC == "PASS")

6. Remove batch structure or model it

Plasma drawn on different days and run on different plates carries a batch signal that is frequently larger than the biological signal you are after. If you have bridging samples (an aliquot of the same plasma run on every plate, which you should insist on), use them. Olink’s bridging normalization computes a per-assay median shift between plates from the bridge samples and applies it. The olink_normalization() function implements this directly.

Without bridges, you have two options. ComBat from the sva package adjusts empirically and works well when batches contain multiple samples, but it will silently remove real variation if a batch coincides with a biological state. Including batch as a covariate in the per-protein model is more conservative and is what we prefer when batch and time are not confounded:

library(limma)
design <- model.matrix(~ 0 + timepoint + batch, data = meta)
fit <- lmFit(log2mat, design)

The general point is that the analysis stage is where most published proteomics variance is introduced. The acquisition stage is not where most of it arises. For that reason the field converged on shared platforms with explicit, inspectable processing steps rather than ad hoc scripts.13

7. Model change over time in one person

With n=1, group comparison statistics do not apply and you need a personal reference. Build it from your baseline timepoints. For each protein, compute the within-person mean and standard deviation on the log2 scale across at least four to six baseline draws. Take those draws under matched conditions, meaning the same time of day, fasting state and tube type.

Then use the reference change value, the standard clinical-chemistry criterion for whether a difference between two serial measurements exceeds analytical and biological noise:

# CVa = analytical CV, CVi = within-person biological CV, both as fractions
rcv <- function(cva, cvi, z = 1.96) sqrt(2) * z * sqrt(cva^2 + cvi^2)

A protein whose change between two visits exceeds its RCV is a candidate for attention. Everything else is noise, and treating it otherwise across a 3,000-protein panel will produce dozens of spurious signals per visit. Apply Benjamini-Hochberg correction across proteins whenever you compute p-values, and report the number of tests.

For trends across many timepoints, fit a per-protein linear model of log2 abundance on time and inspect the slope, or use a Theil-Sen estimator if single outlier visits are distorting the fit. For downstream enrichment and visualization, Perseus remains the most complete interactive environment for this kind of matrix work,3 and OmicScope provides a scripted path from a quantitative matrix to differential expression and pathway-level summaries with consistent statistics across input formats.4

8. Annotate, then cross-check against your other layers

Map UniProt accessions to gene symbols using the UniProt ID mapping API rather than a stale local table, and keep both identifiers in the matrix. Protein groups containing multiple accessions should be labeled as such, because attributing a change to one member of a group you cannot distinguish is a common and avoidable error.

The value of a protein measurement rises sharply when you can place it next to the genome and the transcriptome, which is the argument behind integrating omics layers at the individual level rather than analyzing each in isolation.56 Two checks are worth running routinely. First, for any affinity-panel protein that reads persistently low, query your whole-genome data for coding variants in the target gene: a missense variant in the epitope region can reduce reagent binding and produce a low value that reflects chemistry rather than protein abundance. Second, compare protein changes against RNA-seq for the same gene. Keep in mind that transcript and protein levels correlate only modestly for most genes, so disagreement is informative rather than disqualifying. Proteomics is also sensitive enough to register responses to environmental and metabolic exposures, which is one reason it is being developed as a readout in exposure assessment work.7

None of this is a clinical result. Research-grade plasma proteomics is not run under CLIA, has no established reference intervals for most targets, and cannot be used to diagnose anything. If a protein you care about is a validated clinical analyte, the appropriate next step is a clinician-ordered test on a validated assay, and any interpretation of the result belongs to that clinician.

Common problems

Protein counts collapse in a subset of runs. Check the mzML file sizes first: a truncated conversion produces a valid file with fewer scans. Then check the metadata for column changes or a gradient edit mid-series.

DIA-NN reports far fewer proteins than a published plasma study. Confirm the study depleted or fractionated. Neat plasma on a 20-minute gradient will not reach 2,000 protein groups regardless of search settings, and turning knobs to chase that number produces false identifications instead of real ones.

Clustering separates samples by acquisition date. This is a batch effect until proven otherwise. Verify with a principal component analysis colored by plate and by date before interpreting any biological grouping.

A protein shows a dramatic change at a single timepoint. Check the precursor-level evidence in report.parquet for that run: a protein quantified from three precursors at one visit and one precursor at the next will move for purely technical reasons. Require consistent precursor support before believing a change.

Two platforms disagree about the same protein. This is common and usually unresolvable without a targeted assay. Affinity reagents measure epitope availability and mass spectrometry measures peptide abundance, and post-translational modifications, proteolysis, and complex formation affect them differently. Record the disagreement rather than picking a winner.

Missing values cluster in low-abundance proteins. That is expected and is the main reason to filter by detection rate before imputing. An imputed value for a protein near the detection limit is a statement about your prior. It is not a measurement.

Footnotes

  1. Eric W. Deutsch, Luis Mendoza, David Shteynberg, et al. Trans‐Proteomic Pipeline, a standardized data processing pipeline for large‐scale reproducible proteomics informatics. PROTEOMICS – Clinical Applications, 2015. https://doi.org/10.1002/prca.201400164 ↩ ↩2

  2. Oliver Kohlbacher, Knut Reinert, Clemens Gröpl, et al. TOPP—the OpenMS proteomics pipeline. Bioinformatics, 2007. https://doi.org/10.1093/bioinformatics/btl299 ↩

  3. Stefka Tyanova, Tikira Temu, Pavel Sinitcyn, et al. The Perseus computational platform for comprehensive analysis of (prote)omics data. Nature Methods, 2016. https://doi.org/10.1038/nmeth.3901 ↩ ↩2

  4. Guilherme Reis-de-Oliveira, Victor Corasolla Carregari, Gabriel Rodrigues dos Reis de Sousa, et al. OmicScope unravels systems-level insights from quantitative proteomics data. Nature Communications, 2024. https://doi.org/10.1038/s41467-024-50875-z ↩

  5. Yehudit Hasin, Marcus Seldin, Aldons Lusis. Multi-omics approaches to disease. Genome Biology, 2017. https://doi.org/10.1186/s13059-017-1215-1 ↩

  6. Rui Vitorino. Transforming Clinical Research: The Power of High-Throughput Omics Integration. Proteomes, 2024. https://doi.org/10.3390/proteomes12030025 ↩

  7. Alix Sarah Aldehoff, Isabel Karkossa, Helen Broghammer, et al. Advanced Proteomics Approaches Hold Potential for the Risk Assessment of Metabolism-Disrupting Chemicals as Omics-Based NAM: A Case Study Using the Phthalate Substitute DINCH. Environmental Science & Technology, 2025. https://doi.org/10.1021/acs.est.5c01206 ↩

Woolf Software builds longitudinal molecular profiles of individuals: whole-genome sequencing, blood RNA sequencing, and plasma proteomics, delivered as a detailed report plus every data file. Build your profile.