Using the Python API
This tutorial shows how to use the MS²Rescore Python API for each step of the rescoring process individually. This is useful if you want to customize rescoring for your own Python workflow or if you want to understand how MS²Rescore works. Note that the full MS²Rescore workflow is also available from Python with the single function call ms2rescore.rescore().
[1]:
import logging
import plotly.io
logging.basicConfig(level=logging.INFO)
plotly.io.renderers.default = "plotly_mimetype+notebook"
Reading and parsing peptide-spectrum matches
[2]:
from psm_utils.io import read_file
from ms2rescore.report.charts import score_histogram
Reading the PSM file
MS²Rescore is fully centered around the use of a psm_utils PSMList. This is a unified data representation of PSMs and their various attributes. Internally, it is simply a list of Pydantic data classes which represent PSMs. With the submodule psm_utils.io, we can read PSMs from a variety of file formats. Here, we will read a PSM file in the MaxQuant msms.txt format.
Importantly, for rescoring, the PSM file must contain all target and decoy PSMs, including PSMs that did not pass the FDR threshold. Most search engines must be specifically configured to return all PSMs without FDR filtering.
[3]:
psm_list = read_file("msms.txt", filetype="msms")
psm_list.calculate_qvalues()
For a quick inspection, we can format the PSM list as a Pandas dataframe and display the first few rows:
[4]:
psm_list.to_dataframe().head()
[4]:
| peptidoform | spectrum_id | run | collection | spectrum | is_decoy | score | qvalue | pep | precursor_mz | retention_time | ion_mobility | protein_list | rank | source | provenance_data | metadata | rescoring_features | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | ((A, None), (A, None), (A, None), (A, None), (... | 4703 | 20161213_NGHF_DBJ_SA_Exp3A_HeLa_1ug_7min_15000_02 | None | None | False | 107.660 | 0.001566 | 0.001517 | 478.77982 | 5.2007 | None | [P36578, H3BM89, H3BU31] | None | msms | {'msms_filename': 'msms.txt'} | {'Scan index': '3698', 'Sequence': 'AAAAAAALQA... | {} |
| 1 | ((A, None), (A, None), (A, None), (A, None), (... | 13572 | 20161213_NGHF_DBJ_SA_Exp3A_HeLa_1ug_7min_15000_02 | None | None | False | 107.740 | 0.001566 | 0.004931 | 915.15197 | 11.8470 | None | [O00410, E7ETV3, E7EQT5, C9JZD8] | None | msms | {'msms_filename': 'msms.txt'} | {'Scan index': '11885', 'Sequence': 'AAAAAEQQQ... | {} |
| 2 | ((A, None), (A, None), (A, None), (A, None), (... | 13366 | 20161213_NGHF_DBJ_SA_Exp3A_HeLa_1ug_7min_15000_02 | None | None | False | 137.890 | 0.000673 | 0.000493 | 957.85029 | 11.6900 | None | [O00410, E7ETV3, E7EQT5, C9JZD8] | None | msms | {'msms_filename': 'msms.txt'} | {'Scan index': '11695', 'Sequence': 'AAAAAEQQQ... | {} |
| 3 | ((A, None), (A, None), (A, None), (A, None), (... | 505 | 20161213_NGHF_DBJ_SA_Exp3A_HeLa_1ug_7min_15000_02 | None | None | False | 22.641 | 0.487532 | 0.142020 | 585.28653 | 0.5178 | None | [E9PJF0, E9PQW4, P27361] | None | msms | {'msms_filename': 'msms.txt'} | {'Scan index': '419', 'Sequence': 'AAAAAQGGGGG... | {} |
| 4 | ((A, None), (A, None), (A, None), (A, None), (... | 6589 | 20161213_NGHF_DBJ_SA_Exp3A_HeLa_1ug_7min_15000_02 | None | None | False | 89.403 | 0.003778 | 0.046504 | 823.87389 | 6.6105 | None | [Q9P258] | None | msms | {'msms_filename': 'msms.txt'} | {'Scan index': '5439', 'Sequence': 'AAAAAWEEPS... | {} |
We can also directly plot the current PSM score distributions:
[5]:
score_histogram(psm_list)
Parsing modification names
While psm_utils could take care of all file parsing, we must still map the amino acid modification names that were used by the search engine to ones that are recognized by tools such as MS²PIP and DeepLC. This includes:
Names as used in the Unimod or PSI-MOD databases
Accession numbers as used in the Unimod or PSI-MOD databases
Chemical formulas
Mass shifts in Da
Note that, for instance DeepLC, requires a chemical formula to encode modifications. It will not be able to correctly encode modifications if only the mass shift is provided. It is therefore always preferred to provide a name/accession of a database where this information can be retrieved, or provide the chemical formula directly.
If a chemical formula is provided, other tools, such as MS²PIP, can use it to derive the correct mass shift.
To map modification names, simply provide a dictionary to the psm_list.rename_modifications method.
[6]:
psm_list.rename_modifications({
"gl": "U:Gln->pyro-Glu",
"ox": "U:Oxidation",
"ac": "U:Acetylation",
"de": "U:Deamidation",
})
Assigning fixed modifications
Some search engines (although not many) do not report fixed (sometimes also called static) modifications in their output PSM files. This is for instance the case for MaxQuant. With the method psm_list.add_fixed_modifications, we can systematically assign fixed modifications to their amino acid targets, as was done during the search.
Note that add_fixed_modifications adds the modification in the ProForma 2.0 encoding as a prefix. To fully apply the modifications in the sequence, use the psm_list.apply_fixed_modifications method. A PSM ACDE/2 would therefore go from <[U:4]@C>ACDE/2 to AC[U:4]DE/2.
[7]:
psm_list.add_fixed_modifications([("U:Carbamidomethyl", ["C"])])
psm_list.apply_fixed_modifications()
Adding rescoring features
[8]:
import pandas as pd
from ms2rescore.feature_generators.basic import BasicFeatureGenerator
from ms2rescore.feature_generators.ms2 import MS2FeatureGenerator
from ms2rescore.feature_generators.ms2pip import MS2PIPFeatureGenerator
from ms2rescore.feature_generators.deeplc import DeepLCFeatureGenerator
The PSM list, as read from a MaxQuant msms.txt file, currently does not contain any rescoring features:
[9]:
pd.DataFrame(list(psm_list["rescoring_features"]))
[9]:
| 0 |
|---|
| 1 |
| 2 |
| 3 |
| 4 |
| ... |
| 35551 |
| 35552 |
| 35553 |
| 35554 |
| 35555 |
35556 rows × 0 columns
Note that psm_list[rescoring_features] returns a Numpy array of dictionaries, but we can use Pandas to format it as a table.
Getting precursor information and annotating MS2 spectra
Several feature generators require data from the spectrum files, such as retention times, precursor m/z values, or the MS2 spectra themselves. Rather than having each feature generator parse and annotate spectra independently, MS²Rescore v4.0+ loads and annotates the required data once upfront:
add_precursor_valuesreads spectrum files and attaches rawMS2Spectrumobjects and precursor metadata (RT, precursor m/z) to each PSM.annotate_spectrathen annotates each spectrum with its PSM’s peptidoform, replacing the raw spectrum with anAnnotatedMS2Spectrum. All subsequent feature generators (MS²PIP, MS2, etc.) can reuse these annotations directly, so annotation only happens once.
The following two parameters are especially important:
spectrum_path: Path to the original spectrum files. This can be a single file, or a directory containing multiple files. Supported formats: MGF, mzML.spectrum_id_pattern: A regular expression pattern to match the spectrum ID from the PSM file to the spectrum identifier in the spectrum file.
A key aspect of spectrum parsing is matching peptide identifications to the correct spectra. Many search engines alter spectrum identifiers in their output files, so the identifiers in the PSM file may not directly match those in the spectrum file. For example, MaxQuant reports scan numbers rather than full spectrum titles. If the spectrum files were converted to MGF with ThermoRawFileParser, we can extract these scan numbers from the spectrum TITLE entries using a regular expression pattern. The following pattern extracts the scan number from this example spectrum title:
spectrum_id_pattern = r"scan=(\d+)"
mzspec=20161213_NGHF_DBJ_SA_Exp3A_HeLa_1ug_7min_15000_02.raw: controllerType=0 controllerNumber=1 scan=2
We can test this with the first spectrum in the file:
[10]:
from pyteomics.mgf import read as read_mgf
for spectrum in read_mgf("20161213_NGHF_DBJ_SA_Exp3A_HeLa_1ug_7min_15000_02.mgf"):
print(spectrum["params"]["title"]) # noqa T201
break
mzspec=20161213_NGHF_DBJ_SA_Exp3A_HeLa_1ug_7min_15000_02.raw: controllerType=0 controllerNumber=1 scan=2
[11]:
import re
spectrum_id = re.match(r".*scan=(\d+)$", spectrum["params"]["title"]).group(1)
print(spectrum_id) # noqa T201
2
We can use this scan number to retrieve its corresponding PSM:
[12]:
example_psm = psm_list[psm_list["spectrum_id"] == spectrum_id][0]
example_psm
[12]:
PSM(peptidoform=Peptidoform('ELMKLAPR/2'), spectrum_id='2', run='20161213_NGHF_DBJ_SA_Exp3A_HeLa_1ug_7min_15000_02', collection=None, spectrum=None, is_decoy=False, score=58.194, qvalue=np.float64(0.04256930768065444), pep=0.11486, precursor_mz=479.28114, retention_time=0.0054151, ion_mobility=None, protein_list=['K7ER25', 'K7EJY1', 'Q7Z3S7', 'E7EUE0'], rank=None, source='msms', provenance_data={'msms_filename': 'msms.txt'}, metadata={'Scan index': '0', 'Sequence': 'ELMKLAPR', 'Length': '8', 'Missed cleavages': '1', 'Modifications': 'Unmodified', 'Deamidation (NQ) Probabilities': '', 'Oxidation (M) Probabilities': '', 'Deamidation (NQ) Score Diffs': '', 'Oxidation (M) Score Diffs': '', 'Acetyl (Protein N-term)': '0', 'Deamidation (NQ)': '0', 'Gln->pyro-Glu': '0', 'Oxidation (M)': '0', 'Gene Names': 'CACNA2D4', 'Protein Names': 'Voltage-dependent calcium channel subunit alpha-2/delta-4;Voltage-dependent calcium channel subunit alpha-2-4;Voltage-dependent calcium channel subunit delta-4', 'Fragmentation': 'HCD', 'Mass analyzer': 'FTMS', 'Type': 'MSMS', 'Scan event number': '1', 'Isotope index': '', 'Mass': '956.54772', 'Mass error [ppm]': 'NaN', 'Mass error [Da]': 'NaN', 'Simple mass error [ppm]': '10020052000', 'Delta score': '12.312', 'Score diff': 'NaN', 'Localization prob': 'NaN', 'Combinatorics': '1', 'PIF': '0', 'Fraction of total spectrum': '0', 'Base peak fraction': '0', 'Precursor Full ScanNumber': '-1', 'Precursor Intensity': 'NaN', 'Precursor Apex Fraction': 'NaN', 'Precursor Apex Offset': '0', 'Precursor Apex Offset Time': 'NaN', 'Matches': 'y1;y2;y3;y1-NH3;y2-NH3;y3-NH3;b5;b6;b7;b5-H2O;b6-H2O', 'Intensities': '1301231.4;1312401.4;842082.2;527518.5;3855320.6;670603.1;964535.9;2367717.9;197335.6;30821.9;199087.7', 'Mass Deviations [Da]': '-0.000415732784006195;-0.000550409580171163;-0.00104542525048146;-0.000307003389082183;-0.000491550706556154;-0.000489022332658351;0.00622885583993593;0.00615890510266581;0.00973991597902568;0.0034132017480033;0.00802109195342382', 'Mass Deviations [ppm]': '-2.37399659991276;-2.02228385487352;-3.04602322334689;-1.94191995899886;-1.92654936588088;-1.49922797285001;10.1225059202731;8.97296777490114;12.4323442241631;5.71400675006929;12.0009400088042', 'Masses': '175.119367913784;272.17226644258;343.20987524605;158.092710082889;255.145658482207;326.182769741633;615.34721629166;686.384400030197;783.433582871321;597.339467259452;668.371973157047', 'Number of Matches': '11', 'Intensity coverage': '0.0871847693164245', 'Peak coverage': '0.0728476821192053', 'Neutral loss level': 'None', 'ETD identification type': 'Unknown', 'All scores': '58.1937302663689;45.8819242017191;36.975033213608', 'All sequences': 'ELMKLAPR;GIFPLGTPR;EKAIFPPR', 'All modified sequences': '_ELMKLAPR_;_GIFPLGTPR_;_EKAIFPPR_', 'id': '6392', 'Protein group IDs': '4453', 'Peptide ID': '6020', 'Mod. peptide ID': '6018', 'Evidence ID': '6357', 'Deamidation (NQ) site IDs': '', 'Oxidation (M) site IDs': ''}, rescoring_features={})
Using the correct spectrum_id_pattern, we can parse all spectra:
[13]:
from ms2rescore.parse_spectra import MSDataType, add_precursor_values, annotate_spectra
_ = add_precursor_values(
psm_list,
{MSDataType.ms2_spectra},
spectrum_path="20161213_NGHF_DBJ_SA_Exp3A_HeLa_1ug_7min_15000_02.mgf",
spectrum_id_pattern=r".*scan=(\d+)$",
)
annotate_spectra(psm_list, fragmentation_model="cidhcd", ms2_tolerance=20, ms2_tolerance_mode="ppm")
INFO:ms2rescore.parse_spectra:Parsing precursor info from spectrum files...
INFO:ms2rescore.parse_spectra:Annotating MS2 spectra based on search engine identifications...
Now the PSM list contains the annotated MS2 spectrum with the matching spectrum ID, and the spectrum can be accessed as follows:
[14]:
example_psm.spectrum
[14]:
AnnotatedMS2Spectrum(identifier='mzspec=20161213_NGHF_DBJ_SA_Exp3A_HeLa_1ug_7min_15000_02.raw: controllerType=0 controllerNumber=1 scan=2', peaks=224, annotated=13)
Basic features
Basic features from BasicFeatureGenerator can be added from any PSM list and contain simple feature, such as the search engine score, charge state, and absolute precursor mass error.
[15]:
basic_fgen = BasicFeatureGenerator()
basic_fgen.add_features(psm_list)
INFO:ms2rescore.feature_generators.basic:Adding basic features to PSMs.
[16]:
pd.DataFrame(list(psm_list["rescoring_features"]))
[16]:
| charge_n | charge_1 | charge_2 | charge_3 | charge_4 | charge_5 | charge_6 | abs_ms1_error_ppm | search_engine_score | theoretical_mass | experimental_mass | mass_error | pep_len | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2 | 0 | 1 | 0 | 0 | 0 | 0 | 1.137810 | 107.6600 | 955.546177 | 955.545087 | -0.001090 | 11 |
| 1 | 3 | 0 | 0 | 1 | 0 | 0 | 0 | 0.598941 | 107.7400 | 2742.435725 | 2742.434081 | -0.001644 | 25 |
| 2 | 3 | 0 | 0 | 1 | 0 | 0 | 0 | 0.573291 | 137.8900 | 2870.530688 | 2870.529041 | -0.001647 | 26 |
| 3 | 2 | 0 | 1 | 0 | 0 | 0 | 0 | 0.929236 | 22.6410 | 1168.559595 | 1168.558507 | -0.001088 | 14 |
| 4 | 2 | 0 | 1 | 0 | 0 | 0 | 0 | 0.666129 | 89.4030 | 1645.734325 | 1645.733227 | -0.001098 | 17 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 35551 | 2 | 0 | 1 | 0 | 0 | 0 | 0 | 0.782088 | 76.1000 | 1403.720846 | 1403.719747 | -0.001099 | 12 |
| 35552 | 4 | 0 | 0 | 0 | 1 | 0 | 0 | 1.147761 | 114.6400 | 1906.996328 | 1906.994134 | -0.002193 | 16 |
| 35553 | 2 | 0 | 1 | 0 | 0 | 0 | 0 | 0.813101 | 43.1240 | 1356.697652 | 1356.696547 | -0.001105 | 11 |
| 35554 | 3 | 0 | 0 | 1 | 0 | 0 | 0 | 0.719097 | 23.5620 | 2276.122890 | 2276.121251 | -0.001639 | 18 |
| 35555 | 3 | 0 | 0 | 1 | 0 | 0 | 0 | 1.083807 | 9.1885 | 1513.729834 | 1513.728191 | -0.001644 | 11 |
35556 rows × 13 columns
Now the PSMs contain a simple set of features.
Note that the charge state is present as both an integer and a one-hot encoded vector. This is because charge state can act as both a categorical and a numerical feature. Simply put, sometimes an effect increases or decreases with increasing charge state, and sometimes it is simply different for different charge states.
MS²PIP features
MS²PIP is a machine learning tool that predicts the MS2 spectrum of a peptide given its sequence. It is previously identified MS2 spectra and their corresponding peptide sequences. Because MS²PIP uses the highly performant - but traditional - machine learning approach XGBoost, it can already produce accurate predictions even if trained on smaller spectral libraries. This makes MS²PIP a very flexible platform to train new models on custom datasets. Nevertheless, MS²PIP comes with several pre-trained models, which we will use in this tutorial.
Because traditional proteomics search engines do not fully consider MS2 peak intensities in their scoring functions, adding rescoring features derived from spectrum prediction tools has proved to be a very effective way to further improve the sensitivity of peptide-spectrum matching. Generating features from MS²PIP predictions follows these steps:
Predict MS2 spectra for all PSMs in the dataset, including decoy PSMs and low scoring target PSMs.
Annotate the observed MS2 spectra from the original spectrum files (already done before by MS²Rescore)
Compare the predicted and observed spectra using various similarity metrics, which are returned as rescoring features.
Configuring MS²PIP
MS²PIP requires two parameters:
model: Name of the prediction model to use. This strongly depends on the fragmentation type of your dataset. A list of all MS²PIP models is available on https://ms2pip.readthedocs.io/en/latest/prediction-models/.processes: Number of CPU processes to use for parallel processing.
Note that fragment mass tolerance is no longer configured on MS²PIP directly — it is set once in annotate_spectra (see above) and the resulting AnnotatedMS2Spectrum objects are reused here.
[17]:
ms2pip_fgen = MS2PIPFeatureGenerator(
model="HCD",
)
Generating MS²PIP features
[18]:
ms2pip_fgen.add_features(psm_list)
INFO:ms2rescore.feature_generators.ms2pip:Adding MS²PIP-derived features to PSMs.
INFO:ms2pip.core:Processing spectra and peptides...
WARNING:ms2pip._utils.psm_input:Skipped 2/35556 PSMs with invalid peptidoforms: 2 unsupported amino acid 'U'
[19]:
pd.DataFrame(list(psm_list["rescoring_features"]))
[19]:
| charge_n | charge_1 | charge_2 | charge_3 | charge_4 | charge_5 | charge_6 | abs_ms1_error_ppm | search_engine_score | theoretical_mass | ... | ionb_abs_diff_Q1_norm | iony_mse | min_abs_diff_iontype | abs_diff_Q3 | ionb_abs_diff_Q2_norm | cos_iony_norm | iony_std_abs_diff | dotprod_iony | abs_diff_Q1_norm | ionb_max_abs_diff_norm | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2 | 0 | 1 | 0 | 0 | 0 | 0 | 1.137810 | 107.6600 | 955.546177 | ... | 0.101514 | 0.000048 | 0.0 | 0.009766 | 0.734400 | 0.995659 | 0.004302 | 0.001587 | 0.165929 | 1.684827 |
| 1 | 3 | 0 | 0 | 1 | 0 | 0 | 0 | 0.598941 | 107.7400 | 2742.435725 | ... | 0.029784 | 0.000031 | 0.0 | 0.003249 | 0.264225 | 0.998034 | 0.004661 | 0.005130 | 0.012931 | 2.034264 |
| 2 | 3 | 0 | 0 | 1 | 0 | 0 | 0 | 0.573291 | 137.8900 | 2870.530688 | ... | 0.000000 | 0.000006 | 0.0 | 0.001948 | 0.139024 | 0.997942 | 0.001813 | 0.001095 | 0.001910 | 2.130072 |
| 3 | 2 | 0 | 1 | 0 | 0 | 0 | 0 | 0.929236 | 22.6410 | 1168.559595 | ... | 0.078716 | 0.000448 | 0.0 | 0.014517 | 0.338768 | 0.955242 | 0.016877 | 0.000887 | 0.086232 | 3.987070 |
| 4 | 2 | 0 | 1 | 0 | 0 | 0 | 0 | 0.666129 | 89.4030 | 1645.734325 | ... | 0.006264 | 0.000505 | 0.0 | 0.010269 | 0.162389 | 0.982892 | 0.019758 | 0.000698 | 0.031096 | 3.818113 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 35551 | 2 | 0 | 1 | 0 | 0 | 0 | 0 | 0.782088 | 76.1000 | 1403.720846 | ... | 0.072093 | 0.000628 | 0.0 | 0.018626 | 0.106503 | 0.977440 | 0.015875 | 0.001575 | 0.131833 | 1.677736 |
| 35552 | 4 | 0 | 0 | 0 | 1 | 0 | 0 | 1.147761 | 114.6400 | 1906.996328 | ... | 0.001098 | 0.000005 | 0.0 | 0.002591 | 0.010763 | 0.997821 | 0.001660 | 0.000832 | 0.005419 | 3.105403 |
| 35553 | 2 | 0 | 1 | 0 | 0 | 0 | 0 | 0.813101 | 43.1240 | 1356.697652 | ... | 0.096080 | 0.000517 | 0.0 | 0.014355 | 0.323174 | 0.961556 | 0.016832 | 0.000125 | 0.362248 | 3.921681 |
| 35554 | 3 | 0 | 0 | 1 | 0 | 0 | 0 | 0.719097 | 23.5620 | 2276.122890 | ... | 0.000000 | 0.000064 | 0.0 | 0.004335 | 0.000000 | 0.975086 | 0.006132 | 0.000309 | 0.000000 | 3.860645 |
| 35555 | 3 | 0 | 0 | 1 | 0 | 0 | 0 | 1.083807 | 9.1885 | 1513.729834 | ... | 0.910986 | 0.000033 | 0.0 | 0.005959 | 1.517434 | 0.983124 | 0.004654 | 0.000018 | 0.699540 | 2.873089 |
35556 rows × 84 columns
Generating DeepLC features
[20]:
deeplc_fgen = DeepLCFeatureGenerator(
processes=8,
deeplc_retrain=False,
)
deeplc_fgen.add_features(psm_list)
INFO:ms2rescore.feature_generators.deeplc:Adding DeepLC-derived features to PSMs.
INFO:ms2rescore.feature_generators.deeplc:Predicting retention times with DeepLC...
INFO:ms2rescore.feature_generators.deeplc:Selecting best head and calibrating predicted retention times per run...
Generating features from the MS2 spectra
The MS2 feature generator scores the pre-annotated spectra against their PSM peptidoforms, computing features such as the fraction of explained intensity, the number and percentage of matched b/y ions, the longest consecutive ion series, and a hyperscore. It reuses the AnnotatedMS2Spectrum objects attached to each PSM by annotate_spectra — no additional annotation is performed here.
The fragmentation_model parameter controls which ion series are scored (e.g. "cidhcd" activates a, b, and y ions).
[21]:
ms2_fgen = MS2FeatureGenerator(
fragmentation_model="cidhcd",
)
ms2_fgen.add_features(psm_list)
INFO:ms2rescore.feature_generators.ms2:Adding MS2-derived features to PSMs.
Assessing individual feature performance before rescoring
[22]:
from ms2rescore.report.charts import (
calculate_feature_qvalues,
feature_ecdf_auc_bar,
fdr_plot,
ms2pip_correlation,
rt_scatter,
rt_distribution_baseline,
)
features = pd.DataFrame(list(psm_list["rescoring_features"]))
The following plot shows the number of identified PSM at varying FDR thresholds, where the red line indicates the commonly used 1% FDR threshold.
[23]:
psm_list.calculate_qvalues() # msms.txt does not contain q-values
fig = fdr_plot(psm_list.to_dataframe(), fdr_thresholds=None, log=False)
fig.show()
We can make a similar plot for each of the rescoring features by simply calculating q-values as if the feature were a search engine score.
[24]:
feature_qvalues, feature_ecdf_auc = calculate_feature_qvalues(
features=features,
is_decoy=psm_list["is_decoy"],
)
[25]:
feature_names = {
"basic": basic_fgen.feature_names,
"ms2pip": ms2pip_fgen.feature_names,
"deeplc": deeplc_fgen.feature_names,
"ms2": ms2_fgen.feature_names,
}
feature_generator_map = {name: gen for gen, f_list in feature_names.items() for name in f_list}
feature_qvalues_melt = feature_qvalues.melt(var_name="feature", value_name="q-value")
feature_qvalues_melt["feature_generator"] = feature_qvalues_melt["feature"].map(feature_generator_map)
[26]:
# Plot takes 100MB in saved file...
# fig = px.ecdf(
# data_frame=feature_qvalues_melt,
# x="q-value",
# color="feature_generator",
# line_dash="feature",
# )
# fig.show()
The larger the area under the curve (AUC) is for each line, the better. We can therefore visualize the same information in a more easy-to-interpret bar chart with the AUCs, which were already calculated by the calculate_feature_qvalues function.
[27]:
feature_ecdf_auc["feature_generator"] = feature_ecdf_auc["feature"].map(feature_generator_map)
[28]:
fig = feature_ecdf_auc_bar(feature_ecdf_auc)
fig.show()
Importantly, a low q-value ECDF AUC does not necessarily mean that a feature is not valuable for rescoring. The more orthogonal a feature is to the other information already used in PSM scoring, the more it will help to confidently identify PSMs. For instance, retention time features are completely independent from the information used in a traditional search engine. When combined with other scoring information, they can provide a substantial boost in sensitivity.
The quality of specific rescoring features can also be assessed in a manner specific to the feature. For instance, we can plot the distribution of the MS²PIP pearson correlations for target PSMs that initially passed the FDR threshold:
[29]:
fig = ms2pip_correlation(
features=features,
is_decoy=psm_list["is_decoy"],
qvalue=psm_list["qvalue"]
)
fig.show()
For DeepLC, we can plot the predicted retention time versus the observed retention time:
[30]:
fig = rt_scatter(
df=features[(~psm_list["is_decoy"]) & (psm_list["qvalue"] <= 0.01)], # noqa: E712
predicted_column="predicted_retention_time_best",
observed_column="observed_retention_time_best",
)
fig.show()
DeepLC also provides a function to plot the current relative mean absolute error (rMAE) of the predicted retention times against a distribution of 460 benchmark datasets. The lower the rMAE, the better.
[31]:
fig = rt_distribution_baseline(
df=features[(~psm_list["is_decoy"]) & (psm_list["qvalue"] <= 0.01)], # noqa: E712
predicted_column="predicted_retention_time_best",
observed_column="observed_retention_time_best",
)
fig.show()
Rescoring
Using ristretto directly
MS²Rescore uses ristretto as rescoring engine: a lean, dependency-light (numpy/scikit-learn/pandas only) reimplementation of the Percolator/Käll semi-supervised algorithm, purpose-built to serve MS²Rescore. ristretto takes a plain pandas.DataFrame of identifiers and features, so we first build that DataFrame from the PSM list.
[32]:
import ristretto
feature_names_flat = sorted({f for psm in psm_list for f in psm.rescoring_features})
features_df = pd.DataFrame(
{
"spectrum_id": psm_list["spectrum_id"],
"run": psm_list["run"],
"is_decoy": psm_list["is_decoy"],
"peptidoform": [str(p) for p in psm_list["peptidoform"]],
"score": psm_list["score"],
}
)
feature_values = (
pd.DataFrame(list(psm_list["rescoring_features"]))[feature_names_flat]
.astype(float)
.fillna(0.0)
)
features_df = pd.concat([features_df, feature_values], axis=1)
features_df.head()
[32]:
| spectrum_id | run | is_decoy | peptidoform | score | abs_diff_Q1 | abs_diff_Q1_norm | abs_diff_Q2 | abs_diff_Q2_norm | abs_diff_Q3 | ... | rt_diff_best | search_engine_score | spec_mse | spec_mse_norm | spec_pearson | spec_pearson_norm | spec_spearman | std_abs_diff | std_abs_diff_norm | theoretical_mass | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 4703 | 20161213_NGHF_DBJ_SA_Exp3A_HeLa_1ug_7min_15000_02 | False | AAAAAAALQAK/2 | 107.660 | 0.000594 | 0.165929 | 0.002967 | 0.666364 | 0.009766 | ... | 0.142506 | 107.660 | 0.000251 | 0.739558 | 0.953128 | 0.954095 | 0.918799 | 0.013420 | 0.524816 | 955.546177 |
| 1 | 13572 | 20161213_NGHF_DBJ_SA_Exp3A_HeLa_1ug_7min_15000_02 | False | [UNIMOD:Acetylation]-AAAAAEQQQFYLLLGNLLSPDNVVR/3 | 107.740 | 0.000009 | 0.012931 | 0.000820 | 0.319576 | 0.003249 | ... | 0.252993 | 107.740 | 0.000159 | 0.520783 | 0.937566 | 0.959657 | 0.882700 | 0.011488 | 0.527856 | 2742.435725 |
| 2 | 13366 | 20161213_NGHF_DBJ_SA_Exp3A_HeLa_1ug_7min_15000_02 | False | [UNIMOD:Acetylation]-AAAAAEQQQFYLLLGNLLSPDNVVRK/3 | 137.890 | 0.000001 | 0.001910 | 0.000571 | 0.311654 | 0.001948 | ... | 0.022973 | 137.890 | 0.000136 | 0.433370 | 0.919416 | 0.945299 | 0.890340 | 0.011077 | 0.496484 | 2870.530688 |
| 3 | 505 | 20161213_NGHF_DBJ_SA_Exp3A_HeLa_1ug_7min_15000_02 | False | AAAAAQGGGGGEPR/2 | 22.641 | 0.000062 | 0.086232 | 0.001181 | 0.811038 | 0.014517 | ... | 1.728728 | 22.641 | 0.000461 | 5.768089 | 0.523585 | 0.529264 | 0.503597 | 0.017876 | 1.797819 | 1168.559595 |
| 4 | 6589 | 20161213_NGHF_DBJ_SA_Exp3A_HeLa_1ug_7min_15000_02 | False | AAAAAWEEPSSGN[UNIMOD:Deamidation]GTAR/2 | 89.403 | 0.000022 | 0.031096 | 0.001235 | 0.801425 | 0.010269 | ... | 0.168237 | 89.403 | 0.000427 | 3.596586 | 0.639873 | 0.831775 | 0.886938 | 0.018176 | 1.377847 | 1645.734325 |
5 rows × 124 columns
Then, ristretto.rescore can be called to rescore the PSMs. It runs spectrum-grouped k-fold cross-validation, target-decoy competition, and q-value/PEP calculation, returning a RescoreResult with the PSM-level results plus a peptidoform-level rollup (and peptide-/protein-level rollups, if a peptide_col/protein_col is provided).
[33]:
result = ristretto.rescore(
features_df,
run_col="run",
feature_cols=feature_names_flat,
model="svm",
train_fdr=0.01,
)
result.psms.head()
INFO:ristretto.rescore:Rescoring 35556 PSMs (22190 targets, 13366 decoys) with 119 features using model 'svm'
INFO:ristretto.rescore:Fold 1/3: training on 24113 PSMs, scoring 11443
INFO:ristretto.rescore:Fold 2/3: training on 23517 PSMs, scoring 12039
INFO:ristretto.rescore:Fold 3/3: training on 23482 PSMs, scoring 12074
INFO:ristretto.rescore:Spectrum competition: 35556 PSMs -> 10568 best per spectrum
INFO:ristretto.rescore:Computing PSM-level q-values and PEPs
INFO:ristretto.rescore:Rolling up to peptidoform level on column 'peptidoform'
[33]:
| spectrum_id | run | is_decoy | peptidoform | score | qvalue | pep | |
|---|---|---|---|---|---|---|---|
| 0 | 4703 | 20161213_NGHF_DBJ_SA_Exp3A_HeLa_1ug_7min_15000_02 | False | AAAAAAALQAK/2 | 4.254977 | 0.000345 | 0.00681 |
| 1 | 13572 | 20161213_NGHF_DBJ_SA_Exp3A_HeLa_1ug_7min_15000_02 | False | [UNIMOD:Acetylation]-AAAAAEQQQFYLLLGNLLSPDNVVR/3 | 4.000630 | 0.000345 | 0.00681 |
| 2 | 13366 | 20161213_NGHF_DBJ_SA_Exp3A_HeLa_1ug_7min_15000_02 | False | [UNIMOD:Acetylation]-AAAAAEQQQFYLLLGNLLSPDNVVRK/3 | 9.357508 | 0.000345 | 0.00681 |
| 4 | 6589 | 20161213_NGHF_DBJ_SA_Exp3A_HeLa_1ug_7min_15000_02 | False | AAAAAWEEPSSGN[UNIMOD:Deamidation]GTAR/2 | 2.673495 | 0.000481 | 0.00681 |
| 12 | 1029 | 20161213_NGHF_DBJ_SA_Exp3A_HeLa_1ug_7min_15000_02 | False | AAAGRYR/2 | -1.401264 | 0.040268 | 0.14212 |
We can reuse the fdr_plot function from earlier to plot the number of identifications at each FDR threshold, this time on the rescored PSMs:
[34]:
fig = fdr_plot(result.psms, fdr_thresholds=[0.01])
fig.show()
Or to compare the un-rescored PSMs to the rescored PSMs. ristretto.evaluate runs the exact same competition/q-value/PEP machinery as rescore, but on an already-computed score column instead of training a new model — here, the PSMs’ original search engine score — so the two are directly comparable. We can then use charts.fdr_plot_comparison, the same chart MS²Rescore’s own HTML report uses for this comparison:
[35]:
from ms2rescore.report.charts import fdr_plot_comparison
before_result = ristretto.evaluate(features_df, run_col="run")
fig = fdr_plot_comparison(before_result, result, fdr_threshold=0.01)
fig.show()
INFO:ristretto.rescore:Spectrum competition: 35556 PSMs -> 10568 best per spectrum
INFO:ristretto.rescore:Computing PSM-level q-values and PEPs
INFO:ristretto.rescore:Rolling up to peptidoform level on column 'peptidoform'
Using ristretto directly also allows us to quickly rescore with different feature sets, for instance:
Only basic features
Basic + MS2 features
Basic + MS2 + MS²PIP features
Basic + MS2 + MS²PIP + DeepLC features
[36]:
feature_sets = {
"basic": feature_names["basic"],
"basic_ms2": feature_names["basic"] + feature_names["ms2"],
"basic_ms2_ms2pip": feature_names["basic"] + feature_names["ms2"] + feature_names["ms2pip"],
"basic_ms2_ms2pip_deeplc": feature_names["basic"] + feature_names["ms2"] + feature_names["ms2pip"] + feature_names["deeplc"],
}
[37]:
import plotly.express as px
plot_rows = [
before_result.psms.sort_values("score", ascending=False)
.drop_duplicates(subset=["run", "spectrum_id"], keep="first")
.loc[lambda df: ~df["is_decoy"], ["qvalue"]]
.assign(feature_set="before rescoring")
]
for name, cols in feature_sets.items():
result_i = ristretto.rescore(
features_df, run_col="run", feature_cols=cols, model="svm", train_fdr=0.01
)
best = result_i.psms.sort_values("score", ascending=False).drop_duplicates(
subset=["run", "spectrum_id"], keep="first"
)
plot_rows.append(best.loc[~best["is_decoy"], ["qvalue"]].assign(feature_set=name))
fig = px.ecdf(pd.concat(plot_rows), x="qvalue", color="feature_set", log_x=True, ecdfnorm=None)
fig.update_layout(xaxis_title="FDR threshold", yaxis_title="Identified spectra")
fig.show()
INFO:ristretto.rescore:Rescoring 35556 PSMs (22190 targets, 13366 decoys) with 13 features using model 'svm'
INFO:ristretto.rescore:Fold 1/3: training on 24113 PSMs, scoring 11443
INFO:ristretto.rescore:Fold 2/3: training on 23517 PSMs, scoring 12039
INFO:ristretto.rescore:Fold 3/3: training on 23482 PSMs, scoring 12074
INFO:ristretto.rescore:Spectrum competition: 35556 PSMs -> 10568 best per spectrum
INFO:ristretto.rescore:Computing PSM-level q-values and PEPs
INFO:ristretto.rescore:Rolling up to peptidoform level on column 'peptidoform'
INFO:ristretto.rescore:Rescoring 35556 PSMs (22190 targets, 13366 decoys) with 42 features using model 'svm'
INFO:ristretto.rescore:Fold 1/3: training on 24113 PSMs, scoring 11443
INFO:ristretto.rescore:Fold 2/3: training on 23517 PSMs, scoring 12039
INFO:ristretto.rescore:Fold 3/3: training on 23482 PSMs, scoring 12074
INFO:ristretto.rescore:Spectrum competition: 35556 PSMs -> 10568 best per spectrum
INFO:ristretto.rescore:Computing PSM-level q-values and PEPs
INFO:ristretto.rescore:Rolling up to peptidoform level on column 'peptidoform'
INFO:ristretto.rescore:Rescoring 35556 PSMs (22190 targets, 13366 decoys) with 113 features using model 'svm'
INFO:ristretto.rescore:Fold 1/3: training on 24113 PSMs, scoring 11443
INFO:ristretto.rescore:Fold 2/3: training on 23517 PSMs, scoring 12039
INFO:ristretto.rescore:Fold 3/3: training on 23482 PSMs, scoring 12074
INFO:ristretto.rescore:Spectrum competition: 35556 PSMs -> 10568 best per spectrum
INFO:ristretto.rescore:Computing PSM-level q-values and PEPs
INFO:ristretto.rescore:Rolling up to peptidoform level on column 'peptidoform'
INFO:ristretto.rescore:Rescoring 35556 PSMs (22190 targets, 13366 decoys) with 119 features using model 'svm'
INFO:ristretto.rescore:Fold 1/3: training on 24113 PSMs, scoring 11443
INFO:ristretto.rescore:Fold 2/3: training on 23517 PSMs, scoring 12039
INFO:ristretto.rescore:Fold 3/3: training on 23482 PSMs, scoring 12074
INFO:ristretto.rescore:Spectrum competition: 35556 PSMs -> 10568 best per spectrum
INFO:ristretto.rescore:Computing PSM-level q-values and PEPs
INFO:ristretto.rescore:Rolling up to peptidoform level on column 'peptidoform'
Using MS²Rescore’s full pipeline
The steps above show what happens under the hood. For most use cases, the single ms2rescore.rescore() function call handles the entire workflow – parsing PSMs, generating all configured features, rescoring with ristretto, and writing the output files (and, optionally, the HTML report) – from one configuration dictionary (or JSON/TOML file). See the configuration guide for all available options.
[38]:
from ms2rescore import rescore, parse_configurations
configuration = {
"ms2rescore": {
"psm_file": "msms.txt",
"psm_file_type": "msms",
"spectrum_path": "20161213_NGHF_DBJ_SA_Exp3A_HeLa_1ug_7min_15000_02.mgf",
"spectrum_id_pattern": r".*scan=(\d+)$",
"modification_mapping": {
"gl": "U:Gln->pyro-Glu",
"ox": "U:Oxidation",
"ac": "U:Acetylation",
"de": "U:Deamidation",
},
"fixed_modifications": {"U:Carbamidomethyl": ["C"]},
"feature_generators": {
"basic": {},
"ms2": {"fragmentation_model": "cidhcd"},
"ms2pip": {"model": "HCD"}
}
}
}
parsed_config = parse_configurations([configuration])
rescore(parsed_config)
INFO:ms2rescore.parse_psms:Reading PSMs from PSM file (1/1): 'msms.txt'...
WARNING:ms2rescore.parse_psms:Removed 2 PSMs with invalid amino acids.
INFO:ms2rescore.parse_psms:Found 35554 PSMs, of which 37.59% are decoys.
INFO:ristretto.rescore:Spectrum competition: 35554 PSMs -> 10568 best per spectrum
INFO:ristretto.rescore:Computing PSM-level q-values and PEPs
INFO:ristretto.rescore:Rolling up to peptidoform level on column 'peptidoform'
INFO:ristretto.rescore:Spectrum competition: 35554 PSMs -> 10568 best per spectrum
INFO:ristretto.rescore:Computing PSM-level q-values and PEPs
INFO:ristretto.rescore:Rolling up to peptidoform level on column 'peptidoform'
c:\Users\ralfg\git\ms2rescore-2\.venv\Lib\site-packages\psm_utils\psm_list.py:201: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
.fillna(0) # groupby does not play well with None values
INFO:ms2rescore.parse_psms:Removed 4240 PSMs with rank >= 10.
INFO:ristretto.rescore:Spectrum competition: 31314 PSMs -> 10568 best per spectrum
INFO:ristretto.rescore:Computing PSM-level q-values and PEPs
INFO:ristretto.rescore:Rolling up to peptidoform level on column 'peptidoform'
INFO:ristretto.rescore:Spectrum competition: 31314 PSMs -> 10568 best per spectrum
INFO:ristretto.rescore:Computing PSM-level q-values and PEPs
INFO:ristretto.rescore:Rolling up to peptidoform level on column 'peptidoform'
INFO:ristretto.rescore:Spectrum competition: 31314 PSMs -> 10568 best per spectrum
INFO:ristretto.rescore:Computing PSM-level q-values and PEPs
INFO:ristretto.rescore:Rolling up to peptidoform level on column 'peptidoform'
INFO:ristretto.rescore:Rolling up to peptide level on column 'peptide'
INFO:ms2rescore.core:Found 5610 identified PSMs at 1.00% FDR before rescoring.
INFO:ms2rescore.parse_spectra:Parsing precursor info from spectrum files...
INFO:ms2rescore.parse_spectra:Annotating MS2 spectra based on search engine identifications...
INFO:ms2rescore.feature_generators.basic:Adding basic features to PSMs.
INFO:ms2rescore.feature_generators.ms2:Adding MS2-derived features to PSMs.
INFO:ms2rescore.feature_generators.ms2pip:Adding MS²PIP-derived features to PSMs.
INFO:ms2pip.core:Processing spectra and peptides...
INFO:ms2rescore.core:Rescoring 31314 PSMs with ristretto...
INFO:ristretto.rescore:Spectrum competition: 31314 PSMs -> 10568 best per spectrum
INFO:ristretto.rescore:Computing PSM-level q-values and PEPs
INFO:ristretto.rescore:Rolling up to peptidoform level on column 'peptidoform'
INFO:ristretto.rescore:Spectrum competition: 31314 PSMs -> 10568 best per spectrum
INFO:ristretto.rescore:Computing PSM-level q-values and PEPs
INFO:ristretto.rescore:Rolling up to peptidoform level on column 'peptidoform'
INFO:ristretto.rescore:Rescoring 31314 PSMs (20053 targets, 11261 decoys) with 113 features using model 'svm'
INFO:ristretto.rescore:Fold 1/3: training on 20910 PSMs, scoring 10404
INFO:ristretto.rescore:Fold 2/3: training on 20824 PSMs, scoring 10490
INFO:ristretto.rescore:Fold 3/3: training on 20894 PSMs, scoring 10420
INFO:ristretto.rescore:Computing PSM-level q-values and PEPs
INFO:ristretto.rescore:Rolling up to peptidoform level on column 'peptidoform'
INFO:ristretto.rescore:Rolling up to peptide level on column 'peptide'
INFO:ristretto.rescore:Spectrum competition: 31314 PSMs -> 10568 best per spectrum
INFO:ristretto.rescore:Computing PSM-level q-values and PEPs
INFO:ristretto.rescore:Rolling up to peptidoform level on column 'peptidoform'
INFO:ristretto.rescore:Rolling up to peptide level on column 'peptide'
c:\Users\ralfg\git\ms2rescore-2\.venv\Lib\site-packages\psm_utils\psm_list.py:201: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
.fillna(0) # groupby does not play well with None values
INFO:ms2rescore.core:Identified +1545 (27.54%) PSMs at 1.00% FDR after rescoring, compared to before.
INFO:ms2rescore.core:Writing output to msms.ms2rescore.tsv...