"""Enrichment Analysis Module. Contains different functions to perform enrichment
analysis.
Most things in this module are covered in https://www.youtube.com/watch?v=2NC1QOXmc5o
by Lars Juhl Jensen.
"""
from __future__ import annotations
import logging
import os
import re
import gseapy as gp
import pandas as pd
from pandera.typing.pandas import DataFrame
from acore.enrichment_analysis import permutations_enrichment
from acore.enrichment_analysis.annotate import annotate_features
from acore.enrichment_analysis.statistical_tests.fisher import run_fisher
from acore.enrichment_analysis.statistical_tests.kolmogorov_smirnov import (
run_kolmogorov_smirnov,
)
from acore.multiple_testing import apply_pvalue_correction
from acore.types.enrichment_analysis import EnrichmentAnalysisSchema
logger = logging.getLogger(__name__)
TYPE_COLS_MSG = """
columns: 'terms', 'identifiers', 'foreground',
'background', foreground_pop, background_pop, 'pvalue', 'padj' and 'rejected'.
"""
__all__ = [
"run_fisher",
"run_gsea",
"run_kolmogorov_smirnov",
"run_site_regulation_enrichment",
"run_up_down_regulation_enrichment",
]
# ! undocumented for now (find usage example)
[docs]
def run_site_regulation_enrichment(
regulation_data: pd.DataFrame,
annotation: pd.DataFrame,
identifier: str = "identifier",
groups: list[str] = ("group1", "group2"),
annotation_col: str = "annotation",
rejected_col: str = "rejected",
group_col: str = "group",
method: str = "fisher",
regex: str = "(\\w+~.+)_\\w\\d+\\-\\w+",
correction: str = "fdr_bh",
remove_duplicates: bool = False,
) -> DataFrame[EnrichmentAnalysisSchema]:
r"""
This function runs a simple enrichment analysis for significantly
regulated protein sites in a dataset.
:param regulation_data: pandas.DataFrame resulting from differential
regulation analysis.
:param annotation: pandas.DataFrame with annotations for features
(columns: 'annotation', 'identifier' (feature identifiers), and 'source').
:param str identifier: name of the column from annotation containing
feature identifiers.
:param list groups: column names from regulation_data containing
group identifiers.
:param str annotation_col: name of the column from annotation containing
annotation terms.
:param str rejected_col: name of the column from regulation_data containing
boolean for rejected null hypothesis.
:param str group_col: column name for new column in annotation dataframe
determining if feature belongs to foreground or background.
:param str method: method used to compute enrichment
(only 'fisher' is supported currently).
:param str regex: how to extract the annotated identifier from the site identifier
:return: pandas.DataFrame with columns: 'terms', 'identifiers', 'foreground',
'background', foreground_pop, background_pop, 'pvalue', 'padj' and 'rejected'.
:raises ValueError: if regulation_data is `None` or empty.
Example::
result = run_site_regulation_enrichment(regulation_data,
annotation,
identifier='identifier',
groups=['group1', 'group2'],
annotation_col='annotation',
rejected_col='rejected',
group_col='group',
method='fisher',
match="(\\w+~.+)_\\w\\d+\\-\\w+"
)
"""
result = pd.DataFrame()
if regulation_data is None or regulation_data.empty:
raise ValueError("regulation_data is empty")
new_ids = []
# find any identifiers with a PTM and save only prot+gene identifer
for ident in regulation_data[identifier].tolist():
match = re.search(regex, ident)
if match is not None:
new_ids.append(
match.group(1)
) # removes the PTM extension of the identifier of CKG
else:
new_ids.append(ident)
# so this is normalizing the identifiers to ignore the PTM extension
regulation_data[identifier] = new_ids # matches are used as identifiers
if remove_duplicates:
regulation_data = regulation_data.drop_duplicates(subset=[identifier])
result = run_regulation_enrichment(
regulation_data=regulation_data,
annotation=annotation,
identifier=identifier,
group_col=groups,
annotation_col=annotation_col,
rejected_col=rejected_col,
method=method,
correction=correction,
)
return result
[docs]
def run_up_down_regulation_enrichment(
regulation_data: pd.DataFrame,
annotation: pd.DataFrame,
identifier: str = "identifier",
groups: list[str] = ("group1", "group2"),
annotation_col: str = "annotation",
# rejected_col: str = "rejected", # could be passed
pval_col: str = "pval",
group_col: str = "group",
log2fc_col: str = "log2FC",
method: str = "fisher",
min_detected_in_set: int = 2,
correction: str = "fdr_bh",
correction_alpha: float = 0.05,
lfc_cutoff: float = 1,
permutations: int = 0,
seed: int | None = None,
) -> DataFrame[EnrichmentAnalysisSchema]:
"""
This function runs a simple enrichment analysis for significantly regulated proteins
distinguishing between upregulation per group defined by the `groups`.
Only overrepresentation is tested using one-sided Fisher's exact tests.
:param pandas.DataFrame regulation_data: pandas.DataFrame resulting from differential regulation
analysis (CKG's regulation table).
:param pandas.DataFrame annotation: pandas.DataFrame with annotations for features
(columns: 'annotation', 'identifier' (feature identifiers), and 'source').
:param str identifier: name of the column from annotation containing feature identifiers.
:param list[str] groups: column names from regulation_data containing group identifiers.
See `pandas.DataFrame.groupby`_ for more information.
.. _pandas.DataFrame.groupby: https://pandas.pydata.org/pandas-docs/stable/\
reference/api/pandas.DataFrame.groupby.html
:param str annotation_col: name of the column from annotation containing annotation terms.
:param str rejected_col: name of the column from regulation_data containing boolean for
rejected null hypothesis.
:param str group_col: column name for new column in annotation dataframe determining
if feature belongs to foreground or background.
:param str method: method used to compute enrichment
(only 'fisher' is supported currently).
:param str correction: method to be used for multiple-testing correction.
Use ``'efdr'`` to apply empirical FDR via permutation (requires
``permutations > 0``).
:param float alpha: adjusted p-value cutoff to define significance
:param float lfc_cutoff: log fold-change cutoff to define practical significance
:param int permutations: number of permutations used to estimate the empirical FDR
when ``correction='efdr'``. Ignored for other correction methods.
:param int | None seed: seed of the random number generator used for the
permutations when ``correction='efdr'``, for reproducible results.
:return: DataFrame adhering to EnrichmentAnalysisSchema
:rtype: DataFrame[EnrichmentAnalysisSchema]
Example::
result = run_up_down_regulation_enrichment(
regulation_data,
annotation,
identifier='identifier',
groups=['group1',
'group2'],
annotation_col='annotation',
rejected_col='rejected',
group_col='group',
method='fisher',
correction='fdr_bh',
alpha=0.05,
lfc_cutoff=1,
)
"""
if isinstance(groups, str):
groups = [groups]
if isinstance(groups, tuple):
groups = list(groups)
if len(groups) != 2:
raise ValueError("groups should contains exactly two columns.")
ret = []
# In case of multiple comparisons this is used to get all possible combinations
for g1, g2 in regulation_data.groupby(groups).groups:
df = regulation_data.groupby(groups).get_group((g1, g2))
df["up_pairwise_regulation"] = (df[pval_col] <= correction_alpha) & (
df[log2fc_col] >= lfc_cutoff
)
df["down_pairwise_regulation"] = (df[pval_col] <= correction_alpha) & (
df[log2fc_col] <= -lfc_cutoff
)
comparison_tag = str(g1) + "~" + str(g2)
if not regulation_data[identifier].is_unique:
logger.warning(
"Column '%s' in regulation_data contains duplicated values for comparison %s.",
identifier,
comparison_tag,
)
for rej_col, direction in zip(
("up_pairwise_regulation", "down_pairwise_regulation"),
(f"upregulated in {g1}", f"upregulated in {g2}"),
):
_enrichment = run_regulation_enrichment(
df,
annotation,
identifier=identifier,
annotation_col=annotation_col,
rejected_col=rej_col,
group_col=group_col,
method=method,
min_detected_in_set=min_detected_in_set,
correction=correction,
correction_alpha=correction_alpha,
permutations=permutations,
seed=seed,
)
if _enrichment.empty:
logger.warning(
"No significant enrichment found for comparison %s (%s).",
comparison_tag,
direction,
)
_enrichment["direction"] = direction
_enrichment["comparison"] = comparison_tag
ret.append(_enrichment)
ret = pd.concat(ret)
if not ret.empty:
ret["rejected"] = ret["rejected"].astype(bool)
# whole numbers only (e.g. eFDR of 0 or 1) are converted to int upstream
ret[["pvalue", "padj"]] = ret[["pvalue", "padj"]].astype("Float64")
ret = EnrichmentAnalysisSchema.validate(ret)
return ret
def run_regulation_enrichment(
regulation_data: pd.DataFrame,
annotation: pd.DataFrame,
identifier: str = "identifier",
annotation_col: str = "annotation",
rejected_col: str = "rejected",
group_col: str = "group",
method: str = "fisher",
min_detected_in_set: int = 2,
correction: str = "fdr_bh",
correction_alpha: float = 0.05,
permutations: int = 0,
seed: int | None = None,
) -> pd.DataFrame:
"""
This function runs a simple enrichment analysis for significantly regulated features
in a dataset. Only overrepresentation is tested using one-sided Fisher's exact tests.
:param regulation_data: pandas.DataFrame resulting from differential regulation analysis.
:param annotation: pandas.DataFrame with annotations for features
(columns: 'annotation', 'identifier' (feature identifiers), and 'source').
:param str identifier: name of the column from annotation containing feature identifiers.
It should be present both in `regulation_data` and `annotation`. In `regulation_data`
it should be unique, while in `annotation` it can contain duplicates as one
identifier can be part of multiple pathways.
:param str annotation_col: name of the column from annotation containing annotation terms.
:param str rejected_col: name of the column from `regulation_data` containing boolean for
rejected null hypothesis.
:param str group_col: column name for new column in annotation dataframe determining
if feature belongs to foreground or background.
:param str method: method used to compute enrichment (only 'fisher' is supported currently).
:param str correction: method to be used for multiple-testing correction.
Use ``'efdr'`` to apply empirical FDR via permutation (requires
``permutations > 0``).
:param int permutations: number of permutations used to estimate the empirical FDR
when ``correction='efdr'``. Ignored for other correction methods.
:param int | None seed: seed of the random number generator used for the
permutations when ``correction='efdr'``, for reproducible results.
:return: pandas.DataFrame with columns: 'terms', 'identifiers', 'foreground',
'background', 'foreground_pop', 'background_pop', 'pvalue', 'padj' and 'rejected'.
Example::
result = run_regulation_enrichment(
regulation_data,
annotation,
identifier='identifier',
annotation_col='annotation',
rejected_col='rejected',
group_col='group',
method='fisher',
min_detected_in_set=2,
correction='fdr_bh',
correction_alpha=0.05,
)
"""
# ? can we remove NA features in that column?
if regulation_data[rejected_col].isna().any():
raise ValueError(f"Rejected column '{rejected_col}' contains missing values.")
mask_rejected = regulation_data[rejected_col].astype(bool)
if not regulation_data[identifier].is_unique:
raise ValueError(f"Column '{identifier}' in regulation_data has to be unique.")
foreground_list = regulation_data.loc[mask_rejected, identifier]
background_list = regulation_data.loc[~mask_rejected, identifier]
foreground_pop = len(foreground_list)
background_pop = len(regulation_data[identifier])
# needs to allow for missing annotations
# ! this step needs unique identifiers in the regulation_data
# group_col contains either 'foreground', 'background' or NA
annotation[group_col] = annotate_features(
features=annotation[identifier],
in_foreground=foreground_list,
in_background=background_list,
)
annotation = annotation.dropna(subset=[group_col])
result = run_enrichment(
annotation,
foreground_id="foreground",
background_id="background",
foreground_pop=foreground_pop,
background_pop=background_pop,
annotation_col=annotation_col,
group_col=group_col,
identifier_col=identifier,
method=method,
correction=correction,
min_detected_in_set=min_detected_in_set,
correction_alpha=correction_alpha,
permutations=permutations,
seed=seed,
).convert_dtypes(convert_boolean=False)
return result
def run_enrichment(
data: pd.DataFrame,
foreground_id: str,
background_id: str,
foreground_pop: int,
background_pop: int,
min_detected_in_set: int = 2,
annotation_col: str = "annotation",
group_col: str = "group",
identifier_col: str = "identifier",
method: str = "fisher",
correction: str = "fdr_bh",
correction_alpha: float = 0.05,
permutations: int = 0,
seed: int | None = None,
) -> pd.DataFrame:
"""
Computes overrepresentation of the foreground relative to a given background,
using Fisher's exact test with ``alternative='greater'``, and corrects for
multiple hypothesis testing.
:param data: pandas.DataFrame with annotations for dataset features
(columns: 'annotation', 'identifier', 'group').
:param str foreground_id: group identifier of features that belong to the foreground.
:param str background_id: group identifier of features that belong to the background.
:param int foreground_pop: number of features in the foreground.
:param int background_pop: number of features in the background.
:param int min_detected_in_set: minimum number of features in the foreground
:param str annotation_col: name of the column containing annotation terms.
:param str group_col: name of column containing the group identifiers,
e.g. specifying belonging to 'foreground' or 'background'.
:param str identifier_col: name of column containing dependent variables identifiers.
:param str method: method used to compute enrichment
(only 'fisher' is supported currently).
:param str correction: method to be used for multiple-testing correction.
Use ``'efdr'`` to apply empirical FDR via permutation (requires
``permutations > 0``).
See `statsmodels.stats.multitest.multipletests`_ for other supported methods.
.. _statsmodels.stats.multitest.multipletests: https://www.statsmodels.org/dev/\
generated/statsmodels.stats.multitest.multipletests.html
:param float correction_alpha: adjusted p-value cutoff to define significance.
:param int permutations: number of permutations used to estimate the empirical FDR
when ``correction='efdr'``. Ignored for other correction methods.
:param int | None seed: seed of the random number generator used for the
permutations when ``correction='efdr'``, for reproducible results.
Must be greater than 0 when ``correction='efdr'``.
:return: pandas.DataFrame with columns: annotation terms, features,
number of foreground/background features in each term,
p-values and corrected p-values
Columns are: 'terms', 'identifiers',
'foreground', 'background', 'foreground_pop', 'background_pop',
'pvalue', 'padj' and 'rejected'.
:raises ValueError: if ``correction='efdr'`` and ``permutations`` is not > 0.
Example::
result = run_enrichment(
data,
foreground='foreground',
background='background',
foreground_pop=len(foreground_list),
background_pop=len(background_list),
annotation_col='annotation',
group_col='group',
identifier_col='identifier',
method='fisher',
)
Example with eFDR::
result = run_enrichment(
data,
foreground='foreground',
background='background',
foreground_pop=len(foreground_list),
background_pop=len(background_list),
annotation_col='annotation',
group_col='group',
identifier_col='identifier',
method='fisher',
correction='efdr',
permutations=100,
)
"""
if method != "fisher":
raise ValueError("Only Fisher's exact test is supported at the moment.")
if correction == "efdr" and permutations <= 0:
raise ValueError(
"permutations must be greater than 0 when correction='efdr'. "
"Left at zero so it is set explicitly to e.g. 10000."
)
terms = []
ids = []
pvalues = []
fnum = []
bnum = []
# Aggregate only the identifier column, without copying unrelated metadata.
counts = data.groupby([annotation_col, group_col], observed=True)[
identifier_col
].count()
counts = counts[
counts.index.get_level_values(group_col).isin([foreground_id, background_id])
]
foreground_ids = (
data.loc[data[group_col] == foreground_id, [annotation_col, identifier_col]]
.groupby(annotation_col, observed=True)[identifier_col]
.agg(",".join)
)
for annotation in foreground_ids.index:
num_foreground = int(counts.get((annotation, foreground_id), 0))
num_background = int(counts.get((annotation, background_id), 0))
if num_foreground < min_detected_in_set:
continue
_, pvalue = run_fisher(
[num_foreground, foreground_pop - num_foreground],
[num_background, background_pop - foreground_pop - num_background],
alternative="greater",
)
fnum.append(num_foreground)
bnum.append(num_background)
terms.append(annotation)
pvalues.append(pvalue)
ids.append(foreground_ids.get(annotation, ""))
if len(pvalues) >= 1:
if correction == "efdr":
rejected, padj = permutations_enrichment._compute_efdr_for_enrichment(
data=data.loc[
data[group_col].isin([foreground_id, background_id]),
[annotation_col, identifier_col],
],
observed_pvalues=pvalues,
foreground_pop=foreground_pop,
background_pop=background_pop,
min_detected_in_set=min_detected_in_set,
annotation_col=annotation_col,
identifier_col=identifier_col,
permutations=permutations,
alpha=correction_alpha,
seed=seed,
)
else:
rejected, padj = apply_pvalue_correction(
pvalues,
alpha=correction_alpha,
method=correction,
)
result = pd.DataFrame(
{
"terms": terms,
"identifiers": ids,
"foreground": fnum,
"background": bnum,
"foreground_pop": foreground_pop, # no. of foreground features, constant
"background_pop": background_pop, # no. of included features, constant
"pvalue": pvalues,
"padj": padj,
"rejected": rejected.astype(bool),
}
)
result = result.sort_values(by="padj", ascending=True)
else:
logger.warning(
"No significant enrichment found with the given parameters. "
"Returning an empty DataFrame."
)
# ToDo: Should we return an empty DataFrame with the expected columns?
result = pd.DataFrame()
return result
def run_ssgsea(
data: pd.DataFrame,
annotation: str,
set_index: list[str] | None = None,
annotation_col: str = "annotation",
identifier_col: str = "identifier",
outdir: str = "tmp",
min_size: int = 15,
max_size: int = 500,
scale: bool = False,
permutations: int = 0,
) -> pd.DataFrame:
"""
Project each sample within a data set onto a space of gene set enrichment scores using
the single sample gene set enrichment analysis (ssGSEA) projection methodology
described in Barbie et al., 2009:
https://www.nature.com/articles/nature08460#Sec3 (search "Single Sample" GSEA).
:param pd.DataFrame data: pandas.DataFrame with the quantified features
(i.e. subject x proteins)
:param str annotation: pandas.DataFrame with the annotation to be used in the
enrichment (i.e. CKG pathway annotation file)
:param list[str] set_index: column/s to be used as index. Enrichment will be
calculated for these values (i.e ["subject"] will return subjects x pathways
matrix of enrichment scores)
:param str annotation_col: name of the column containing annotation terms.
:param str identifier_col: name of column containing dependent variables identifiers.
:param str out_dir: directory path where results will be stored
(default None, tmp folder is used)
:param int min_size: minimum number of features (i.e. proteins) in enriched terms
(i.e. pathways)
:param int max_size: maximum number of features (i.e. proteins) in enriched terms
(i.e. pathways)
:param bool scale: whether or not to scale the data
:param int permutations: number of permutations used in the ssgsea analysis
:return: pandas.DataFrame containing unnormalized enrichment scores (`ES`)
for each sample, and normalized enrichment scores (`NES`)
with the enriched `Term` and sample `Name`.
:rtype: pandas.DataFrame
Example::
stproject = "P0000008"
p = project.Project(
stproject,
datasets={},
knowledge=None,
report={},
configuration_files=None,
)
p.build_project(False)
p.generate_report()
proteomics_dataset = p.get_dataset("proteomics")
annotations = proteomics_dataset.get_dataframe("pathway annotation")
processed = proteomics_dataset.get_dataframe('processed')
result = run_ssgsea(
processed,
annotations,
annotation_col='annotation',
identifier_col='identifier',
set_index=['group',
'sample',
'subject'],
outdir=None,
min_size=10,
scale=False,
permutations=0
)
"""
df = data.copy()
if not os.path.exists(outdir):
os.makedirs(outdir)
# Comine columns to create a unique name for each set (?)
name = []
if set_index is None:
index = data.index.to_frame()
set_index = index.columns.tolist()
else:
index = data[set_index]
df = df.drop(set_index, axis=1)
for _, row in index.iterrows():
name.append(
"_".join(row[set_index].tolist())
) # this assumes strings as identifiers
df["Name"] = name
index.index = name
df = df.set_index("Name").transpose()
if annotation_col not in annotation:
raise ValueError(
f"Missing Annotation Column: {annotation_col}"
" as specified by `annotation_col`"
)
if identifier_col not in annotation:
raise ValueError(
f"Missing Identifier Column: {identifier_col}"
" as specified by `identifier_col`"
)
# ! Legacy implementation that was missing the description column of gmt files:
# ?gmt-columns: term<TAB>description<TAB>gene1<TAB>gene2
# grouped_annotations = (
# annotation.groupby(annotation_col)[identifier_col].apply(list).reset_index()
# )
# gene_sets = os.path.join(outdir, "ss_gsea" + ".gmt")
# with open(gene_sets, "w", encoding="utf8") as out:
# for _, row in grouped_annotations.iterrows():
# out.write(
# row[annotation_col]
# + "\t"
# + "\t".join(list(filter(None, row[identifier_col])))
# + "\n"
# )
# In case annotations were not provided by acore itself:
annotation = annotation.dropna(subset=[identifier_col])
annotation = annotation[annotation[identifier_col] != ""]
gene_sets = annotation.groupby(annotation_col)[identifier_col].apply(list).to_dict()
enrichment = gp.ssgsea(
data=df,
gene_sets=gene_sets,
outdir=outdir,
min_size=min_size,
max_size=max_size,
scale=scale,
permutation_num=permutations,
no_plot=True,
processes=1,
seed=10,
format="png",
)
result = pd.DataFrame(enrichment.res2d).set_index("Name")
# potentially return wide format in separate format
# result = {"es": enrichment_es, "nes": enrichment_nes}
return result
[docs]
def run_gsea(
regulation_data: pd.DataFrame,
annotation: pd.DataFrame,
identifier: str = "identifier",
ranking_col: str = "log2FC",
annotation_col: str = "annotation",
identifier_col: str = "identifier",
min_size: int = 15,
max_size: int = 500,
permutation_num: int = 1000,
seed: int = 42,
weight: float = 1.0,
ascending: bool = False,
correction_alpha: float = 0.05,
) -> pd.DataFrame:
"""
Run Gene Set Enrichment Analysis (GSEA) on a pre-ranked gene list using
`gseapy.prerank <https://gseapy.readthedocs.io/en/latest/run.html#gseapy.prerank>`_.
Genes are ranked by `ranking_col` (e.g. log2 fold-change from differential
expression analysis) and enrichment is computed for each gene set defined in
`annotation`. Unlike overrepresentation analysis (ORA), GSEA does not require
a significance cutoff to split genes into foreground/background; it uses the
full ranked list instead.
:param pd.DataFrame regulation_data: DataFrame with differential regulation
results. Must contain the column specified by `identifier` and the
column specified by `ranking_col`. Each identifier must appear at most
once (duplicates are not allowed).
:param pd.DataFrame annotation: DataFrame with functional annotations.
Must contain `annotation_col` and `identifier_col`.
:param str identifier: Column in `regulation_data` containing feature
identifiers. These must match the identifiers in `annotation_col` of
the `annotation` DataFrame.
:param str ranking_col: Column in `regulation_data` used to rank the
features (e.g. ``'log2FC'``). Features are ranked in descending order
by default so that strongly up-regulated features appear at the top.
:param str annotation_col: Column in `annotation` containing annotation
term names (e.g. pathway names).
:param str identifier_col: Column in `annotation` containing feature
identifiers that map to `identifier` in `regulation_data`.
:param int min_size: Minimum number of features in a gene set that are also
present in the ranking. Gene sets smaller than this threshold are
excluded.
:param int max_size: Maximum number of features in a gene set that are also
present in the ranking. Gene sets larger than this threshold are
excluded.
:param int permutation_num: Number of permutations used to estimate nominal
p-values. Higher values give more precise p-values but increase runtime.
Default: 1000.
:param int seed: Random seed for reproducibility. Default: 42.
:param float weight: Exponent for the enrichment score weighting.
``weight=1`` (default) corresponds to weighted GSEA; ``weight=0``
gives classic (unweighted) GSEA.
:param bool ascending: Sort order for the ranking. ``False`` (default)
means descending (highest ranking metric first, suited for log2FC).
:param float correction_alpha: FDR q-value cutoff used to define the
``'rejected'`` column. Default: 0.05.
:return: DataFrame with one row per gene set. Columns are: ``'Term'``,
``'ES'`` (enrichment score), ``'NES'`` (normalised enrichment score),
``'NOM p-val'`` (nominal p-value), ``'FDR q-val'`` (gseapy's
permutation-based FDR q-value), ``'FWER p-val'``, ``'Tag %'``,
``'Gene %'``, ``'Lead_genes'`` (semi-colon-separated leading-edge genes)
and ``'rejected'`` (``'FDR q-val' <= correction_alpha``).
:rtype: pd.DataFrame
Example::
result = run_gsea(
regulation_data=diff_reg,
annotation=annotations,
identifier='identifier',
ranking_col='log2FC',
annotation_col='annotation',
identifier_col='identifier',
min_size=5,
permutation_num=100,
)
"""
if regulation_data is None or regulation_data.empty:
raise ValueError("regulation_data is empty.")
if identifier not in regulation_data.columns:
raise ValueError(
f"Column '{identifier}' not found in regulation_data. "
"Specify the correct column name via `identifier`."
)
if ranking_col not in regulation_data.columns:
raise ValueError(
f"Column '{ranking_col}' not found in regulation_data. "
"Specify the correct column name via `ranking_col`."
)
if not regulation_data[identifier].is_unique:
raise ValueError(
f"Column '{identifier}' in regulation_data contains duplicate values. "
"Each identifier must appear at most once."
)
if annotation_col not in annotation.columns:
raise ValueError(
f"Missing annotation column '{annotation_col}' in annotation DataFrame. "
"Specify the correct column name via `annotation_col`."
)
if identifier_col not in annotation.columns:
raise ValueError(
f"Missing identifier column '{identifier_col}' in annotation DataFrame. "
"Specify the correct column name via `identifier_col`."
)
# Build the pre-ranked Series: index = feature identifier, value = ranking metric
rnk = regulation_data.set_index(identifier)[ranking_col].dropna()
# Build gene sets dict from the annotation DataFrame
gene_sets = annotation.groupby(annotation_col)[identifier_col].apply(list).to_dict()
enrichment = gp.prerank(
rnk=rnk,
gene_sets=gene_sets,
min_size=min_size,
max_size=max_size,
permutation_num=permutation_num,
weight=weight,
ascending=ascending,
seed=seed,
no_plot=True,
outdir=None,
)
result = pd.DataFrame(enrichment.res2d).drop(columns=["Name"])
# 'Tag %' and 'Gene %' are strings like "5/20" and "10.00%", so they stay as is.
numeric_cols = ["ES", "NES", "NOM p-val", "FDR q-val", "FWER p-val"]
non_numeric_cols = result[numeric_cols].select_dtypes(exclude="number").columns
if len(non_numeric_cols):
result[non_numeric_cols] = result[non_numeric_cols].apply(
pd.to_numeric, errors="coerce"
)
fdr = result["FDR q-val"]
result["rejected"] = (fdr <= correction_alpha).where(fdr.notna()).astype("boolean")
return result