Data in msmu
Overview
In LC-MS/MS "shotgun" proteomics, data analysis typically follows a hierarchical path—starting from PSM-level data (PSM or precursor), progressing to peptides, and finally reaching proteins. Each stage introduces its own set of feature annotations, quantification matrices, and tool-specific metadata. As a result, shotgun proteomics data naturally form a multi-level and multi-dimensional structure: PSM/precursor, peptide, protein; feature metadata; sample annotations; and QC metrics.
To manage these properties consistently, msmu adopts MuData from the scverse ecosystem as the fundamental data format. MuData, together with its constituent AnnData objects, is widely used in scRNA-seq to manage complex data matrices and their associated metadata. The same structure fits proteomics naturally: identification-level attributes, quantification values, and sample information can all be stored cleanly and explored in an integrated way.
msmu works with data formatted as a MuData object composed of multiple AnnData modalities.
Therefore, understanding the usage of MuData and AnnData helps when working with msmu.
A MuData object used in msmu is organized by modalities, each corresponding to a specific processing level such as psm, peptide, and protein:
mdata
mdata["psm"]
# or
mdata["protein"]
As a general AnnData object, each individual modality contains .X, .var, .varm, .obs, .obsm, .uns, etc.
.Xis a matrix holding the quantification data. It is dense for peptide- and protein-level modalities, and sparse where features belong to one run or one plex (see The quantification matrix)..varis a dataframe containing metadata of features for each level. As an example,.varinpsmmodality (for PSMs or precursors) contains information describing scan number, filename, PEP, q-value, etc., withfilename.scanas index..varmis a dictionary-like structure to store additional per-feature matrices, such as boolean masks for filtering features..obsis a dataframe containing metadata of samples, such as sample name, condition, replicate number, etc., withfilenameorchannelas index..obsmis a dictionary-like structure to store additional per-sample matrices, such as PCA or UMAP coordinates..unsis a dictionary-like structure to store unstructured annotations, such as decoy features pulled from search results..layersis a dictionary-like structure to store additional per-feature quantification matrices, such as imputed values. Some functions inmsmuprovide options to read from or write to.layers.
Data Ingestion from DB search tools
Although different search tools return result files with heterogenous formats, their contents can typically be organized into two main conceptual parts to construct peptide- and protein-level data.
- Identification data - Identified features with associated annotations
- Quantification data - Quantitative values for features across samples
read_* functions in msmu extract the essential columns required for QC and downstream processing and migrate them into the .var of the psm modality. read_* functions are implemented in msmu/_read_write/_reader_registry
read_*functions (currently available)read_sage()read_diann()read_maxquant()read_fragpipe()
- Inputs
identification_file: A file path to identification dataquantification_file: A file path to quantification data (if applicable) (for tools outputting separate quantification files like Sage)label: used label (tmtorlabel_free)acquisition: acquisition method (dda, ordia) (for tools supporting both DDA and DIA like MaxQuant)drop_search_result(optional, defaultFalse): skip keeping the raw search output in.varm["search_result"]. It is a full copy of the search table, so dropping it noticeably lowers memory and file size when you do not need to trace values back to the original columns.
- Output
mudata: Data ingested MuData object
- Columns migrated into
mdata["psm"].varfilename,peptide(modified),stripped_peptide,scan_num,proteins,missed_cleavages,peptide_length,charge,PEP,q-value,contaminant
proteinsholds parsed UniProt accessions in one canonical form across every reader,[rev_][Cont_]<accession>(e.g.P07339,Cont_P02769), andcontaminantflags whether any member of the group carried a contaminant marker. See Filter.- Decoy features are isolated from
.varand stored in.uns["decoy"]for later use in FDR calculation. - Quantification data for LFQ (DDA) is stored in
peptidemodality. - Raw information from a search tool is stored in
mdata["psm"].varm["search_result"]
mdata = mm.read_sage(
identification_file="path/to/results.sage.tsv",
quantification_file="path/to/tmt.tsv",
label="tmt", # or "label_free"
)
mdata = mm.read_diann(
identification_file="path/to/report.tsv",
)
mdata = mm.read_maxquant(
identification_file="path/to/output_file",
label="tmt", # or "label_free"
acquisition="dda", # or "dia"
)
mdata = mm.read_fragpipe(
identification_file="path/to/output_file/psm.tsv",
quantification_file="path/to/quantification_file/combined_modified_peptide.tsv", # for LFQ
label="tmt", # or "label_free"
)
The quantification matrix
Two different things leave a cell of .X empty. A missing value could have been measured but
was not; a structurally absent cell could never have carried a number, because the sample and
the feature belong to different acquisition units — a TMT reporter channel exists only for the PSMs
of its own plex. Which cells are measurable is fixed by the design before any data exists, so it
fixes both the shape of the matrix and how msmu stores it.
| Experiment | Quantification in | obs (samples) |
var (features) |
Design | .X |
|---|---|---|---|---|---|
| DDA, label-free | peptide |
run | peptide | complete | dense |
| DDA, TMT — single plex | psm |
channel | filename.scan |
complete | dense |
| DDA, TMT — multi-plex | psm |
channel_set |
filename.scan |
block diagonal | sparse |
| DIA — precursor level | psm |
run | run.precursor |
one cell per feature | sparse |
The matrices are drawn at one scale, and colour marks the group a feature belongs to. The bar under
each one is the same grid seen as storage: the outline is every cell, the fill is what .X really
keeps — a dense array allocates every slot, including the struck-through empty ones, while a sparse
one keeps only the coloured cells and stops. For label-free DDA the quantification arrives at peptide level, so the
peptide modality carries it and psm keeps identifications only.
Multi-plex TMT. Before splitting, obs is the C reporter channels, so row 126 stands for a
different sample in every plex. split_tmt() relabels them into C x n_set distinct channel_set
samples; a cell is then measurable only when the sample and the PSM share a plex, which makes the
matrix block diagonal with C x m_k blocks and 1/n_set of it measurable.
DIA. The feature id carries the run (var is run.precursor), so a feature has exactly one
measurable cell — the degenerate case where every block is a single row, with 1/n_run
measurable. Each report row maps to one cell, so read_diann() writes them straight into the
sparse store and no wide table is built.
Both grouped forms keep only the observed cells, in a SciPy sparse .X.
The sparsity is an implementation detail of the storage, not of the analysis. Every function that
reads .X goes through a sparse-aware path, so absent cells stay NaN and are never read as a
measured zero — preprocessing (log2_transform, normalise, scale_data,
correct_batch_effect, collapse_obs), summarisation (to_peptide, to_protein, to_ptm),
statistics (run_de, corr, pca, umap), plotting and the exporters all behave the same as on
a dense matrix. Normalisation, scaling and batch correction also return sparse output for sparse
input, so the saving is not spent at the first preprocessing step.
Summarisation removes the grouping — a peptide is one feature across every run and plex, so its cells are all measurable — and peptide- and protein-level modalities are dense again:
mdata = mm.read_diann(identification_file="path/to/report.parquet")
mdata["psm"].X # <6x5038 sparse matrix ...>, 5,037 stored cells (16.7%)
mdata = mm.pp.to_peptide(mdata)
mdata["peptide"].X # dense ndarray
To read the values out yourself, use mm.io.to_readable(), which restores absent cells as NaN.
AnnData's own mdata["psm"].to_df() densifies a sparse .X with zeros, so absent
measurements silently become 0 intensities:
mm.io.to_readable(mdata, modality="psm") # absent cells -> NaN
mdata["psm"].to_df() # absent cells -> 0 (do not use on a sparse .X)
to_readable() returns .var and the quantification side by side; narrow it with the optional
include / exclude (feature columns to keep or drop) and quantification=False (annotations
only).