Metadata-Version: 2.4
Name: pysigscore
Version: 0.1.0
Summary: A flexible framework for gene set enrichment benchmarking in bulk and single-cell transcriptomics.
Author-email: Tommaso Giacomello <tommaso.giacomello@phd.unibocconi.it>
Maintainer-email: Tommaso Giacomello <tommaso.giacomello@phd.unibocconi.it>
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/bioinformatics-hub/pysigscore
Project-URL: Repository, https://github.com/bioinformatics-hub/pysigscore
Project-URL: Documentation, https://github.com/bioinformatics-hub/pysigscore#readme
Project-URL: Issues, https://github.com/bioinformatics-hub/pysigscore/issues
Keywords: bioinformatics,gene set enrichment,gene signatures,RNA-seq,single-cell RNA-seq,transcriptomics,omics
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.12
Requires-Python: !=3.11.*,<3.13,>=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.23
Requires-Dist: scipy>=1.9
Requires-Dist: pandas>=1.5
Requires-Dist: anndata>=0.9
Requires-Dist: scikit-learn>=1.1
Requires-Dist: numba>=0.56
Requires-Dist: h5py>=3.7
Requires-Dist: matplotlib>=3.5
Requires-Dist: seaborn>=0.12
Provides-Extra: aucell
Requires-Dist: pyscenic>=0.12; extra == "aucell"
Requires-Dist: ctxcore>=0.2; extra == "aucell"
Requires-Dist: setuptools<81; extra == "aucell"
Provides-Extra: gsea
Requires-Dist: gseapy>=1.1; extra == "gsea"
Provides-Extra: full
Requires-Dist: pyscenic>=0.12; extra == "full"
Requires-Dist: ctxcore>=0.2; extra == "full"
Requires-Dist: setuptools<81; extra == "full"
Requires-Dist: gseapy>=1.1; extra == "full"
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"
Requires-Dist: pytest-cov>=5; extra == "test"
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-cov>=5; extra == "dev"
Requires-Dist: build>=1.2; extra == "dev"
Requires-Dist: twine>=5; extra == "dev"
Dynamic: license-file

# pysigscore

[![PyPI version](https://badge.fury.io/py/pysigscore.svg)](https://badge.fury.io/py/pysigscore)
![Python 3.10](https://img.shields.io/badge/Python-3.10-blue.svg)
![Python 3.12](https://img.shields.io/badge/Python-3.12-blue.svg)
[![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)

**pysigscore** is a Python framework for computing and comparing gene signature scores in bulk and single-cell transcriptomics data. It also evaluates score significance and signature robustness and provides plotting utilities for result comparison.

---

## Features

- **18 built-in scores:** 14 direct statistics, one Z-score, and three rank-based methods (AUCell, ssGSEA, and GSVA).
- **Custom scorer:** supply your own scoring function and compare it with the built-in methods.
- **Flexible transformation pipelines:** log, min-max scale, standardisation, KNN smoothing, step, and quantile normalisation — composable as ordered pipelines.
- **Permutation and analytical p-values:** assess significance of each computed score.
- **Leave-one-out (LOO) robustness experiments:** show robustness of each gene signature.
- **Summary metrics and visualisation:** histograms, boxplots, violin plots, heatmaps, cross-scorer comparisons, sample-set comparisons and statistical test plots.
- **HDF5 serialisation:** save and reload the object to restart the analysis.

---

## Installation

### From source (development)

```bash
git clone https://github.com/bioinformatics-hub/pysigscore.git
cd pysigscore
pip install -e ".[full, dev]"
```

### Core dependencies

Installed automatically:
```text
numpy>=1.23
scipy>=1.9
pandas>=1.5
anndata>=0.9
scikit-learn>=1.1
numba>=0.56
h5py>=3.7
matplotlib>=3.5
seaborn>=0.12
```

### Optional dependencies

| Extra | Methods unlocked | Install command |
|-------|-----------------|-----------------|
| `aucell` | AUCell | `pip install -e ".[aucell]"` |
| `gsea` | ssGSEA, GSVA | `pip install -e ".[gsea]"` |
| `full` | `aucell` + `gsea` | `pip install -e ".[full]"` |
| `test` | pytests | `pip install -e ".[test]"` |
| `dev` |  dev libraries + `test` | `pip install -e ".[dev]"` |

Version details in `pyproject.toml`

---

## Quick Start

```python
import anndata as ad
from pysigscore import Scorer, configure_logging

# Optional: show progress messages during computations
configure_logging()

# ── 1. Prepare data (cells × genes AnnData) ─────────────────────────────────
adata = ad.read_h5ad("your_data.h5ad")   # rows = cells/samples, cols = genes

# ── 2. Initialise scorer ────────────────────────────────────────────────────
scorer = Scorer(
    random_state=42,
    name="MyProject",     # output folder name
    path=".",             # parent directory
)

# ── 3. Import expression data ──────────────────────────────────────────────
scorer.import_adata(adata)
# or from a DataFrame:
# scorer.import_adata_as_dataframe(df, cluster=cluster_labels)

# ── 4. Import gene sets ────────────────────────────────────────────────────
geneset_dict = {
    "Hallmarks_1": ["VEGFA", "GAPDH", "SLC2A1"],
    "Hallmarks_2":  ["MDM2", "CDKN1A", "BBC3"],
}
scorer.import_geneset_dict(geneset_dict, keys="genesets")

# ── 5. Compute enrichment scores ───────────────────────────────────────────
scorer.compute_scores(
    transform_list_data=["raw", "log"],      # score raw and log-transformed expression
    transform_list_enrich=["raw"],            # keep score matrices untransformed
    enrich_grid={
        "Mean":   {"remove_na": True},
        "Median": {"remove_na": True},
        "Z":      {"remove_na": True, "ddof": 0},
    },
    p_value_grid={                           # 1,000 permutation p-values for Mean/Median;
        "Mean": 1000,                       # analytical two-sided normal p-values for Z
        "Median": 1000,
        "Z": True,
    },
    loo_grid={"Median": ["log"]},           # LOO errors for Median on log-transformed data
    geneset_list_loo_genewise=["Hallmarks_1"],
)
# CSV files are saved under MyProject/<ScoreName>/.

# ── 6. Return results of objects of interest ───────────────────────────────

# Return computed matrices
results = scorer.return_values(
    value="score",
    enrich=["Mean"],
    data_trans=["raw"],
    enrich_trans=["raw"],
    geneset_list=["Hallmarks_1"],
    samples_list=None,  # use all samples
)

# ── 7. Compute Summary Metrics ──────────────────────────────────────────────

# Compute one mean Z score for Hallmarks_2 across all samples
metrics = scorer.return_metrics(
    value="score",
    metric="mean",
    enrich=["Z"],
    axis=0,
    data_trans=["raw"],
    enrich_trans=["raw"],
    geneset_list=["Hallmarks_2"],
    samples_list=None,
    return_results=True,
)
# Saved as MyProject/Z/metrics/Z_score_mean.csv

```

---

## API Overview

### `Scorer` class

```python
Scorer(random_state=42, name='Project01', path=None, dtype=None)
```

| Method | Description |
|--------|-------------|
| `import_adata(adata, cluster_column_name=None)` | Load a samples × genes AnnData object; optional cluster labels |
| `import_adata_as_dataframe(df, cluster=None)` | Load a samples × genes DataFrame; optional cluster labels |
| `import_geneset_dict(geneset_info, keys='genesets', ...)` | Load gene sets from a dict |
| `import_geneset_matrix(geneset_info, ...)` | Load gene sets from a binary DataFrame or ndarray |
| `import_geneset_matrix_from_celloracle(base_GRN, ...)` | Load from a CellOracle base GRN DataFrame |
| `compute_scores(...)` | Compute selected scores for each requested transformation, with optional p-values and LOO errors |
| `return_values(value, enrich, data_trans, enrich_trans, ...)` | Return selected result matrices as `{csv_stem: DataFrame}` |
| `return_metrics(value, metric, ...)` | Save summary metric CSVs and optionally return them |
| `plot_values(value, method, ...)` | Save histogram, boxplot, violin or scatter plots |
| `plot_heatmap(value, ...)` | Save heatmaps of score, pvalue or LOO matrices |
| `plot_heatmap_loo_genewise(enrich, ...)` | Plot gene-wise LOO errors and optionally return the matrices |
| `plot_heatmap_corr(value, ...)` | Save geneset correlation heatmaps of scores, pvalues or LOO matrices |
| `plot_comparison_distribution(...)` | Compare score, p-value, or LOO distributions across scoring methods |
| `plot_cross_scorer_corr(...)` | Plot cross-scorer correlation for one gene set |
| `plot_distribution_sampleset(...)` | Compare one geneset across multiple sample groups |
| `stat_test_geneset_between_samples(...)` | Mann–Whitney U comparison between two sample groups |
| `stat_test_samples_between_genesets(...)` | Mann–Whitney U comparison between two genesets |
| `to_hdf5(file_path)` | Serialise the Scorer to HDF5 (`.pysigscore.object`) |

Use the module-level `load_pysigscore(file_path)` function to restore a saved `Scorer` object.


### Supported Data Transformations

Data and post enrichment transformations are the following:

| Transformation  | Description |
|-----------------|-------------|
| raw | No transformation |
| log | Either ln(1+x) or log2(1+x) |
| scale | min-max scale |
| standard | Standard-deviation or MAD scaling, with optional centring |
| KNN | CellOracle KNN smoothing |
| quantile | Equalise data distributions across genes or samples |
| step | Apply step thresholding to the data |

Transformations can be composed as space-separated names, such as `"log KNN standard"` or `"log scale"`; `"raw"` must be used alone. The default parameters can be overridden with `transform_hyper`:

```python
transform_hyper = {"log": {"type": "natural_log"},
                          "KNN": {"div_by_std": False, "max_n_components": 50,
                                  "pca_curvature": 0.002, "k": None,
                                  "metric": "euclidean", "diag": 1.0,
                                  "n_pca_dims": None, "b_sight": None,
                                  "b_maxl": None, "group_constraint": None,
                                  "n_jobs": -1},
                           "scale": {"feature_range": (0,1),"axis_scale": 0},
                           "standard": {"subtract": False, "robust": False, "axis_standard": 0},
                           "step": {"y": (-1.0, 0.0, 1.0), "thr": None,
                                    "method": "median", "axis_step": 0},
                           "quantile": {"kind": "mergesort", "axis_quantile": 0}}
```

### Supported enrichment scores

| Score | Key | Description |
|-------|-----|-------------|
| Sum | `'Sum'` | Sum of all values in the signature |
| Weighted Sum | `'WeightedSum'` | Sum with gene-specific weights |
| Mean | `'Mean'` | Arithmetic average of values in the signature |
| Weighted Mean | `'WeightedMean'` | Mean after applying gene-specific weights |
| Median | `'Median'` | Median of all values in the signature |
| Trimmed Mean | `'TrimmedMean'` | Mean after removing extreme values of signature genes |
| Mode | `'Mode'` | Most frequent value in the signature |
| Mid-Range | `'MidRange'` | Average of min and max values in the signature  |
| Mid-Hinge | `'MidHinge'` | Average of first and third quartiles in the signature |
| Tukey Trimean | `'TriMean'` | Weighted average of median and quartiles in the signature  |
| IQR | `'IQR'` | Interquartile range (Q3 − Q1) of values in the signature |
| IQM | `'IQM'` | Mean of values within the IQR in the signature  |
| MAD | `'MAD'` | Median absolute deviation of signature genes |
| AAD | `'AAD'` | Average absolute deviation of signature genes  |
| Z-score | `'Z'` | Signature mean relative to the sample-wide mean and standard deviation |
| AUCell | `'Aucell'` | AUC-based gene set activity score |
| ssGSEA | `'ssGSEA'` | Single-sample GSEA enrichment score |
| GSVA | `'GSVA'` | Non-parametric gene set variation score |
| Custom | `'Custom'` | User-defined scoring function |

Pass only the methods to run in `enrich_grid`; omitted parameters use the defaults shown below.

```python
enrich_grid = {
    "Sum":         {"remove_na": True},
    "WeightedSum": {"remove_na": True},
    "Mean":        {"remove_na": True},
    "WeightedMean":{"remove_na": True},
    "Median":      {"remove_na": True},
    "TrimmedMean": {"remove_na": True, "quantile": 0.05},
    "Mode":        {"remove_na": True},
    "MidRange":    {"remove_na": True},
    "MidHinge":    {"remove_na": True},
    "TriMean":     {"remove_na": True},
    "IQR":         {"remove_na": True},
    "IQM":         {"remove_na": True},
    "MAD":         {"remove_na": True},
    "AAD":         {"remove_na": True},
    "Z":           {"remove_na": True, "ddof": 0},
    "Aucell":      {"auc_threshold": 0.05, "noweights": False},
    "ssGSEA":      {"sample_norm_method": "rank", "correl_norm_type": "rank",
                    "min_size": None, "max_size": None, "weight": 0,
                    "ascending": False, "NES": True, "p_val": "NOM p-val"},
    "GSVA":        {"min_size": None, "max_size": None, "weight": 0,
                    "kcdf": None, "mx_diff": True, "abs_rnk": False},
    "Custom":      {"custom_func": None, "custom_pval": None},
}
```

### Custom scorer

A custom scorer receives `arr_geneset`, containing the signature-gene values for one sample, and `arr_full`, containing all gene values for that sample. It must return one numeric score.

```python
def custom_score_example(arr_geneset, arr_full):
    return float(arr_geneset.max() - arr_geneset.min())

scorer.compute_scores(
    enrich_grid={"Custom": {"custom_func": custom_score_example}},
)
```

---

## Summaries

`return_metrics` summarises saved score, p-value, or average LOO matrices using the mean, median, standard deviation, or coefficient of variation (`cv`). Use `axis=0` to summarise each gene set across samples and `axis=1` to summarise each sample across gene sets. Results are saved in `<path>/<name>/<ScoreName>/metrics/`; set `return_results=True` to also return them as `{csv_stem: DataFrame}`.

---

## Plotting

### Single-matrix visualisation

`plot_values` creates histograms, boxplots, violin plots, or scatter plots from one saved matrix. `axis=0` plots each gene set across samples, `axis=1` plots each sample across gene sets, and `whole=True` plots all entries together. `plot_heatmap` displays a selected matrix, while `plot_heatmap_loo_genewise` displays the error caused by removing each gene from a signature. PDFs are saved in `<path>/<name>/<ScoreName>/plots/`.

```python
# Distribution of one gene set across samples
scorer.plot_values(
    value="score",
    method="boxplot",
    enrich=["Median"],
    data_trans=["log"],
    enrich_trans=["raw"],
    geneset_list=["Hallmarks_1"],
    axis=0,
    name="per_geneset",
)

# Full score matrix
scorer.plot_heatmap(
    value="score",
    enrich=["Mean"],
    data_trans=["raw"],
    enrich_trans=["raw"],
    geneset_list=["Hallmarks_1", "Hallmarks_2"],
    name="scores",
)

# Per-gene LOO errors computed in the Quick Start
scorer.plot_heatmap_loo_genewise(
    enrich=["Median"],
    data_trans=["log"],
    geneset_list=["Hallmarks_1"],
    name="loo",
)
```

### Multiple-matrix comparisons

| Method | Purpose |
|--------|---------|
| `plot_comparison_distribution` | Compare one gene set's distribution across scoring methods |
| `plot_cross_scorer_corr` | Correlate scoring methods across samples for one gene set |
| `plot_distribution_sampleset` | Compare one gene set across user-defined sample groups |
| `stat_test_geneset_between_samples` | Test whether the first sample group has lower values than the second |
| `stat_test_samples_between_genesets` | Test whether the first gene set has lower values than the second |

The two statistical methods use a one-sided Mann–Whitney U test, so group or gene-set order matters. All comparison plots are saved in `<path>/<name>/Comparisons/`.

```python
groups = {
    # Replace these placeholders with sample IDs from scorer.samples.
    "Control": ["Cell_1", "Cell_2"],
    "Treatment": ["Cell_3", "Cell_4"],
}

scorer.plot_comparison_distribution(
    value="score", method="boxplot",
    enrich=["Mean", "Median", "Z"],
    data_trans="raw", enrich_trans="raw",
    geneset="Hallmarks_1", name="scorer_distribution",
)

scorer.plot_cross_scorer_corr(
    value="score", enrich=["Mean", "Median", "Z"],
    data_trans="raw", enrich_trans="raw",
    geneset="Hallmarks_1", samples_list=None,
    corr_method="spearman", name="scorer_corr",
)

scorer.plot_distribution_sampleset(
    value="score", method="violinplot", enrich="Mean",
    data_trans="raw", enrich_trans="raw",
    geneset="Hallmarks_1", samples_dict=groups,
    name="sample_groups",
)

scorer.stat_test_geneset_between_samples(
    value="score", method="boxplot", enrich="Mean",
    data_trans="raw", enrich_trans="raw",
    geneset="Hallmarks_1", samples_dict=groups,
    name="sample_test",
)

scorer.stat_test_samples_between_genesets(
    value="score", method="boxplot", enrich="Mean",
    data_trans="raw", enrich_trans="raw",
    geneset_list=["Hallmarks_1", "Hallmarks_2"],
    samples_list=None, name="geneset_test",
)
```

---

## Output Structure

Each scorer stores its matrices, metrics, and plots under `<path>/<name>/<ScoreName>/`. Cross-scorer and sample-group comparisons are stored under `<path>/<name>/Comparisons/`. The examples above produce the following main files (`...` represents the compared labels inserted into statistical-test filenames):

```text
MyProject/
    Mean/
        scores/
            Mean_score_Data_raw_Enrich_raw.csv
            Mean_score_Data_log_Enrich_raw.csv
        pvalues/
            Mean_pvalue_Data_raw_n_perm_1000.csv
            Mean_pvalue_Data_log_n_perm_1000.csv
        plots/
            Heatmap_Mean_score_Data_raw_Enrich_raw_scores.pdf
    Median/
        scores/
            Median_score_Data_raw_Enrich_raw.csv
            Median_score_Data_log_Enrich_raw.csv
        pvalues/
            Median_pvalue_Data_raw_n_perm_1000.csv
            Median_pvalue_Data_log_n_perm_1000.csv
        loo_average/
            Median_loo_Data_log_Metric_MAE.csv
        loo_genewise/
            Median_loo_genewise_Hallmarks_1_Data_log_Metric_MAE.csv
        plots/
            BoxPlot_Median_score_Data_log_Enrich_raw_Hallmarks_1_per_geneset.pdf
            Heatmap_Median_loo_genewise_Hallmarks_1_Data_log_Metric_MAE_loo.pdf
    Z/
        scores/
            Z_score_Data_raw_Enrich_raw.csv
            Z_score_Data_log_Enrich_raw.csv
        pvalues/
            Z_pvalue_Data_raw.csv
            Z_pvalue_Data_log.csv
        metrics/
            Z_score_mean.csv
    Comparisons/
        CompDist_score_Data_raw_Enrich_raw_Hallmarks_1_scorer_distribution.pdf
        CrossScorerCorr_score_Data_raw_Enrich_raw_Hallmarks_1_scorer_corr.pdf
        DistrSampleSet_Mean_score_Data_raw_Enrich_raw_Hallmarks_1_sample_groups.pdf
        MW_SampleSet_Mean_score_Data_raw_Enrich_raw_..._sample_test.pdf
        MW_Genesets_Mean_score_Data_raw_Enrich_raw_..._geneset_test.pdf
```

Score matrices, p-value matrices, and average LOO matrices have **samples/cells as rows** and **gene sets as columns**.
Gene-wise LOO files have **samples/cells as rows** and **genes from one selected gene set as columns**.

---

## Notes

- **KNN smoothing** requires enough cells to produce a meaningful neighbourhood graph.
- **LOO experiments** become more expensive as the number of samples and signature genes grows; rank-based methods can be especially slow.

---

## Code Matters

Small parts of the code, explicitly cited, are inspired by these two libraries:

- https://github.com/morris-lab/CellOracle/tree/master/celloracle
- https://github.com/afrendeiro/page-enrichment

Claude Code Sonnet 4.6 was also used for code review and documentation. All AI-written code was manually reviewed and tested.

---

## pysigQC-metrics

As an optional step, **pysigQC** can be used before scoring to assess how well each gene set is represented in the input expression data:

- https://github.com/Gennappio/pysigQC-metrics (smaller version)
- https://github.com/Gennappio/pysigQC (more extended version)

---

## Citation

If you use pysigscore in your research, please cite:

```bibtex
@article{10.1093/bioadv/vbag021,
        author = {Barberis, Alessandro and Buffa, Francesca M},
        title = {Sigscores: summary scores for molecular signatures in R},
        journal = {Bioinformatics Advances},
        volume = {6},
        number = {1},
        pages = {vbag021},
        year = {2026},
        month = {01},
        issn = {2635-0041},
        doi = {10.1093/bioadv/vbag021},
        url = {https://doi.org/10.1093/bioadv/vbag021},
    }
```
