Skip to content

feat: automatic K selection (CellCharter's ClusterAutoK) - #1276

Draft
selmanozleyen wants to merge 9 commits into
mainfrom
feat/cluster-auto-k-seed
Draft

selmanozleyen wants to merge 9 commits into
mainfrom
feat/cluster-auto-k-seed

Conversation

@selmanozleyen

@selmanozleyen selmanozleyen commented Aug 22, 2026 •

Copy link
Copy Markdown
Member

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?

@selmanozleyen selmanozleyen changed the title Rename random_state to seed and improve niche function clarity feat: automatic K selection (CellCharter's ClusterAutoK) Aug 22, 2026
@codecov

codecov Bot commented Aug 22, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.33074% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.27%. Comparing base (3a76b27) to head (8c571ab).

Files with missing lines Patch % Lines
src/squidpy/gr/_autok.py 96.12% 3 Missing and 3 partials ⚠️
src/squidpy/gr/_clusterers.py 63.63% 4 Missing ⚠️
src/squidpy/gr/_cluster_auto_k.py 96.72% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1276      +/-   ##
==========================================
+ Coverage   78.86%   79.27%   +0.41%     
==========================================
  Files          65       67       +2     
  Lines        9611     9860     +249     
  Branches     1591     1629      +38     
==========================================
+ Hits         7580     7817     +237     
- Misses       1500     1508       +8     
- Partials      531      535       +4     
Files with missing lines Coverage Δ
src/squidpy/gr/_nhood.py 62.99% <100.00%> (+1.06%) ⬆️
src/squidpy/gr/_niche.py 88.31% <100.00%> (+0.31%) ⬆️
src/squidpy/gr/_cluster_auto_k.py 96.72% <96.72%> (ø)
src/squidpy/gr/_clusterers.py 80.48% <63.63%> (-6.61%) ⬇️
src/squidpy/gr/_autok.py 96.12% <96.12%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@grst

grst commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

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

This class allows to infer an approximate posterior distribution over the parameters of a Gaussian mixture distribution. The effective number of components can be inferred from the data.

The downside is that it's quite slow - not sure how hard it would be to make a fast GPU implementation for that @Intron7.

Comment thread src/squidpy/gr/_cluster_auto_k.py Outdated
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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/squidpy/gr/_autok.py
previous_curve: np.ndarray | None = None
converged = False

for run in range(max_runs):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/squidpy/gr/_autok.py Outdated
# 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The runs only differ at the level of random seeds. Would it make sense to combine this with e.g. bootstrapping?

@selmanozleyen selmanozleyen Aug 25, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure, it sounds interesting but also sounds out of scope for this PR. wdyt @sarajimenez @timtreis ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ?

@selmanozleyen
selmanozleyen force-pushed the feat/cluster-auto-k-seed branch from ca085f2 to afc5110 Compare September 3, 2026 12:03
@selmanozleyen
selmanozleyen changed the base branch from main to feat/experimental-params-typeddicts September 3, 2026 12:04
@selmanozleyen
selmanozleyen force-pushed the feat/experimental-params-typeddicts branch from 6dcb8f6 to c522628 Compare September 15, 2026 16:50
@selmanozleyen
selmanozleyen force-pushed the feat/cluster-auto-k-seed branch from 9b765d2 to 1f3f110 Compare September 25, 2026 16:05
@selmanozleyen
selmanozleyen force-pushed the feat/experimental-params-typeddicts branch from b9df9e0 to ae5d9d7 Compare September 25, 2026 19:42
@selmanozleyen
selmanozleyen force-pushed the feat/cluster-auto-k-seed branch from 1f3f110 to 743c3bc Compare September 25, 2026 19:56
@selmanozleyen
selmanozleyen force-pushed the feat/experimental-params-typeddicts branch from ae5d9d7 to 694c55e Compare September 25, 2026 20:05
@selmanozleyen
selmanozleyen force-pushed the feat/cluster-auto-k-seed branch 4 times, most recently from 1ef005e to e57e3bd Compare September 25, 2026 22:04
@selmanozleyen
selmanozleyen force-pushed the feat/experimental-params-typeddicts branch from fee85a0 to de01832 Compare September 25, 2026 22:25
@selmanozleyen
selmanozleyen force-pushed the feat/cluster-auto-k-seed branch 2 times, most recently from 49e32ae to e9b8b9a Compare September 29, 2026 14:58
@selmanozleyen
selmanozleyen force-pushed the feat/experimental-params-typeddicts branch from de01832 to 145bfee Compare September 29, 2026 14:58
@selmanozleyen
selmanozleyen force-pushed the feat/cluster-auto-k-seed branch from e9b8b9a to 07c64ef Compare September 29, 2026 16:05
@selmanozleyen
selmanozleyen force-pushed the feat/experimental-params-typeddicts branch from 145bfee to 08c7369 Compare September 29, 2026 16:05
@selmanozleyen
selmanozleyen force-pushed the feat/cluster-auto-k-seed branch from 07c64ef to 6a7f8e8 Compare October 1, 2026 12:15
@selmanozleyen
selmanozleyen force-pushed the feat/experimental-params-typeddicts branch 2 times, most recently from 06b7fa8 to 0c3243f Compare October 1, 2026 12:22
@selmanozleyen
selmanozleyen force-pushed the feat/cluster-auto-k-seed branch from 6a7f8e8 to 2308673 Compare October 1, 2026 12:22
@selmanozleyen
selmanozleyen force-pushed the feat/experimental-params-typeddicts branch from 0c3243f to 16d7cc2 Compare October 5, 2026 12:26
@selmanozleyen
selmanozleyen force-pushed the feat/cluster-auto-k-seed branch from 4d49338 to 8fc996f Compare October 5, 2026 12:26
Base automatically changed from feat/experimental-params-typeddicts to main October 9, 2026 14:34
@selmanozleyen
selmanozleyen force-pushed the feat/cluster-auto-k-seed branch 2 times, most recently from 8c571ab to 572bc93 Compare October 27, 2026 17:25
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
@selmanozleyen
selmanozleyen force-pushed the feat/cluster-auto-k-seed branch from 0089ea7 to 4275371 Compare October 27, 2026 23:58

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add automatic K selection (CellCharter's ClusterAutoK) for the cellcharter niche flavor

3 participants