grassp.tl.enrichment_to_cluster_distribution

grassp.tl.enrichment_to_cluster_distribution#

enrichment_to_cluster_distribution(enr_res, cluster_key='leiden', ranking_metric='Adjusted P-value Bonferroni', threshold=0.05, temperature=1.0, s0=0.0, s_max=300.0, unknown_label='unknown', weight_by='evidence')[source]#

Turn a per-cluster enrichment table into soft label distributions.

The default annotation pipeline assigns each cluster its single most significant term (a hard, one-hot label). This discards the fact that a cluster may be only marginally enriched, or enriched for several terms of near-equal significance. When such a hard label is subsequently propagated over the neighbour graph (competitive_propagation()) it produces over-confident, and often wrong, annotations in poorly resolved regions.

This function instead converts the full per-(cluster, term) enrichment table into a probability distribution over a shared compartment vocabulary, with an explicit unknown class that absorbs uncertainty. The resulting distribution can be broadcast to proteins and used as a soft seed for competitive_propagation() (see soft_cluster_annotation()).

For every cluster c and term t an evidence score is computed as

\[s(c, t) = \mathrm{clip}\bigl(\log_{10}(\text{threshold} / p_{\text{adj}}(c, t)),\; 0,\; s_{\max}\bigr)\]

so that a term sitting exactly at threshold contributes zero evidence, stronger enrichment contributes more (up to s_max), and only terms with p_adj <= threshold count. Probabilities follow a tempered softmax with an explicit unknown logit s0:

w(c, t)       = exp(s(c, t) / temperature)   for significant terms
w(c, unknown) = exp(s0 / temperature)
Q[c, :]       = w / w.sum()

With the defaults (s0 = 0) the unknown class ties a term that is only marginally significant, so weakly/ambiguously enriched clusters keep most of their mass on unknown instead of committing to a wrong label. As temperature -> 0 the distribution collapses onto the single best term, recovering the behaviour of the hard pipeline.

Parameters:
enr_res DataFrame

Per-(cluster, term) enrichment table as returned by calculate_cluster_enrichment() (return_enrichment_res=True). Must contain the columns "Term", ranking_metric and cluster_key.

cluster_key str (default: 'leiden')

Name of the column in enr_res holding the cluster labels.

ranking_metric Literal['Adjusted P-value', 'Adjusted P-value Bonferroni', 'P-value'] (default: 'Adjusted P-value Bonferroni')

p-value column used as the evidence metric. Defaults to "Adjusted P-value Bonferroni" (corrected for both term- and cluster-multiplicity), matching the pipeline default.

threshold float (default: 0.05)

Significance threshold. Terms with ranking_metric > threshold (or missing) are treated as non-significant and contribute no evidence.

temperature float (default: 1.0)

Softmax temperature. Larger values give more diffuse distributions; temperature -> 0 approaches winner-take-all.

s0 float (default: 0.0)

Evidence logit assigned to the unknown class. 0 (default) corresponds to a hypothetical term sitting exactly at threshold.

s_max float (default: 300.0)

Upper cap on the per-term evidence score log10(threshold/padj). Its only role is to bound a literal padj == 0 (which would give inf); it must stay well above realistic -log10(padj) values. The default 300 is effectively “off”. Do not set this small: a small cap collapses the evidence of any two strongly-enriched terms to equal, producing spurious ties between a compartment and an overlapping one (e.g. Lysosome/Endosome, 40S ribosome/Nucleolus).

weight_by Literal['evidence', 'odds_ratio'] (default: 'evidence')

How admissible (significant) terms are weighted against each other:

  • "evidence" (default): logit is the clipped log10(threshold/padj) evidence score — confidence-weighted, size-dependent.

  • "odds_ratio": logit is ln of the Haldane-Anscombe odds ratio (gseapy’s already-HA-corrected "Odds Ratio" column). Effect-size weighted and size-independent, so P(A)/P(B) = (OR_A/OR_B)^(1/T). The p-value still gates which terms are admissible; only significant terms contribute regardless of this setting.

unknown_label str | None (default: 'unknown')

Name of the appended background/unknown category. If None, no unknown class is added: the distribution is purely relative over significant terms and clusters with no significant term receive a uniform fallback (which propagates to a high-entropy, unresolved distribution). Provided mainly to ablate the value of the unknown class against the entropy-null resolver.

Return type:

tuple[DataFrame, list[str]]

Returns:

Qpandas.DataFrame

Row-stochastic matrix indexed by cluster label, with one column per compartment in the vocabulary (plus unknown_label as the final column unless unknown_label is None). Each row sums to 1.

categorieslist of str

The ordered column vocabulary (significant terms, plus unknown_label last unless it is None), suitable as seed_categories for competitive_propagation().