Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 27 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,58 +11,67 @@
[style-image]: https://img.shields.io/badge/code%20style-black-000000.svg
[style-url]: https://github.com/psf/black

Sonata is a Python toolkit for mutational signature analysis with non-negative
matrix factorization (NMF). It stores count matrices and learned signatures in
[AnnData](https://anndata.readthedocs.io/en/latest/) objects and provides model
fitting, analysis helpers, and plotting functions for signature workflows.
Sonata is a Python toolkit for fitting and analyzing mutational signatures. It
fits signatures and exposures in
[AnnData](https://anndata.readthedocs.io/en/latest/) objects and provides
analysis and plotting APIs for signature workflows.

## Installation

```bash
pip install sonata-learn
```

The package is installed as `sonata-learn` and imported as `sonata`.

## Quickstart

```python
import anndata as ad
import pandas as pd
import sonata as so

counts = pd.read_csv("data/hrdetect_counts_training.csv", index_col=0).T
adata = ad.AnnData(counts)

model = so.models.NMF(n_signatures=6)
model.fit(adata.copy())
model.fit(adata)

so.pl.barplot(model.asignatures)
so.pl.stacked_barplot(model.exposures, annotate_obs=False)
so.pl.stacked_barplot(model.exposures)

so.tl.reduce_dimension(
model.adata,
basis="exposures",
method="umap",
random_state=42,
)
so.pl.embedding(model.adata, basis="umap")
```

## Tutorial
## Data Format

Sonata expects mutation counts in an `AnnData` object:

- `adata.X`: count matrix with shape `n_samples x n_mutation_types`.
- `adata.obs`: optional sample annotations.
- `adata.var`: optional mutation-type annotations.

After fitting, the model stores learned signatures in `model.asignatures` and
sample exposures in `model.adata.obsm["exposures"]`.

## Documentation

For a complete workflow covering data preparation, NMF, visualization,
fixed signatures, Cornet, and simple model selection, see the
[Markdown tutorial](docs/tutorial.md). A runnable notebook with the same
analysis and figure-generation code is available at
[docs/tutorial.ipynb](docs/tutorial.ipynb).
[Markdown tutorial][tutorial-md]. A runnable notebook with the same analysis and
figure-generation code is available at [docs/tutorial.ipynb][tutorial-ipynb].

## Models

Sonata currently exposes three NMF models:
Sonata currently exposes three algorithms:

- `so.models.NMF`: NMF with the generalized Kullback-Leibler divergence.
- `so.models.MvNMF`: minimum-volume NMF.
- `so.models.Cornet`: correlated NMF with sample and signature embeddings.
- `so.models.Cornet`: correlated NMF with joint sample and signature embeddings.

## License

MIT

[tutorial-md]: https://github.com/parklab/Sonata/blob/main/docs/tutorial.md
[tutorial-ipynb]: https://github.com/parklab/Sonata/blob/main/docs/tutorial.ipynb
Binary file modified docs/images/cornet-signatures.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/nmf-exposures.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/nmf-fixed-signatures.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/nmf-sample-correlation.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/nmf-signature-correlation.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/nmf-signatures.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 8 additions & 8 deletions docs/tutorial.ipynb

Large diffs are not rendered by default.

808 changes: 807 additions & 1 deletion poetry.lock

Large diffs are not rendered by default.

59 changes: 39 additions & 20 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,36 +1,54 @@
[tool.poetry]
[project]
name = "sonata-learn"
version = "0.1.0"
description = "A Python toolkit for nonnegative matrix factorization-based signature analysis"
description = "A Python toolkit for fitting and analyzing mutational signatures"
readme = "README.md"
requires-python = ">=3.10,<3.14"
license = "MIT"
authors = ["Benedikt Geiger"]
authors = [
{ name = "Benedikt Geiger", email = "benedikt_geiger@g.harvard.edu" },
]
maintainers = [
"Benedikt Geiger <benedikt_geiger@g.harvard.edu>",
{ name = "Benedikt Geiger", email = "benedikt_geiger@g.harvard.edu" },
]
keywords = [
"AnnData",
"bioinformatics",
"cancer genomics",
"mutational signatures",
"NMF",
]
packages = [{ include = "sonata", from = "src" }]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Science/Research",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Scientific/Engineering :: Bio-Informatics",
]
dependencies = [
"adjustText>=1.3,<2",
"anndata>=0.11,<0.13",
"fastcluster>=1.2.6,<2",
"matplotlib>=3.8,<4",
"numba>=0.61,<0.63",
"numpy>=1.26,<3",
"pandas>=2.2,<3",
"scikit-learn>=1.6,<2",
"scipy>=1.13,<2",
"seaborn>=0.13,<0.14",
"umap-learn>=0.5.7,<0.6",
]

[project.urls]
Homepage = "https://github.com/parklab/Sonata"
Repository = "https://github.com/parklab/Sonata"
Documentation = "https://github.com/parklab/Sonata/blob/main/docs/tutorial.md"

readme = "README.md"

[tool.poetry.dependencies]
python = ">=3.10,<3.14"
adjustText = ">=1.3,<2"
anndata = ">=0.11,<0.13"
fastcluster = ">=1.2.6,<2"
matplotlib = ">=3.8,<4"
numba = ">=0.61,<0.63"
numpy = ">=1.26,<3"
pandas = ">=2.2,<3"
scikit-learn = ">=1.6,<2"
scipy = ">=1.13,<2"
seaborn = ">=0.13,<0.14"
umap-learn = ">=0.5.7,<0.6"
[tool.poetry]
packages = [{ include = "sonata", from = "src" }]

[tool.poetry.group.dev.dependencies]
mypy = ">=1.15,<2"
Expand All @@ -39,6 +57,7 @@ pytest = ">=8.3,<9"
pre-commit = ">=4.2,<5"
tox = ">=4.25,<5"
types-seaborn = ">=0.13,<0.14"
twine = "^6.2.0"

[tool.pytest.ini_options]
# /site-packages/umap/__init__.py:36: DeprecationWarning: pkg_resources is deprecated as an API.
Expand Down
13 changes: 10 additions & 3 deletions src/sonata/consts.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from matplotlib.colors import LinearSegmentedColormap

NUCLEOTIDES = ["A", "C", "G", "T"]

SBS_TYPES_6 = ["C>A", "C>G", "C>T", "T>A", "T>C", "T>G"]
Expand Down Expand Up @@ -53,16 +55,21 @@

# Trinucleotide colors for the 96 dimensional mutation spectrum
COLORS_TRINUCLEOTIDES = [
(0.33, 0.75, 0.98),
"#427aa1ff",
(0.0, 0.0, 0.0),
(0.85, 0.25, 0.22),
"#d1664aff",
(0.78, 0.78, 0.78),
(0.51, 0.79, 0.24),
"#64b3aaff",
(0.89, 0.67, 0.72),
]

COLORS_SBS96 = [COLORS_TRINUCLEOTIDES[i // 16] for i in range(96)]

DIVERGING_PALETTE = LinearSegmentedColormap.from_list(
"sonata_diverging",
["#427aa1ff", "#FAFAFA", "#e07a5fff"],
)

COLORS_INDEL = [
"#FCBD6F", # 1bp Del C
"#FD8001", # 1bp Del T
Expand Down
4 changes: 2 additions & 2 deletions src/sonata/models/cornet.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ def update_signature_embeddings(self, aux: np.ndarray) -> None:
self.asignatures.obsm["embeddings"][k, :] = _utils_cornet.update_embedding(
embedding_init,
self.adata.obsm["embeddings"],
self.asignatures.obs["offsets"][k],
self.asignatures.obs["offsets"].iloc[k],
self.adata.obs["offsets"].values,
self.variance,
aux_row,
Expand All @@ -251,7 +251,7 @@ def update_sample_embeddings(self, aux: np.ndarray) -> None:
self.adata.obsm["embeddings"][d, :] = _utils_cornet.update_embedding(
embedding_init,
self.asignatures.obsm["embeddings"],
self.adata.obs["offsets"][d],
self.adata.obs["offsets"].iloc[d],
self.asignatures.obs["offsets"].values,
self.variance,
aux_col,
Expand Down
12 changes: 9 additions & 3 deletions src/sonata/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@
from scipy.cluster import hierarchy
from scipy.spatial.distance import pdist

from .consts import COLORS_INDEL83, COLORS_SBS96, INDEL_TYPES_83, SBS_TYPES_96
from .consts import (
COLORS_INDEL83,
COLORS_SBS96,
DIVERGING_PALETTE,
INDEL_TYPES_83,
SBS_TYPES_96,
)
from .utils import _concat_light, _get_basis_obsm, _get_basis_obsp, match_to_catalog

if TYPE_CHECKING:
Expand Down Expand Up @@ -389,7 +395,7 @@ def umap_multiple(adatas: Iterable[AnnData], **kwargs) -> Axes:
def correlation_pandas(
corr: pd.DataFrame,
figsize: tuple[float, float] = (4.0, 4.0),
cmap: Colormap | str | None = "vlag",
cmap: Colormap | str | None = DIVERGING_PALETTE,
fmt: str = ".2f",
**kwargs,
) -> ClusterGrid:
Expand Down Expand Up @@ -807,7 +813,7 @@ def stacked_barplot(
_, ax = plt.subplots(figsize=(0.3 * n_obs, 4))

if colors is None:
colors = sns.color_palette("deep") * (1 + n_dimensions // 10)
colors = sns.color_palette("Set3") * (1 + n_dimensions // 12)

bottom = np.zeros(n_obs)

Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
11 changes: 4 additions & 7 deletions tests/test_nmf.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import pickle

import numpy as np
import pandas as pd
import pytest
Expand Down Expand Up @@ -62,11 +60,10 @@ def test_objective_function(model_init, objective_init):
class TestUpdatesNMF:
@pytest.fixture
def WH_updated(self, n_signatures):
with open(
f"{PATH_TEST_DATA}/WH_updated_joint_nsigs{n_signatures}.pkl", "rb"
) as f:
WH_updated = pickle.load(f)
return WH_updated
suffix = f"_updated_joint_nsigs{n_signatures}.npy"
path_W = f"{PATH_TEST_DATA}/W{suffix}"
path_H = f"{PATH_TEST_DATA}/H{suffix}"
return np.load(path_W), np.load(path_H)

def test_update_parameters(self, model_init, WH_updated):
model_init._update_parameters()
Expand Down
Loading