carve-validate 1.0.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.
- carve_validate-1.0.0/LICENSE +21 -0
- carve_validate-1.0.0/PKG-INFO +176 -0
- carve_validate-1.0.0/README.md +125 -0
- carve_validate-1.0.0/pyproject.toml +91 -0
- carve_validate-1.0.0/setup.cfg +4 -0
- carve_validate-1.0.0/src/carve/__init__.py +8 -0
- carve_validate-1.0.0/src/carve/_accuracy.py +42 -0
- carve_validate-1.0.0/src/carve/_consensus.py +213 -0
- carve_validate-1.0.0/src/carve/_grids.py +153 -0
- carve_validate-1.0.0/src/carve/_output.py +132 -0
- carve_validate-1.0.0/src/carve/_pipeline.py +182 -0
- carve_validate-1.0.0/src/carve/_plotting.py +1568 -0
- carve_validate-1.0.0/src/carve/_runner.py +561 -0
- carve_validate-1.0.0/src/carve/_selection.py +401 -0
- carve_validate-1.0.0/src/carve/_types.py +130 -0
- carve_validate-1.0.0/src/carve/_utils.py +306 -0
- carve_validate-1.0.0/src/carve/api.py +1763 -0
- carve_validate-1.0.0/src/carve/cluster.py +332 -0
- carve_validate-1.0.0/src/carve_validate.egg-info/PKG-INFO +176 -0
- carve_validate-1.0.0/src/carve_validate.egg-info/SOURCES.txt +33 -0
- carve_validate-1.0.0/src/carve_validate.egg-info/dependency_links.txt +1 -0
- carve_validate-1.0.0/src/carve_validate.egg-info/requires.txt +34 -0
- carve_validate-1.0.0/src/carve_validate.egg-info/top_level.txt +1 -0
- carve_validate-1.0.0/tests/test_accuracy.py +89 -0
- carve_validate-1.0.0/tests/test_api.py +594 -0
- carve_validate-1.0.0/tests/test_cluster.py +249 -0
- carve_validate-1.0.0/tests/test_consensus.py +260 -0
- carve_validate-1.0.0/tests/test_grids.py +184 -0
- carve_validate-1.0.0/tests/test_output.py +115 -0
- carve_validate-1.0.0/tests/test_pipeline.py +174 -0
- carve_validate-1.0.0/tests/test_plotting.py +502 -0
- carve_validate-1.0.0/tests/test_runner.py +339 -0
- carve_validate-1.0.0/tests/test_selection.py +317 -0
- carve_validate-1.0.0/tests/test_types.py +60 -0
- carve_validate-1.0.0/tests/test_utils.py +234 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kai Wycik
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: carve-validate
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Cluster Analysis with Resampling for Validation and Exploration: stability- and generalizability-based clustering validation.
|
|
5
|
+
Author-email: Kai Wycik <kai.wycik@columbia.edu>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/DataSlingers/CARVE
|
|
8
|
+
Project-URL: Repository, https://github.com/DataSlingers/CARVE
|
|
9
|
+
Project-URL: Issues, https://github.com/DataSlingers/CARVE/issues
|
|
10
|
+
Keywords: clustering,validation,stability,consensus,scRNA-seq,scikit-learn
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
15
|
+
Requires-Python: <3.13,>=3.12
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: numpy<2.1,>=2.0
|
|
19
|
+
Requires-Dist: scipy<1.17,>=1.16
|
|
20
|
+
Requires-Dist: scikit-learn<1.8,>=1.7.2
|
|
21
|
+
Requires-Dist: pandas<2.3,>=2.2
|
|
22
|
+
Requires-Dist: joblib<1.6,>=1.5
|
|
23
|
+
Requires-Dist: tqdm<5,>=4.67
|
|
24
|
+
Requires-Dist: numba<0.61,>=0.60
|
|
25
|
+
Requires-Dist: llvmlite<0.44,>=0.43
|
|
26
|
+
Requires-Dist: umap-learn>=0.5.6
|
|
27
|
+
Requires-Dist: pynndescent<0.6,>=0.5.10
|
|
28
|
+
Requires-Dist: matplotlib>=3.9.4
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: ruff; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
33
|
+
Provides-Extra: notebooks
|
|
34
|
+
Requires-Dist: jupyterlab<5,>=4.2; extra == "notebooks"
|
|
35
|
+
Requires-Dist: ipykernel<7,>=6.29; extra == "notebooks"
|
|
36
|
+
Requires-Dist: ipywidgets>=8.1.7; extra == "notebooks"
|
|
37
|
+
Requires-Dist: plotly>=6.3.0; extra == "notebooks"
|
|
38
|
+
Requires-Dist: rpy2>=3.6.3; extra == "notebooks"
|
|
39
|
+
Requires-Dist: pyreadr>=0.5.3; extra == "notebooks"
|
|
40
|
+
Requires-Dist: scanpy>=1.11.4; extra == "notebooks"
|
|
41
|
+
Requires-Dist: scikit-misc>=0.5.1; extra == "notebooks"
|
|
42
|
+
Requires-Dist: anywidget>=0.9.18; extra == "notebooks"
|
|
43
|
+
Requires-Dist: igraph>=1.0.0; extra == "notebooks"
|
|
44
|
+
Requires-Dist: leidenalg>=0.11.0; extra == "notebooks"
|
|
45
|
+
Requires-Dist: pyteomics>=4.7.5; extra == "notebooks"
|
|
46
|
+
Requires-Dist: lxml>=6.0.2; extra == "notebooks"
|
|
47
|
+
Requires-Dist: scvi>=0.6.8; extra == "notebooks"
|
|
48
|
+
Requires-Dist: cmap>=0.7.0; extra == "notebooks"
|
|
49
|
+
Requires-Dist: glasbey>=0.3.0; extra == "notebooks"
|
|
50
|
+
Dynamic: license-file
|
|
51
|
+
|
|
52
|
+
[](https://github.com/DataSlingers/CARVE/actions/workflows/ci.yml)
|
|
53
|
+
[](https://github.com/DataSlingers/CARVE/actions/workflows/r-ci.yml)
|
|
54
|
+

|
|
55
|
+

|
|
56
|
+
|
|
57
|
+
# CARVE
|
|
58
|
+
|
|
59
|
+
**Cluster Analysis with Resampling for Validation and Exploration**
|
|
60
|
+
|
|
61
|
+
Choosing the number of clusters is hard, especially for high-dimensional biological data where standard internal clustering validation indices (CVIs) are often unreliable. CARVE measures clustering robustness through two resampling-based concepts: **stability** (reproducibility of cluster assignments under data subsampling) and **generalizability** (agreement between held-out cluster labels and predictions from a classifier trained on a subsample of the data). CARVE reports global, cluster-level, and sample-level diagnostics with visualizations, all through a scikit-learn-compatible API.
|
|
62
|
+
|
|
63
|
+
<p align="center">
|
|
64
|
+
<img src="https://raw.githubusercontent.com/DataSlingers/CARVE/main/carve_overview.png" width="700" alt="CARVE overview">
|
|
65
|
+
</p>
|
|
66
|
+
|
|
67
|
+
## Features
|
|
68
|
+
|
|
69
|
+
- Scikit-learn-compatible API: `CARVE` extends `BaseEstimator` with a `fit` / `get_labels` / `get_k` workflow
|
|
70
|
+
- Stability (ARI on subsample overlap) and generalizability (ARI on held-out predictions) metrics
|
|
71
|
+
- Diagnostics at the global, per-cluster, and per-sample level
|
|
72
|
+
- Metrics: stability and generalizability ARIs, consensus PAC, Gini, cross-entropy, and predictive accuracy
|
|
73
|
+
- Selection rules: `max`, `1se` (one-standard-error), and `quantile`
|
|
74
|
+
- Custom spectral clustering with self-tuning affinity (based on Zelnik-Manor & Perona, *Self-Tuning Spectral Clustering*, NeurIPS 2004)
|
|
75
|
+
- Plots: metric-over-*k* curves, consensus heatmaps, box plots, violin plots, and scatter plots
|
|
76
|
+
- Parallel resampling via joblib (`n_jobs`)
|
|
77
|
+
|
|
78
|
+
## Installation
|
|
79
|
+
|
|
80
|
+
CARVE requires **Python 3.12**.
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install carve-validate
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The distribution is named `carve-validate`; the import name is `carve`:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from carve import CARVE
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### From source (development)
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
git clone https://github.com/DataSlingers/CARVE.git
|
|
96
|
+
cd CARVE
|
|
97
|
+
pip install -e ".[dev]" # linting + testing
|
|
98
|
+
pip install -e ".[notebooks]" # Jupyter, Scanpy, scVI, etc.
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Quick Start
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from carve import CARVE
|
|
105
|
+
from sklearn.datasets import make_blobs
|
|
106
|
+
|
|
107
|
+
# Generate synthetic data
|
|
108
|
+
X, y_true = make_blobs(n_samples=500, n_features=10, centers=5, random_state=42)
|
|
109
|
+
|
|
110
|
+
# Fit CARVE
|
|
111
|
+
carve = CARVE(n_clusters=10, n_resamples=120, subsample_ratio=0.7, n_jobs=4)
|
|
112
|
+
carve.fit(X)
|
|
113
|
+
|
|
114
|
+
# Select best k and retrieve labels
|
|
115
|
+
k = carve.get_k(measure="generalizability", rule="1se")
|
|
116
|
+
labels = carve.get_labels(measure="generalizability", rule="1se")
|
|
117
|
+
print(f"Selected k={k}")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
See [`notebooks/Tutorial.ipynb`](notebooks/Tutorial.ipynb) for a walkthrough, and [`notebooks/case_studies/`](notebooks/case_studies/) for real-world analyses on scRNA-seq and mass cytometry datasets.
|
|
121
|
+
|
|
122
|
+
## Visualization
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
# Metric curves across k
|
|
126
|
+
carve.plot_metric_over_n_clusters(measure="stability", rule="1se")
|
|
127
|
+
|
|
128
|
+
# Consensus heatmap for the selected solution
|
|
129
|
+
carve.plot_consensus_matrix(measure="generalizability", rule="1se")
|
|
130
|
+
|
|
131
|
+
# Per-cluster stability violin plot
|
|
132
|
+
carve.plot_cluster_violin(source="gini", measure="generalizability", rule="1se")
|
|
133
|
+
|
|
134
|
+
# 2D scatter with score-encoded marker size and opacity
|
|
135
|
+
carve.plot_cluster_scatter(source="gini", measure="generalizability", rule="1se")
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
All plotting methods return a matplotlib `Axes` object and accept `save` and `dpi` parameters for export.
|
|
139
|
+
|
|
140
|
+
## Citation
|
|
141
|
+
|
|
142
|
+
If you use CARVE in your research, please cite:
|
|
143
|
+
|
|
144
|
+
> Wycik, K. R., Tang, T. M., Zikry, T. M., & Allen, G. I. (2026). *CARVE: Cluster
|
|
145
|
+
> Analysis with Resampling for Validation and Exploration.* Zenodo.
|
|
146
|
+
> https://doi.org/10.5281/zenodo.20448965
|
|
147
|
+
|
|
148
|
+
```bibtex
|
|
149
|
+
@software{wycik2026carve,
|
|
150
|
+
author = {Wycik, Kai R. and Tang, Tiffany M. and Zikry, Tarek M. and Allen, Genevera I.},
|
|
151
|
+
title = {{CARVE}: Cluster Analysis with Resampling for Validation and Exploration},
|
|
152
|
+
year = {2026},
|
|
153
|
+
publisher = {Zenodo},
|
|
154
|
+
doi = {10.5281/zenodo.20448965},
|
|
155
|
+
url = {https://doi.org/10.5281/zenodo.20448965}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Authors
|
|
160
|
+
|
|
161
|
+
- [Kai R. Wycik](mailto:kai.wycik@columbia.edu) — Columbia University
|
|
162
|
+
- Tiffany M. Tang — University of Notre Dame
|
|
163
|
+
- Tarek M. Zikry — UNC Chapel Hill
|
|
164
|
+
- Genevera I. Allen — Columbia University
|
|
165
|
+
|
|
166
|
+
## Contributing
|
|
167
|
+
|
|
168
|
+
Contributions are welcome! Please open an [issue](https://github.com/DataSlingers/CARVE/issues) or submit a pull request.
|
|
169
|
+
|
|
170
|
+
This project uses [Ruff](https://docs.astral.sh/ruff/) for linting and formatting, and [pytest](https://docs.pytest.org/) for testing:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
ruff check src/ # lint
|
|
174
|
+
ruff format src/ # format
|
|
175
|
+
pytest -v # run tests
|
|
176
|
+
```
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
[](https://github.com/DataSlingers/CARVE/actions/workflows/ci.yml)
|
|
2
|
+
[](https://github.com/DataSlingers/CARVE/actions/workflows/r-ci.yml)
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
# CARVE
|
|
7
|
+
|
|
8
|
+
**Cluster Analysis with Resampling for Validation and Exploration**
|
|
9
|
+
|
|
10
|
+
Choosing the number of clusters is hard, especially for high-dimensional biological data where standard internal clustering validation indices (CVIs) are often unreliable. CARVE measures clustering robustness through two resampling-based concepts: **stability** (reproducibility of cluster assignments under data subsampling) and **generalizability** (agreement between held-out cluster labels and predictions from a classifier trained on a subsample of the data). CARVE reports global, cluster-level, and sample-level diagnostics with visualizations, all through a scikit-learn-compatible API.
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<img src="https://raw.githubusercontent.com/DataSlingers/CARVE/main/carve_overview.png" width="700" alt="CARVE overview">
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- Scikit-learn-compatible API: `CARVE` extends `BaseEstimator` with a `fit` / `get_labels` / `get_k` workflow
|
|
19
|
+
- Stability (ARI on subsample overlap) and generalizability (ARI on held-out predictions) metrics
|
|
20
|
+
- Diagnostics at the global, per-cluster, and per-sample level
|
|
21
|
+
- Metrics: stability and generalizability ARIs, consensus PAC, Gini, cross-entropy, and predictive accuracy
|
|
22
|
+
- Selection rules: `max`, `1se` (one-standard-error), and `quantile`
|
|
23
|
+
- Custom spectral clustering with self-tuning affinity (based on Zelnik-Manor & Perona, *Self-Tuning Spectral Clustering*, NeurIPS 2004)
|
|
24
|
+
- Plots: metric-over-*k* curves, consensus heatmaps, box plots, violin plots, and scatter plots
|
|
25
|
+
- Parallel resampling via joblib (`n_jobs`)
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
CARVE requires **Python 3.12**.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install carve-validate
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The distribution is named `carve-validate`; the import name is `carve`:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from carve import CARVE
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### From source (development)
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
git clone https://github.com/DataSlingers/CARVE.git
|
|
45
|
+
cd CARVE
|
|
46
|
+
pip install -e ".[dev]" # linting + testing
|
|
47
|
+
pip install -e ".[notebooks]" # Jupyter, Scanpy, scVI, etc.
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quick Start
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from carve import CARVE
|
|
54
|
+
from sklearn.datasets import make_blobs
|
|
55
|
+
|
|
56
|
+
# Generate synthetic data
|
|
57
|
+
X, y_true = make_blobs(n_samples=500, n_features=10, centers=5, random_state=42)
|
|
58
|
+
|
|
59
|
+
# Fit CARVE
|
|
60
|
+
carve = CARVE(n_clusters=10, n_resamples=120, subsample_ratio=0.7, n_jobs=4)
|
|
61
|
+
carve.fit(X)
|
|
62
|
+
|
|
63
|
+
# Select best k and retrieve labels
|
|
64
|
+
k = carve.get_k(measure="generalizability", rule="1se")
|
|
65
|
+
labels = carve.get_labels(measure="generalizability", rule="1se")
|
|
66
|
+
print(f"Selected k={k}")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
See [`notebooks/Tutorial.ipynb`](notebooks/Tutorial.ipynb) for a walkthrough, and [`notebooks/case_studies/`](notebooks/case_studies/) for real-world analyses on scRNA-seq and mass cytometry datasets.
|
|
70
|
+
|
|
71
|
+
## Visualization
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
# Metric curves across k
|
|
75
|
+
carve.plot_metric_over_n_clusters(measure="stability", rule="1se")
|
|
76
|
+
|
|
77
|
+
# Consensus heatmap for the selected solution
|
|
78
|
+
carve.plot_consensus_matrix(measure="generalizability", rule="1se")
|
|
79
|
+
|
|
80
|
+
# Per-cluster stability violin plot
|
|
81
|
+
carve.plot_cluster_violin(source="gini", measure="generalizability", rule="1se")
|
|
82
|
+
|
|
83
|
+
# 2D scatter with score-encoded marker size and opacity
|
|
84
|
+
carve.plot_cluster_scatter(source="gini", measure="generalizability", rule="1se")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
All plotting methods return a matplotlib `Axes` object and accept `save` and `dpi` parameters for export.
|
|
88
|
+
|
|
89
|
+
## Citation
|
|
90
|
+
|
|
91
|
+
If you use CARVE in your research, please cite:
|
|
92
|
+
|
|
93
|
+
> Wycik, K. R., Tang, T. M., Zikry, T. M., & Allen, G. I. (2026). *CARVE: Cluster
|
|
94
|
+
> Analysis with Resampling for Validation and Exploration.* Zenodo.
|
|
95
|
+
> https://doi.org/10.5281/zenodo.20448965
|
|
96
|
+
|
|
97
|
+
```bibtex
|
|
98
|
+
@software{wycik2026carve,
|
|
99
|
+
author = {Wycik, Kai R. and Tang, Tiffany M. and Zikry, Tarek M. and Allen, Genevera I.},
|
|
100
|
+
title = {{CARVE}: Cluster Analysis with Resampling for Validation and Exploration},
|
|
101
|
+
year = {2026},
|
|
102
|
+
publisher = {Zenodo},
|
|
103
|
+
doi = {10.5281/zenodo.20448965},
|
|
104
|
+
url = {https://doi.org/10.5281/zenodo.20448965}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Authors
|
|
109
|
+
|
|
110
|
+
- [Kai R. Wycik](mailto:kai.wycik@columbia.edu) — Columbia University
|
|
111
|
+
- Tiffany M. Tang — University of Notre Dame
|
|
112
|
+
- Tarek M. Zikry — UNC Chapel Hill
|
|
113
|
+
- Genevera I. Allen — Columbia University
|
|
114
|
+
|
|
115
|
+
## Contributing
|
|
116
|
+
|
|
117
|
+
Contributions are welcome! Please open an [issue](https://github.com/DataSlingers/CARVE/issues) or submit a pull request.
|
|
118
|
+
|
|
119
|
+
This project uses [Ruff](https://docs.astral.sh/ruff/) for linting and formatting, and [pytest](https://docs.pytest.org/) for testing:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
ruff check src/ # lint
|
|
123
|
+
ruff format src/ # format
|
|
124
|
+
pytest -v # run tests
|
|
125
|
+
```
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "carve-validate"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Cluster Analysis with Resampling for Validation and Exploration: stability- and generalizability-based clustering validation."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12,<3.13"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Kai Wycik", email = "kai.wycik@columbia.edu" },
|
|
15
|
+
]
|
|
16
|
+
keywords = [
|
|
17
|
+
"clustering",
|
|
18
|
+
"validation",
|
|
19
|
+
"stability",
|
|
20
|
+
"consensus",
|
|
21
|
+
"scRNA-seq",
|
|
22
|
+
"scikit-learn",
|
|
23
|
+
]
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Development Status :: 3 - Alpha",
|
|
26
|
+
"Intended Audience :: Science/Research",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
|
29
|
+
]
|
|
30
|
+
dependencies = [
|
|
31
|
+
"numpy>=2.0,<2.1",
|
|
32
|
+
"scipy>=1.16,<1.17",
|
|
33
|
+
"scikit-learn>=1.7.2,<1.8",
|
|
34
|
+
"pandas>=2.2,<2.3",
|
|
35
|
+
"joblib>=1.5,<1.6",
|
|
36
|
+
"tqdm>=4.67,<5",
|
|
37
|
+
"numba>=0.60,<0.61",
|
|
38
|
+
"llvmlite>=0.43,<0.44",
|
|
39
|
+
"umap-learn>=0.5.6",
|
|
40
|
+
"pynndescent>=0.5.10,<0.6",
|
|
41
|
+
"matplotlib>=3.9.4",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
Homepage = "https://github.com/DataSlingers/CARVE"
|
|
46
|
+
Repository = "https://github.com/DataSlingers/CARVE"
|
|
47
|
+
Issues = "https://github.com/DataSlingers/CARVE/issues"
|
|
48
|
+
|
|
49
|
+
[project.optional-dependencies]
|
|
50
|
+
dev = [
|
|
51
|
+
"ruff",
|
|
52
|
+
"pytest",
|
|
53
|
+
"pytest-cov",
|
|
54
|
+
]
|
|
55
|
+
notebooks = [
|
|
56
|
+
"jupyterlab>=4.2,<5",
|
|
57
|
+
"ipykernel>=6.29,<7",
|
|
58
|
+
"ipywidgets>=8.1.7",
|
|
59
|
+
"plotly>=6.3.0",
|
|
60
|
+
"rpy2>=3.6.3",
|
|
61
|
+
"pyreadr>=0.5.3",
|
|
62
|
+
"scanpy>=1.11.4",
|
|
63
|
+
"scikit-misc>=0.5.1",
|
|
64
|
+
"anywidget>=0.9.18",
|
|
65
|
+
"igraph>=1.0.0",
|
|
66
|
+
"leidenalg>=0.11.0",
|
|
67
|
+
"pyteomics>=4.7.5",
|
|
68
|
+
"lxml>=6.0.2",
|
|
69
|
+
"scvi>=0.6.8",
|
|
70
|
+
"cmap>=0.7.0",
|
|
71
|
+
"glasbey>=0.3.0",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
[tool.setuptools.packages.find]
|
|
75
|
+
where = ["src"]
|
|
76
|
+
exclude = ["carve.sim", "carve.sim.*"]
|
|
77
|
+
|
|
78
|
+
[tool.pytest.ini_options]
|
|
79
|
+
testpaths = ["tests"]
|
|
80
|
+
|
|
81
|
+
[tool.coverage.run]
|
|
82
|
+
source = ["carve"]
|
|
83
|
+
omit = ["*/carve/sim/*"]
|
|
84
|
+
|
|
85
|
+
[tool.coverage.report]
|
|
86
|
+
show_missing = true
|
|
87
|
+
|
|
88
|
+
[dependency-groups]
|
|
89
|
+
dev = [
|
|
90
|
+
"optuna>=4.8.0",
|
|
91
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Accuracy-based generalizability metrics."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from ._utils import align_cluster_labels
|
|
6
|
+
|
|
7
|
+
# (sample_indices, true_labels, predicted_labels) triple from a prediction run
|
|
8
|
+
PredictionRun = tuple[np.ndarray, np.ndarray, np.ndarray]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def compute_generalizability_scores(
|
|
12
|
+
n_samples: int,
|
|
13
|
+
runs: list[PredictionRun],
|
|
14
|
+
) -> np.ndarray:
|
|
15
|
+
"""Compute per-sample generalizability from prediction runs.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
n_samples : int
|
|
20
|
+
Total number of samples in the original dataset.
|
|
21
|
+
runs : list of tuple
|
|
22
|
+
Each run is (sample_indices, true_labels, predicted_labels).
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
scores : ndarray of shape (n_samples,)
|
|
27
|
+
Per-sample accuracy across available runs. Samples never evaluated
|
|
28
|
+
receive score 0.
|
|
29
|
+
"""
|
|
30
|
+
correct = np.zeros(n_samples, int)
|
|
31
|
+
total = np.zeros(n_samples, int)
|
|
32
|
+
|
|
33
|
+
for sample_idx, true_labels, predicted_labels in runs:
|
|
34
|
+
aligned_predictions = align_cluster_labels(true_labels, predicted_labels)
|
|
35
|
+
correct_mask = (true_labels == aligned_predictions).astype(int)
|
|
36
|
+
np.add.at(correct, sample_idx, correct_mask)
|
|
37
|
+
np.add.at(total, sample_idx, 1)
|
|
38
|
+
|
|
39
|
+
out = np.zeros(n_samples, float)
|
|
40
|
+
valid = total > 0
|
|
41
|
+
out[valid] = correct[valid] / total[valid]
|
|
42
|
+
return out
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Consensus matrix utilities and stability metrics.
|
|
2
|
+
|
|
3
|
+
This module builds consensus matrices from repeated clustering runs and
|
|
4
|
+
derives stability statistics such as Gini, cross-entropy, and PAC.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from scipy.cluster.hierarchy import linkage, leaves_list
|
|
9
|
+
from scipy.spatial.distance import squareform
|
|
10
|
+
|
|
11
|
+
# (sample_indices, labels) pair from a single clustering run
|
|
12
|
+
SampledLabels = tuple[np.ndarray, np.ndarray]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def compute_consensus_matrix(
|
|
16
|
+
n_samples: int,
|
|
17
|
+
runs: list[SampledLabels],
|
|
18
|
+
*,
|
|
19
|
+
return_counts: bool = False,
|
|
20
|
+
) -> np.ndarray | tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
21
|
+
"""Compute a consensus matrix from resampled clustering runs.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
n_samples : int
|
|
26
|
+
Total number of samples in the original dataset.
|
|
27
|
+
runs : list of tuple of (ndarray, ndarray)
|
|
28
|
+
Each run is (sample_indices, labels), where ``sample_indices`` are
|
|
29
|
+
integer indices into the original dataset and ``labels`` are cluster
|
|
30
|
+
assignments for those indices.
|
|
31
|
+
return_counts : bool, default=False
|
|
32
|
+
If True, also return raw co-cluster and co-sample counts.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
consensus_matrix : ndarray of shape (n_samples, n_samples)
|
|
37
|
+
Fraction of times each sample pair co-clustered when co-sampled.
|
|
38
|
+
Pairs never co-sampled are set to NaN.
|
|
39
|
+
co_cluster_counts : ndarray of shape (n_samples, n_samples)
|
|
40
|
+
Raw counts of co-clustering across runs (only if ``return_counts``).
|
|
41
|
+
co_sample_counts : ndarray of shape (n_samples, n_samples)
|
|
42
|
+
Raw counts of co-sampling across runs (only if ``return_counts``).
|
|
43
|
+
"""
|
|
44
|
+
# co_cluster_counts = np.zeros((n_samples, n_samples), dtype=float)
|
|
45
|
+
# co_sample_counts = np.zeros((n_samples, n_samples), dtype=float)
|
|
46
|
+
|
|
47
|
+
# for sample_idx, labels in runs:
|
|
48
|
+
# co_sample_counts[np.ix_(sample_idx, sample_idx)] += 1
|
|
49
|
+
|
|
50
|
+
# for label in np.unique(labels):
|
|
51
|
+
# label_idx = sample_idx[labels == label]
|
|
52
|
+
# co_cluster_counts[np.ix_(label_idx, label_idx)] += 1
|
|
53
|
+
|
|
54
|
+
N = sum(len(np.unique(labels)) for _, labels in runs)
|
|
55
|
+
|
|
56
|
+
S = np.zeros((n_samples, len(runs)), dtype=np.float32)
|
|
57
|
+
B = np.zeros((n_samples, N), dtype=np.float32)
|
|
58
|
+
|
|
59
|
+
col = 0
|
|
60
|
+
for r, (sample_idx, labels) in enumerate(runs):
|
|
61
|
+
S[sample_idx, r] = 1
|
|
62
|
+
for label in np.unique(labels):
|
|
63
|
+
members = sample_idx[labels == label]
|
|
64
|
+
B[members, col] = 1
|
|
65
|
+
col += 1
|
|
66
|
+
|
|
67
|
+
co_sample_counts = S @ S.T
|
|
68
|
+
co_cluster_counts = B @ B.T
|
|
69
|
+
|
|
70
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
71
|
+
consensus_matrix = co_cluster_counts / co_sample_counts
|
|
72
|
+
consensus_matrix[co_sample_counts == 0] = np.nan
|
|
73
|
+
|
|
74
|
+
if return_counts:
|
|
75
|
+
return consensus_matrix, co_cluster_counts, co_sample_counts
|
|
76
|
+
else:
|
|
77
|
+
return consensus_matrix
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def reorder_consensus_matrix(
|
|
81
|
+
consensus_matrix: np.ndarray,
|
|
82
|
+
*,
|
|
83
|
+
fill_nan_for_order: float = 0.0,
|
|
84
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
85
|
+
"""Reorder a consensus matrix by hierarchical clustering of distances.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
consensus_matrix : ndarray of shape (n_samples, n_samples)
|
|
90
|
+
Consensus matrix with values in [0, 1] and NaNs for never-sampled pairs.
|
|
91
|
+
fill_nan_for_order : float, default=0.0
|
|
92
|
+
Value used to replace NaNs when computing the clustering order only.
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
reordered : ndarray of shape (n_samples, n_samples)
|
|
97
|
+
Consensus matrix reordered by dendrogram leaves.
|
|
98
|
+
order : ndarray of shape (n_samples,)
|
|
99
|
+
The permutation of indices applied to rows and columns.
|
|
100
|
+
"""
|
|
101
|
+
matrix_for_order = np.nan_to_num(consensus_matrix, nan=fill_nan_for_order)
|
|
102
|
+
distances = squareform(1.0 - matrix_for_order, checks=False)
|
|
103
|
+
linkage_matrix = linkage(distances, method="average")
|
|
104
|
+
order = leaves_list(linkage_matrix)
|
|
105
|
+
|
|
106
|
+
return consensus_matrix[np.ix_(order, order)], order
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def compute_consensus_metrics(
|
|
110
|
+
consensus_matrices: list[np.ndarray],
|
|
111
|
+
) -> tuple[list[np.ndarray], list[np.ndarray], list[float]]:
|
|
112
|
+
"""Compute stability metrics for multiple consensus matrices.
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
consensus_matrices : list of ndarray
|
|
117
|
+
Consensus matrices to evaluate.
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
gini_list : list of ndarray
|
|
122
|
+
Per-sample stability scores based on the Gini-style statistic.
|
|
123
|
+
ce_list : list of ndarray
|
|
124
|
+
Per-sample stability scores based on cross-entropy.
|
|
125
|
+
pac_list : list of float
|
|
126
|
+
Global PAC-based stability scores (1 - PAC).
|
|
127
|
+
"""
|
|
128
|
+
gini_list = []
|
|
129
|
+
ce_list = []
|
|
130
|
+
pac_list = []
|
|
131
|
+
|
|
132
|
+
for consensus_matrix in consensus_matrices:
|
|
133
|
+
s_gini, s_ce = stability_from_consensus(consensus_matrix)
|
|
134
|
+
pac = compute_consensus_pac(consensus_matrix)
|
|
135
|
+
|
|
136
|
+
gini_list.append(s_gini)
|
|
137
|
+
ce_list.append(s_ce)
|
|
138
|
+
pac_list.append(pac)
|
|
139
|
+
|
|
140
|
+
return gini_list, ce_list, pac_list
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def stability_from_consensus(
|
|
144
|
+
consensus_matrix: np.ndarray,
|
|
145
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
146
|
+
"""Compute per-sample stability scores from a consensus matrix.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
consensus_matrix : ndarray of shape (n_samples, n_samples)
|
|
151
|
+
Consensus matrix with values in [0, 1] and NaNs for never-sampled pairs.
|
|
152
|
+
|
|
153
|
+
Returns
|
|
154
|
+
-------
|
|
155
|
+
stability_gini : ndarray of shape (n_samples,)
|
|
156
|
+
Gini-based stability score per sample in [0, 1].
|
|
157
|
+
stability_ce : ndarray of shape (n_samples,)
|
|
158
|
+
Cross-entropy-based stability score per sample in [0, 1].
|
|
159
|
+
"""
|
|
160
|
+
consensus_probs = np.array(consensus_matrix, dtype=float, copy=True)
|
|
161
|
+
np.fill_diagonal(consensus_probs, np.nan)
|
|
162
|
+
|
|
163
|
+
term = consensus_probs * (1.0 - consensus_probs)
|
|
164
|
+
|
|
165
|
+
clipped = np.clip(consensus_probs, 1e-12, 1.0 - 1e-12)
|
|
166
|
+
entropy = -(clipped * np.log(clipped) + (1.0 - clipped) * np.log(1.0 - clipped))
|
|
167
|
+
|
|
168
|
+
# Gini uncertainty in [0, 0.5]; CE uncertainty in [0, log2]
|
|
169
|
+
uncertainty_gini = 2.0 * np.nanmean(term, axis=1)
|
|
170
|
+
uncertainty_ce = np.nanmean(entropy, axis=1)
|
|
171
|
+
|
|
172
|
+
# Rescale to [0, 1] stability scores
|
|
173
|
+
stability_gini = 1.0 - np.clip(2.0 * uncertainty_gini, 0.0, 1.0)
|
|
174
|
+
stability_ce = 1.0 - np.clip(uncertainty_ce / np.log(2.0), 0.0, 1.0)
|
|
175
|
+
|
|
176
|
+
return stability_gini, stability_ce
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def compute_consensus_pac(
|
|
180
|
+
consensus_matrix: np.ndarray,
|
|
181
|
+
*,
|
|
182
|
+
tau: float = 0.05,
|
|
183
|
+
) -> float:
|
|
184
|
+
"""Compute PAC-based stability score from a consensus matrix.
|
|
185
|
+
|
|
186
|
+
PAC measures the proportion of ambiguous consensus values in
|
|
187
|
+
(tau, 1-tau). This function returns 1 - PAC so that larger values
|
|
188
|
+
indicate greater stability.
|
|
189
|
+
|
|
190
|
+
Parameters
|
|
191
|
+
----------
|
|
192
|
+
consensus_matrix : ndarray of shape (n_samples, n_samples)
|
|
193
|
+
Consensus matrix with values in [0, 1] and NaNs for never-sampled pairs.
|
|
194
|
+
tau : float, default=0.05
|
|
195
|
+
Ambiguity threshold defining the PAC interval.
|
|
196
|
+
|
|
197
|
+
Returns
|
|
198
|
+
-------
|
|
199
|
+
score : float
|
|
200
|
+
Stability score in [0, 1], or NaN if no valid pairs exist.
|
|
201
|
+
"""
|
|
202
|
+
consensus_probs = np.array(consensus_matrix, dtype=float, copy=True)
|
|
203
|
+
n_samples = consensus_probs.shape[0]
|
|
204
|
+
mask = ~np.eye(n_samples, dtype=bool)
|
|
205
|
+
values = consensus_probs[mask]
|
|
206
|
+
values = values[~np.isnan(values)]
|
|
207
|
+
|
|
208
|
+
if values.size == 0:
|
|
209
|
+
return np.nan
|
|
210
|
+
|
|
211
|
+
ambiguous = ((values > tau) & (values < (1 - tau))).sum()
|
|
212
|
+
pac = ambiguous / values.size
|
|
213
|
+
return 1.0 - pac
|