Repository navigation
feat: automatic K selection (CellCharter's ClusterAutoK) - #1276
selmanozleyen wants to merge 22 commits into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1276 +/- ##
==========================================
+ Coverage 78.95% 79.52% +0.56%
==========================================
Files 63 65 +2
Lines 9439 9617 +178
Branches 1574 1599 +25
==========================================
+ Hits 7453 7648 +195
+ Misses 1469 1458 -11
+ Partials 517 511 -6
🚀 New features to boost your workflow:
|
|
I think a Bayesian GMM could also be very interesting for the same purpose: https://scikit-learn.org/stable/modules/generated/sklearn.mixture.BayesianGaussianMixture.html
The downside is that it's quite slow - not sure how hard it would be to make a fast GPU implementation for that @Intron7. |
| stable K is the one whose partition is least sensitive to adding a cluster. | ||
|
|
||
| Reimplements the K selection of CellCharter's ``ClusterAutoK``, using | ||
| :class:`~sklearn.mixture.GaussianMixture` for the fits. |
There was a problem hiding this comment.
Does this have to be tied to the GaussianMixture? We deliberately split the niche calling procedure such that arbitrary embeddings and clustering algos can be combined... In principle, why not use the same approach for Leiden?
There was a problem hiding this comment.
I didn't want to define a clusterer protocol publicly. And to have a hard-coded list of clusterers I wanted some opinions first. Bayesian GMM is a good hint
| previous_curve: np.ndarray | None = None | ||
| converged = False | ||
|
|
||
| for run in range(max_runs): |
There was a problem hiding this comment.
this is embarrassingly parallel. Probably the clustering algorithm has some sort of threading, too, but did you evaluate how well it scales to many cores? Maybe parallelizing at this level could make sense.
There was a problem hiding this comment.
I think we rely on the clustering algorithm to be utilizing the cores as much as possible so that any other threading would oversubscribe only. Also we can only parallelize runs if tolerance is 0, ie we don't early stop.
But from my small benches I didn't see much improvement than 1.5x tbh. And BLAS was already enough.
There was a problem hiding this comment.
I was planning to open a separate issue the next days to discuss a public "extensibility" interface for the niche algos. We can discuss it again in that context.
| # keyed by (run, K) rather than drawn in sequence, so appending a candidate K | ||
| # does not re-seed every other K's fits | ||
| random_state = int(np.random.SeedSequence([seed, run, k]).generate_state(1)[0]) | ||
| labels, nll = _fit_once(X, k, random_state, model_params) |
There was a problem hiding this comment.
The runs only differ at the level of random seeds. Would it make sense to combine this with e.g. bootstrapping?
There was a problem hiding this comment.
I am not sure, it sounds interesting but also sounds out of scope for this PR. wdyt @sarajimenez @timtreis ?
There was a problem hiding this comment.
I do think it's out of the scope of this PR, we're interested in the changes between Ks not across runs. Bootstrapping would be interesting in case we want to add significance. Am I reading this correct, @grst ?
ca085f2 to
afc5110
Compare
6dcb8f6 to
c522628
Compare
9b765d2 to
1f3f110
Compare
b9df9e0 to
ae5d9d7
Compare
1f3f110 to
743c3bc
Compare
ae5d9d7 to
694c55e
Compare
1ef005e to
e57e3bd
Compare
fee85a0 to
de01832
Compare
49e32ae to
e9b8b9a
Compare
de01832 to
145bfee
Compare
e9b8b9a to
07c64ef
Compare
145bfee to
08c7369
Compare
07c64ef to
6a7f8e8
Compare
06b7fa8 to
0c3243f
Compare
6a7f8e8 to
2308673
Compare
0c3243f to
16d7cc2
Compare
4d49338 to
8fc996f
Compare
8c571ab to
572bc93
Compare
Rebuilt on #1284's niche refactor, reusing its pieces instead of keeping parallel ones: main's Clusterer and LeidenClusterer, calculate_niche_custom, the hop shells and nhood_aggregate. New on top of them: - sweep_auto_k and AutoKClusterer (gr/_autok.py), with ClusterAutoKResult next to them; SweepableClusterer extends main's Clusterer with the number-of-clusters parameter the sweep sets per fit - gr.cluster_auto_k and gr.cluster_stability - calculate_niche_cellcharter takes a (min, max) tuple or a sequence for n_clusters and then sweeps, with max_runs, convergence_tol and store_labels; _fit_clusterers writes an estimator's extra labelings and diagnostics when it leaves them - gr.nhood_entropy, the entropy of nhood_aggregate's neighborhood composition
…e hint at the right argument
…, NaN entropy without neighbors
0089ea7 to
4275371
Compare
Rename SweepableClusterer to KClusterer and export it with Clusterer. calculate_niche_cellcharter takes any KClusterer; a fixed K sets whichever of n_clusters/n_components the clusterer has.
A clusterer without a number of clusters is fitted as it is; n_clusters defaults to None (10 for a KClusterer) and must stay None for one without K.
calculate_niche_cellcharter keeps an int n_clusters plus clusterer; select K with cluster_auto_k on its embedding. Removes AutoKClusterer and the niche hooks, renames nll to fit_loss, keeps empty components as categories.
cluster_auto_k needs use_rep ('X' for adata.X); stability rows indexed by K; labels outside 0..K-1 rejected; TypeError for non-clusterers. calculate_niche_cellcharter keeps a given clusterer's K when n_clusters is None. Docs get their own sections.
calculate_niche_cellcharter: n_clusters (default 10) always sets a clusterer's K; K detection shared via k_param. cluster_auto_k validates before copying, guards BayesianGaussianMixture on sparse input, rejects non-integer labels, documents adata[mask].copy(). Tests merged and parametrized, always-true asserts tightened, missing cases added.
Clusterer and KClusterer are re-exported from squidpy.types and documented on its page, like the result tuples; squidpy.gr still exports them.
One fit path via fit_predict, failed fits get an add_note instead of message matching, no estimator-specific sparse guard, validation in one place, k_param next to the protocols, n_clusters defaults to a visible 10, tests no longer monkeypatch internals.
One _auto_k helper for the repeated sweep arguments, one relabeling KMeans instead of four classes, table layout asserted once, copy and store_labels in one sweep, obsm and SpatialData inputs in one parametrized test.
fixes: #1244
will add more description but I want to start somewhere @sarajimenez, @FrancescaDr these are all the things I could find in https://github.com/Jieran-S/hitchhikers_guide_to_spatial_project/blob/spatial_clustering/notebooks/Spatially_aware_clustering/sac.ipynb.
Anything else needed? If everything is there, how does this state look like for you?