MaxQuant + Perseus — Proteomics Analysis Pipeline
Overview
MaxQuant is the community-standard software for label-free quantification (LFQ) and SILAC proteomics. It performs database search, protein grouping, and intensity-based quantification from raw LC-MS/MS files, producing proteinGroups.txt as the primary output. Downstream statistical analysis — filtering, normalization, imputation, differential abundance testing, and visualization — is performed in Python using pandas, scipy, and matplotlib/seaborn, mirroring the Perseus workflow in a reproducible scripting environment.
When to Use
- Performing label-free quantification (LFQ) of proteins across multiple biological conditions — MaxQuant's MaxLFQ algorithm is the community benchmark
- Running SILAC (stable isotope labeling) experiments with light/heavy or triple-label designs
- Processing iTRAQ or TMT isobaric labeling experiments via MaxQuant's reporter ion quantification
- Identifying and quantifying proteins when you need the widely-cited MaxQuant output format (
proteinGroups.txt) for comparison with published datasets - Performing statistical differential abundance analysis on MaxQuant outputs without installing Perseus (GUI-only, Windows)
- Generating publication-quality volcano plots and GO enrichment from proteomics data in a reproducible Python workflow
- Use Proteome Discoverer instead when working with Thermo raw files requiring instrument-native processing or Sequest HT
- Use FragPipe/MSFragger instead for GPU-accelerated database search (3–10× faster) or when processing DIA (data-independent acquisition) data
Prerequisites
- MaxQuant: Windows software; download from https://maxquant.org/ (v2.4+); requires .NET 6 runtime
- Python packages:
pandas,numpy,scipy,matplotlib,seaborn,statsmodels,gseapy - Data requirements: Thermo
.rawfiles or mzML-converted files; FASTA protein database (UniProt reviewed + contaminant database) - Environment: MaxQuant runs on Windows (GUI or CLI); Python analysis runs cross-platform
pip install pandas numpy scipy matplotlib seaborn statsmodels gseapy
# Install pyMaxQuant for programmatic mqpar.xml configuration
pip install pymaxquant
Quick Start
import pandas as pd
import numpy as np
# Load MaxQuant output
df = pd.read_csv("combined/txt/proteinGroups.txt", sep="\t", low_memory=False)
print(f"Raw protein groups: {len(df)}")
# Filter contaminants, reverse decoys, only-by-site
mask = (
(df["Potential contaminant"] != "+") &
(df["Reverse"] != "+") &
(df["Only identified by site"] != "+")
)
df = df[mask].copy()
print(f"After filtering: {len(df)} protein groups")
# Extract LFQ intensity columns
lfq_cols = [c for c in df.columns if c.startswith("LFQ intensity ")]
print(f"LFQ columns: {lfq_cols}")
# Log2-transform (0 → NaN)
lfq = df[lfq_cols].replace(0, np.nan)
lfq = np.log2(lfq)
print(f"Valid values per sample:\n{lfq.notna().sum()}")
Workflow
Step 1: Configure MaxQuant Parameters via mqpar.xml
MaxQuant is controlled by an XML parameter file (mqpar.xml). Edit it programmatically to set file paths, enzyme, modifications, and quantification type before running the search.
import xml.etree.ElementTree as ET
def update_mqpar(template_path: str, output_path: str,
raw_files: list[str], fasta_path: str,
experiment_names: list[str]) -> None:
"""Update mqpar.xml with sample-specific file paths."""
tree = ET.parse(template_path)
root = tree.getroot()
# Set raw file paths
file_paths_node = root.find(".//filePaths")
file_paths_node.clear()
for rf in raw_files:
elem = ET.SubElement(file_paths_node, "string")
elem.text = rf
# Set experiment names (maps files to conditions)
experiments_node = root.find(".//experiments")
experiments_node.clear()
for name in experiment_names:
elem = ET.SubElement(experiments_node, "string")
elem.text = name
# Set FASTA database
fasta_node = root.find(".//fastaFiles/FastaFileInfo/fastaFilePath")
fasta_node.text = fasta_path
tree.write(output_path, xml_declaration=True, encoding="utf-8")
print(f"Written: {output_path}")
# Example usage
raw_files = [
r"C:\Data\ctrl_rep1.raw",
r"C:\Data\ctrl_rep2.raw",
r"C:\Data\treat_rep1.raw",
r"C:\Data\treat_rep2.raw",
]
update_mqpar(
template_path="mqpar_template.xml",
output_path="mqpar.xml",
raw_files=raw_files,
fasta_path=r"C:\Databases\human_uniprot_contaminants.fasta",
experiment_names=["ctrl", "ctrl", "treat", "treat"],
)
Key mqpar.xml parameters (set in template or edit directly):
<!-- Enzyme and search settings -->
<enzymes>
<string>Trypsin/P</string>
</enzymes>
<maxMissedCleavages>2</maxMissedCleavages>
<variableModifications>
<string>Oxidation (M)</string>
<string>Acetyl (Protein N-term)</string>
</variableModifications>
<fixedModifications>
<string>Carbamidomethyl (C)</string>
</fixedModifications>
<!-- LFQ settings -->
<lfqMode>1</lfqMode> <!-- 1 = LFQ enabled -->
<lfqMinRatioCount>2</lfqMinRatioCount> <!-- minimum peptides for LFQ -->
<matchBetweenRuns>True</matchBetweenRuns>
<!-- FDR thresholds -->
<peptideFdr>0.01</peptideFdr>
<proteinFdr>0.01</proteinFdr>
Step 2: Run MaxQuant from Command Line (Windows)
MaxQuant can be run headlessly from the Windows command prompt using the bundled MaxQuantCmd.exe.
REM Windows Command Prompt — run MaxQuant with configured mqpar.xml
REM Adjust path to match your MaxQuant installation directory
set MQ_PATH=C:\Program Files\MaxQuant\bin\MaxQuantCmd.exe
set MQPAR=C:\Projects\proteomics\mqpar.xml
"%MQ_PATH%" "%MQPAR%"
REM For specific workflow steps only (useful for reruns):
REM Step IDs: 0=write tables, 1=feature detection, 7=peptide identification
"%MQ_PATH%" "%MQPAR%" --steps 1,7,11
# Cross-platform: run MaxQuant under Wine on Linux/macOS (CI/server use)
wine MaxQuantCmd.exe mqpar.xml
# Monitor progress log
tail -f combined/proc/#runningTimes.txt
Step 3: Load and Filter proteinGroups.txt
Filter out reverse decoys, potential contaminants, and proteins only identified by modification site.
import pandas as pd
import numpy as np
def load_protein_groups(path: str) -> pd.DataFrame:
"""Load MaxQuant proteinGroups.txt with quality filters applied."""
df = pd.read_csv(path, sep="\t", low_memory=False)
print(f"Total protein groups: {len(df)}")
# Remove reverse decoys, contaminants, and only-by-site hits
n_before = len(df)
df = df[
(df.get("Reverse", pd.Series("")) != "+") &
(df.get("Potential contaminant", pd.Series("")) != "+") &
(df.get("Only identified by site", pd.Series("")) != "+")
].copy()
print(f"After quality filter: {len(df)} ({n_before - len(df)} removed)")
# Parse gene names (take first entry for multi-gene groups)
df["Gene names"] = df["Gene names"].fillna("Unknown").str.split(";").str[0]
# Set unique index on majority protein ID
df = df.set_index("Majority protein IDs")
return df
# Load output
pg = load_protein_groups("combined/txt/proteinGroups.txt")
# Identify LFQ intensity columns
lfq_cols = [c for c in pg.columns if c.startswith("LFQ intensity ")]
print(f"LFQ samples ({len(lfq_cols)}): {lfq_cols}")
# Output: LFQ samples (6): ['LFQ intensity ctrl_1', 'LFQ intensity ctrl_2', ...]
Step 4: Log2 Transform and Median Normalize LFQ Intensities
Replace zero intensities with NaN (missing values in MaxQuant are exported as 0), log2-transform, then apply per-sample median centering.
def prepare_lfq_matrix(df: pd.DataFrame, lfq_cols: list[str]) -> pd.DataFrame:
"""Extract, transform, and normalize LFQ intensity matrix."""
# Extract and rename columns (strip 'LFQ intensity ' prefix)
lfq = df[lfq_cols].copy()
lfq.columns = [c.replace(