File size: 11,108 Bytes
352e308 93a454a 352e308 18391e3 352e308 6e9f020 352e308 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 | """
Transcription factor and pathway activity scoring workflow helpers.
Two families of functions live here:
1. Manifest-aware parameter helpers
----------------------------------
get_scoring_params Extract organism, contrast, and applicable workflows
from a dataset manifest.
2. Generic sample-level scoring
------------------------------
score_bulk_samples_with_decoupler
Run decoupleR activity estimation (ULM/MLM/zscore) on a
pre-normalised samples Γ genes expression DataFrame.
Returns a samples Γ activities DataFrame and diagnostic
metadata. Does not hard-code any dataset.
"""
from __future__ import annotations
from typing import Any
import pandas as pd
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
SUPPORTED_RESOURCES: frozenset[str] = frozenset({"progeny", "collectri", "hallmark"})
SUPPORTED_METHODS: frozenset[str] = frozenset({"ulm", "mlm", "zscore"})
_RESOURCE_DESCRIPTIONS: dict[str, str] = {
"progeny": "PROGENy β 14 cancer signalling pathway signatures",
"collectri": "CollecTRI β TFβtarget regulons (~1 000 TFs)",
"hallmark": "MSigDB Hallmark β 50 curated gene sets",
}
# ---------------------------------------------------------------------------
# Manifest helpers
# ---------------------------------------------------------------------------
def get_scoring_params(manifest: "dict | object") -> dict[str, Any]:
"""
Extract activity scoring parameters from a dataset manifest.
Accepts both a DatasetManifest dataclass instance and a raw dict.
Returns
-------
dict with keys:
organism (str) β "human" or "mouse".
contrast (dict) β design_factor, test_group, control_group, method.
workflows (list) β which workflow modules apply to this dataset.
"""
if hasattr(manifest, "organism"):
return {
"organism": manifest.organism,
"contrast": (manifest.default_contrasts or [{}])[0]
if hasattr(manifest, "default_contrasts") else {},
"workflows": getattr(manifest, "valid_workflows", []),
}
return {
"organism": manifest.get("organism", "human"),
"contrast": manifest.get("contrast", {}),
"workflows": manifest.get("workflows", []),
}
# ---------------------------------------------------------------------------
# Internal helper
# ---------------------------------------------------------------------------
def _find_network_target_col(net: "pd.DataFrame") -> "str | None":
"""Return the target/gene column name in a decoupler network, or None."""
for col in ("target", "gene", "Gene", "to"):
if col in net.columns:
return col
return None
# ---------------------------------------------------------------------------
# Generic sample-level scoring
# ---------------------------------------------------------------------------
def score_bulk_samples_with_decoupler(
expression_df: "pd.DataFrame",
resource: str = "progeny",
organism: str = "human",
method: str = "ulm",
min_n: int = 5,
_network: "pd.DataFrame | None" = None,
) -> dict[str, Any]:
"""
Score bulk expression samples using decoupleR activity estimation.
Computes per-sample activity scores for pathways (PROGENy), TF regulons
(CollecTRI), or hallmark gene sets by fitting a linear model (ULM/MLM)
or weighted sum (WSUM) across the genes in each regulon/pathway.
This function is generic β it does not assume any specific dataset.
Pass any pre-normalised samples Γ genes expression matrix.
Input requirements
------------------
expression_df:
Samples as rows (index = sample IDs), genes as columns (HGNC symbols).
Values must be pre-normalised (log-CPM, log-intensity, log2-TPM, etc.).
Do NOT pass raw integer counts β activity estimation assumes continuous,
roughly symmetric expression values.
Output
------
activity_df:
Same row index as expression_df. Columns are activity names
(pathway names, TF names, or hallmark names).
Parameters
----------
resource:
"progeny" β PROGENy pathway signatures (14 pathways).
"collectri" β CollecTRI TF regulons (~1 000 TFs, human/mouse).
"hallmark" β MSigDB Hallmark gene sets (50 sets).
organism:
"human" or "mouse".
method:
"ulm" β Univariate Linear Model (recommended; fastest).
"mlm" β Multivariate Linear Model (accounts for co-linearity).
"zscore" β Z-score (simpler, no distributional assumptions).
min_n:
Minimum expected samples. Emits a warning when fewer are present.
_network:
For testing only. Supply a pre-loaded network DataFrame to bypass
the decoupler network download (dc.op.*). Not exposed in MCP tools.
Returns
-------
dict with keys:
activity_df (pd.DataFrame) β samples Γ activities.
pvalue_df (pd.DataFrame) β samples Γ activities (p-values).
n_samples (int)
n_activities (int) β number of scored activities (sources).
n_network_genes (int) β total target genes in the network.
n_matched_genes (int) β network genes present in expression_df.
coverage_pct (float) β % of network genes matched; None if unknown.
resource (str)
organism (str)
method (str)
warnings (list[str])
Raises
------
ValueError if resource or method is not in the supported set.
"""
import decoupler as dc
# A caller-supplied network (``_network``) is a custom signature β e.g. a
# Loveless-derived cell-state signature scored against a bulk cohort
# (ADR-0006 Role 2). In that case ``resource`` is just a free-form label for
# outputs/diagnostics, so the built-in-resource gate does not apply.
if _network is None and resource not in SUPPORTED_RESOURCES:
raise ValueError(
f"resource must be one of {sorted(SUPPORTED_RESOURCES)}, got '{resource}'"
)
if method not in SUPPORTED_METHODS:
raise ValueError(
f"method must be one of {sorted(SUPPORTED_METHODS)}, got '{method}'"
)
run_warnings: list[str] = []
# ββ Load network βββββββββββββββββββββββββββββββββββββββββββββββββββββ
if _network is not None:
net = _network.copy()
elif resource == "progeny":
net = dc.op.progeny(organism=organism)
elif resource == "collectri":
net = dc.op.collectri(organism=organism)
else: # hallmark
net = dc.op.hallmark(organism=organism)
# ββ Gene coverage ββββββββββββββββββββββββββββββββββββββββββββββββββββ
target_col = _find_network_target_col(net)
expr_genes = set(expression_df.columns.astype(str))
if target_col:
network_genes: set[str] = set(net[target_col].astype(str).unique())
n_network = len(network_genes)
n_matched = len(network_genes & expr_genes)
coverage_pct: float | None = (
round(100 * n_matched / n_network, 1) if n_network > 0 else 0.0
)
if coverage_pct is not None and coverage_pct < 20:
run_warnings.append(
f"Low gene coverage: {n_matched}/{n_network} network genes "
f"({coverage_pct}%) found in expression matrix. "
"Check that column names are HGNC gene symbols."
)
elif coverage_pct is not None and coverage_pct < 50:
run_warnings.append(
f"Moderate gene coverage: {n_matched}/{n_network} network genes "
f"({coverage_pct}%) found in expression matrix."
)
else:
n_network = len(net)
n_matched = 0
coverage_pct = None
run_warnings.append(
"Could not identify target gene column in network β "
"gene coverage check skipped."
)
# ββ Sample count βββββββββββββββββββββββββββββββββββββββββββββββββββββ
n_samples = len(expression_df)
if n_samples < min_n:
run_warnings.append(
f"Expression matrix has {n_samples} sample(s), below min_n={min_n}. "
"Activity estimates from very small cohorts should be interpreted "
"with caution."
)
# ββ Pre-flight: zero-overlap guard βββββββββββββββββββββββββββββββββββ
if target_col and n_matched == 0:
raise ValueError(
f"Zero gene overlap: none of the {n_network} {resource} network "
f"genes were found in the expression columns. "
"Ensure column names are HGNC gene symbols."
)
# ββ Run scoring ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Wrap decoupler's AssertionError (too few overlapping targets per source)
# into a ValueError with a more actionable message.
try:
if method == "ulm":
acts, pvals = dc.mt.ulm(data=expression_df, net=net)
elif method == "mlm":
acts, pvals = dc.mt.mlm(data=expression_df, net=net)
else: # zscore
acts, pvals = dc.mt.zscore(data=expression_df, net=net)
except AssertionError as exc:
raise ValueError(
f"decoupleR could not score with resource='{resource}': {exc}. "
f"Gene coverage: {n_matched}/{n_network} ({coverage_pct}%). "
"Each source requires β₯5 overlapping target genes by default. "
"Ensure expression columns are HGNC gene symbols and sufficient "
"genes are covered."
) from exc
run_warnings.append(
f"Activity scores computed with method='{method}', "
f"resource='{resource}' ({_RESOURCE_DESCRIPTIONS.get(resource, '')}), "
f"organism='{organism}'. "
"Input is assumed to be pre-normalised expression. "
"Do not interpret activity scores as log fold-changes."
)
return {
"activity_df": acts,
"pvalue_df": pvals,
"n_samples": n_samples,
"n_activities": int(acts.shape[1]),
"n_network_genes": n_network,
"n_matched_genes": n_matched,
"coverage_pct": coverage_pct,
"resource": resource,
"organism": organism,
"method": method,
"warnings": run_warnings,
}
|