Source code for sc_tools.utils.signatures
"""
Signature utility functions.
"""
from __future__ import annotations
import anndata as ad
import pandas as pd
def get_signature_columns_from_obsm(
adata: ad.AnnData,
obsm_key: str = "signature_score_z",
) -> list[str]:
"""
Get signature column names from adata.obsm (full-path style, e.g. Myeloid/Macrophage_Core).
Parameters
----------
adata : AnnData
Annotated data object
obsm_key : str
Key in adata.obsm (default: 'signature_score_z')
Returns
-------
list of str
Column names, or empty list if key is missing
"""
if obsm_key not in adata.obsm:
return []
arr = adata.obsm[obsm_key]
if hasattr(arr, "columns"):
return list(arr.columns)
return []
[docs]
def get_signature_columns(adata: ad.AnnData, prefix: str = "sig:", suffix: str = "_z") -> list[str]:
"""
Get all signature column names. Prefer obsm['signature_score_z'] if present; else obs columns.
Parameters
----------
adata : AnnData
Annotated data object
prefix : str
Prefix for obs-based signature columns (default: 'sig:')
suffix : str
Suffix for obs-based signature columns (default: '_z')
Returns
-------
list of str
List of signature column names (obsm full-path or obs sig:..._z)
"""
obsm_cols = get_signature_columns_from_obsm(adata, obsm_key="signature_score_z")
if obsm_cols:
return sorted(obsm_cols)
sig_cols = [col for col in adata.obs.columns if col.startswith(prefix) and col.endswith(suffix)]
return sorted(sig_cols)
def get_signature_df(
adata: ad.AnnData,
use_z: bool = True,
obsm_key: str | None = None,
) -> pd.DataFrame:
"""
Return a DataFrame of signature scores. Prefer obsm; fall back to obs for backward compatibility.
Parameters
----------
adata : AnnData
Annotated data object
use_z : bool
If True (default), use z-scored scores (signature_score_z); else signature_score
obsm_key : str or None
If set, use this obsm key; else use signature_score_z or signature_score
Returns
-------
DataFrame
Index = adata.obs_names; columns = signature names (full-path or obs column names)
"""
key = obsm_key
if key is None:
key = "signature_score_z" if use_z else "signature_score"
if key in adata.obsm:
df = adata.obsm[key]
if hasattr(df, "copy"):
return df.copy()
return pd.DataFrame(df, index=adata.obs_names)
# Fallback: build from obs (prefix sig:, suffix _z)
prefix, suffix = "sig:", "_z"
cols = [c for c in adata.obs.columns if c.startswith(prefix) and c.endswith(suffix)]
if not cols:
return pd.DataFrame(index=adata.obs_names)
return adata.obs[cols].copy()
[docs]
def filter_signatures(
signature_list: list[str],
include: list[str] | None = None,
exclude: list[str] | None = None,
) -> list[str]:
"""
Filter signatures based on include/exclude criteria.
Parameters
----------
signature_list : list
List of signature column names (e.g., 'sig:Tumor_Cells-EMT_Tumor_z')
include : list or None
List of signatures to include. Can be:
- Exact matches (with or without 'sig:' prefix and '_z' suffix)
- Patterns (substring search, case-insensitive)
- None to include all
exclude : list or None
List of signatures to exclude. Same format as include.
- None to exclude none
Returns
-------
list
Filtered list of signatures
"""
if include is None and exclude is None:
return signature_list
filtered = []
# Normalize signature names for matching (remove prefix/suffix)
def normalize_sig(sig):
"""Remove prefix and suffix for comparison."""
normalized = sig
if normalized.startswith("sig:"):
normalized = normalized[4:]
if normalized.endswith("_z"):
normalized = normalized[:-2]
return normalized.lower()
# Normalize all signatures
sig_normalized = {sig: normalize_sig(sig) for sig in signature_list}
for sig in signature_list:
sig_norm = sig_normalized[sig]
include_match = False
exclude_match = False
# Check include list
if include is not None:
for pattern in include:
# Normalize pattern
if pattern.startswith("sig:"):
pattern_norm = normalize_sig(pattern)
else:
pattern_norm = pattern.lower()
# Check exact match (normalized) or substring match
if pattern_norm == sig_norm or pattern_norm in sig_norm:
include_match = True
break
else:
# No include list means include all
include_match = True
# Check exclude list
if exclude is not None:
for pattern in exclude:
# Normalize pattern
if pattern.startswith("sig:"):
pattern_norm = normalize_sig(pattern)
else:
pattern_norm = pattern.lower()
# Check exact match (normalized) or substring match
if pattern_norm == sig_norm or pattern_norm in sig_norm:
exclude_match = True
break
# Include if matches include criteria and doesn't match exclude
if include_match and not exclude_match:
filtered.append(sig)
return filtered
[docs]
def clean_sig_name(sig: str, max_length: int = 40) -> str:
"""
Clean signature name for display.
Parameters
----------
sig : str
Signature name (e.g., 'sig:Tumor_Cells/EMT_Tumor_z')
max_length : int
Maximum length for truncated names (default: 40)
Returns
-------
str
Cleaned signature name
"""
# Remove 'sig:' prefix and '_z' suffix
cleaned = sig.replace("sig:", "").replace("_z", "")
# Replace '/' with '-' for readability
cleaned = cleaned.replace("/", "-")
# Truncate if too long
if len(cleaned) > max_length:
cleaned = cleaned[: max_length - 3] + "..."
return cleaned