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.
Files changed (34) hide show
  1. scstability-0.1.0/.gitignore +32 -0
  2. scstability-0.1.0/CITATION.cff +35 -0
  3. scstability-0.1.0/LICENSE +29 -0
  4. scstability-0.1.0/PKG-INFO +392 -0
  5. scstability-0.1.0/README.md +346 -0
  6. scstability-0.1.0/benchmarks/compare_seed_stability.py +380 -0
  7. scstability-0.1.0/benchmarks/fetch_ground_truth.py +139 -0
  8. scstability-0.1.0/benchmarks/fetch_large_pbmc.py +141 -0
  9. scstability-0.1.0/benchmarks/scaling.py +198 -0
  10. scstability-0.1.0/benchmarks/validate_real_data.py +322 -0
  11. scstability-0.1.0/benchmarks/validation.ipynb +1473 -0
  12. scstability-0.1.0/docs/API.md +397 -0
  13. scstability-0.1.0/docs/images/validation_5cl_curve.png +0 -0
  14. scstability-0.1.0/docs/make_validation_figure.py +88 -0
  15. scstability-0.1.0/environment.yml +32 -0
  16. scstability-0.1.0/examples/pbmc_walkthrough.ipynb +1432 -0
  17. scstability-0.1.0/pyproject.toml +171 -0
  18. scstability-0.1.0/src/scstability/__init__.py +22 -0
  19. scstability-0.1.0/src/scstability/_cluster.py +281 -0
  20. scstability-0.1.0/src/scstability/_core.py +504 -0
  21. scstability-0.1.0/src/scstability/_metrics.py +533 -0
  22. scstability-0.1.0/src/scstability/pl/__init__.py +9 -0
  23. scstability-0.1.0/src/scstability/pl/_plots.py +658 -0
  24. scstability-0.1.0/src/scstability/py.typed +0 -0
  25. scstability-0.1.0/tests/conftest.py +120 -0
  26. scstability-0.1.0/tests/test_cluster.py +279 -0
  27. scstability-0.1.0/tests/test_core.py +732 -0
  28. scstability-0.1.0/tests/test_edge_cases.py +270 -0
  29. scstability-0.1.0/tests/test_metamorphic.py +157 -0
  30. scstability-0.1.0/tests/test_metrics.py +553 -0
  31. scstability-0.1.0/tests/test_metrics_oracle.py +203 -0
  32. scstability-0.1.0/tests/test_plots.py +341 -0
  33. scstability-0.1.0/tests/test_public_api.py +227 -0
  34. 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
+ [![Tests](https://github.com/ateferos77/scstability/actions/workflows/test.yml/badge.svg)](https://github.com/ateferos77/scstability/actions/workflows/test.yml)
52
+ [![License: BSD-3-Clause](https://img.shields.io/badge/License-BSD--3--Clause-green.svg)](https://github.com/ateferos77/scstability/blob/main/LICENSE)
53
+ [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](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
+ ![Stability across resolutions on five genotype-labelled cell lines](https://raw.githubusercontent.com/ateferos77/scstability/main/docs/images/validation_5cl_curve.png)
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).