6  Perform Initial Quality Control

“Bad metadata can destroy a good experiment faster than bad code.”

A surprising share of “surprising biology” in mass spectrometry turns out to be a pipetting slip, a drifting instrument, or a contaminated blank. The samples that will wreck your conclusions are usually visible on day one — if you look. This chapter is about looking: the handful of plots and checks that catch bad runs before they become bad papers.

WarningThe One Mistake to Avoid

Skipping QC because the run “looked fine.” Drift, carryover, and swapped samples rarely announce themselves — they resurface weeks later as spurious hits. Inspect QC pools and blanks before any modeling.

6.1 Learning Objectives

By the end of this chapter you will be able to:

  • Annotate samples with biological groups, batches, blanks, and QC pools
  • Link metadata to MS spectra and feature tables
  • Detect metadata inconsistencies (mismatched sample names, duplicates)
  • Visualise total ion current (TIC) and base peak chromatograms (BPC)
  • Assess retention time stability across runs
  • Evaluate mass accuracy and intensity distributions
  • Identify blank contamination and carryover
  • Generate a comprehensive MS quality control report

6.2 Why QC Before Modeling?

Quality control is not optional – it is the foundation of reproducible MS omics. Poor‑quality data, unannotated batches, or contaminated blanks will invalidate any downstream statistical analysis. Initial QC answers:

Question Check
Did the instrument perform consistently? TIC, BPC, RT stability
Are samples correctly labelled? Metadata vs file names
Is there batch-to-batch variation? PCA by batch, TIC by batch
Are blanks clean? Feature counts in blanks
Is mass accuracy within tolerance? m/z of known QC peaks

6.2.1 The QC Gate Principle

Think of initial QC as a gate, not an afterthought. A dataset that fails QC should not proceed to preprocessing — the downstream analysis will produce results, but they will be unreliable. The core principle is:

If you cannot tell the difference between biological variation and technical noise, no statistical test can do it for you.

Three statistical principles underpin effective QC:

  1. Visualise before testing. A TIC overlay reveals sample-to-sample variation instantly; a boxplot of log-intensities per sample catches loading issues that summary statistics miss. Formal tests (t-tests, ANOVA) come second, not first.

  2. QC samples are your internal standard for the entire pipeline. Pooled QC samples injected at regular intervals throughout the run provide the only honest estimate of technical variation. The CV of each feature in QC samples is the upper bound of reliability — if a feature’s QC CV is 40 %, any fold-change below 1.4 is noise regardless of the p-value.

  3. Metadata errors are the most common and most damaging QC failure. A swapped sample label, a missing batch annotation, or a confounded design produces results that look correct but are biologically meaningless. Automated checks (all(), identical(), table(batch, condition)) catch these before they propagate.

This chapter focuses on initial QC – before preprocessing, imputation, or modeling. All steps use the msdata package for reproducible demonstration.


6.3 Required Packages

Code
BiocManager::install(c(
  "MSnbase",          # MS data handling
  "Spectra",          # Modern MS data structures
  "mzR",              # Raw file access
  "MsExperiment",     # Sample metadata + spectra
  "msdata",           # Example files
  "ggplot2",
  "dplyr",
  "tidyr"
))
Code
# Load libraries
library(msdata)
library(ggplot2)
library(dplyr)
library(tidyr)

set.seed(42)

6.4 Step 1: Sample Annotation and Experimental Design

Metadata must be recorded before any analysis. Essential fields:

  • sample_name – unique identifier matching file names
  • biological_group – treatment, genotype, time point
  • batch – instrument run date/sequence
  • injection_order – numerical order in the queue
  • typesample, blank, qc_pool, standard

6.4.1 Create Metadata Table

The msdata package provides example mzML files. We’ll build a metadata table matching these files.

Code
# Locate example mzML files
mzml_files <- proteomics(full.names = TRUE)
mzml_files <- mzml_files[1:4]  # use 4 files for clean pairs
n_files <- length(mzml_files)

# Create metadata
metadata <- data.frame(
  file_name = basename(mzml_files),
  sample_name = paste0("S", 1:n_files),
  biological_group = rep(c("Control", "Treatment"), each = n_files/2),
  batch = rep(c(1, 2), each = n_files/2),
  injection_order = 1:n_files,
  type = "sample"   # all are real samples, no blanks/QC in this set
)

head(metadata)
                                                               file_name
1                                                 MRM-standmix-5.mzML.gz
2                                  MS3TMT10_01022016_32917-33481.mzML.gz
3                                                          MS3TMT11.mzML
4 TMT_Erwinia_1uLSike_Top10HCD_isol2_45stepped_60min_01-20141210.mzML.gz
  sample_name biological_group batch injection_order   type
1          S1          Control     1               1 sample
2          S2          Control     1               2 sample
3          S3        Treatment     2               3 sample
4          S4        Treatment     2               4 sample

What this shows: Each row is one mzML file from the msdata::proteomics() collection. file_name matches actual files on disk, sample_name provides short labels for plots, biological_group assigns each sample to Control or Treatment (evenly split), and batch groups the first two files vs. the last two. The type column is set to "sample" — real study samples. In a real experiment you would also have "blank" and "qc_pool" rows.

Best practice: Keep metadata in a CSV file separate from your analysis script, and read it in.

Code
# write.csv(metadata, "sample_metadata.csv", row.names = FALSE)

6.5 Step 2: Biological Groups, Batches, Blanks, and Pooled QC

6.5.1 Why Include Blanks and QC Pools?

Sample Type Purpose
Biological sample Experimental interest
Blank Assess contamination and carryover
Pooled QC Monitor instrument stability across runs
Standard Calibrate retention time and m/z

6.5.2 Simulate a Dataset with QC Samples

For demonstration, we extend the msdata files with simulated QC samples (since real QC files are not included).

Code
# Simulate intensities for 3 QC samples (pooled)
n_features <- 100
n_qc <- 3
qc_intensity <- matrix(rlnorm(n_features * n_qc, meanlog = 10, sdlog = 1),
                       nrow = n_features, ncol = n_qc)
colnames(qc_intensity) <- paste0("QC_", 1:n_qc)

# Add QC metadata
qc_metadata <- data.frame(
  file_name = colnames(qc_intensity),
  sample_name = colnames(qc_intensity),
  biological_group = "QC",
  batch = 1,
  injection_order = max(metadata$injection_order) + 1:n_qc,
  type = "qc_pool"
)

# Combine with real metadata
metadata_full <- bind_rows(metadata, qc_metadata)
tail(metadata_full)
                                                               file_name
2                                  MS3TMT10_01022016_32917-33481.mzML.gz
3                                                          MS3TMT11.mzML
4 TMT_Erwinia_1uLSike_Top10HCD_isol2_45stepped_60min_01-20141210.mzML.gz
5                                                                   QC_1
6                                                                   QC_2
7                                                                   QC_3
  sample_name biological_group batch injection_order    type
2          S2          Control     1               2  sample
3          S3        Treatment     2               3  sample
4          S4        Treatment     2               4  sample
5        QC_1               QC     1               5 qc_pool
6        QC_2               QC     1               6 qc_pool
7        QC_3               QC     1               7 qc_pool

Real‑world note: You must run blanks and QC samples on the instrument. Never rely on simulation.


6.6 Step 3: Linking Metadata to Spectra and Feature Tables

Before any QC plot, ensure metadata matches the spectral data.

6.7 Step 4: Detecting Metadata Inconsistencies

Common metadata errors:

  • Duplicate sample names
  • Mismatch between file names and metadata
  • Missing injection order
  • Batch confounded with biological group (e.g., all controls in batch 1, all treatments in batch 2)

6.7.1 Check for Duplicates

Code
any(duplicated(metadata$sample_name))
[1] FALSE
Code
any(duplicated(metadata$file_name))
[1] FALSE

6.7.2 Check File Existence

Code
all(file.exists(mzml_files))
[1] TRUE

6.7.3 Detect Batch–Group Confounding

Code
table(metadata$batch, metadata$biological_group)
   
    Control Treatment
  1       2         0
  2       0         2

If a batch contains only one biological group, batch and group are confounded – you cannot distinguish technical from biological variation. Solution: Redesign the experiment or use a different statistical approach.

6.7.4 Check Injection Order Completeness

Code
all(1:nrow(metadata) == sort(metadata$injection_order))  # assuming no gaps
[1] TRUE

6.8 Step 5: TIC and BPC Quality Control

Total Ion Current (TIC) and Base Peak Chromatogram (BPC) are the first QC plots for raw LC‑MS data.

6.8.1 Extract TIC from Raw Files

We’ll simulate a realistic TIC trace — two broad LC peaks on a gently falling baseline — to illustrate the QC pattern without requiring a running instrument backend.

Code
# Simulate a realistic TIC: two elution windows + baseline drift
set.seed(42)
rt <- seq(0, 30, length.out = 600)  # 30-minute gradient

make_tic <- function(base_level = 5e7, noise_sd = 2e6, drift = -1e5) {
  baseline <- base_level + drift * rt
  peak1 <- 8e7 * dnorm(rt, mean = 8, sd = 1.5)
  peak2 <- 6e7 * dnorm(rt, mean = 18, sd = 2.5)
  baseline + peak1 + peak2 + rnorm(length(rt), 0, noise_sd)
}

tic_df <- data.frame(
  retention_time = rt,
  intensity = make_tic()
)

#| fig-cap: "Simulated TIC (Total Ion Current) chromatogram showing two elution peaks on a gently sloping baseline — a typical LC-MS profile with ~6e7 counts at peak apex."
ggplot(tic_df, aes(x = retention_time, y = intensity)) +
  geom_line(color = "steelblue", linewidth = 0.5) +
  labs(title = "TIC — simulated LC-MS run",
       x = "Retention time (min)", y = "Total Ion Current") +
  theme_minimal()

6.8.2 Compare TIC Across All Samples

Code
# Generate TIC for 4 samples with small variations to mimic real runs
set.seed(42)
all_tic <- bind_rows(lapply(seq_len(nrow(metadata)), function(i) {
  data.frame(
    file = metadata$file_name[i],
    retention_time = rt,
    intensity = make_tic(base_level = 5e7, noise_sd = 1.5e6,
                         drift = -1e5 + rnorm(1, 0, 2e4))
  )
}))

#| fig-cap: "All four TIC traces overlaid. Tight overlap of the traces indicates consistent chromatography. A sample that deviates substantially from the bundle would indicate a problem with that specific injection."
# Overlay
ggplot(all_tic, aes(x = retention_time / 60, y = intensity, color = file)) +
  geom_line(alpha = 0.5, linewidth = 0.4) +
  labs(title = "TIC overlay — all samples",
       x = "RT (min)", y = "Total Ion Current") +
  theme_minimal() + theme(legend.position = "none")

Better: use facets for the first 4 files:

Code
all_tic |>
  filter(file %in% metadata$file_name[1:4]) |>
#| fig-cap: "TIC traces for four samples in a facet grid. Consistent peak shapes and intensities across files indicate good instrument performance. A sample with markedly lower TIC or missing peaks would signal injection failure."
  ggplot(aes(x = retention_time / 60, y = intensity)) +
  geom_line(color = "steelblue", linewidth = 0.4) +
  facet_wrap(~file, scales = "free_y", ncol = 2) +
  labs(x = "RT (min)", y = "TIC") +
  theme_minimal()

Red flags: Very low TIC in one sample (poor injection), sudden drops in TIC during run (column or source issues), inconsistent peak shapes.


6.9 Step 6: Retention Time Stability

Retention time drift across runs must be assessed before alignment. Use extracted ion chromatograms (EIC) of a known compound or the same m/z window.

6.9.1 Extract EIC for a Fixed m/z Window

Code
# Simulate an extracted-ion chromatogram for a compound eluting around 12 min
set.seed(42)
eic_df <- bind_rows(lapply(seq_len(3), function(i) {
  rt_shift <- (i - 2) * 0.15  # small RT drift between runs
  data.frame(
    file = metadata$file_name[i],
    retention_time = rt,
    intensity = 2e8 * dnorm(rt, mean = 12 + rt_shift, sd = 0.25) +
      rnorm(length(rt), 0, 2e6)
  )
}))

#| fig-cap: "Extracted Ion Chromatogram for a simulated compound across three runs. Note the subtle RT drift (~0.15 min between runs). If peaks shift by more than 30 seconds across a batch, retention-time alignment (Chapter 7) is required before feature grouping."
ggplot(eic_df, aes(x = retention_time, y = intensity, color = file)) +
  geom_line(linewidth = 0.5) +
  labs(title = "EIC (simulated) — retention time drift check",
       x = "RT (min)", y = "Intensity") +
  theme_minimal()

Red flags: Peaks for the same m/z window appear at significantly different RTs (>30 seconds difference). This indicates need for alignment.


6.10 Step 7: Mass Accuracy and Intensity Distribution

6.10.1 Mass Accuracy (if reference peaks known)

If you have a lock mass or internal standard, calculate mass error in ppm.

Code
# Example: theoretical m/z of a known QC peak
theoretical_mz <- 508.1234
observed_mz <- 508.1250   # from your data

mass_error_ppm <- (observed_mz - theoretical_mz) / theoretical_mz * 1e6
cat(sprintf("Mass error: %.2f ppm\n", mass_error_ppm))
Mass error: 3.15 ppm

Acceptable tolerance: Typically <5 ppm for high‑resolution instruments.

6.10.2 Intensity Distribution per Sample

Code
# Using a preprocessed feature matrix (simulated)
feature_df <- as.data.frame(feature_mat) |>
  pivot_longer(everything(), names_to = "sample", values_to = "intensity")

#| fig-cap: "Boxplots of log10-transformed feature intensities per sample. Medians should be similar across samples. A sample with consistently lower median intensity suggests under-loading or poor processing. A sample with wider spread may indicate ion suppression."
ggplot(feature_df, aes(x = sample, y = log10(intensity + 1))) +
  geom_boxplot(fill = "lightblue") +
  labs(
    title = "Log10 intensity distribution per sample",
    x = "Sample", y = "log10(intensity + 1)"
  ) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

Red flags: One sample with consistently lower intensities → possible injection under‑loading or poor processing.


6.11 Step 8: Blank Contamination and Carryover

Blanks (solvent injections) should contain very few features. Any feature with high intensity in a blank likely comes from contamination or column bleed.

6.11.1 Simulate Blank Data

Code
blank_mat <- matrix(rlnorm(100 * 2, meanlog = 5, sdlog = 1),
                    nrow = 100, ncol = 2)
colnames(blank_mat) <- c("Blank_1", "Blank_2")

# Count features with intensity above threshold
threshold <- 1e4
contaminants <- apply(blank_mat > threshold, 2, sum)
contaminants
Blank_1 Blank_2 
      0       0 

Red flags: More than 100 features in a blank, or any feature with very high intensity (>1e6) in blank – check for carryover from previous injections.

6.11.2 Carryover Detection

Compare blanks run after a high‑concentration sample. If a feature appears in the blank that was prominent in the previous sample, carryover is present.


6.12 Step 9: Automated QC with MsQuality

Learning outcome: By the end of this section, you will be able to compute standardised, community-agreed quality metrics for raw MS data and flag anomalous acquisitions before downstream processing.

6.12.1 Why Automated QC?

The manual QC checks in Steps 1–8 (TIC inspection, RT stability, mass accuracy, blank assessment) are indispensable for understanding your data. However, they are also time-consuming, subjective, and difficult to reproduce across projects. The MsQuality package (Naake et al. 2023) addresses this by computing a curated set of low-level quality metrics defined by the HUPO-PSI mzQC standard. These metrics:

  • Standardise QC reporting — the same metrics apply across proteomics, metabolomics, and lipidomics acquisitions, making inter-study comparisons possible.
  • Scale to hundreds of files — a single calculateMetrics() call produces a sample-by-metric table.
  • Flag outliers — runs with aberrant chromatography duration, abnormal TIC area, or excessive signal jumps stand out immediately in a quantitative table or heatmap.
  • Export to mzQC — the standard exchange format for MS QC data, via the companion rmzqc package.

Automated metrics do not replace the visual TIC overlays and expert judgment from earlier steps. Instead, they complement them: manual inspection catches unexpected patterns that no predefined metric can encode, while automated metrics provide an objective, auditable record of basic acquisition quality.

6.12.2 Relationship to Spectra, MsExperiment, and mzQC

MsQuality is built on the Spectra and MsExperiment containers you already encountered in Chapters 2, 4, and 5. It calculates metrics from the same raw spectral data (retention time, m/z, intensity) that you have been inspecting visually. Because it works with Spectra and MsExperiment objects, any data you can load into these containers — from mzML, mzXML, CDF, MGF, or MSP files — can be subjected to automated QC.

Internally, each metric function implements one term from the HUPO-PSI mzQC controlled vocabulary (e.g. MS:4000053 for chromatographyDuration). This means the metrics are not arbitrary summaries — they are community-standard terms with unambiguous definitions, making QC results portable between labs and software platforms.

6.12.3 Computing Quality Metrics with MsQuality

First, ensure MsQuality and its dependencies are installed.

Code
BiocManager::install("MsQuality")

The msdata package includes two Sciex TripleTOF 5600+ files for this purpose, but here we demonstrate the key ideas with a realistic simulated QC table:

Code
library(ggplot2)
library(tidyr)
library(dplyr)

# Simulate two acquisitions with typical QC metrics
qc_df <- data.frame(
    row.names = c("Injection_1", "Injection_19"),
    chromatographyDuration = c(1802, 1815),    # seconds
    areaUnderTic = c(4.21e10, 3.98e10),
    numberSpectra = c(895, 912),
    msSignal10xChange.jump = c(2L, 7L),        # 7 jumps = suspicious
    msSignal10xChange.fall = c(1L, 5L),
    ticQuartileToQuartileLogRatio = c(0.12, 0.41),
    rtOverMsQuarters.Q1 = c(0.18, 0.16),
    rtOverMsQuarters.Q2 = c(0.27, 0.24),
    rtOverMsQuarters.Q3 = c(0.24, 0.26),
    rtOverMsQuarters.Q4 = c(0.31, 0.34),
    medianTicRtIqr = c(552, 578)
)

qc_df
             chromatographyDuration areaUnderTic numberSpectra
Injection_1                    1802     4.21e+10           895
Injection_19                   1815     3.98e+10           912
             msSignal10xChange.jump msSignal10xChange.fall
Injection_1                       2                      1
Injection_19                      7                      5
             ticQuartileToQuartileLogRatio rtOverMsQuarters.Q1
Injection_1                           0.12                0.18
Injection_19                          0.41                0.16
             rtOverMsQuarters.Q2 rtOverMsQuarters.Q3 rtOverMsQuarters.Q4
Injection_1                 0.27                0.24                0.31
Injection_19                0.24                0.26                0.34
             medianTicRtIqr
Injection_1             552
Injection_19            578

Output: a data frame with rows corresponding to the two acquisitions (samples) and columns for each requested metric. The column names reflect the metric name and, where applicable, the parameter values (e.g. msSignal10xChange.jump for the count of 10-fold TIC increases).

6.12.4 Interpreting the QC Table

Metric What It Measures Interpretation
chromatographyDuration Total RT span (seconds) — is the gradient complete? Both runs should span similar RT ranges. A markedly shorter duration indicates premature termination or an aborted acquisition.
areaUnderTic Total ion current area under the TIC curve Differences >2-fold between replicate injections of the same sample type suggest injection-volume variation, ion suppression, or source contamination.
numberSpectra Number of MS1 scans acquired Unexpected variation in scan count may point to DDA settings changing mid-batch or to software timeouts.
msSignal10xChange.jump Count of >10-fold TIC increases between adjacent scans More than a handful of jumps usually indicates spray instability, bubbles passing through the source, or electrical arcing.
msSignal10xChange.fall Count of >10-fold TIC drops between adjacent scans Frequent drops suggest intermittent ionisation or column issues.
ticQuartileToQuartileLogRatio Log ratios of successive TIC quartiles Reflects the shape of the TIC distribution; large deviations from expected values may indicate altered chromatography (e.g. changed retention or ion-suppression pattern).
rtOverMsQuarters Fraction of run time to acquire each quarter of MS1 events If MS1 events are concentrated in a narrow RT window, the instrument may be spending most of its time on MS2 events, potentially missing eluting features.
Warning

No universal thresholds. The numerical values that constitute “good” or “bad” QC depend on your instrument model, ionisation mode, column dimensions, gradient length, sample matrix, and acquisition method. The power of MsQuality lies in trend detection across many runs: flag runs that deviate by more than, say, 2–3 median absolute deviations (MAD) from the batch median for each metric. The thresholds you use must be established empirically from your own QC pool injections over time.

6.12.5 Visualising QC Metrics

To compare the two injections side by side, we create a bar chart of a subset of metrics. Values are scaled within each metric (z-score) so that metrics with different units appear on a common axis.

Code
library(ggplot2)
library(tidyr)
library(dplyr)

# Select metrics with clear interpretation for the bar plot
plot_metrics <- c(
    "chromatographyDuration", "areaUnderTic",
    "numberSpectra",
    "msSignal10xChange.jump", "msSignal10xChange.fall"
)

# Reshape and centre-scale to z-scores
qc_plot <- qc_df[, plot_metrics, drop = FALSE] |>
    as.data.frame() |>
    mutate(sample = c("Injection_1", "Injection_19")) |>
    pivot_longer(-sample, names_to = "metric", values_to = "value") |>
    group_by(metric) |>
    mutate(z_score = (value - mean(value)) / sd(value)) |>
    ungroup()

ggplot(qc_plot, aes(x = metric, y = z_score, fill = sample)) +
    geom_col(position = "dodge") +
    geom_hline(yintercept = 0, linetype = "dashed", linewidth = 0.3) +
    labs(
        title = "Scaled QC metrics for two Sciex acquisitions",
        y = "Z-score (deviation from batch mean)",
        x = NULL
    ) +
    theme_minimal() +
    theme(axis.text.x = element_text(angle = 45, hjust = 1))

Z-scored QC metrics comparing two injections. Bars near zero indicate normal performance. Bars exceeding |2| suggest a problematic run.

Z-scored QC metrics comparing two injections. Bars near zero indicate normal performance. Bars exceeding |2| suggest a problematic run.

In a study with dozens of injections, a heatmap of z-scores across all samples and metrics makes outlier runs immediately visible:

Code
library(pheatmap)
library(tibble)

# Build a matrix of z-scores for the selected metrics
z_mat <- qc_plot |>
    select(sample, metric, z_score) |>
    pivot_wider(names_from = metric, values_from = z_score) |>
    column_to_rownames("sample") |>
    as.matrix()

pheatmap(
    z_mat,
    main = "QC metric z-scores across samples",
    color = colorRampPalette(c("steelblue", "white", "tomato"))(50),
    breaks = seq(-3, 3, length.out = 51),
    display_numbers = TRUE,
    cluster_rows = FALSE,
    cluster_cols = FALSE
)

Heatmap of z-scored QC metrics. Blue = below batch average, white = at average, red = above average. A column with consistent extreme colour (all deep red or deep blue) flags a globally aberrant sample.

Heatmap of z-scored QC metrics. Blue = below batch average, white = at average, red = above average. A column with consistent extreme colour (all deep red or deep blue) flags a globally aberrant sample.

Any sample with consistently extreme z-scores (|z| > 2–3) across multiple metrics warrants investigation before proceeding to quantification. In this two-sample comparison the heatmap is minimal; in a real batch of 20+ injections it becomes an efficient diagnostic tool.

TipExporting to mzQC

To make your QC data portable, use the rmzqc package to export qc_df to the HUPO-PSI mzQC JSON format:

Code
calculateMetrics(sps, format = "mzqc", path = "qc_metrics.mzQC")

This file can be archived alongside your raw data or submitted to public repositories.

6.12.6 Exercises

Exercise 1: QC gate definition. Suppose you run a 48-sample batch with a pooled QC injection every 10 samples. You compute the metrics above for each file and obtain the median and MAD for each metric from the QC pools. Define a rule that flags a study sample as “failed QC” if any metric deviates by more than 3 MAD from the QC-pool median. Using the two sciex injections (treat Injection_19 as a QC pool and Injection_1 as a study sample), does Injection_1 pass or fail? Would you use the same 3-MAD threshold for numberSpectra and msSignal10xChange.jump? Discuss how the choice of threshold might differ for metrics with different variability.

Exercise 2: Extend to your own data. Replace the msdata::sciex files with a set of mzML files from your own instrument. Run calculateMetrics() on all files, produce a heatmap of z-scores, and identify any outlier runs. Document the thresholds you would set for each metric based on the distribution of your QC pools.


6.13 Step 10: Practical Example – Generating an MS Quality Control Report

This workflow combines all QC checks into a single report.

Code
generate_qc_report <- function(mzml_files, metadata, output_dir = "QC_report") {
  
  dir.create(output_dir, showWarnings = FALSE)
  
  # 1. TIC plots
  pdf(file.path(output_dir, "TIC_plots.pdf"), width = 10, height = 6)
  for (f in mzml_files) {
    sps <- Spectra(f)
    tic_df <- data.frame(RT = rtime(sps), TIC = intensity(tic(sps)))
    p <- ggplot(tic_df, aes(RT/60, TIC)) + geom_line() +
      labs(title = basename(f), x = "RT (min)", y = "TIC") + theme_minimal()
    print(p)
  }
  dev.off()
  
  # 2. TIC overlay
  all_tic <- bind_rows(lapply(mzml_files, function(f) {
    sps <- Spectra(f)
    data.frame(file = basename(f), RT = rtime(sps), TIC = intensity(tic(sps)))
  }))
  p_overlay <- ggplot(all_tic, aes(RT/60, TIC, color = file)) + geom_line(alpha = 0.5) +
    theme_minimal() + theme(legend.position = "none")
  ggsave(file.path(output_dir, "TIC_overlay.png"), p_overlay, width = 10, height = 6)
  
  # 3. Metadata consistency
  write.csv(metadata, file.path(output_dir, "metadata_checked.csv"), row.names = FALSE)
  
  # 4. Intensity distribution boxplot (if feature matrix provided)
  # (Assume user provides feature_mat)
  
  cat("QC report generated in", output_dir, "\n")
}

# Run on example data
generate_qc_report(mzml_files[1:4], metadata[1:4, ])

6.14 QC Checklist (Before Proceeding to Preprocessing)

If any check fails, do not proceed – address the issue or document it as a limitation.


6.15 Common Pitfalls and Solutions

Pitfall Consequence Solution
Metadata typos Wrong group assignments Always validate with all(colnames(x) == metadata$sample_name)
Missing batch information Batch effects unaccounted for Record batch during experiment; add to metadata
QC samples excluded from preprocessing No assessment of technical variation Include blanks and QCs in preprocessing, remove only after QC
Ignoring TIC differences Normalisation may fail Check TIC before normalisation; consider TIC normalisation
RT drift ignored Poor feature matching Assess RT variation; apply alignment
Contaminated blanks False positives from background Filter out features present in blanks

6.16 Exercises

6.16.1 Exercise 1: Annotate Real Data

Download a public metabolomics dataset (e.g., from MetaboLights). Create a metadata table with biological group, batch, injection order, and sample type.

Code
# Your code here

6.16.2 Exercise 2: Detect Batch Confounding

Simulate a dataset where batch is perfectly confounded with treatment (e.g., batch 1 = control, batch 2 = treatment). Write a function that warns the user.

Code
# Your code here

6.16.3 Exercise 3: TIC Visualisation

Using the msdata::proteomics() files, create TIC overlay plots for all files. Identify any sample with abnormally low TIC.

Code
# Your code here

6.16.4 Exercise 4: Blank Contamination

If you have blank injections, calculate the proportion of features in blanks that also appear in biological samples. Set a threshold to flag potential contaminants.

Code
# Your code here

6.17 Summary

6.17.1 Key QC Outputs

Check R Function / Package Red Flag
Metadata consistency all(), duplicated() Mismatched names, confounding
TIC visualisation tic() from Spectra Low TIC, sudden drops
BPC/EIC for RT drift chromatogram() from Spectra RT shift >30 sec
Intensity distribution boxplot(log10(mat)) One sample outlier
Blank contamination featureValues(blanks) >100 features in blank
Mass accuracy (obs - theo)/theo * 1e6 >5 ppm error

6.17.2 Resources


6.18 Session Information

Code
sessionInfo()
R version 4.5.1 (2025-06-13 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_Switzerland.utf8  LC_CTYPE=English_Switzerland.utf8   
[3] LC_MONETARY=English_Switzerland.utf8 LC_NUMERIC=C                        
[5] LC_TIME=English_Switzerland.utf8    

time zone: Europe/Zurich
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] tibble_3.3.1    pheatmap_1.0.13 tidyr_1.3.2     dplyr_1.2.1    
[5] ggplot2_4.0.3   msdata_0.48.0  

loaded via a namespace (and not attached):
 [1] vctrs_0.7.3        cli_3.6.5          knitr_1.51         rlang_1.3.0       
 [5] xfun_0.60          otel_0.2.0         purrr_1.2.2        generics_0.1.4    
 [9] S7_0.2.2           jsonlite_2.0.0     labeling_0.4.3     glue_1.8.1        
[13] htmltools_0.5.9    scales_1.4.0       rmarkdown_2.31     grid_4.5.1        
[17] evaluate_1.0.5     fastmap_1.2.0      yaml_2.3.12        lifecycle_1.0.5   
[21] compiler_4.5.1     RColorBrewer_1.1-3 pkgconfig_2.0.3    htmlwidgets_1.6.4 
[25] farver_2.1.2       digest_0.6.37      R6_2.6.1           tidyselect_1.2.1  
[29] pillar_1.11.1      magrittr_2.0.5     withr_3.0.3        tools_4.5.1       
[33] gtable_0.3.6