# Bio Applied Proteomics

> Compute peptide b/y ion masses, run trypsin/PMF search, quantify LFQ protein abundance (volcano plots), and calculate PTM shifts and protein inference. Use for MS/MS peptide ID, PMF search, LFQ quantification, or PTM analysis.

- Skill: `pavel-kravchenko/bio-applied-proteomics` (Agent Skill)
- Install (CLI): `npx skillmds@latest add pavel-kravchenko/bio-applied-proteomics`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pavel-kravchenko/bio-applied-proteomics/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: pavel-kravchenko (https://skillmd.com/u/pavel-kravchenko)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/pavel-kravchenko/bio-applied-proteomics

---


# Applied Proteomics

## When to Use

- Interpreting an MS/MS spectrum's b/y ion ladder or checking a peptide-spectrum match by hand
- Simulating in-silico tryptic digestion (with missed cleavages) and matching masses via peptide mass fingerprinting (PMF)
- Quantifying label-free (LFQ) or TMT protein abundance between two conditions and building volcano/MA plots
- Computing PTM mass shifts (phosphorylation, oxidation, acetylation) on identified peptides
- Resolving protein inference when peptides map to multiple protein entries (razor peptide / parsimony)

## Version Compatibility

- Python ≥3.10, NumPy ≥1.26, pandas ≥2.2, Matplotlib ≥3.8, SciPy ≥1.12
- Applies to bottom-up (shotgun) LC-MS/MS workflows (Orbitrap/Q-TOF, DDA); concepts map onto Mascot/MaxQuant/MSFragger/Comet search output

## Prerequisites

- `pip install numpy pandas matplotlib scipy`
- Familiarity with amino acid chemistry and basic mass spectrometry (m/z, charge states)
- Related skills: `bio-proteomics-peptide-identification`, `bio-proteomics-quantification`

## MS/MS Fragmentation: b/y Ions

**Goal:** derive peptide and fragment-ion masses from a sequence to interpret or simulate an MS/MS spectrum.
**Approach:** sum monoisotopic residue masses; b ions are N-terminal fragments (+ proton, no water), y ions are C-terminal fragments (+ water + proton).

```python
import numpy as np

AA_MONO_MASS = {
    'A': 71.03711,  'R': 156.10111, 'N': 114.04293, 'D': 115.02694,
    'C': 103.00919, 'E': 129.04259, 'Q': 128.05858, 'G': 57.02146,
    'H': 137.05891, 'I': 113.08406, 'L': 113.08406, 'K': 128.09496,
    'M': 131.04049, 'F': 147.06841, 'P': 97.05276,  'S': 87.03203,
    'T': 101.04768, 'W': 186.07931, 'Y': 163.06333, 'V': 99.06841,
}
H2O = 18.01056
H = 1.00728  # proton mass


def peptide_mass(seq: str) -> float:
    """Neutral monoisotopic mass of a peptide (sum of residues + one water)."""
    return sum(AA_MONO_MASS[aa] for aa in seq) + H2O


def by_ions(seq: str) -> tuple[list[float], list[float]]:
    """Singly-charged b (N-term) and y (C-term) ion m/z for every cleavage site."""
    n = len(seq)
    b_ions, y_ions = [], []
    for i in range(1, n):
        b_ions.append(sum(AA_MONO_MASS[seq[j]] for j in range(i)) + H)
        y_ions.append(sum(AA_MONO_MASS[seq[j]] for j in range(i, n)) + H2O + H)
    return b_ions, y_ions


peptide = "ACDEFGHIK"
b, y = by_ions(peptide)
print(f"[M+H]+ = {peptide_mass(peptide) + H:.4f}")
for i, (bi, yi) in enumerate(zip(b, y), 1):
    print(f"b{i}={bi:.4f}  y{len(peptide) - i}={yi:.4f}")
```

## Trypsin Digestion and Peptide Mass Fingerprinting

**Goal:** identify a protein from a set of observed peptide masses without MS/MS (PMF), or generate the candidate peptide list for a database search.
**Approach:** cleave after K/R unless followed by P; compute theoretical masses for every candidate protein and count matches within a ppm tolerance.

```python
import numpy as np


def trypsin_digest(sequence: str, missed_cleavages: int = 0) -> list[str]:
    """In-silico trypsin digestion: cleaves after K/R unless followed by P."""
    seq = sequence.upper()
    sites = [0]
    for i in range(len(seq) - 1):
        if seq[i] in ('K', 'R') and seq[i + 1] != 'P':
            sites.append(i + 1)
    sites.append(len(seq))
    fragments = [seq[sites[i]:sites[i + 1]] for i in range(len(sites) - 1)]
    fragments = [f for f in fragments if f]
    if missed_cleavages == 0:
        return fragments
    result = list(fragments)
    for mc in range(1, missed_cleavages + 1):
        for i in range(len(fragments) - mc):
            result.append(''.join(fragments[i:i + mc + 1]))
    return sorted(set(result), key=lambda x: sequence.index(x))


def simple_pmf_search(
    observed_masses: list[float],
    database: dict[str, str],
    tolerance_ppm: float = 20.0,
    missed_cleavages: int = 1,
) -> list[tuple[str, int, float]]:
    """Match observed masses against theoretical tryptic peptides per protein.

    Returns (protein_name, n_matched, fraction_matched) sorted by n_matched desc.
    """
    results = []
    obs = np.array(sorted(observed_masses))
    for name, seq in database.items():
        theo_peptides = trypsin_digest(seq, missed_cleavages)
        theo_masses = np.array(sorted(
            peptide_mass(p) for p in theo_peptides if all(aa in AA_MONO_MASS for aa in p)
        ))
        matched = sum(1 for m in obs if np.any(np.abs(theo_masses - m) <= m * tolerance_ppm * 1e-6))
        results.append((name, matched, matched / len(obs) if obs.size else 0.0))
    return sorted(results, key=lambda x: -x[1])
```

## PTM Mass Shifts

**Goal:** account for post-translational modifications when computing a modified peptide's mass.
**Approach:** add the modification's monoisotopic delta mass at the modified residue(s); common shifts below.

```python
PTM_MASS_SHIFT = {
    'phospho':        79.9663,   # S/T/Y
    'oxidation':       15.9949,  # M (variable mod)
    'acetylation':     42.0106,  # protein N-term or K
    'carbamidomethyl': 57.0215,  # C (fixed mod after alkylation with iodoacetamide)
    'deamidation':      0.9840,  # N/Q
}


def modified_peptide_mass(seq: str, mods: dict[int, str]) -> float:
    """Neutral mass of a peptide with PTMs.

    mods: {0-based residue index: modification name in PTM_MASS_SHIFT}.
    """
    base = peptide_mass(seq)
    return base + sum(PTM_MASS_SHIFT[m] for m in mods.values())


print(f"{modified_peptide_mass('ACSDEFGHIK', {2: 'phospho'}):.4f} Da")  # phosphopeptide: pS at position 2
```

## Protein Inference (Razor Peptides)

**Goal:** assign shared (non-unique) peptides to a single "razor" protein instead of double-counting them across all matching entries.
**Approach:** greedy parsimony — process peptides with the fewest candidate proteins first, assign each to whichever candidate already has the most peptide evidence.

```python
from collections import Counter


def infer_proteins_razor(peptide_to_proteins: dict[str, set[str]]) -> dict[str, str]:
    """Simplified MaxQuant-style razor-peptide protein inference.

    Returns {peptide: assigned_protein}.
    """
    protein_counts = Counter()
    for prots in peptide_to_proteins.values():
        for p in prots:
            protein_counts[p] += 1
    assignment = {}
    for pep, prots in sorted(peptide_to_proteins.items(), key=lambda kv: len(kv[1])):
        assignment[pep] = max(prots, key=lambda p: protein_counts[p])
    return assignment
```

## Label-Free Quantification and Volcano Plot

**Goal:** compare protein abundance between two conditions from replicate LFQ log2 intensities.
**Approach:** median-normalize each sample, compute per-protein log2 fold change, and test significance with Welch's t-test across replicates.

```python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats


def median_normalize(log2_intensities: np.ndarray) -> np.ndarray:
    """Median-center each sample's log2 intensities (columns = replicates)."""
    return log2_intensities - np.nanmedian(log2_intensities, axis=0)


def lfq_differential_abundance(intensities_a: np.ndarray, intensities_b: np.ndarray) -> pd.DataFrame:
    """Per-protein log2FC + Welch's t-test p-value between two conditions.

    intensities_a/b: (n_proteins, n_replicates) log2-transformed intensity arrays.
    """
    log2fc = np.nanmean(intensities_b, axis=1) - np.nanmean(intensities_a, axis=1)
    _, pvals = stats.ttest_ind(intensities_b, intensities_a, axis=1, nan_policy='omit', equal_var=False)
    pvals = np.nan_to_num(pvals, nan=1.0)
    return pd.DataFrame({'log2fc': log2fc, 'pvalue': pvals, 'neg_log10p': -np.log10(pvals)})


np.random.seed(7)
n_proteins, n_reps = 200, 3
base = np.random.normal(27, 3, (n_proteins, 1))
cond_a = median_normalize(base + np.random.normal(0, 0.3, (n_proteins, n_reps)))
fc_true = np.zeros((n_proteins, 1))
fc_true[np.random.choice(n_proteins, 20, replace=False)] = np.random.uniform(1, 3, (20, 1)) * np.random.choice([-1, 1], (20, 1))
cond_b = median_normalize(base + fc_true + np.random.normal(0, 0.3, (n_proteins, n_reps)))

de = lfq_differential_abundance(cond_a, cond_b)
sig = (de.log2fc.abs() > 1) & (de.pvalue < 0.05)
plt.scatter(de.log2fc, de.neg_log10p, c=np.where(sig, 'tomato', 'steelblue'), alpha=0.6, s=20)
plt.axvline(1, ls='--', c='gray'); plt.axvline(-1, ls='--', c='gray'); plt.axhline(1.3, ls='--', c='gray')
plt.xlabel('log2 fold change'); plt.ylabel('-log10(p-value)'); plt.title(f'{sig.sum()} DE proteins')
plt.tight_layout(); plt.show()
```

## Pitfalls

- Trypsin does **not** cleave before proline — skipping this rule generates false theoretical peptides and misses real ones.
- b ions carry no C-terminal OH (only + proton); confusing b/y mass formulas is the most common manual-calculation bug.
- PMF alone cannot resolve complex mixtures (multiple proteins per sample) — use LC-MS/MS with target-decoy FDR filtering instead.
- Carbamidomethylation on cysteine is a **fixed** modification after standard iodoacetamide alkylation; forgetting it shifts every Cys-containing peptide by 57.02 Da.
- PTM site localization is ambiguous when a peptide has multiple candidate S/T/Y residues — needs a localization score (AScore, PTM score), not just the mass shift.
- Naive protein inference (assigning a shared peptide to *every* matching protein) inflates protein-group counts; use razor/parsimony logic.
- Missing values in LFQ intensity matrices should be handled with proper imputation (e.g. left-censored/MNAR-aware methods), not zero-filling, before differential testing.

## See Also

- `bio-proteomics-peptide-identification`
- `bio-proteomics-quantification`
- `bio-proteomics-ptm-analysis`
- `bio-proteomics-protein-inference`

