From 38a838cf92eddec4a01aa8437eb69b772371855f Mon Sep 17 00:00:00 2001 From: mschwoerer <82171591+mschwoer@users.noreply.github.com> Date: Wed, 28 Jan 2026 18:27:30 +0100 Subject: [PATCH 01/11] CC: I want to apply the @.claude/constants_pattern.md to the `precursor_df`, `fragment_df`, `candidate_df` and `psm_df` classes (and their derivatives, like e.g. `unique_psm_df`. For now, only substitute "precursor_idx" and only in `precursor_df`. If "precursor_idx" is used on a dataframe with an ambiguous name (e.g. "df"), try to infer from the name that was passed to the respective method. Use the constant from the corresponding classes: @alphadia/constants/keys.py#L62-65 --- .claude/constants_pattern.md | 228 ++++++++++++++++++ alphadia/constants/keys.py | 30 +++ alphadia/libtransform/decoy.py | 11 +- alphadia/libtransform/harmonize.py | 7 +- alphadia/outputtransform/outputaccumulator.py | 6 +- alphadia/workflow/managers/fdr_manager.py | 1 + .../workflow/peptidecentric/ng/ng_mapper.py | 7 +- .../peptidecentric/optimization_handler.py | 3 +- .../workflow/peptidecentric/peptidecentric.py | 4 +- 9 files changed, 286 insertions(+), 11 deletions(-) create mode 100644 .claude/constants_pattern.md diff --git a/.claude/constants_pattern.md b/.claude/constants_pattern.md new file mode 100644 index 000000000..d0e73b830 --- /dev/null +++ b/.claude/constants_pattern.md @@ -0,0 +1,228 @@ +# String Constants Pattern - Instruction Set for Coding Agents + +## Purpose + +This document provides instructions for introducing string constants for access of +- DataFrame (specifically pd.DataFrame) columns and +- dictionary keys +using the `string constants` pattern. + +--- + +## Motivation + +**Problem**: Raw string literals for DataFrame column or dictionary keys access are error-prone and hard to maintain. + +```python +# BAD: Magic strings scattered throughout codebase: Typo risk, no IDE support, hard to refactor +some_df["column_name"] +some_dict["key_name"] +``` + +**Solution**: Centralized, immutable string constants with IDE support. + +```python +# GOOD: Centralized constants: IDE autocomplete and navigation, compile-time checking, easy refactoring +some_df[Cols.COLUMN_NAME] +some_dict[Keys.KEY_NAME] +``` + +**Benefits**: +1. **Single source of truth** - Column names defined once in a constants module +2. **Immutability** - Constants cannot be modified at runtime +3. **IDE support** - Autocomplete, "find usages", safe renaming +4. **Runtime validation** - `get_values()` method for validating against allowed values +5. **Discoverability** - Related constants grouped in semantic classes + +--- + +## Pattern Implementation + +The `ConstantsClass` metaclass is required to enforce immutability and provide utility methods. +Add it if it does not already exist in your codebase. + +```python +class ConstantsClass(type): + """A metaclass for classes that should only contain string constants.""" + + def __setattr__(self, name, value): + raise TypeError("Constants class cannot be modified") + + def get_values(cls): + """Get all user-defined string values of the class.""" + return [ + value + for key, value in cls.__dict__.items() + if not key.startswith("__") and isinstance(value, str) + ] +``` + +--- + +## How to Apply This Pattern + +### Step 1: Identify Magic Strings + +Search for raw string DataFrame access patterns: +```python +# Patterns to find and replace: +df["column_name"] # Bracket access with string literal +df.column_name # Attribute access (also problematic) +some_dict["key_name"] # Dict access for column configs +``` + +### Step 2: Group Related Constants + +Create a new class for semantically related columns. Naming convention: `Cols` or `Keys`. + +```python +class UserCols(metaclass=ConstantsClass): + """String constants for user table columns.""" + + ID = "id" + NAME = "name" + EMAIL = "email" +``` + +### Step 3: Add to Constants Module + +Place the new class in the project's constants module (e.g., `constants/keys.py`), grouped with related classes. + +### Step 4: Replace Magic Strings + +```python +# BEFORE +df["id"] +df["name"] + +if "name" in df.columns: + ... + + +# AFTER +from constants.keys import UserCols + +df[UserCols.ID] +df[UserCols.NAME] + +# make sure to also capture occurrences like this: +if UserCols.NAME in df.columns: + ... +``` + +Important note: +The constants have dataframe-type scope, so don't blindly replace all strings across dataframes. Only replace those relevant to the specific dataframe. +There can well be cases where two dataframes share the same column name, but you should only replace the string literal in the context of the specific dataframe. + +Example: +```python + +# BEFORE +user_df = user_df[ + user_df["id"].isin(some_other_df["id"]) +] + + +# AFTER: WRONG - DO NOT DO THIS +user_df = user_df[ + user_df[UserCols.ID].isin(some_other_df[UserCols.ID]) +] + +# AFTER: RIGHT - DO THIS - OPTION 1/2 +user_df = user_df[ + user_df[UserCols.ID].isin(some_other_df["id"]) +] + +# AFTER: RIGHT - DO THIS - OPTION 2/2 +user_df = user_df[ + user_df[UserCols.ID].isin(some_other_df[SomeOtherCols.ID]) +] +``` + +### Step 5: Use for Validation (optional) + +```python +if column not in UserCols.get_values(): + raise ValueError(f"Invalid column: {column}. Valid: {UserCols.get_values()}") +``` + +--- + +## Rules and Conventions + +1. **Location**: All constant classes go in a dedicated constants module +2. **Naming**: + - Class: `Cols` for DataFrame columns, `Keys` for config/dict keys + - Constants: `UPPER_SNAKE_CASE` + - Values: `lower_snake_case` strings +3. **Docstring**: Every class must have a docstring explaining its purpose +4. **Grouping**: Group related constants logically, e.g. for a specific type of DataFrame. Duplications (e.g. `Cols.ID` in case several dataframes share the same column) are allowed. +5. **Bracket notation**: Always use `df[ConstantClass.COLUMN]`, never attribute access `df.column` + +--- + +## Examples + +### Example 1: DataFrame Columns + +```python +class OrderCols(metaclass=ConstantsClass): + """String constants for order table columns.""" + + ORDER_ID = "order_id" + CUSTOMER_ID = "customer_id" + TOTAL = "total" + STATUS = "status" +``` + +**Usage**: +```python +from constants.keys import OrderCols + +# DataFrame access +orders_df[OrderCols.ORDER_ID].values +orders_df[OrderCols.TOTAL].sum() + +# Configuration +config = { + "group_by": [OrderCols.CUSTOMER_ID], + "aggregate": [OrderCols.TOTAL], +} +``` + +### Example 2: Allowed Values with Validation + +```python +class OrderStatus(metaclass=ConstantsClass): + """String constants for order status values.""" + + PENDING = "pending" + SHIPPED = "shipped" + DELIVERED = "delivered" +``` + +**Usage with validation**: +```python +from constants.keys import OrderStatus + +if status not in OrderStatus.get_values(): + raise ValueError( + f"Unknown status: {status}. " + f"Valid options are {OrderStatus.get_values()}" + ) +``` + + +--- + +## Checklist for Adding New Constants + +- [ ] Identified all magic strings to replace +- [ ] Created class with `metaclass=ConstantsClass` +- [ ] Added descriptive docstring +- [ ] Used `UPPER_SNAKE_CASE` for constant names +- [ ] Used `lower_snake_case` for string values +- [ ] Added class to constants module +- [ ] Updated imports in affected files +- [ ] Replaced all magic strings with constant references +- [ ] Used bracket notation `df[Const.COL]` not attribute access diff --git a/alphadia/constants/keys.py b/alphadia/constants/keys.py index e1a80d6a0..5cd0a891e 100644 --- a/alphadia/constants/keys.py +++ b/alphadia/constants/keys.py @@ -60,6 +60,36 @@ class SearchStepFiles(metaclass=ConstantsClass): FRAG_TRANSFER_FILE_NAME = "frag.transfer.parquet" +class PrecursorDfCols(metaclass=ConstantsClass): + """String constants for accessing precursor dataframe columns.""" + + PRECURSOR_IDX = "precursor_idx" + + +class FeaturesDfCols(metaclass=ConstantsClass): + """String constants for accessing feature dataframe columns.""" + + PRECURSOR_IDX = "precursor_idx" + + +class PsmDfCols(metaclass=ConstantsClass): + """String constants for accessing psm dataframe columns.""" + + PRECURSOR_IDX = "precursor_idx" + + +class CandidatesDfCols(metaclass=ConstantsClass): + """String constants for accessing candidates dataframe columns.""" + + PRECURSOR_IDX = "precursor_idx" + + +class FragmentDfCols(metaclass=ConstantsClass): + """String constants for accessing fragment dataframe columns.""" + + PRECURSOR_IDX = "precursor_idx" + + class InferenceStrategy(metaclass=ConstantsClass): """String constants for protein inference strategies.""" diff --git a/alphadia/libtransform/decoy.py b/alphadia/libtransform/decoy.py index 493f02e77..d68bb09cf 100644 --- a/alphadia/libtransform/decoy.py +++ b/alphadia/libtransform/decoy.py @@ -4,6 +4,7 @@ from alphabase.spectral_library.base import SpecLibBase from alphabase.spectral_library.decoy import decoy_lib_provider +from alphadia.constants.keys import PrecursorDfCols from alphadia.libtransform.base import ProcessingStep logger = logging.getLogger() @@ -48,15 +49,19 @@ def forward(self, input: SpecLibBase) -> SpecLibBase: decoy_lib._precursor_df["decoy"] = 1 # keep original precursor_idx and only create new ones for decoys - start_precursor_idx = input.precursor_df["precursor_idx"].max() + 1 - decoy_lib._precursor_df["precursor_idx"] = np.arange( + start_precursor_idx = ( + input.precursor_df[PrecursorDfCols.PRECURSOR_IDX].max() + 1 + ) + decoy_lib._precursor_df[PrecursorDfCols.PRECURSOR_IDX] = np.arange( start_precursor_idx, start_precursor_idx + len(decoy_lib.precursor_df) ) input.append(decoy_lib) input._precursor_df.sort_values("elution_group_idx", inplace=True) input._precursor_df.reset_index(drop=True, inplace=True) - input.precursor_df["precursor_idx"] = np.arange(len(input.precursor_df)) + input.precursor_df[PrecursorDfCols.PRECURSOR_IDX] = np.arange( + len(input.precursor_df) + ) input.remove_unused_fragments() return input diff --git a/alphadia/libtransform/harmonize.py b/alphadia/libtransform/harmonize.py index 42ee37ee8..8fdd0eb22 100644 --- a/alphadia/libtransform/harmonize.py +++ b/alphadia/libtransform/harmonize.py @@ -5,6 +5,7 @@ from alphabase.protein import fasta from alphabase.spectral_library.base import SpecLibBase +from alphadia.constants.keys import PrecursorDfCols from alphadia.libtransform.base import ProcessingStep from alphadia.utils import get_isotope_columns @@ -92,8 +93,10 @@ def forward(self, input: SpecLibBase) -> SpecLibBase: "Elution group indices already present, skipping initialization" ) - if "precursor_idx" not in input.precursor_df.columns: - input.precursor_df["precursor_idx"] = np.arange(len(input.precursor_df)) + if PrecursorDfCols.PRECURSOR_IDX not in input.precursor_df.columns: + input.precursor_df[PrecursorDfCols.PRECURSOR_IDX] = np.arange( + len(input.precursor_df) + ) else: logger.info("Precursor indices already present, skipping initialization") return input diff --git a/alphadia/outputtransform/outputaccumulator.py b/alphadia/outputtransform/outputaccumulator.py index d884b84fc..bf4b4ded1 100644 --- a/alphadia/outputtransform/outputaccumulator.py +++ b/alphadia/outputtransform/outputaccumulator.py @@ -34,7 +34,7 @@ from alphabase.spectral_library.flat import SpecLibFlat from tqdm import tqdm -from alphadia.constants.keys import CalibCols, SearchStepFiles +from alphadia.constants.keys import CalibCols, PrecursorDfCols, SearchStepFiles logger = logging.getLogger() @@ -136,7 +136,9 @@ def build_speclibflat_from_quant( speclib._precursor_df[col] = values frag_df = frag_df[ - frag_df["precursor_idx"].isin(speclib._precursor_df["precursor_idx"]) + frag_df["precursor_idx"].isin( + speclib._precursor_df[PrecursorDfCols.PRECURSOR_IDX] + ) ] speclib._fragment_df = frag_df[ [ diff --git a/alphadia/workflow/managers/fdr_manager.py b/alphadia/workflow/managers/fdr_manager.py index 9922681e8..adf0a300c 100644 --- a/alphadia/workflow/managers/fdr_manager.py +++ b/alphadia/workflow/managers/fdr_manager.py @@ -18,6 +18,7 @@ logger = logging.getLogger() +# TODO: unused def get_group_columns(competitive: bool, group_channels: bool) -> list[str]: """ Determine the group columns based on competitiveness and channel grouping. diff --git a/alphadia/workflow/peptidecentric/ng/ng_mapper.py b/alphadia/workflow/peptidecentric/ng/ng_mapper.py index 2151b63ca..2830fa633 100644 --- a/alphadia/workflow/peptidecentric/ng/ng_mapper.py +++ b/alphadia/workflow/peptidecentric/ng/ng_mapper.py @@ -13,6 +13,7 @@ ) from alphadia_search_rs import SpecLibFlat as SpecLibFlatNG +from alphadia.constants.keys import PrecursorDfCols from alphadia.raw_data import DiaData @@ -64,7 +65,7 @@ def speclib_to_ng( fragment_df = speclib.fragment_df return SpecLibFlatNG.from_arrays( - precursor_df["precursor_idx"].values.astype(np.uint64), + precursor_df[PrecursorDfCols.PRECURSOR_IDX].values.astype(np.uint64), precursor_df["mz_library"].values.astype(np.float32), precursor_df[precursor_mz_column].values.astype(np.float32), precursor_df["rt_library"].values.astype(np.float32), @@ -126,7 +127,9 @@ def parse_candidates( ) candidates_df = candidates_df.merge( - spectral_library.precursor_df[["precursor_idx", "elution_group_idx", "decoy"]], + spectral_library.precursor_df[ + [PrecursorDfCols.PRECURSOR_IDX, "elution_group_idx", "decoy"] + ], on="precursor_idx", how="left", ) diff --git a/alphadia/workflow/peptidecentric/optimization_handler.py b/alphadia/workflow/peptidecentric/optimization_handler.py index 8c81ad29a..7a5eb48b4 100644 --- a/alphadia/workflow/peptidecentric/optimization_handler.py +++ b/alphadia/workflow/peptidecentric/optimization_handler.py @@ -2,6 +2,7 @@ import pandas as pd from alphabase.spectral_library.flat import SpecLibFlat +from alphadia.constants.keys import PrecursorDfCols from alphadia.constants.settings import MAX_FRAGMENT_MZ_TOLERANCE from alphadia.raw_data import DiaData from alphadia.reporting.reporting import Pipeline @@ -542,7 +543,7 @@ def _filter_dfs(self, precursor_df: pd.DataFrame, fragments_df: pd.DataFrame): precursor_df_filtered = precursor_df[qval_mask & decoy_mask] precursor_idx_mask = fragments_df["precursor_idx"].isin( - precursor_df_filtered["precursor_idx"] + precursor_df_filtered[PrecursorDfCols.PRECURSOR_IDX] ) mass_error_mask = ( np.abs(fragments_df["mass_error"]) <= MAX_FRAGMENT_MZ_TOLERANCE diff --git a/alphadia/workflow/peptidecentric/peptidecentric.py b/alphadia/workflow/peptidecentric/peptidecentric.py index a85faf222..4c39474ec 100644 --- a/alphadia/workflow/peptidecentric/peptidecentric.py +++ b/alphadia/workflow/peptidecentric/peptidecentric.py @@ -8,6 +8,7 @@ from alphadia.workflow.peptidecentric.ng.ng_mapper import get_feature_names except ImportError: pass +from alphadia.constants.keys import PrecursorDfCols from alphadia.fdr.classifiers import BinaryClassifierLegacyNewBatching from alphadia.fragcomp.utils import candidate_hash from alphadia.workflow import base @@ -235,7 +236,8 @@ def extraction(self): fragments_df["precursor_idx"].values, fragments_df["rank"].values ) precursor_df["candidate_idx"] = candidate_hash( - precursor_df["precursor_idx"].values, precursor_df["rank"].values + precursor_df[PrecursorDfCols.PRECURSOR_IDX].values, + precursor_df["rank"].values, ) fragments_df = fragments_df[ From 8b24722a3b04a6c91c6f728e3cf7b75ce5b494c2 Mon Sep 17 00:00:00 2001 From: mschwoerer <82171591+mschwoer@users.noreply.github.com> Date: Wed, 28 Jan 2026 18:34:47 +0100 Subject: [PATCH 02/11] second pass --- alphadia/search/scoring/scoring.py | 13 ++++++++----- alphadia/search/selection/selection.py | 17 ++++++++++------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/alphadia/search/scoring/scoring.py b/alphadia/search/scoring/scoring.py index 095e99a0a..93b6d2fa0 100644 --- a/alphadia/search/scoring/scoring.py +++ b/alphadia/search/scoring/scoring.py @@ -6,7 +6,7 @@ import numpy as np import pandas as pd -from alphadia.constants.keys import CalibCols +from alphadia.constants.keys import CalibCols, PrecursorDfCols from alphadia.raw_data import DiaData from alphadia.search.jitclasses.fragment_container import FragmentContainer from alphadia.search.scoring.config import CandidateScoringConfig @@ -237,7 +237,9 @@ def precursors_flat_df(self, precursors_flat_df) -> None: precursors_flat_schema.validate( precursors_flat_df, warn_on_critical_values=True ) - self._precursors_flat_df = precursors_flat_df.sort_values(by="precursor_idx") + self._precursors_flat_df = precursors_flat_df.sort_values( + by=PrecursorDfCols.PRECURSOR_IDX + ) @property def fragments_flat_df(self) -> pd.DataFrame: @@ -310,7 +312,8 @@ def assemble_score_group_container( candidates_df, self.precursors_flat_df, precursor_columns, - on=["precursor_idx"], + left_on=["precursor_idx"], + right_on=[PrecursorDfCols.PRECURSOR_IDX], how="left", ) @@ -513,7 +516,7 @@ def merge_precursor_data( df, precursors_flat_df, precursor_df_columns, - on=["precursor_idx"], + on=[PrecursorDfCols.PRECURSOR_IDX], how="left", ) @@ -573,7 +576,7 @@ def collect_fragments( df, self.precursors_flat_df, precursor_df_columns, - on=["precursor_idx"], + on=[PrecursorDfCols.PRECURSOR_IDX], how="left", ) diff --git a/alphadia/search/selection/selection.py b/alphadia/search/selection/selection.py index fe01717fd..38c524d00 100644 --- a/alphadia/search/selection/selection.py +++ b/alphadia/search/selection/selection.py @@ -8,7 +8,7 @@ import pandas as pd from alphadia import utils -from alphadia.constants.keys import CalibCols +from alphadia.constants.keys import CalibCols, PrecursorDfCols from alphadia.raw_data import DiaData, DiaDataJIT from alphadia.search.jitclasses.fragment_container import FragmentContainer from alphadia.search.selection import fft @@ -595,9 +595,9 @@ def __init__( """ self.dia_data_jit: DiaDataJIT = dia_data.to_jitclass() - self.precursors_flat = precursors_flat.sort_values("precursor_idx").reset_index( - drop=True - ) + self.precursors_flat = precursors_flat.sort_values( + PrecursorDfCols.PRECURSOR_IDX + ).reset_index(drop=True) self.fragments_flat = fragments_flat self.config_jit = config.to_jitclass() @@ -668,8 +668,11 @@ def __call__(self, thread_count: int = 10, debug: bool = False) -> pd.DataFrame: candidate_df = candidate_container_to_df(candidate_container) candidate_with_precursors_df = candidate_df.merge( - self.precursors_flat[["precursor_idx", "elution_group_idx", "decoy"]], - on="precursor_idx", + self.precursors_flat[ + [PrecursorDfCols.PRECURSOR_IDX, "elution_group_idx", "decoy"] + ], + left_on="precursor_idx", + right_on=PrecursorDfCols.PRECURSOR_IDX, how="left", ) @@ -724,7 +727,7 @@ def _assemble_precursor_container( ).astype(np.uint32) return PrecursorFlatContainer( - precursors_flat["precursor_idx"].values, + precursors_flat[PrecursorDfCols.PRECURSOR_IDX].values, precursors_flat["flat_frag_start_idx"].values, precursors_flat["flat_frag_stop_idx"].values, candidate_start_index, From 6ea03a143da7135dbd56a1bd407a53683531ba5b Mon Sep 17 00:00:00 2001 From: mschwoerer <82171591+mschwoer@users.noreply.github.com> Date: Wed, 28 Jan 2026 18:39:38 +0100 Subject: [PATCH 03/11] psm_df --- alphadia/fragcomp/fragcomp.py | 6 +++--- alphadia/outputtransform/grouping.py | 18 ++++++++++-------- .../quantification/quant_builder.py | 4 ++-- alphadia/outputtransform/utils.py | 3 ++- alphadia/search/scoring/scoring.py | 3 +-- 5 files changed, 18 insertions(+), 16 deletions(-) diff --git a/alphadia/fragcomp/fragcomp.py b/alphadia/fragcomp/fragcomp.py index 1863c3736..d2049fbd7 100644 --- a/alphadia/fragcomp/fragcomp.py +++ b/alphadia/fragcomp/fragcomp.py @@ -9,7 +9,7 @@ from alphatims import utils as timsutils from pandas.errors import SettingWithCopyWarning -from alphadia.constants.keys import CalibCols +from alphadia.constants.keys import CalibCols, PsmDfCols from alphadia.fragcomp.utils import add_frag_start_stop_idx, candidate_hash from alphadia.utils import USE_NUMBA_CACHING @@ -255,7 +255,7 @@ def __call__( warnings.simplefilter(action="ignore", category=(SettingWithCopyWarning)) psm_df["_candidate_idx"] = candidate_hash( - psm_df["precursor_idx"].values, psm_df["rank"].values + psm_df[PsmDfCols.PRECURSOR_IDX].values, psm_df["rank"].values ) frag_df["_candidate_idx"] = candidate_hash( frag_df["precursor_idx"].values, frag_df["rank"].values @@ -266,7 +266,7 @@ def __call__( # important to sort by window_idx and proba psm_df.sort_values( - by=["window_idx", "proba", "precursor_idx"], inplace=True + by=["window_idx", "proba", PsmDfCols.PRECURSOR_IDX], inplace=True ) # last sort to break ties valid = np.ones(len(psm_df)).astype(bool) diff --git a/alphadia/outputtransform/grouping.py b/alphadia/outputtransform/grouping.py index 08493cef3..9eb75fb83 100644 --- a/alphadia/outputtransform/grouping.py +++ b/alphadia/outputtransform/grouping.py @@ -4,6 +4,8 @@ import pandas as pd from numpy.typing import NDArray +from alphadia.constants.keys import PsmDfCols + def _group_and_parsimony( precursor_idx: NDArray[np.int64], @@ -128,12 +130,12 @@ def perform_grouping( raise ValueError("Selected column must be 'genes' or 'proteins'") # create non-duplicated view of precursor table - unique_mask = ~psm_df.duplicated(subset=["precursor_idx"], keep="first") + unique_mask = ~psm_df.duplicated(subset=[PsmDfCols.PRECURSOR_IDX], keep="first") # make sure column is string and subset to relevant columns psm_df[genes_or_proteins] = psm_df[genes_or_proteins].astype(str) unique_psm_df = psm_df.loc[ - unique_mask, ["precursor_idx", genes_or_proteins, decoy_column] + unique_mask, [PsmDfCols.PRECURSOR_IDX, genes_or_proteins, decoy_column] ] # greedy set cover on all proteins if there is only one decoy class @@ -141,12 +143,12 @@ def perform_grouping( if len(unique_decoys) == 1: unique_psm_df[decoy_column] = -1 unique_psm_df["pg_master"], unique_psm_df["pg"] = _group_and_parsimony( - unique_psm_df["precursor_idx"].values, + unique_psm_df[PsmDfCols.PRECURSOR_IDX].values, unique_psm_df[genes_or_proteins].values, return_parsimony_groups, ) unique_psm_df = unique_psm_df[ - ["precursor_idx", "pg_master", "pg", genes_or_proteins] + [PsmDfCols.PRECURSOR_IDX, "pg_master", "pg", genes_or_proteins] ] else: # handle case with multiple decoy classes @@ -156,7 +158,7 @@ def perform_grouping( # greedy set cover on targets target_df = unique_psm_df[target_mask].copy() target_df["pg_master"], target_df["pg"] = _group_and_parsimony( - target_df["precursor_idx"].values, + target_df[PsmDfCols.PRECURSOR_IDX].values, target_df[genes_or_proteins].values, return_parsimony_groups, ) @@ -164,13 +166,13 @@ def perform_grouping( # greedy set cover on decoys decoy_df = unique_psm_df[decoy_mask].copy() decoy_df["pg_master"], decoy_df["pg"] = _group_and_parsimony( - decoy_df["precursor_idx"].values, + decoy_df[PsmDfCols.PRECURSOR_IDX].values, decoy_df[genes_or_proteins].values, return_parsimony_groups, ) unique_psm_df = pd.concat([target_df, decoy_df])[ - ["precursor_idx", "pg_master", "pg", genes_or_proteins] + [PsmDfCols.PRECURSOR_IDX, "pg_master", "pg", genes_or_proteins] ] # heuristic grouping: from each initial precursor's protein ID set, filter out proteins that @@ -191,4 +193,4 @@ def filter_allowed_pg(pg): unique_psm_df.drop(columns=[genes_or_proteins], inplace=True) - return psm_df.merge(unique_psm_df, on="precursor_idx", how="left") + return psm_df.merge(unique_psm_df, on=PsmDfCols.PRECURSOR_IDX, how="left") diff --git a/alphadia/outputtransform/quantification/quant_builder.py b/alphadia/outputtransform/quantification/quant_builder.py index 61c439596..67e5ef4ee 100644 --- a/alphadia/outputtransform/quantification/quant_builder.py +++ b/alphadia/outputtransform/quantification/quant_builder.py @@ -10,7 +10,7 @@ import pandas as pd from quantselect.output import run_quantselect -from alphadia.constants.keys import NormalizationMethods +from alphadia.constants.keys import NormalizationMethods, PsmDfCols from alphadia.utils import USE_NUMBA_CACHING from alphadia.workflow.config import Config @@ -99,7 +99,7 @@ def prepare_df( pd.DataFrame Filtered fragment dataframe with ion hash """ - df = df[df["precursor_idx"].isin(psm_df["precursor_idx"])].copy() + df = df[df["precursor_idx"].isin(psm_df[PsmDfCols.PRECURSOR_IDX])].copy() df["ion"] = _ion_hash( df["precursor_idx"].values, df["number"].values, diff --git a/alphadia/outputtransform/utils.py b/alphadia/outputtransform/utils.py index 221730bb5..c184163cf 100644 --- a/alphadia/outputtransform/utils.py +++ b/alphadia/outputtransform/utils.py @@ -8,6 +8,7 @@ from alphadia.constants.keys import ( INTERNAL_TO_OUTPUT_MAPPING, InferenceStrategy, + PsmDfCols, ) from alphadia.outputtransform import grouping @@ -163,7 +164,7 @@ def log_protein_fdr_summary(psm_df: pd.DataFrame) -> None: Precursor table with protein grouping and FDR filtering applied """ pg_count = psm_df[psm_df["decoy"] == 0]["pg"].nunique() - precursor_count = psm_df[psm_df["decoy"] == 0]["precursor_idx"].nunique() + precursor_count = psm_df[psm_df["decoy"] == 0][PsmDfCols.PRECURSOR_IDX].nunique() logger.info( "================ Protein FDR =================", diff --git a/alphadia/search/scoring/scoring.py b/alphadia/search/scoring/scoring.py index 93b6d2fa0..e6fdee221 100644 --- a/alphadia/search/scoring/scoring.py +++ b/alphadia/search/scoring/scoring.py @@ -312,8 +312,7 @@ def assemble_score_group_container( candidates_df, self.precursors_flat_df, precursor_columns, - left_on=["precursor_idx"], - right_on=[PrecursorDfCols.PRECURSOR_IDX], + on=[PrecursorDfCols.PRECURSOR_IDX], how="left", ) From 25a83464f82b2739f456dbdef8861bc0545c8224 Mon Sep 17 00:00:00 2001 From: mschwoerer <82171591+mschwoer@users.noreply.github.com> Date: Wed, 28 Jan 2026 18:45:47 +0100 Subject: [PATCH 04/11] candidates_df --- alphadia/search/scoring/scoring.py | 8 ++++++-- alphadia/workflow/peptidecentric/extraction_handler.py | 5 +++-- alphadia/workflow/peptidecentric/ng/ng_mapper.py | 4 ++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/alphadia/search/scoring/scoring.py b/alphadia/search/scoring/scoring.py index e6fdee221..1a62debbb 100644 --- a/alphadia/search/scoring/scoring.py +++ b/alphadia/search/scoring/scoring.py @@ -6,7 +6,11 @@ import numpy as np import pandas as pd -from alphadia.constants.keys import CalibCols, PrecursorDfCols +from alphadia.constants.keys import ( + CalibCols, + CandidatesDfCols, + PrecursorDfCols, +) from alphadia.raw_data import DiaData from alphadia.search.jitclasses.fragment_container import FragmentContainer from alphadia.search.scoring.config import CandidateScoringConfig @@ -336,7 +340,7 @@ def assemble_score_group_container( score_group_container.build_from_df( candidates_df["elution_group_idx"].values, candidates_df["score_group_idx"].values, - candidates_df["precursor_idx"].values, + candidates_df[CandidatesDfCols.PRECURSOR_IDX].values, candidates_df["channel"].values, candidates_df["rank"].values, candidates_df["flat_frag_start_idx"].values, diff --git a/alphadia/workflow/peptidecentric/extraction_handler.py b/alphadia/workflow/peptidecentric/extraction_handler.py index edcdd31d6..70e4dada0 100644 --- a/alphadia/workflow/peptidecentric/extraction_handler.py +++ b/alphadia/workflow/peptidecentric/extraction_handler.py @@ -11,7 +11,7 @@ SelectionParameters, ) -from alphadia.constants.keys import CalibCols +from alphadia.constants.keys import CalibCols, CandidatesDfCols from alphadia.fragcomp.utils import candidate_hash from alphadia.raw_data import DiaData from alphadia.raw_data.alpharaw_wrapper import DEFAULT_VALUE_NO_MOBILITY @@ -681,7 +681,8 @@ def perform_fdr_and_filter_candidates( features_df["precursor_idx"].values, features_df["rank"].values ) candidates_df["_candidate_idx"] = candidate_hash( - candidates_df["precursor_idx"].values, candidates_df["rank"].values + candidates_df[CandidatesDfCols.PRECURSOR_IDX].values, + candidates_df["rank"].values, ) # apply FDR to PSMs diff --git a/alphadia/workflow/peptidecentric/ng/ng_mapper.py b/alphadia/workflow/peptidecentric/ng/ng_mapper.py index 2830fa633..d9ea46c9e 100644 --- a/alphadia/workflow/peptidecentric/ng/ng_mapper.py +++ b/alphadia/workflow/peptidecentric/ng/ng_mapper.py @@ -13,7 +13,7 @@ ) from alphadia_search_rs import SpecLibFlat as SpecLibFlatNG -from alphadia.constants.keys import PrecursorDfCols +from alphadia.constants.keys import CandidatesDfCols, PrecursorDfCols from alphadia.raw_data import DiaData @@ -153,7 +153,7 @@ def candidates_to_ng( cycle_len = dia_data.cycle.shape[1] candidates = CandidateCollection.from_arrays( - candidates_df["precursor_idx"].values.astype(np.uint64), + candidates_df[CandidatesDfCols.PRECURSOR_IDX].values.astype(np.uint64), candidates_df["rank"].values.astype(np.uint64), candidates_df["score"].values.astype(np.float32), candidates_df["scan_center"].values.astype(np.uint64), From f0b64a5f90e2c2367bacd6c2990698380750491c Mon Sep 17 00:00:00 2001 From: mschwoerer <82171591+mschwoer@users.noreply.github.com> Date: Wed, 28 Jan 2026 18:47:32 +0100 Subject: [PATCH 05/11] psm_df --- alphadia/fdr/_fdrx/stats.py | 6 ++++-- alphadia/fdr/fdr.py | 9 +++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/alphadia/fdr/_fdrx/stats.py b/alphadia/fdr/_fdrx/stats.py index 6a9875975..8cc314297 100644 --- a/alphadia/fdr/_fdrx/stats.py +++ b/alphadia/fdr/_fdrx/stats.py @@ -2,6 +2,8 @@ import numpy as np import pandas as pd +from alphadia.constants.keys import PsmDfCols + def get_pep( psm_df: pd.DataFrame, @@ -96,7 +98,7 @@ def add_q_values( """ EPSILON = 1e-6 df = df.sort_values( - [decoy_proba_column, decoy_column, "precursor_idx"], ascending=True + [decoy_proba_column, decoy_column, PsmDfCols.PRECURSOR_IDX], ascending=True ) # last sort to break ties # translate the decoy probabilities to target probabilities @@ -160,7 +162,7 @@ def keep_best( group_columns = ["channel", "mod_seq_charge_hash"] df = df.reset_index(drop=True) df = df.sort_values( - [score_column, *group_columns, "precursor_idx"], ascending=True + [score_column, *group_columns, PsmDfCols.PRECURSOR_IDX], ascending=True ) # last sort to break ties df = df.groupby(group_columns).head(1) df = df.sort_index().reset_index(drop=True) diff --git a/alphadia/fdr/fdr.py b/alphadia/fdr/fdr.py index 769a4209d..304248cf3 100644 --- a/alphadia/fdr/fdr.py +++ b/alphadia/fdr/fdr.py @@ -8,6 +8,7 @@ import numpy as np import pandas as pd +from alphadia.constants.keys import PsmDfCols from alphadia.fdr.plotting import plot_fdr from alphadia.fdr.utils import manage_torch_threads, train_test_split_ from alphadia.fragcomp.fragcomp import FragmentCompetition @@ -133,13 +134,13 @@ def perform_fdr( # noqa: C901, PLR0913 # too complex, too many arguments else ["elution_group_idx"] ) else: - group_columns = ["precursor_idx"] + group_columns = [PsmDfCols.PRECURSOR_IDX] predicted_proba = classifier.predict_proba(X)[:, 1] psm_df["proba"] = predicted_proba psm_df.sort_values( - ["proba", "precursor_idx"], ascending=True, inplace=True + ["proba", PsmDfCols.PRECURSOR_IDX], ascending=True, inplace=True ) # last sort to break ties psm_df = get_q_values(psm_df, "proba", "_decoy") @@ -206,7 +207,7 @@ def keep_best( """ if group_columns is None: - group_columns = ["channel", "precursor_idx"] + group_columns = ["channel", PsmDfCols.PRECURSOR_IDX] df = df.reset_index(drop=True) df = df.sort_values( [score_column, *group_columns], ascending=True @@ -272,7 +273,7 @@ def get_q_values( """ if extra_sort_columns is None: - extra_sort_columns = ["precursor_idx"] + extra_sort_columns = [PsmDfCols.PRECURSOR_IDX] df = df.sort_values( [score_column, decoy_column, *extra_sort_columns], ascending=True From a103f2504344b05e0dde2347687fc3a408b7b1aa Mon Sep 17 00:00:00 2001 From: mschwoerer <82171591+mschwoer@users.noreply.github.com> Date: Wed, 28 Jan 2026 19:04:35 +0100 Subject: [PATCH 06/11] candidates_df --- alphadia/workflow/peptidecentric/ng/ng_mapper.py | 3 ++- .../transfer_library_requantification_handler.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/alphadia/workflow/peptidecentric/ng/ng_mapper.py b/alphadia/workflow/peptidecentric/ng/ng_mapper.py index d9ea46c9e..00b98978b 100644 --- a/alphadia/workflow/peptidecentric/ng/ng_mapper.py +++ b/alphadia/workflow/peptidecentric/ng/ng_mapper.py @@ -130,7 +130,8 @@ def parse_candidates( spectral_library.precursor_df[ [PrecursorDfCols.PRECURSOR_IDX, "elution_group_idx", "decoy"] ], - on="precursor_idx", + left_on="precursor_idx", + right_on=PrecursorDfCols.PRECURSOR_IDX, how="left", ) diff --git a/alphadia/workflow/peptidecentric/transfer_library_requantification_handler.py b/alphadia/workflow/peptidecentric/transfer_library_requantification_handler.py index d30713c75..1d83d715c 100644 --- a/alphadia/workflow/peptidecentric/transfer_library_requantification_handler.py +++ b/alphadia/workflow/peptidecentric/transfer_library_requantification_handler.py @@ -5,7 +5,7 @@ from alphabase.spectral_library.base import SpecLibBase from alphabase.spectral_library.flat import SpecLibFlat -from alphadia.constants.keys import CalibCols +from alphadia.constants.keys import CalibCols, CandidatesDfCols from alphadia.fragcomp.utils import add_frag_start_stop_idx, candidate_hash from alphadia.raw_data import DiaData from alphadia.reporting.reporting import Pipeline @@ -127,7 +127,8 @@ def requantify( # establish mapping scored_candidates["_candidate_idx"] = candidate_hash( - scored_candidates["precursor_idx"].values, scored_candidates["rank"].values + scored_candidates[CandidatesDfCols.PRECURSOR_IDX].values, + scored_candidates["rank"].values, ) frag_df["_candidate_idx"] = candidate_hash( frag_df["precursor_idx"].values, frag_df["rank"].values From d22407cbe5cca42ebf1fdc0821e50802dad9eefa Mon Sep 17 00:00:00 2001 From: mschwoerer <82171591+mschwoer@users.noreply.github.com> Date: Wed, 28 Jan 2026 19:10:51 +0100 Subject: [PATCH 07/11] fragments_df --- alphadia/fragcomp/fragcomp.py | 4 ++-- alphadia/outputtransform/outputaccumulator.py | 9 +++++++-- alphadia/workflow/peptidecentric/optimization_handler.py | 6 +++--- alphadia/workflow/peptidecentric/peptidecentric.py | 5 +++-- .../transfer_library_requantification_handler.py | 4 ++-- 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/alphadia/fragcomp/fragcomp.py b/alphadia/fragcomp/fragcomp.py index d2049fbd7..6ff9172ba 100644 --- a/alphadia/fragcomp/fragcomp.py +++ b/alphadia/fragcomp/fragcomp.py @@ -9,7 +9,7 @@ from alphatims import utils as timsutils from pandas.errors import SettingWithCopyWarning -from alphadia.constants.keys import CalibCols, PsmDfCols +from alphadia.constants.keys import CalibCols, FragmentDfCols, PsmDfCols from alphadia.fragcomp.utils import add_frag_start_stop_idx, candidate_hash from alphadia.utils import USE_NUMBA_CACHING @@ -258,7 +258,7 @@ def __call__( psm_df[PsmDfCols.PRECURSOR_IDX].values, psm_df["rank"].values ) frag_df["_candidate_idx"] = candidate_hash( - frag_df["precursor_idx"].values, frag_df["rank"].values + frag_df[FragmentDfCols.PRECURSOR_IDX].values, frag_df["rank"].values ) psm_df = add_frag_start_stop_idx(psm_df, frag_df) diff --git a/alphadia/outputtransform/outputaccumulator.py b/alphadia/outputtransform/outputaccumulator.py index bf4b4ded1..7992aca34 100644 --- a/alphadia/outputtransform/outputaccumulator.py +++ b/alphadia/outputtransform/outputaccumulator.py @@ -34,7 +34,12 @@ from alphabase.spectral_library.flat import SpecLibFlat from tqdm import tqdm -from alphadia.constants.keys import CalibCols, PrecursorDfCols, SearchStepFiles +from alphadia.constants.keys import ( + CalibCols, + FragmentDfCols, + PrecursorDfCols, + SearchStepFiles, +) logger = logging.getLogger() @@ -136,7 +141,7 @@ def build_speclibflat_from_quant( speclib._precursor_df[col] = values frag_df = frag_df[ - frag_df["precursor_idx"].isin( + frag_df[FragmentDfCols.PRECURSOR_IDX].isin( speclib._precursor_df[PrecursorDfCols.PRECURSOR_IDX] ) ] diff --git a/alphadia/workflow/peptidecentric/optimization_handler.py b/alphadia/workflow/peptidecentric/optimization_handler.py index 7a5eb48b4..1b9fb8cf3 100644 --- a/alphadia/workflow/peptidecentric/optimization_handler.py +++ b/alphadia/workflow/peptidecentric/optimization_handler.py @@ -2,7 +2,7 @@ import pandas as pd from alphabase.spectral_library.flat import SpecLibFlat -from alphadia.constants.keys import PrecursorDfCols +from alphadia.constants.keys import FragmentDfCols, PrecursorDfCols from alphadia.constants.settings import MAX_FRAGMENT_MZ_TOLERANCE from alphadia.raw_data import DiaData from alphadia.reporting.reporting import Pipeline @@ -542,7 +542,7 @@ def _filter_dfs(self, precursor_df: pd.DataFrame, fragments_df: pd.DataFrame): decoy_mask = precursor_df["decoy"] == 0 precursor_df_filtered = precursor_df[qval_mask & decoy_mask] - precursor_idx_mask = fragments_df["precursor_idx"].isin( + precursor_idx_mask = fragments_df[FragmentDfCols.PRECURSOR_IDX].isin( precursor_df_filtered[PrecursorDfCols.PRECURSOR_IDX] ) mass_error_mask = ( @@ -552,7 +552,7 @@ def _filter_dfs(self, precursor_df: pd.DataFrame, fragments_df: pd.DataFrame): fragments_df_filtered = fragments_df[ precursor_idx_mask & mass_error_mask ].sort_values( - by=["correlation", "precursor_idx"], ascending=False + by=["correlation", FragmentDfCols.PRECURSOR_IDX], ascending=False ) # last sort to break ties # Determine the number of fragments to keep diff --git a/alphadia/workflow/peptidecentric/peptidecentric.py b/alphadia/workflow/peptidecentric/peptidecentric.py index 4c39474ec..e340d36de 100644 --- a/alphadia/workflow/peptidecentric/peptidecentric.py +++ b/alphadia/workflow/peptidecentric/peptidecentric.py @@ -8,7 +8,7 @@ from alphadia.workflow.peptidecentric.ng.ng_mapper import get_feature_names except ImportError: pass -from alphadia.constants.keys import PrecursorDfCols +from alphadia.constants.keys import FragmentDfCols, PrecursorDfCols from alphadia.fdr.classifiers import BinaryClassifierLegacyNewBatching from alphadia.fragcomp.utils import candidate_hash from alphadia.workflow import base @@ -233,7 +233,8 @@ def extraction(self): # to be optimized later fragments_df["candidate_idx"] = candidate_hash( - fragments_df["precursor_idx"].values, fragments_df["rank"].values + fragments_df[FragmentDfCols.PRECURSOR_IDX].values, + fragments_df["rank"].values, ) precursor_df["candidate_idx"] = candidate_hash( precursor_df[PrecursorDfCols.PRECURSOR_IDX].values, diff --git a/alphadia/workflow/peptidecentric/transfer_library_requantification_handler.py b/alphadia/workflow/peptidecentric/transfer_library_requantification_handler.py index 1d83d715c..76a156bf9 100644 --- a/alphadia/workflow/peptidecentric/transfer_library_requantification_handler.py +++ b/alphadia/workflow/peptidecentric/transfer_library_requantification_handler.py @@ -5,7 +5,7 @@ from alphabase.spectral_library.base import SpecLibBase from alphabase.spectral_library.flat import SpecLibFlat -from alphadia.constants.keys import CalibCols, CandidatesDfCols +from alphadia.constants.keys import CalibCols, CandidatesDfCols, FragmentDfCols from alphadia.fragcomp.utils import add_frag_start_stop_idx, candidate_hash from alphadia.raw_data import DiaData from alphadia.reporting.reporting import Pipeline @@ -131,7 +131,7 @@ def requantify( scored_candidates["rank"].values, ) frag_df["_candidate_idx"] = candidate_hash( - frag_df["precursor_idx"].values, frag_df["rank"].values + frag_df[FragmentDfCols.PRECURSOR_IDX].values, frag_df["rank"].values ) scored_candidates = add_frag_start_stop_idx(scored_candidates, frag_df) From 4d8cdc5fb79d01f0f95a2308e6686f2a0e2f5ee1 Mon Sep 17 00:00:00 2001 From: mschwoerer <82171591+mschwoer@users.noreply.github.com> Date: Wed, 28 Jan 2026 19:11:21 +0100 Subject: [PATCH 08/11] schemas --- alphadia/validation/schemas.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/alphadia/validation/schemas.py b/alphadia/validation/schemas.py index 9ace7253b..a06bd5e7e 100644 --- a/alphadia/validation/schemas.py +++ b/alphadia/validation/schemas.py @@ -2,7 +2,7 @@ import numpy as np -from alphadia.constants.keys import CalibCols +from alphadia.constants.keys import CalibCols, CandidatesDfCols, PrecursorDfCols from alphadia.validation.base import Optional, Required, Schema logger = logging.getLogger() @@ -13,7 +13,7 @@ [ Required("elution_group_idx", np.uint32), Optional("score_group_idx", np.uint32), - Required("precursor_idx", np.uint32), + Required(PrecursorDfCols.PRECURSOR_IDX, np.uint32), Required("channel", np.uint32), Required("decoy", np.uint8), Required("flat_frag_start_idx", np.uint32), @@ -52,7 +52,7 @@ "candidates_df", [ Required("elution_group_idx", np.uint32), - Required("precursor_idx", np.uint32), + Required(CandidatesDfCols.PRECURSOR_IDX, np.uint32), Required("rank", np.uint8), Required("scan_start", np.int64), Required("scan_stop", np.int64), From 8ca6ed3ca1ab4171694a75134c69a75a877bad21 Mon Sep 17 00:00:00 2001 From: mschwoerer <82171591+mschwoer@users.noreply.github.com> Date: Wed, 28 Jan 2026 19:21:08 +0100 Subject: [PATCH 09/11] some more --- alphadia/constants/keys.py | 2 +- alphadia/outputtransform/outputaccumulator.py | 11 +++++++--- alphadia/outputtransform/protein_fdr.py | 3 ++- .../quantification/fragment_accumulator.py | 20 +++++++++++++------ .../quantification/quant_builder.py | 20 +++++++++++++------ alphadia/search/scoring/scoring.py | 2 +- alphadia/workflow/managers/fdr_manager.py | 2 +- .../peptidecentric/optimization_handler.py | 2 +- 8 files changed, 42 insertions(+), 20 deletions(-) diff --git a/alphadia/constants/keys.py b/alphadia/constants/keys.py index 5cd0a891e..1c7f36030 100644 --- a/alphadia/constants/keys.py +++ b/alphadia/constants/keys.py @@ -217,7 +217,7 @@ class StatCalibrationCols(metaclass=ConstantsClass): INTERNAL_TO_OUTPUT_MAPPING = { "peptide_lfq_intensity": PeptideOutputCols.INTENSITY, "precursor_lfq_intensity": PrecursorOutputCols.INTENSITY, - "precursor_idx": PrecursorOutputCols.IDX, + "precursor_idx": PrecursorOutputCols.IDX, # precursor_idx: UNCLEAR "elution_group_idx": PrecursorOutputCols.ELUTION_GROUP_IDX, "rank": PrecursorOutputCols.RANK, "naa": PrecursorOutputCols.NAA, diff --git a/alphadia/outputtransform/outputaccumulator.py b/alphadia/outputtransform/outputaccumulator.py index 7992aca34..9180b8ea1 100644 --- a/alphadia/outputtransform/outputaccumulator.py +++ b/alphadia/outputtransform/outputaccumulator.py @@ -38,6 +38,7 @@ CalibCols, FragmentDfCols, PrecursorDfCols, + PsmDfCols, SearchStepFiles, ) @@ -72,7 +73,7 @@ def build_speclibflat_from_quant( if mandatory_precursor_columns is None: mandatory_precursor_columns = [ - "precursor_idx", + PsmDfCols.PRECURSOR_IDX, "sequence", "flat_frag_start_idx", "flat_frag_stop_idx", @@ -149,7 +150,7 @@ def build_speclibflat_from_quant( [ "mz", "intensity", - "precursor_idx", + FragmentDfCols.PRECURSOR_IDX, "frag_idx", "correlation", "number", @@ -339,7 +340,11 @@ def update(self, speclibase: base.SpecLibBase): # Sort by modseqhash and proba in ascending order self.consensus_speclibase._precursor_df = ( self.consensus_speclibase._precursor_df.sort_values( - ["mod_seq_hash", "proba", "precursor_idx"], # last sort to break ties + [ + "mod_seq_hash", + "proba", + PrecursorDfCols.PRECURSOR_IDX, + ], # last sort to break ties ascending=[True, True, True], ) ) diff --git a/alphadia/outputtransform/protein_fdr.py b/alphadia/outputtransform/protein_fdr.py index f8ca7b9b1..5d6f9f6e6 100644 --- a/alphadia/outputtransform/protein_fdr.py +++ b/alphadia/outputtransform/protein_fdr.py @@ -4,6 +4,7 @@ from sklearn.neural_network import MLPClassifier from sklearn.preprocessing import StandardScaler +from alphadia.constants.keys import PsmDfCols from alphadia.exceptions import TooFewProteinsError from alphadia.fdr import fdr from alphadia.fdr.plotting import plot_fdr @@ -24,7 +25,7 @@ def perform_protein_fdr(psm_df: pd.DataFrame, figure_path: str) -> pd.DataFrame: "proteins": group["proteins"].iloc[0], "decoy": group["decoy"].iloc[0], "count": len(group), - "n_precursor": len(group["precursor_idx"].unique()), + "n_precursor": len(group[PsmDfCols.PRECURSOR_IDX].unique()), "n_peptides": len(group["sequence"].unique()), "n_runs": len(group["run"].unique()), "mean_score": group["proba"].mean(), diff --git a/alphadia/outputtransform/quantification/fragment_accumulator.py b/alphadia/outputtransform/quantification/fragment_accumulator.py index 8483efb47..4df1c9187 100644 --- a/alphadia/outputtransform/quantification/fragment_accumulator.py +++ b/alphadia/outputtransform/quantification/fragment_accumulator.py @@ -5,6 +5,7 @@ import numpy as np import pandas as pd +from alphadia.constants.keys import FragmentDfCols, PrecursorDfCols, PsmDfCols from alphadia.outputtransform.quantification.quant_builder import prepare_df logger = logging.getLogger() @@ -76,7 +77,7 @@ def accumulate( df_list = [] for col in self.columns: - feat_df = df[["precursor_idx", "ion", col]].copy() + feat_df = df[[FragmentDfCols.PRECURSOR_IDX, "ion", col]].copy() feat_df.rename(columns={col: raw_name}, inplace=True) df_list.append(feat_df) @@ -85,14 +86,14 @@ def accumulate( for idx, col in enumerate(self.columns): df_list[idx] = df_list[idx].merge( - df[["ion", col, "precursor_idx"]], - on=["ion", "precursor_idx"], + df[["ion", col, FragmentDfCols.PRECURSOR_IDX]], + on=["ion", FragmentDfCols.PRECURSOR_IDX], how="outer", ) df_list[idx].rename(columns={col: raw_name}, inplace=True) precursor_metadata_df = self.psm_df.groupby( - "precursor_idx", as_index=False + PsmDfCols.PRECURSOR_IDX, as_index=False ).agg({"pg": "first", "mod_seq_hash": "first", "mod_seq_charge_hash": "first"}) return { @@ -150,6 +151,13 @@ def _add_precursor_idx( Fragment data with precursor metadata columns added """ df.fillna(0, inplace=True) - df["precursor_idx"] = df["precursor_idx"].astype(np.uint32) - df = df.merge(precursor_metadata_df, on="precursor_idx", how="left") + df[FragmentDfCols.PRECURSOR_IDX] = df[FragmentDfCols.PRECURSOR_IDX].astype( + np.uint32 + ) + df = df.merge( + precursor_metadata_df, + left_on=FragmentDfCols.PRECURSOR_IDX, + right_on=PrecursorDfCols.PRECURSOR_IDX, + how="left", + ) return df diff --git a/alphadia/outputtransform/quantification/quant_builder.py b/alphadia/outputtransform/quantification/quant_builder.py index 67e5ef4ee..a3909f66e 100644 --- a/alphadia/outputtransform/quantification/quant_builder.py +++ b/alphadia/outputtransform/quantification/quant_builder.py @@ -10,7 +10,7 @@ import pandas as pd from quantselect.output import run_quantselect -from alphadia.constants.keys import NormalizationMethods, PsmDfCols +from alphadia.constants.keys import FragmentDfCols, NormalizationMethods, PsmDfCols from alphadia.utils import USE_NUMBA_CACHING from alphadia.workflow.config import Config @@ -99,15 +99,17 @@ def prepare_df( pd.DataFrame Filtered fragment dataframe with ion hash """ - df = df[df["precursor_idx"].isin(psm_df[PsmDfCols.PRECURSOR_IDX])].copy() + df = df[ + df[FragmentDfCols.PRECURSOR_IDX].isin(psm_df[PsmDfCols.PRECURSOR_IDX]) + ].copy() df["ion"] = _ion_hash( - df["precursor_idx"].values, + df[FragmentDfCols.PRECURSOR_IDX].values, df["number"].values, df["type"].values, df["charge"].values, df["loss_type"].values, ) - return df[["precursor_idx", "ion"] + columns] + return df[[FragmentDfCols.PRECURSOR_IDX, "ion"] + columns] class QuantBuilder: @@ -169,7 +171,13 @@ def filter_frag_df( c for c in intensity_df.columns if c - not in ["precursor_idx", "ion", "pg", "mod_seq_hash", "mod_seq_charge_hash"] + not in [ + FragmentDfCols.PRECURSOR_IDX, + "ion", + "pg", + "mod_seq_hash", + "mod_seq_charge_hash", + ] ] quality_df["total"] = np.mean(quality_df[run_columns].values, axis=1) @@ -209,7 +217,7 @@ def direct_lfq( # drop all other columns as they will be interpreted as samples columns_to_drop = list( - {"precursor_idx", "pg", "mod_seq_hash", "mod_seq_charge_hash"} + {FragmentDfCols.PRECURSOR_IDX, "pg", "mod_seq_hash", "mod_seq_charge_hash"} - {lfq_config.quant_level} ) intensity_df = intensity_df.drop(columns=columns_to_drop) diff --git a/alphadia/search/scoring/scoring.py b/alphadia/search/scoring/scoring.py index 1a62debbb..da1a1e8b8 100644 --- a/alphadia/search/scoring/scoring.py +++ b/alphadia/search/scoring/scoring.py @@ -442,7 +442,7 @@ def collect_candidates( precursor_idx, rank, features = psm_proto_df.to_precursor_df() candidates_psm_df = pd.DataFrame(features, columns=feature_columns) - candidates_psm_df["precursor_idx"] = precursor_idx + candidates_psm_df["precursor_idx"] = precursor_idx # precursor_idx: UNCLEAR candidates_psm_df["rank"] = rank candidates_psm_df = self.merge_candidate_data( diff --git a/alphadia/workflow/managers/fdr_manager.py b/alphadia/workflow/managers/fdr_manager.py index adf0a300c..d9fe82834 100644 --- a/alphadia/workflow/managers/fdr_manager.py +++ b/alphadia/workflow/managers/fdr_manager.py @@ -43,7 +43,7 @@ def get_group_columns(competitive: bool, group_channels: bool) -> list[str]: else ["elution_group_idx"] ) else: - group_columns = ["precursor_idx"] + group_columns = ["precursor_idx"] # precursor_idx: UNCLEAR return group_columns diff --git a/alphadia/workflow/peptidecentric/optimization_handler.py b/alphadia/workflow/peptidecentric/optimization_handler.py index 1b9fb8cf3..5a75a1123 100644 --- a/alphadia/workflow/peptidecentric/optimization_handler.py +++ b/alphadia/workflow/peptidecentric/optimization_handler.py @@ -420,7 +420,7 @@ def _process_batch(self): precursor_quantified_w_features_df = precursor_quantified_df.merge( precursor_w_features_df, how="left", - on=["precursor_idx", "rank"], + on=[PrecursorDfCols.PRECURSOR_IDX, "rank"], suffixes=("", "__y"), validate="one_to_one", ) From 32e3bbe87ff4d4279396142b43619bf186180248 Mon Sep 17 00:00:00 2001 From: mschwoerer <82171591+mschwoer@users.noreply.github.com> Date: Wed, 28 Jan 2026 19:31:15 +0100 Subject: [PATCH 10/11] some more II --- alphadia/search/scoring/scoring.py | 5 +++-- alphadia/search/scoring/utils.py | 21 +++++++++++-------- alphadia/search/selection/config_df.py | 3 ++- alphadia/search/selection/selection.py | 4 ++-- alphadia/validation/schemas.py | 4 ++-- .../peptidecentric/extraction_handler.py | 10 ++++++--- .../workflow/peptidecentric/ng/ng_mapper.py | 11 +++++----- 7 files changed, 34 insertions(+), 24 deletions(-) diff --git a/alphadia/search/scoring/scoring.py b/alphadia/search/scoring/scoring.py index da1a1e8b8..60b01c350 100644 --- a/alphadia/search/scoring/scoring.py +++ b/alphadia/search/scoring/scoring.py @@ -10,6 +10,7 @@ CalibCols, CandidatesDfCols, PrecursorDfCols, + PsmDfCols, ) from alphadia.raw_data import DiaData from alphadia.search.jitclasses.fragment_container import FragmentContainer @@ -489,7 +490,7 @@ def merge_candidate_data( df, candidates_df, candidate_columns, - on=["precursor_idx", "rank"], + on=[CandidatesDfCols.PRECURSOR_IDX, "rank"], how="left", ) @@ -546,7 +547,7 @@ def collect_fragments( """ colnames = [ - "precursor_idx", + PsmDfCols.PRECURSOR_IDX, "rank", CalibCols.MZ_LIBRARY, "mz", diff --git a/alphadia/search/scoring/utils.py b/alphadia/search/scoring/utils.py index f7fd1432b..4737afcad 100644 --- a/alphadia/search/scoring/utils.py +++ b/alphadia/search/scoring/utils.py @@ -5,6 +5,7 @@ import numba as nb import numpy as np import pandas as pd +from constants.keys import CandidatesDfCols, PrecursorDfCols from numba.extending import overload_method from alphadia.search.jitclasses.fragment_container import FragmentContainer @@ -93,7 +94,7 @@ def candidate_features_to_candidates( required_columns = [ "elution_group_idx", - "precursor_idx", + "precursor_idx", # precursor_idx: UNCLEAR "rank", "scan_start", "scan_stop", @@ -160,7 +161,7 @@ def multiplex_candidates( # the candidate used for multiplexing is the best scoring candidate in each elution group best_candidate_view = ( best_candidate_view.sort_values( - ["proba", "precursor_idx"] + ["proba", CandidatesDfCols.PRECURSOR_IDX], ) # last sort to break ties .groupby("elution_group_idx") .first() @@ -180,10 +181,12 @@ def multiplex_candidates( ] # remove original precursors precursors_flat_view = precursors_flat_view[ - ["elution_group_idx", "precursor_idx", "channel"] + ["elution_group_idx", PrecursorDfCols.PRECURSOR_IDX, "channel"] ] # reduce precursors to the elution group level - best_candidate_view = best_candidate_view.drop(columns=["precursor_idx"]) + best_candidate_view = best_candidate_view.drop( + columns=[CandidatesDfCols.PRECURSOR_IDX] + ) if "channel" in best_candidate_view.columns: best_candidate_view = best_candidate_view.drop(columns=["channel"]) @@ -389,12 +392,12 @@ def channel_score_groups(elution_group_idx, decoy, rank): # if no rank is present, pretend rank 0 if "rank" in input_df.columns: input_df = input_df.sort_values( - by=["elution_group_idx", "decoy", "rank", "precursor_idx"] + by=["elution_group_idx", "decoy", "rank", CandidatesDfCols.PRECURSOR_IDX] ) # last sort to break ties rank_values = input_df["rank"].values else: input_df = input_df.sort_values( - by=["elution_group_idx", "decoy", "precursor_idx"] + by=["elution_group_idx", "decoy", CandidatesDfCols.PRECURSOR_IDX] ) # last sort to break ties rank_values = np.zeros(len(input_df), dtype=np.uint32) @@ -405,9 +408,9 @@ def channel_score_groups(elution_group_idx, decoy, rank): else: input_df["score_group_idx"] = np.arange(len(input_df), dtype=np.uint32) - return input_df.sort_values(by=["score_group_idx", "precursor_idx"]).reset_index( - drop=True - ) # last sort to break ties + return input_df.sort_values( + by=["score_group_idx", CandidatesDfCols.PRECURSOR_IDX] + ).reset_index(drop=True) # last sort to break ties @overload_method( diff --git a/alphadia/search/selection/config_df.py b/alphadia/search/selection/config_df.py index c479c4e80..edc0aded6 100644 --- a/alphadia/search/selection/config_df.py +++ b/alphadia/search/selection/config_df.py @@ -5,6 +5,7 @@ import numba as nb import numpy as np import pandas as pd +from constants.keys import CandidatesDfCols from alphadia.search.jitclasses.jit_config import JITConfig @@ -256,7 +257,7 @@ def __init__( def get_candidate_df_column_names(self) -> list[str]: """Get the column names for the candidate DataFrame.""" return [ - "precursor_idx", + CandidatesDfCols.PRECURSOR_IDX, "rank", "score", "scan_center", diff --git a/alphadia/search/selection/selection.py b/alphadia/search/selection/selection.py index 38c524d00..865897ac2 100644 --- a/alphadia/search/selection/selection.py +++ b/alphadia/search/selection/selection.py @@ -8,7 +8,7 @@ import pandas as pd from alphadia import utils -from alphadia.constants.keys import CalibCols, PrecursorDfCols +from alphadia.constants.keys import CalibCols, CandidatesDfCols, PrecursorDfCols from alphadia.raw_data import DiaData, DiaDataJIT from alphadia.search.jitclasses.fragment_container import FragmentContainer from alphadia.search.selection import fft @@ -671,7 +671,7 @@ def __call__(self, thread_count: int = 10, debug: bool = False) -> pd.DataFrame: self.precursors_flat[ [PrecursorDfCols.PRECURSOR_IDX, "elution_group_idx", "decoy"] ], - left_on="precursor_idx", + left_on=CandidatesDfCols.PRECURSOR_IDX, right_on=PrecursorDfCols.PRECURSOR_IDX, how="left", ) diff --git a/alphadia/validation/schemas.py b/alphadia/validation/schemas.py index a06bd5e7e..d03bd5f84 100644 --- a/alphadia/validation/schemas.py +++ b/alphadia/validation/schemas.py @@ -76,7 +76,7 @@ features_schema = Schema( "candidate_features_df", [ - Required("precursor_idx", np.uint32), + Required("precursor_idx", np.uint32), # precursor_idx: UNCLEAR Required("elution_group_idx", np.uint32), Required("rank", np.uint8), Required("decoy", np.uint8), @@ -107,7 +107,7 @@ fragment_features_schema = Schema( "fragment_features_df", [ - Required("precursor_idx", np.uint32), + Required("precursor_idx", np.uint32), # precursor_idx: UNCLEAR Required("rank", np.uint8), Required("elution_group_idx", np.uint32), Required(CalibCols.MZ_LIBRARY, np.float32), diff --git a/alphadia/workflow/peptidecentric/extraction_handler.py b/alphadia/workflow/peptidecentric/extraction_handler.py index 70e4dada0..60edb7f4c 100644 --- a/alphadia/workflow/peptidecentric/extraction_handler.py +++ b/alphadia/workflow/peptidecentric/extraction_handler.py @@ -10,6 +10,7 @@ ScoringParameters, SelectionParameters, ) +from constants.keys import PrecursorDfCols from alphadia.constants.keys import CalibCols, CandidatesDfCols from alphadia.fragcomp.utils import candidate_hash @@ -631,8 +632,10 @@ def quantify_candidates( if precursor_fdr_df is not None: precursor_df = precursor_df.merge( - precursor_fdr_df[["precursor_idx", "rank", "qval", "proba"]], - on=["precursor_idx", "rank"], + precursor_fdr_df[ + [PrecursorDfCols.PRECURSOR_IDX, "rank", "qval", "proba"] + ], + on=[PrecursorDfCols.PRECURSOR_IDX, "rank"], how="left", ) @@ -678,7 +681,8 @@ def perform_fdr_and_filter_candidates( """ features_df["_candidate_idx"] = candidate_hash( - features_df["precursor_idx"].values, features_df["rank"].values + features_df["precursor_idx"].values, + features_df["rank"].values, # precursor_idx: features ) candidates_df["_candidate_idx"] = candidate_hash( candidates_df[CandidatesDfCols.PRECURSOR_IDX].values, diff --git a/alphadia/workflow/peptidecentric/ng/ng_mapper.py b/alphadia/workflow/peptidecentric/ng/ng_mapper.py index 00b98978b..b01e44245 100644 --- a/alphadia/workflow/peptidecentric/ng/ng_mapper.py +++ b/alphadia/workflow/peptidecentric/ng/ng_mapper.py @@ -114,7 +114,7 @@ def parse_candidates( candidates_df = pd.DataFrame( { - "precursor_idx": precursor_idx, + CandidatesDfCols.PRECURSOR_IDX: precursor_idx, "rank": rank, "score": score, "scan_center": scan_center, @@ -130,7 +130,7 @@ def parse_candidates( spectral_library.precursor_df[ [PrecursorDfCols.PRECURSOR_IDX, "elution_group_idx", "decoy"] ], - left_on="precursor_idx", + left_on=CandidatesDfCols.PRECURSOR_IDX, right_on=PrecursorDfCols.PRECURSOR_IDX, how="left", ) @@ -179,14 +179,15 @@ def to_features_df( features_df = features_df.merge( spectral_library.precursor_df[ [ - "precursor_idx", + PrecursorDfCols.PRECURSOR_IDX, "decoy", "elution_group_idx", "channel", "proteins", ] ], - on="precursor_idx", + left_on="precursor_idx", # precursor_idx: features + right_on=PrecursorDfCols.PRECURSOR_IDX, how="left", ) @@ -203,7 +204,7 @@ def parse_quantification( precursor_dict, fragment_dict = quantified_speclib.to_dict_arrays() precursor_df = pd.DataFrame(precursor_dict).rename( - columns={"idx": "precursor_idx"} + columns={"idx": PrecursorDfCols.PRECURSOR_IDX} ) # TODO: remove when #96 is merged fragments_df = pd.DataFrame(fragment_dict).rename( From 5b06f5bc30157860dbd048758fa6e21969bbcf6a Mon Sep 17 00:00:00 2001 From: mschwoerer <82171591+mschwoer@users.noreply.github.com> Date: Wed, 28 Jan 2026 19:34:21 +0100 Subject: [PATCH 11/11] fix imports --- alphadia/search/scoring/utils.py | 2 +- alphadia/search/selection/config_df.py | 2 +- alphadia/workflow/peptidecentric/extraction_handler.py | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/alphadia/search/scoring/utils.py b/alphadia/search/scoring/utils.py index 4737afcad..a637860ed 100644 --- a/alphadia/search/scoring/utils.py +++ b/alphadia/search/scoring/utils.py @@ -5,9 +5,9 @@ import numba as nb import numpy as np import pandas as pd -from constants.keys import CandidatesDfCols, PrecursorDfCols from numba.extending import overload_method +from alphadia.constants.keys import CandidatesDfCols, PrecursorDfCols from alphadia.search.jitclasses.fragment_container import FragmentContainer from alphadia.utils import USE_NUMBA_CACHING from alphadia.validation.schemas import ( diff --git a/alphadia/search/selection/config_df.py b/alphadia/search/selection/config_df.py index edc0aded6..cdf36a802 100644 --- a/alphadia/search/selection/config_df.py +++ b/alphadia/search/selection/config_df.py @@ -5,8 +5,8 @@ import numba as nb import numpy as np import pandas as pd -from constants.keys import CandidatesDfCols +from alphadia.constants.keys import CandidatesDfCols from alphadia.search.jitclasses.jit_config import JITConfig logger = logging.getLogger() diff --git a/alphadia/workflow/peptidecentric/extraction_handler.py b/alphadia/workflow/peptidecentric/extraction_handler.py index 60edb7f4c..af1a94b19 100644 --- a/alphadia/workflow/peptidecentric/extraction_handler.py +++ b/alphadia/workflow/peptidecentric/extraction_handler.py @@ -10,9 +10,8 @@ ScoringParameters, SelectionParameters, ) -from constants.keys import PrecursorDfCols -from alphadia.constants.keys import CalibCols, CandidatesDfCols +from alphadia.constants.keys import CalibCols, CandidatesDfCols, PrecursorDfCols from alphadia.fragcomp.utils import candidate_hash from alphadia.raw_data import DiaData from alphadia.raw_data.alpharaw_wrapper import DEFAULT_VALUE_NO_MOBILITY