Edit on GitHub

starplast.proteomics

Turn a PRIDE deposit's own search output into one number per gene.

What this reads, and what it refuses to

The submitter's result tables: MaxQuant *Sites.txt and evidence.txt, and mzIdentML .mzid. Not the raw spectra -- see scripts/fetch_candidates.py for why that matters, but briefly: a .raw is an instrument recording, and turning one into protein identifications needs a search engine, a sequence database and a set of parameters. Those choices are a study of their own, and inventing them here would produce numbers that look like measurements and are not.

So every column this produces is a count of what the AUTHORS reported, and the docstring of each slot it fills should say so. "How many lactylation sites did this study report on this gene" is a smaller claim than "how many lactylation sites does this gene have", and it is the one the data supports.

Why counts rather than intensities

Intensity is comparable within one experiment and not across two. These deposits come from different laboratories, instruments and normalisation choices, and a column mixing them would rank genes by whose mass spectrometer was more sensitive. A site count is coarse, robust to all of that, and answers the question the slot actually asks -- is this gene modified this way, and heavily.

Genes absent from a deposit get NaN rather than zero, and the difference is the whole of what makes the column honest: a gene the study never saw is not a gene with no sites. na_policy="indicator" downstream then treats "not measured" as its own signal, which it is.

GENE = re.compile('\\b(TGME49_\\d{6}|TGGT1_\\d{6})\\b')
RESULT_FILES = re.compile('(Sites?\\.txt|evidence\\.txt|proteinGroups\\.txt|\\.mzid)$', re.IGNORECASE)
ARCHIVE_FILES = re.compile('\\.(txt|tsv|csv|mzid)$', re.IGNORECASE)
NOT_EVIDENCE = re.compile('\\.(fasta|fa|faa)$|parameters|summary', re.IGNORECASE)
def gene_counts(text: str) -> pandas.Series:

How many times each gene is named in a result table.

Counted from the identifiers themselves rather than from a named column, because these tables do not agree on what that column is called: MaxQuant writes Proteins, Leading proteins and Protein; an mzIdentML puts the accession in an attribute. What every one of them does is spell the gene, so that is what is counted -- one row of a sites table is one reported site.

GENERIC_TABLES = re.compile('allPeptides|matchedFeatures|ms\\d*Scans|msms|peptides\\.txt|modificationSpecificPeptides|Carbamidomethyl|Deamidation|Oxidation', re.IGNORECASE)
def deposit_counts(folder: str, limit_files: int = 40, wants: str = '') -> pandas.Series:

Sites per gene for one downloaded deposit, across the tables that measure wants.

wants is the modification the SLOT asked for, and a deposit's own file names are what decide whether a table measures it. Without that, a folder of eleven Sites tables answers every slot with all eleven.

DEPOSITS = (('Tg', 'acetylation', 'PXD079431', 'n_acetylation_sites', 'acetyl|GCN5|FLAG'), ('Tg', 'interaction_proximity_labelling', 'PXD059579', 'n_proximity_partners', 'mzid|Results'), ('Tg', 'S_nitrosylation', 'PXD046083', 'n_nitrosylation_sites', 'iodo ?TMT|nitrosyl|SNO'), ('Tg', 'ubiquitination_SUMOylation', 'PXD042937', 'n_ubiquitination_sites', 'GlyGly|ubiquitin'), ('Tg', 'lactylation', 'PXD031526', 'n_lactylation_sites', 'La \\(K\\)Sites'), ('Tg', 'glycosylation', 'PXD004426', 'n_o_fucosyl_peptides', 'mzid|AAL'))
QUARANTINE = 'datasets/quarantine/2026_08_16_pride'
def load_all( base: str, index, log=<built-in function print>, resolve=None) -> pandas.DataFrame:

Every verified deposit as columns, aligned to a gene index.

The differentiation reporter screen rides along here rather than in screens.crispr_screens, which reads the published fitness tables from the dataset archive. This one is in quarantine and is computed from counts, so it belongs with the other deposits that were verified by being read.

def column_for( folder: str, column: str, index, wants: str = '', resolve=None) -> pandas.DataFrame:

One deposit as one column, aligned to a gene index, NaN where the study saw nothing.

NaN and not zero. A gene this deposit never reported is a gene nobody measured for this modification, and writing zero would state that it carries none -- a claim the data cannot make and one that would put thousands of unmeasured genes at the bottom of every ranking.

Accessions go through resolve before the join. A deposit keyed on the type I strain writes TGGT1_273760 where the node table says TGME49_, and matching those as strings joins nothing -- silently, because a join that matches no rows looks exactly like a study with no coverage. The lactylation deposit is entirely TGGT1_ and reported 0 of its 524 genes until this existed.