scstability 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scstability-0.1.0/.gitignore +32 -0
- scstability-0.1.0/CITATION.cff +35 -0
- scstability-0.1.0/LICENSE +29 -0
- scstability-0.1.0/PKG-INFO +392 -0
- scstability-0.1.0/README.md +346 -0
- scstability-0.1.0/benchmarks/compare_seed_stability.py +380 -0
- scstability-0.1.0/benchmarks/fetch_ground_truth.py +139 -0
- scstability-0.1.0/benchmarks/fetch_large_pbmc.py +141 -0
- scstability-0.1.0/benchmarks/scaling.py +198 -0
- scstability-0.1.0/benchmarks/validate_real_data.py +322 -0
- scstability-0.1.0/benchmarks/validation.ipynb +1473 -0
- scstability-0.1.0/docs/API.md +397 -0
- scstability-0.1.0/docs/images/validation_5cl_curve.png +0 -0
- scstability-0.1.0/docs/make_validation_figure.py +88 -0
- scstability-0.1.0/environment.yml +32 -0
- scstability-0.1.0/examples/pbmc_walkthrough.ipynb +1432 -0
- scstability-0.1.0/pyproject.toml +171 -0
- scstability-0.1.0/src/scstability/__init__.py +22 -0
- scstability-0.1.0/src/scstability/_cluster.py +281 -0
- scstability-0.1.0/src/scstability/_core.py +504 -0
- scstability-0.1.0/src/scstability/_metrics.py +533 -0
- scstability-0.1.0/src/scstability/pl/__init__.py +9 -0
- scstability-0.1.0/src/scstability/pl/_plots.py +658 -0
- scstability-0.1.0/src/scstability/py.typed +0 -0
- scstability-0.1.0/tests/conftest.py +120 -0
- scstability-0.1.0/tests/test_cluster.py +279 -0
- scstability-0.1.0/tests/test_core.py +732 -0
- scstability-0.1.0/tests/test_edge_cases.py +270 -0
- scstability-0.1.0/tests/test_metamorphic.py +157 -0
- scstability-0.1.0/tests/test_metrics.py +553 -0
- scstability-0.1.0/tests/test_metrics_oracle.py +203 -0
- scstability-0.1.0/tests/test_plots.py +341 -0
- scstability-0.1.0/tests/test_public_api.py +227 -0
- scstability-0.1.0/tests/test_real_data.py +118 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Build and packaging artefacts
|
|
2
|
+
/dist/
|
|
3
|
+
/build/
|
|
4
|
+
*.egg-info/
|
|
5
|
+
|
|
6
|
+
# Python bytecode and caches
|
|
7
|
+
__pycache__/
|
|
8
|
+
*.py[cod]
|
|
9
|
+
|
|
10
|
+
# Test and lint caches
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
.mypy_cache/
|
|
13
|
+
.ruff_cache/
|
|
14
|
+
.coverage
|
|
15
|
+
coverage.xml
|
|
16
|
+
htmlcov/
|
|
17
|
+
|
|
18
|
+
# Environments
|
|
19
|
+
.venv/
|
|
20
|
+
venv/
|
|
21
|
+
|
|
22
|
+
# Datasets the notebooks and benchmark scripts download on first run.
|
|
23
|
+
# They are large, reproducible and never belong in the repository.
|
|
24
|
+
/data/
|
|
25
|
+
|
|
26
|
+
# Scratch worker written by benchmarks/scaling.py
|
|
27
|
+
/benchmarks/_scaling_worker.py
|
|
28
|
+
|
|
29
|
+
# Editor and OS noise
|
|
30
|
+
.DS_Store
|
|
31
|
+
.idea/
|
|
32
|
+
.vscode/
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
title: "scstability: bootstrap cluster stability for single-cell RNA-seq"
|
|
3
|
+
message: >-
|
|
4
|
+
If you use this software, please cite both the software and the
|
|
5
|
+
method it implements, which is Hennig (2007).
|
|
6
|
+
type: software
|
|
7
|
+
authors:
|
|
8
|
+
- family-names: Rostami
|
|
9
|
+
given-names: Atefe
|
|
10
|
+
email: ateferos77@gmail.com
|
|
11
|
+
repository-code: "https://github.com/ateferos77/scstability"
|
|
12
|
+
url: "https://pypi.org/project/scstability/"
|
|
13
|
+
version: 0.1.0
|
|
14
|
+
date-released: 2026-09-02
|
|
15
|
+
license: BSD-3-Clause
|
|
16
|
+
keywords:
|
|
17
|
+
- single-cell
|
|
18
|
+
- scRNA-seq
|
|
19
|
+
- clustering
|
|
20
|
+
- stability
|
|
21
|
+
- leiden
|
|
22
|
+
- scanpy
|
|
23
|
+
references:
|
|
24
|
+
- type: article
|
|
25
|
+
title: "Cluster-wise assessment of cluster stability"
|
|
26
|
+
authors:
|
|
27
|
+
- family-names: Hennig
|
|
28
|
+
given-names: Christian
|
|
29
|
+
journal: "Computational Statistics & Data Analysis"
|
|
30
|
+
volume: 52
|
|
31
|
+
issue: 1
|
|
32
|
+
start: 258
|
|
33
|
+
end: 271
|
|
34
|
+
year: 2007
|
|
35
|
+
doi: "10.1016/j.csda.2006.11.025"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Atefe Rostami
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: scstability
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Bootstrap cluster stability for single-cell RNA-seq: which of your clusters survive resampling?
|
|
5
|
+
Project-URL: Homepage, https://github.com/ateferos77/scstability
|
|
6
|
+
Project-URL: Source, https://github.com/ateferos77/scstability
|
|
7
|
+
Project-URL: Issues, https://github.com/ateferos77/scstability/issues
|
|
8
|
+
Author-email: Atefe Rostami <ateferos77@gmail.com>
|
|
9
|
+
License-Expression: BSD-3-Clause
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: bioinformatics,clustering,leiden,scRNA-seq,scanpy,single-cell,stability
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Natural Language :: English
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Requires-Dist: anndata>=0.10
|
|
23
|
+
Requires-Dist: igraph>=0.10
|
|
24
|
+
Requires-Dist: leidenalg>=0.10.1
|
|
25
|
+
Requires-Dist: matplotlib>=3.8
|
|
26
|
+
Requires-Dist: numpy>=1.26
|
|
27
|
+
Requires-Dist: pandas>=2.1
|
|
28
|
+
Requires-Dist: scanpy>=1.10
|
|
29
|
+
Requires-Dist: scipy>=1.11
|
|
30
|
+
Requires-Dist: tqdm>=4.65
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
33
|
+
Requires-Dist: pre-commit; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
36
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
37
|
+
Requires-Dist: scikit-learn; extra == 'dev'
|
|
38
|
+
Provides-Extra: docs
|
|
39
|
+
Requires-Dist: ipykernel; extra == 'docs'
|
|
40
|
+
Requires-Dist: nbconvert; extra == 'docs'
|
|
41
|
+
Requires-Dist: nbformat; extra == 'docs'
|
|
42
|
+
Provides-Extra: release
|
|
43
|
+
Requires-Dist: build; extra == 'release'
|
|
44
|
+
Requires-Dist: twine; extra == 'release'
|
|
45
|
+
Description-Content-Type: text/markdown
|
|
46
|
+
|
|
47
|
+
# scstability
|
|
48
|
+
|
|
49
|
+
**Which of your single-cell clusters survive resampling, and which dissolve?**
|
|
50
|
+
|
|
51
|
+
[](https://github.com/ateferos77/scstability/actions/workflows/test.yml)
|
|
52
|
+
[](https://github.com/ateferos77/scstability/blob/main/LICENSE)
|
|
53
|
+
[](https://www.python.org/downloads/)
|
|
54
|
+
|
|
55
|
+
Leiden will return clusters from pure noise, confidently. The silhouette score
|
|
56
|
+
will not save you: it rewards compactness, which a slice carved out of a
|
|
57
|
+
continuum also has. `scstability` answers the question that actually matters:
|
|
58
|
+
**would this cluster still be here if you had sequenced a different sample of
|
|
59
|
+
the same cells?**
|
|
60
|
+
|
|
61
|
+
It implements the cluster-wise Jaccard stability of **Hennig (2007)** over
|
|
62
|
+
subsampled reclusterings. AnnData-first, scanpy-compatible, one function call.
|
|
63
|
+
|
|
64
|
+

|
|
65
|
+
|
|
66
|
+
<sub>Five human cell lines mixed and sequenced together, each cell assigned to
|
|
67
|
+
its line by SNP genotype (Tian et al., *Nature Methods* 2019). The package sees
|
|
68
|
+
only PCA coordinates, never the genotype, and finds the true five groups
|
|
69
|
+
stable at 0.980, with everything finer collapsing. See
|
|
70
|
+
[Does it work?](https://github.com/ateferos77/scstability#does-it-work)</sub>
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Contents
|
|
75
|
+
|
|
76
|
+
- [What it does](https://github.com/ateferos77/scstability#what-it-does) · [Installation](https://github.com/ateferos77/scstability#installation) · [Quick start](https://github.com/ateferos77/scstability#quick-start)
|
|
77
|
+
- [Reading the score](https://github.com/ateferos77/scstability#reading-the-score): **read this before trusting a number**
|
|
78
|
+
- [Does it work?](https://github.com/ateferos77/scstability#does-it-work) · [Performance](https://github.com/ateferos77/scstability#performance)
|
|
79
|
+
- [API](https://github.com/ateferos77/scstability#api), and the [full reference](https://github.com/ateferos77/scstability/blob/main/docs/API.md)
|
|
80
|
+
- [Prior art](https://github.com/ateferos77/scstability#prior-art) · [Limitations](https://github.com/ateferos77/scstability#limitations) · [Citing](https://github.com/ateferos77/scstability#citing)
|
|
81
|
+
|
|
82
|
+
Two executed notebooks carry the detail:
|
|
83
|
+
**[usage walkthrough](https://github.com/ateferos77/scstability/blob/main/examples/pbmc_walkthrough.ipynb)** on 11,000 real PBMCs,
|
|
84
|
+
and **[the validation](https://github.com/ateferos77/scstability/blob/main/benchmarks/validation.ipynb)**.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## What it does
|
|
89
|
+
|
|
90
|
+
1. **Cluster the full data** at each resolution. This is the reference.
|
|
91
|
+
2. **Draw 80% of the cells** without replacement, rebuild the kNN graph on just
|
|
92
|
+
those cells, and recluster from scratch.
|
|
93
|
+
3. **Match each reference cluster** to the resampled cluster it overlaps most,
|
|
94
|
+
and record the **Jaccard index** of that best match.
|
|
95
|
+
4. **Repeat**, then take the mean per cluster. That is its stability.
|
|
96
|
+
5. **Report the weakest cluster** per resolution, because a clustering is only
|
|
97
|
+
as trustworthy as its least reproducible part.
|
|
98
|
+
|
|
99
|
+
Cells are sampled **without** replacement on purpose: duplicates sit at
|
|
100
|
+
distance zero and corrupt a kNN graph. The embedding is held fixed, so what is
|
|
101
|
+
measured is the instability of graph construction and community detection, not
|
|
102
|
+
of PCA.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Installation
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
pip install scstability
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Requires Python 3.12+ and scanpy 1.10+.
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
git clone https://github.com/ateferos77/scstability
|
|
116
|
+
cd scstability
|
|
117
|
+
pip install -e ".[dev]"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Quick start
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
import scanpy as sc
|
|
126
|
+
import scstability as scs
|
|
127
|
+
|
|
128
|
+
adata = sc.read_h5ad("my_data.h5ad") # needs adata.obsm["X_pca"]
|
|
129
|
+
|
|
130
|
+
result = scs.stability_sweep(adata, resolutions=[0.2, 0.4, 0.8, 1.2, 1.6])
|
|
131
|
+
|
|
132
|
+
print(result.summary()) # one row per resolution
|
|
133
|
+
print(result.recommend()) # the resolution to use
|
|
134
|
+
result.to_adata(adata) # scores into adata.obs
|
|
135
|
+
scs.pl.stability_curve(result) # the figure above
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
resolution n_clusters min_cluster_stability median_cluster_stability
|
|
140
|
+
0.1 5 0.980 1.000
|
|
141
|
+
0.2 6 0.880 0.995
|
|
142
|
+
0.4 9 0.187 0.928
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
That is the whole interface. Every argument is documented in the
|
|
146
|
+
**[API reference](https://github.com/ateferos77/scstability/blob/main/docs/API.md)**.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Reading the score
|
|
151
|
+
|
|
152
|
+
Each score is a Jaccard index in `[0, 1]`. The bands are Hennig's, as
|
|
153
|
+
`fpc::clusterboot` states them.
|
|
154
|
+
|
|
155
|
+
| mean Jaccard | band | what to do |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| **≥ 0.85** | highly stable | Report it. A real, reproducible group |
|
|
158
|
+
| **0.75 - 0.85** | stable | Report it. Sound enough to build on |
|
|
159
|
+
| **0.60 - 0.75** | uncertain | A pattern is there, but do not build a claim on this cluster alone |
|
|
160
|
+
| **< 0.60** | not trustworthy | Treat as dissolved |
|
|
161
|
+
|
|
162
|
+
Two things that will mislead you if you skip them:
|
|
163
|
+
|
|
164
|
+
**Read `min_cluster_stability`, not the median.** A resolution is only as
|
|
165
|
+
trustworthy as its weakest cluster. Real data routinely shows a median of 0.94
|
|
166
|
+
beside a minimum of 0.18. The median hides exactly the cluster you needed.
|
|
167
|
+
|
|
168
|
+
**Read `n_clusters` alongside the score.** A partition with one cluster scores
|
|
169
|
+
near 1.0 on structureless data, because the resample collapses the same way.
|
|
170
|
+
`recommend()` guards against this; your own reading of `summary()` must too.
|
|
171
|
+
|
|
172
|
+
**Do not over-read small differences in `min_cluster_stability`.** It is a
|
|
173
|
+
minimum over clusters, so it is an extreme order statistic, driven by whichever
|
|
174
|
+
cluster happens to be worst. Measured on the genotype-labelled data by varying
|
|
175
|
+
only `random_state`: at a resolution where clusters are genuinely marginal, the
|
|
176
|
+
standard deviation is around 0.09 and the observed range across ten seeds
|
|
177
|
+
reached 0.33, which is wider than a whole interpretation band. Raising `n_boot`
|
|
178
|
+
narrows it (0.096 to 0.069 going from 10 to 50, with the reference clustering
|
|
179
|
+
held fixed) but does not remove it, because the reference partition itself
|
|
180
|
+
shifts slightly with the seed. Where a cluster sits near a band edge, run the
|
|
181
|
+
sweep under two or three seeds before drawing a conclusion, and read
|
|
182
|
+
`jaccard_q25`/`jaccard_q75` rather than the point estimate alone.
|
|
183
|
+
|
|
184
|
+
The bands apply to `jaccard_mean`. Bootstrap Jaccards are left-skewed, so the
|
|
185
|
+
median runs optimistically; `jaccard_median` and the quartiles are reported
|
|
186
|
+
alongside as distribution *shape*.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Does it work?
|
|
191
|
+
|
|
192
|
+
**[benchmarks/validation.ipynb](https://github.com/ateferos77/scstability/blob/main/benchmarks/validation.ipynb)** is the evidence,
|
|
193
|
+
executed, with every figure and table rendered, so it can be read without
|
|
194
|
+
running anything.
|
|
195
|
+
|
|
196
|
+
Most single-cell "ground truth" is circular: the cell-type labels were produced
|
|
197
|
+
by clustering the same matrix. `sc_10x_5cl` escapes that: five cell lines,
|
|
198
|
+
each cell assigned by **SNP genotype**.
|
|
199
|
+
|
|
200
|
+
| check | result |
|
|
201
|
+
|---|---|
|
|
202
|
+
| recovers the true K = 5 | **0.980**, collapsing to 0.187 two resolutions later |
|
|
203
|
+
| correlation with genotype identity | **Spearman +0.575**, p = 1e-07, 73 clusters |
|
|
204
|
+
| versus a matched unimodal null | **0.980** real vs **0.322** null at K = 5 |
|
|
205
|
+
| tracks *set recovery*, not local purity | purity is 1.0 for **all** 73 clusters and cannot discriminate at all; stability separates them |
|
|
206
|
+
| adds information over seed stability | seed adds **+0.000** R² once sampling is known; sampling adds **+0.056** over seed |
|
|
207
|
+
|
|
208
|
+
Also: 135 tests plus 2 slow real-data tests, 98% coverage, a set-based oracle
|
|
209
|
+
over 700 random configurations, and public-API tests that run in a clean
|
|
210
|
+
subprocess.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Performance
|
|
215
|
+
|
|
216
|
+
5 resolutions × 20 bootstraps = 100 reclusterings, on subsamples of a real 68k
|
|
217
|
+
PBMC dataset:
|
|
218
|
+
|
|
219
|
+
| cells | wall clock | peak RAM |
|
|
220
|
+
|---|---|---|
|
|
221
|
+
| 2,000 | 12 s | 0.4 GB |
|
|
222
|
+
| 10,000 | 60 s | 0.7 GB |
|
|
223
|
+
| 20,000 | 3.5 min | 1.7 GB |
|
|
224
|
+
| **68,000** | **10 min** | **2.1 GB** |
|
|
225
|
+
|
|
226
|
+
Mildly super-linear (1.46× relative to linear). Memory is dominated by the
|
|
227
|
+
graph, not by anything this package allocates.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## API
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
scs.stability_sweep(adata, resolutions, *, n_boot=20, frac=0.8,
|
|
235
|
+
use_rep="X_pca", n_neighbors=15, random_state=0,
|
|
236
|
+
progress=True) -> StabilityResult
|
|
237
|
+
|
|
238
|
+
result.summary(stable_threshold=0.75) # DataFrame, one row per resolution
|
|
239
|
+
result.recommend(threshold=0.75) # float, a resolution from the grid
|
|
240
|
+
result.to_adata(adata, key_added="stability")
|
|
241
|
+
|
|
242
|
+
scs.pl.stability_curve(result, threshold=0.75, ax=None)
|
|
243
|
+
scs.pl.cluster_stability(result, resolution, ax=None)
|
|
244
|
+
scs.pl.stability_umap(adata, result, resolution, ax=None)
|
|
245
|
+
|
|
246
|
+
scs.HENNIG_BANDS # the interpretation table, as data
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**[Full API reference](https://github.com/ateferos77/scstability/blob/main/docs/API.md)**: every parameter, return value, error
|
|
250
|
+
and warning, with recipes.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Prior art
|
|
255
|
+
|
|
256
|
+
Two different things get called cluster stability, and keeping them apart is
|
|
257
|
+
the whole point:
|
|
258
|
+
|
|
259
|
+
- **Seed stability.** Rerun the same clustering on the same graph with a
|
|
260
|
+
different seed. Measures whether community detection lands in a consistent
|
|
261
|
+
local optimum. Cheap.
|
|
262
|
+
- **Sampling stability.** Recluster a *resample of the cells*. Measures
|
|
263
|
+
whether the cluster would survive sequencing a different subset. Expensive.
|
|
264
|
+
**This is what `scstability` measures.**
|
|
265
|
+
|
|
266
|
+
In R this is solved: `fpc::clusterboot` (Hennig 2007), `chooseR`,
|
|
267
|
+
`bluster::bootstrapStability`, `scclusteval`, `ClustAssess`.
|
|
268
|
+
|
|
269
|
+
In Python the landscape is **not empty**, and every existing tool measures
|
|
270
|
+
something else:
|
|
271
|
+
|
|
272
|
+
| Tool | Measures |
|
|
273
|
+
|---|---|
|
|
274
|
+
| `ClustAssessPy` | Element-centric consistency **across seeds** on a fixed graph |
|
|
275
|
+
| `scICE` (Julia) | Inconsistency coefficient **across seeds** |
|
|
276
|
+
| `pyclustree` | Draws the tree of assignments across resolutions; no resampling, no score |
|
|
277
|
+
| `constclust` | Meta-clustering over a parameter grid; appears unmaintained |
|
|
278
|
+
| `reval`, `skstab` | Generic stability validation, not single-cell, not AnnData-aware |
|
|
279
|
+
| `scanpy` | No stability functionality ([scverse/scanpy#3533](https://github.com/scverse/scanpy/issues/3533)) |
|
|
280
|
+
|
|
281
|
+
> Python has clustering-stability tooling, and all of it measures stability
|
|
282
|
+
> across random seeds. The subsampling-based, cluster-wise Jaccard approach has
|
|
283
|
+
> no maintained, installable, AnnData-first Python implementation.
|
|
284
|
+
|
|
285
|
+
### "scICE showed reseeding is 30× cheaper and gets the same signal"
|
|
286
|
+
|
|
287
|
+
A fair objection from a strong paper, so we measured it against `ClustAssessPy`
|
|
288
|
+
with identical clustering in both arms
|
|
289
|
+
([notebook](https://github.com/ateferos77/scstability/blob/main/benchmarks/validation.ipynb), section 4).
|
|
290
|
+
|
|
291
|
+
**The objection is largely right.** At cluster level the two agree closely
|
|
292
|
+
(Spearman +0.907), and zero of 73 clusters were seed-stable yet
|
|
293
|
+
sampling-unstable. If you want a ranking on well-separated data, **use
|
|
294
|
+
`ClustAssessPy`, which is cheaper and will usually agree.**
|
|
295
|
+
|
|
296
|
+
The difference is asymmetric rather than large. Element-centric consistency
|
|
297
|
+
**saturates**: 45% of clusters sit at ECS ≥ 0.99, where it can no longer tell a
|
|
298
|
+
real cluster from an arbitrary one, while their true quality still ranges 0.13
|
|
299
|
+
to 1.00. Measured threshold-free, seed stability adds **+0.000** R² to
|
|
300
|
+
predicting ground truth once sampling stability is known; sampling adds
|
|
301
|
+
**+0.056** over seed. If you need to know whether one specific
|
|
302
|
+
confident-looking cluster is real, reseeding cannot tell you, because it never
|
|
303
|
+
removes a cell.
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Limitations
|
|
308
|
+
|
|
309
|
+
1. **It does not find the true number of clusters.** A reproducible over-split
|
|
310
|
+
of a stable cluster is still reproducible. On the genotype-labelled data it
|
|
311
|
+
returns 6 where the truth is 5.
|
|
312
|
+
2. **It does not replace biological validation.** A stable cluster can still be
|
|
313
|
+
a doublet artefact or an uncorrected batch. Stability is necessary, not
|
|
314
|
+
sufficient.
|
|
315
|
+
3. **It holds the embedding fixed.** Instability of PCA or integration is a
|
|
316
|
+
larger and slower question.
|
|
317
|
+
4. **It does not invent a metric.** The measure is Hennig (2007).
|
|
318
|
+
5. **It measures sampling stability, not seed stability.** For the latter, use
|
|
319
|
+
`ClustAssessPy` or `scICE`.
|
|
320
|
+
6. **A single cluster scores ~1.0** on structureless data. Always read
|
|
321
|
+
`n_clusters`.
|
|
322
|
+
7. **Very small clusters are noisy.** Check `jaccard_q25`/`jaccard_q75`.
|
|
323
|
+
8. **Per-cell scores saturate** on well-separated data.
|
|
324
|
+
9. **`min_cluster_stability` is noisy near band edges.** It is a minimum over
|
|
325
|
+
clusters, so its sampling variance is larger than any individual cluster's.
|
|
326
|
+
See [Reading the score](https://github.com/ateferos77/scstability#reading-the-score).
|
|
327
|
+
10. **Benchmarked against `ClustAssessPy` only.** `scICE` and `chooseR` have not
|
|
328
|
+
been run, and no number for either appears in this repository. Correctness
|
|
329
|
+
is validated on 3,822 and 2,531 cells; scale is measured separately to
|
|
330
|
+
68,000. Those are different claims on different data.
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## Citing
|
|
335
|
+
|
|
336
|
+
Please cite the **method**, which is not ours:
|
|
337
|
+
|
|
338
|
+
> Hennig, C. (2007). Cluster-wise assessment of cluster stability.
|
|
339
|
+
> *Computational Statistics & Data Analysis*, 52(1), 258-271.
|
|
340
|
+
|
|
341
|
+
<details>
|
|
342
|
+
<summary>Related work, depending on what you claim</summary>
|
|
343
|
+
|
|
344
|
+
> Patterson-Cross, R.B., Levine, A.J. & Menon, V. (2021). Selecting single cell
|
|
345
|
+
> clustering parameter values using subsampling-based robustness metrics.
|
|
346
|
+
> *BMC Bioinformatics*, 22, 39.
|
|
347
|
+
|
|
348
|
+
> Tang, M. et al. (2021). Evaluating single-cell cluster stability using the
|
|
349
|
+
> Jaccard similarity index. *Bioinformatics*, 37(15), 2212-2214.
|
|
350
|
+
|
|
351
|
+
> Tian, L. et al. (2019). Benchmarking single cell RNA-sequencing analysis
|
|
352
|
+
> pipelines using mixture control experiments. *Nature Methods*, 16, 479-487.
|
|
353
|
+
> *(the validation data)*
|
|
354
|
+
|
|
355
|
+
> Baek, S. et al. (2025). scICE: enhancing clustering reliability and
|
|
356
|
+
> efficiency of scRNA-seq data with multi-resolution consensus clustering.
|
|
357
|
+
> *Nature Communications*. *(seed stability)*
|
|
358
|
+
|
|
359
|
+
> Tibshirani, R., Walther, G. & Hastie, T. (2001). Estimating the number of
|
|
360
|
+
> clusters in a data set via the gap statistic. *JRSS-B*, 63(2), 411-423.
|
|
361
|
+
> *(the matched null)*
|
|
362
|
+
|
|
363
|
+
> Liu, Y. et al. (2008). Statistical significance of clustering for
|
|
364
|
+
> high-dimension, low-sample-size data. *JASA*, 103(483), 1281-1293.
|
|
365
|
+
|
|
366
|
+
> von Luxburg, U. (2010). Clustering stability: an overview. *Foundations and
|
|
367
|
+
> Trends in Machine Learning*, 2(3), 235-274. *(why stability alone cannot
|
|
368
|
+
> choose K)*
|
|
369
|
+
|
|
370
|
+
</details>
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## Development
|
|
375
|
+
|
|
376
|
+
```bash
|
|
377
|
+
micromamba env create -f environment.yml && micromamba activate scstability
|
|
378
|
+
pip install -e ".[dev]"
|
|
379
|
+
pre-commit install
|
|
380
|
+
|
|
381
|
+
pytest # fast suite
|
|
382
|
+
pytest -m slow # real-data tests
|
|
383
|
+
pytest --doctest-modules src/scstability # docstring examples
|
|
384
|
+
ruff check . && ruff format --check .
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
The test suite is the specification: if you change behaviour, a test changes
|
|
388
|
+
with it.
|
|
389
|
+
|
|
390
|
+
## License
|
|
391
|
+
|
|
392
|
+
BSD 3-Clause. See [LICENSE](https://github.com/ateferos77/scstability/blob/main/LICENSE).
|