Skip to content

Filter

Filtering in msmu is split into two steps and is implemented in msmu.pp.add_filter and msmu.pp.apply_filter.

  1. add_filter() creates a boolean mask and stores it as a named filter column.
  2. apply_filter() applies one or more stored masks to subset the modality.

Define filters

add_filter() supports multiple sources via on:

  • on="var": read column from .var, store mask in .varm["filter"]
  • on="obs": read column from .obs, store mask in .obsm["filter"]
  • on="varm": read column from .varm[key], store mask in .varm["filter"]
  • on="obsm": read column from .obsm[key], store mask in .obsm["filter"]

key is required when on is "varm" or "obsm".

The keep argument accepts conditional operators such as eq, ne, lt, le, gt, ge, contains, and not_contains.

Use name to give a filter a short, explicit identifier:

mdata = mm.pp.add_filter(
    mdata, modality="psm", on="varm", key="qc",
    column="score", keep="gt", value=0.01, name="qc_score",
)
mdata = mm.pp.apply_filter(mdata, modality="psm", columns=["qc_score"])

Names must be nonblank strings without / so their definitions can be saved in HDF5. Explicit names are shared across the modality's observation and feature filters. The source, column, operator, and value are recorded in adata.uns["filter_conditions"][name]. Reusing a name with the same condition recomputes its mask; a different condition raises an error. An existing mask without a recorded definition cannot be overwritten with an explicit name.

When name is omitted, stored filter column names follow this pattern: {column}_{keep}_{value} for obs and var. For matrix sources, the name also includes the source and key, for example varm['qc'].score_gt_15.0. Use that full name when selecting matrix filters with columns.

Missing source values never satisfy a filter, including ne and not_contains. Missing masks introduced by concatenating datasets remain unapplied to rows where that filter was not recorded.

# feature-level filter from .var
mdata = mm.pp.add_filter(
    mdata,
    modality="psm",
    on="var",
    column="q_value",
    keep="lt",
    value=0.01,
)

# sample-level filter from .obs
mdata = mm.pp.add_filter(
    mdata,
    modality="psm",
    on="obs",
    column="condition",
    keep="not_contains",
    value="BLANK",
)

Apply filters

apply_filter() controls target axis with on:

  • on="all" (default): apply both .varm["filter"] and .obsm["filter"]
  • on="var": apply only .varm["filter"]
  • on="obs": apply only .obsm["filter"]
  • columns=[...] (optional): apply only selected filter columns by name

When on="all" and one side does not have a stored filter table, that axis is skipped. A warning is printed if neither side has filters. When on="var" or on="obs" and the requested filter table is missing, an error is raised. Invalid on values also raise an error.

The function also prints which filter columns are applied. The provenance logger records the successful call in mdata.uns["_log"], but does not capture printed output.

mdata = mm.pp.apply_filter(mdata, modality="psm", on="all")

# apply only selected filters
mdata = mm.pp.apply_filter(
    mdata,
    modality="psm",
    on="var",
    columns=["q_value_lt_0.01"],
)

Missingness

A quantification filter needs the missing counts on .var first. pp.calculate_missingness records n_missing and missing_ratio per feature and, given an .obs column, missing_ratio_<level> per group, so the usual filter calls apply:

mdata = mm.pp.calculate_missingness(mdata, modality="protein", group_obs="condition")
mdata = mm.pp.add_filter(mdata, modality="protein", column="missing_ratio", keep="lt", value=0.5)
mdata = mm.pp.apply_filter(mdata, modality="protein")

Decoys carry no quantification, so they receive n_missing = 0 and missing_ratio = 0.0: a "missing at most" filter keeps every decoy and the target/decoy counts behind the q-values stay intact, while a filter in the other direction (ge/gt on missingness) would drop them. The columns are a snapshot — recompute after removing samples, since apply_filter does not update them.

Contaminants and decoys

Readers record identification status rather than acting on it:

  • Decoys are separated out of .var at read time and kept in .uns["decoy"], where the q-value machinery needs them. Filters you add are applied to them in parallel, through .uns["decoy_filter"], so target and decoy features stay matched.
  • Contaminants stay in .var with a contaminant flag (1 / 0). msmu records the flag and leaves the decision to you: a contaminant accession can also be a genuine protein of the sample (the parsed group keeps both, e.g., Cont_P07339;P07339), so whether such a feature should go is a protein-level judgment rather than something to drop at the PSM level.

Contaminant markers differ per FASTA (contam_, Cont_, CON__, and each engine's decoy tag in front of them), so the reader normalizes every accession to one canonical form [rev_][Cont_]<accession> and sets the flag from the parse. If you do filter on contaminant status, filter on the flag rather than matching a marker on the protein string — a string match silently stops matching when the search uses a different contaminant FASTA.

Validation and compatibility

Requested filter names must all exist on the selected axis (either axis for on="all"). Missing names raise an error; filters are not partially applied. Variable filters must also exist in the decoy mask when decoys are present, so the column a variable filter uses must exist in .uns["decoy"] as well; add_filter refuses otherwise. Give decoys the column with a recorded edit rather than by hand — dt.assign with on="decoy" for a constant, or dt.map with target="<modality>.decoy" to carry a sample annotation over by filename — so the step stays in provenance. Applying a subset of filters preserves the other decoy conditions for later calls.

contains and not_contains retain pandas regular-expression semantics: A.B also matches AxB. Escape regex metacharacters for literal matching.

Existing stored filter names are not renamed. Newly recorded matrix filters use the source-qualified names above; repeating the same condition replaces its mask. Old workflows that regenerate matrix filters using the former names, relied on missing values passing, or requested nonexistent columns may no longer replay unchanged. Verified replay can report a hash mismatch, and explicit old matrix filter names can raise an error. Re-record those workflows with the current API; do not disable verification to hide a changed result.