Metadata-Version: 2.5
Name: StripeSankey
Version: 0.4.0
Summary: Fit a soft-clustering sweep across k and read it as one volume-accurate Sankey figure -- built for microbiome data, not limited to it
Project-URL: Homepage, https://gitlab.kuleuven.be/aida-lab/public/StripeSankey
Project-URL: Documentation, https://gitlab.kuleuven.be/aida-lab/public/StripeSankey#readme
Project-URL: Repository, https://gitlab.kuleuven.be/aida-lab/public/StripeSankey.git
Project-URL: Bug Tracker, https://gitlab.kuleuven.be/aida-lab/public/StripeSankey/-/issues
Author-email: Peiyang Huo <peiyang.huo@kuleuven.be>
Maintainer-email: Peiyang Huo <peiyang.huo@kuleuven.be>
License: MIT License
        
        Copyright (c) 2025 peiyang
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: anywidget,interactive,jupyter,microbiome,sankey,topic-modeling,visualization,widget
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: Jupyter
Classifier: Framework :: Jupyter :: JupyterLab
Classifier: Framework :: Jupyter :: JupyterLab :: 3
Classifier: Framework :: Jupyter :: JupyterLab :: 4
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
Classifier: Topic :: Scientific/Engineering :: Visualization
Requires-Python: >=3.9
Requires-Dist: anywidget>=0.9.0
Requires-Dist: cloudpickle>=3.0
Requires-Dist: gensim>=4.0.0
Requires-Dist: leidenalg>=0.12.0
Requires-Dist: matplotlib>=3.3.0
Requires-Dist: networkx>=2.5
Requires-Dist: numpy>=1.20.0
Requires-Dist: pandas>=1.3.0
Requires-Dist: python-igraph>=1.0.0
Requires-Dist: python-louvain>=0.16
Requires-Dist: scikit-learn>=1.0
Requires-Dist: scipy>=1.7.0
Requires-Dist: tracywidom>=0.3.0
Requires-Dist: traitlets>=5.0.0
Provides-Extra: dev
Requires-Dist: black; extra == 'dev'
Requires-Dist: mypy; extra == 'dev'
Requires-Dist: pytest-cov; extra == 'dev'
Requires-Dist: pytest>=6.0; extra == 'dev'
Requires-Dist: ruff; extra == 'dev'
Provides-Extra: docs
Requires-Dist: myst-parser; extra == 'docs'
Requires-Dist: sphinx; extra == 'docs'
Requires-Dist: sphinx-rtd-theme; extra == 'docs'
Description-Content-Type: text/markdown

<p align="center">
  <img src="https://gitlab.kuleuven.be/aida-lab/public/StripeSankey/-/raw/main/docs/StripeSankey_logo.png" alt="StripeSankey" width="420">
</p>


---

StripeSankey fits a mixed membership model at several `k` (a *sweep*) and draws all of it as one figure. Every ribbon entering or leaving a node sums to exactly that node, and a ribbon's thickness is a number of samples you can read off.

The pipeline supports LDA and NMF, and takes three tables: a **count matrix**, a **per-feature annotation** (like taxa in microbiome) and **per-sample metadata**.

| table | index | columns |
|---|---|---|
| `counts` | sample id | feature id |
| `feature_annotation` | feature id | annotation levels |
| `metadata` | sample id | sample variables |

The pipeline also lets users bring their own sweep, computed elsewhere (see *Fit your customized models*).

![From a count table to the StripeSankey: one model per k, a chance-corrected consensus across k, and the figure coloured by it](https://gitlab.kuleuven.be/aida-lab/public/StripeSankey/-/raw/main/docs/methodology.png)

```bash
pip install stripesankey
```

## Fit models with StripeSankey

### 1 — `prepare`
`prepare` checks that the three tables line up (matching sample and feature ids, usable counts), collapses the counts to one annotation `level` if you give one, and writes the result to `base/intermediate/` for `sweep` to read.

```python
import stripesankey as ss
base = 'LOCAL FOLDER PATH TO SAVE CACHE'
# fit LDA/NMF models via StripeSankey, example of microbiome count table
ss.prepare(base, counts, feature_annotation, metadata, level="genus")
```
`level` is any column of `feature_annotation`. For instance, in microbiome or ecology, you may want to group your count table at genus level.


### 2 — `sweep`

One fit per k, each saved to disk as it finishes.

```python
ss.sweep(base, k_range=range(2, 11), implementation="nmf", random_state=42)
```

Implementations include:

| Algorithm | implementation | source lib | API |
|---|---|---|---|
| `LDA` | Variational Bayes | [*gensim*](https://radimrehurek.com/gensim/auto_examples/tutorials/run_lda.html#sphx-glr-auto-examples-tutorials-run-lda-py) | lda_gensim |
| `LDA` | Markov Chain Monte Carlo | [*MALLET*](https://mimno.github.io/Mallet/) | lda_mallet |
| `NMF` | generalized KL loss, initialised with *nndsvdar* | [*scikit-learn*](https://scikit-learn.org/stable/modules/generated/sklearn.decomposition.NMF.html) | nmf |

`lda_mallet` takes one more argument, `mallet`: the path to MALLET's `bin/mallet` launcher. MALLET is a Java program and is not bundled with StripeSankey, so [download it](https://mimno.github.io/Mallet/) yourself and make sure Java is availble.

```python
ss.sweep(base, k_range=range(2, 11), implementation="lda_mallet",
         mallet="/path/to/Mallet", random_state=42)
```

Expect `lda_mallet` to be much slower than `lda_gensim` and `nmf`, for two reasons:

1. MALLET fits LDA by Markov chain Monte Carlo (collapsed Gibbs sampling)
2. Convergence has to be watched.


After fitting, results are saved in the *base* folder with the structure below:

```
base/
├── intermediate/                       # written by prepare
│   ├── features.csv                    
│   ├── feature_annotation.csv
│   ├── manifest.json
│   └── mallet/corpus.txt               # the count table as MALLET's input
└── results/                            # written by sweep
    ├── W/W_{k}.csv                     # one file per k
    ├── H/H_{k}.csv                     # one file per k
    ├── diagnostics/topic_diff_{k}.csv  # LDA (gensim) only
    ├── diagnostics/ll_{k}.csv          # LDA (MALLET) only
    ├── consensus.json                  # consensus cluster of each sample
    ├── run_info.json                   # k list, implementation, convergence per k
    └── metrics.csv
```

| | shape | on disk |
|---|---|---|
| `W` | sample × LC | `results/W/W_{k}.csv` |
| `H` | LC × feature | `results/H/H_{k}.csv` |

## Fit your customized models

### Bring your own model via python

Define a function that takes `k` and a seed and returns an unfitted model, then pass it as `implementation`. `sweep` fits it at every `k` and saves `W`, `H` and the consensus, as it does for the built-in implementations:

```python
from sklearn.decomposition import NMF

def my_nmf(k, seed):
    return NMF(n_components=k, beta_loss="kullback-leibler", solver="mu",
               init="nndsvdar", max_iter=3000, random_state=seed)

ss.sweep(base, k_range=range(2, 11), implementation=my_nmf, random_state=42)
```

The model needs scikit-learn's interface: `fit_transform(X)` returns `W` (sample × LC) and `components_` holds `H` (LC × feature).

### Bring your own sweep via other programming languages

If your model runs in another language, such as R, or in a well-established tool like *ADMIXTURE*, run the sweep there and save each `W` as `base/results/W/W_{k}.csv`: the sample id in the first column, then one column per LC, rows summing to 1, and the same samples in the same order at every `k`. Then run `sweep` without `k_range`:

```python
ss.sweep(base)
```
[`example/population`](https://gitlab.kuleuven.be/aida-lab/public/StripeSankey/-/tree/main/example/population) does this with [pong](https://github.com/ramachandran-lab/pong)'s precomputed ADMIXTURE results for the 1000 Genomes (K = 2…8).

## Plotting

Both figures read the sweep from `base`, wherever it was fitted.

### stripesankey — coloured by consensus cluster

```python
fig = ss.stripesankey(base, metadata_df=metadata, hover_field=["group"])
```

![stripesankey on the 1000 Genomes example, K=2…8](https://gitlab.kuleuven.be/aida-lab/public/StripeSankey/-/raw/main/example/population/viz/stripesankey_population.png)

*An interactive version is available in [`example/population/viz/stripesankey_K2-8.html`](https://gitlab.kuleuven.be/aida-lab/public/StripeSankey/-/blob/main/example/population/viz/stripesankey_K2-8.html).*

Click a band, a ribbon or a cluster chip to trace those samples across every
`k`. Click a `K =` label to focus that column — it restacks by the first
`hover_field`. Read the selection back from `fig.selected_samples`.

### metasankey — the same figure, cut by metadata

```python
fig = ss.metasankey(base, metadata_df=metadata, hover_field=["group"])
```

![metasankey by region on the 1000 Genomes example, K=2…8](https://gitlab.kuleuven.be/aida-lab/public/StripeSankey/-/raw/main/example/population/viz/metasankey_population.png)

*An interactive version is available in [`example/population/viz/metasankey_K2-8.html`](https://gitlab.kuleuven.be/aida-lab/public/StripeSankey/-/blob/main/example/population/viz/metasankey_K2-8.html).*

Identical layout, one thing changed: a node is sliced into a metadata field's
categories instead of into clusters.

### affinity heatmap — the matrix the clusters came from

```python
Q = ss.consensus_affinity(ss.load_W(base))                                         # the affinity matrix
fig, order, labels = ss.affinity_heatmap(Q, above=metadata, annotations=["group"])  # the plot
```

<p align="center">
  <img src="https://gitlab.kuleuven.be/aida-lab/public/StripeSankey/-/raw/main/example/population/viz/affinity_K2-8.png" alt="Consensus affinity Q for the 1000 Genomes example, ordered by consensus cluster, with cluster and region strips" width="600">
</p>

`Q` is the signed, chance-corrected co-membership: positive means two samples
share components *more* than chance, negative *less*. Samples are ordered by
consensus cluster by default.


## Hard labels

For an ensemble of **hard** labels instead of soft assignments — k-means at
several `k`, or different algorithms at one `k`:

```python
fig = ss.hardstripe(labels_df, colorby="consensus", metadata_df=metadata)
```

## Citation


## License

MIT
