Source code for sc_tools.qc.metrics

"""
QC metrics: wrappers around scanpy for spots/cells and genes.

Provides:
- calculate_qc_metrics: total_counts, n_genes_by_counts, pct_counts_mt (and optionally hb).
- filter_cells, filter_genes: count-based filtering.
- highly_variable_genes: HVG selection (wraps scanpy).

Two usage points: pre-normalization (raw counts) and post-normalization (optional second pass).
"""

from __future__ import annotations

import re
from typing import Any

import numpy as np
import pandas as pd
from anndata import AnnData

__all__ = [
    "calculate_qc_metrics",
    "filter_cells",
    "filter_genes",
    "highly_variable_genes",
]


[docs] def calculate_qc_metrics( adata: AnnData, *, mt_pattern: str | re.Pattern | None = "^(MT-|mt-|Mt)", hb_pattern: str | re.Pattern | None = "^(HB|Hb|HBEGF)", qc_vars: list[str] | None = None, inplace: bool = True, percent_top: list[int] | None = (50, 100, 200, 500), log1p: bool = False, modality: str = "visium", **kwargs: Any, ) -> pd.DataFrame | None: """ Compute QC metrics for spots/cells and genes (wrap scanpy). Optionally marks mitochondrial and hemoglobin genes and adds pct_counts_mt (and pct_counts_hb) to adata.obs. Parameters ---------- adata : AnnData Annotated data (raw counts recommended). mt_pattern : str or compiled regex or None Pattern to mark mitochondrial genes in adata.var (default MT- / mt- / Mt). If None, mt genes are not marked and pct_counts_mt is not computed. Automatically set to None for protein-based modalities (e.g. ``"imc"``). hb_pattern : str or compiled regex or None Pattern to mark hemoglobin genes for pct_counts_hb. If None, not computed. Automatically set to None for protein-based modalities. qc_vars : list of str or None If None, built from mt_pattern and hb_pattern: ['mt'] and optionally ['mt','hb']. Passed to scanpy as qc_vars (column names in adata.var). inplace : bool If True, add metrics to adata.obs and adata.var (default True). percent_top : list of int or None Passed to scanpy (default (50, 100, 200, 500)). Automatically capped to ``n_vars`` to avoid IndexError on small panels. log1p : bool Passed to scanpy (default False). modality : str Data modality. Protein-based modalities (``"imc"``) skip MT/HB patterns. **kwargs Passed to scanpy.pp.calculate_qc_metrics. Returns ------- DataFrame or None If inplace is False, returns (obs_df, var_df) concatenation as per scanpy; otherwise None. """ import scanpy as sc # Protein-based modalities have no mitochondrial or hemoglobin genes _protein_modalities = {"imc"} if modality in _protein_modalities: mt_pattern = None hb_pattern = None var_names = pd.Series(adata.var_names) if mt_pattern is not None: if isinstance(mt_pattern, str): mt_pattern = re.compile(mt_pattern, re.IGNORECASE) adata.var["mt"] = var_names.str.match(mt_pattern).values if hb_pattern is not None: if isinstance(hb_pattern, str): hb_pattern = re.compile(hb_pattern, re.IGNORECASE) adata.var["hb"] = var_names.str.match(hb_pattern).values if qc_vars is None: qc_vars = [] if mt_pattern is not None and "mt" in adata.var.columns: qc_vars.append("mt") if hb_pattern is not None and "hb" in adata.var.columns: qc_vars.append("hb") # Cap percent_top to n_vars to avoid IndexError on small panels (e.g. 52-protein IMC) if percent_top is not None: n_vars = adata.n_vars percent_top = tuple(p for p in percent_top if p <= n_vars) or None return sc.pp.calculate_qc_metrics( adata, qc_vars=qc_vars, inplace=inplace, percent_top=percent_top, log1p=log1p, **kwargs, )
[docs] def filter_cells( adata: AnnData, min_counts: int | None = None, min_genes: int | None = None, max_counts: int | None = None, max_genes: int | None = None, inplace: bool = True, **kwargs: Any, ) -> AnnData | None: """ Filter out spots/cells by counts and number of genes (wrap scanpy). Parameters ---------- adata : AnnData Annotated data (should have total_counts and n_genes_by_counts in obs from calculate_qc_metrics). min_counts, min_genes, max_counts, max_genes : int or None Thresholds; None means do not apply. Passed to scanpy. inplace : bool If True, filter in place (default True). **kwargs Passed to scanpy.pp.filter_cells. Returns ------- AnnData or None Filtered object if inplace=False, else None. """ import scanpy as sc return sc.pp.filter_cells( adata, min_counts=min_counts, min_genes=min_genes, max_counts=max_counts, max_genes=max_genes, inplace=inplace, **kwargs, )
[docs] def filter_genes( adata: AnnData, min_counts: int | None = None, min_cells: int | None = None, max_counts: int | None = None, max_cells: int | None = None, inplace: bool = True, **kwargs: Any, ) -> AnnData | None: """ Filter out genes by counts and number of cells (wrap scanpy). Parameters ---------- adata : AnnData Annotated data. min_counts, min_cells, max_counts, max_cells : int or None Thresholds; None means do not apply. Passed to scanpy. inplace : bool If True, filter in place (default True). **kwargs Passed to scanpy.pp.filter_genes. Returns ------- AnnData or None Filtered object if inplace=False, else None. """ import scanpy as sc return sc.pp.filter_genes( adata, min_counts=min_counts, min_cells=min_cells, max_counts=max_counts, max_cells=max_cells, inplace=inplace, **kwargs, )
[docs] def highly_variable_genes( adata: AnnData, flavor: str = "seurat", n_top_genes: int | None = None, min_mean: float = 0.0125, max_mean: float = 3, min_disp: float = 0.5, max_disp: float = np.inf, batch_key: str | None = None, subset: bool = False, inplace: bool = True, **kwargs: Any, ) -> pd.DataFrame | None: """ Mark or subset highly variable genes (wrap scanpy). Parameters ---------- adata : AnnData Annotated data (normalized, e.g. log1p, recommended for seurat_v3). flavor : str 'seurat', 'seurat_v3', or 'cell_ranger' (default 'seurat'). n_top_genes : int or None If set, use this many top genes (common for seurat_v3). min_mean, max_mean, min_disp, max_disp : float Passed to scanpy (flavor-dependent). batch_key : str or None If set, compute HVGs per batch (e.g. 'sample'). subset : bool If True, subset adata to HVGs (default False). inplace : bool If True, add 'highly_variable' etc. to adata.var (default True). **kwargs Passed to scanpy.pp.highly_variable_genes. Returns ------- DataFrame or None If inplace=False, returns the var DataFrame with HVG columns; else None. """ import scanpy as sc return sc.pp.highly_variable_genes( adata, flavor=flavor, n_top_genes=n_top_genes, min_mean=min_mean, max_mean=max_mean, min_disp=min_disp, max_disp=max_disp, batch_key=batch_key, subset=subset, inplace=inplace, **kwargs, )