Predefined subtype support, configurable optimisation weights, AnnData-based I/O, testing suite - #6
Open
robinfallegger wants to merge 64 commits into
Open
robinfallegger wants to merge 64 commits into
robinfallegger wants to merge 64 commits into
Conversation
…placed with errors and warnings instead (more user customisation)
…ti-level subtype annotations
…s common to both kmeans and predefined subtype definitions
…urn AnnData setup_reference() now supports predefined, pre-annotated subtype hierarchies as an alternative to k-means-discovered subtypes: cell_type_key accepts a single column (k-means path, as before) or a list of columns ordered coarsest to finest (predefined path, new). Output changed from a dict to a real AnnData: X holds the DE-gene-subset centroids, obs holds the full subtype hierarchy (one column per level, indexed by the finest subtype), var_names holds the DE genes, and uns['ratios'] holds per-major-type cell fractions. Saving/loading a reference is now just adata.write_h5ad()/sc.read_h5ad(), no conversion step needed. validate_reference_input() now validates every level of a multi-key hierarchy (column existence and missing values), not just the coarsest. kws_kmeans/kws_predefined/kws_de_genes forward extra keyword arguments to the underlying subtype-discovery and gene-selection functions, for advanced use without growing setup_reference's own parameter list. Note: DOT.__init__ still expects the old dict shape and has not been updated to accept this AnnData output yet.
Records the subtype hierarchy's column order (coarsest to finest) explicitly in uns, rather than leaving callers to assume obs.columns[0] is the coarsest level.
The new implementation in setup_reference matches the old behaviour (tested in test_legacy_equivalence.py)
- Capture X_sp's per-spot norm before freeing it - Split the l_c block into two passes over existing spot batches (accumulate, then apply correction) - Reconstruct each batch's raw values from X_sp_row_norm * the stored norm - Add float16/float32 branching to both passes' matmuls, matching the existing pattern
- clamp(min=1e-10) before log2 silently diverged from R's clip-after-log2 approach - log2(0): was -33.22, now -20 (matches R) - negative inputs: was -33.22, now 0 (matches R's NaN->0 handling) - +inf: was 0, now -20 (matches R's is.infinite() catching both signs) - add TestSafeLog2, pinning these boundary values directly
…tead of internally
…-clustered labels
…lan_subtype_refinement warnings into aggregate form
Author
|
Just to address your comments in issues #4:
For changes proposed in #5:
I also added a test suite to check that this actually all works. I also checked that the new setup reference function returns the same results as the original function. Happy to discuss changes you think are necessary. At some point I wanted to add some docs with simple vignettes using examples from the original paper. |
Owner
|
Thanks @robinfallegger . These changes look very promising overall. As soon I find some time I'll do a code review, and propose some restructuring changes to keep the essence of how DOT was built, while leaving it flexible as you did here. |
Author
|
Sounds good! Let me know if anything is unclear or needs restructuring |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Sorry this became a much bigger PR than I had originally thought. The main goal was to increase tuning and make storing reference centroids easier in python. More details below and in CHANGELOG.md
Closes #4
Closes #5
Summary
This PR adds the option to use a predefined cell/cluster subtype hierarchy instead of running kmeans subclustering. The preprocessing functions
setup_referenceandsetup_spatialnow returnAnnDataobjects, allowing you to easily store reference clusters for future use. The optimisation weights are now exposed and can be tuned by the user. It also adds tests for main components.In detail
setup_referencechanged how it handles preprocessing. It no longer runs QC filtering or HVG selection automatically. It validates your input instead and raises an error telling you whichscanpyfunction to run first. It also exposes the k-means subtype-count heuristic and per-type cell cap as parameters instead of hardcoding them. The defaults match the previous behaviour. This would also allow to dropscanpyfrom the dependencies.Predefined subtype support lets you pass your own hierarchical cell subtype annotations into
setup_reference. Cluster centroids can be directly computed from these annotations, increasing preprocessing speed by entirely skipping kmeans clustering. A newselect_de_genes()step supports pluggable DE gene scoring.When running with fine-grained subtype annotations,
setup_referencecan also further sub-cluster predefined subtypes that are under-resolved relative to the k-means heuristic (that defines the number of expected clusters based on the number of cells). This prevents major cell types with few sub-clusters from being under-represented in the optimisation just because they were coarsely pre-labelled. Insetup_reference, setrefine_undersized=Trueand passkws_refineto control it. This refinement runs kmeans clustering only on major cell types with too few subtypes compared to their number of cells. The new functionplan_subtype_refinement()returns a dataframe with cell counts per major cell type, and expected-vs-current cluster counts.setup_referenceandsetup_spatialnow returnAnnDataobjects instead ofdicts.DOT()still accepts the olddictformat but raises aFutureWarning.DOT()now acceptsgene_weight,spot_weight,spatial_weight,ratios_weight,sparsity_coef, andcluster_weightdirectly. These parameters are transformed to the lambdas used in the loss-function used in the optimisation after normalisation with respect to the dimensionality of the feature space, number of spatial units or the number of clusters as described in the original DOT publication (see the new_default_weights_to_lambdasfunction). Alternatively, you can set any lambda value directly, or pass your ownweights_to_lambdasfunction.cluster_weightnow implementsl_c. This is the cluster-wise cosine term from the R package. It was missing from the Python implementation until now. It is off by default (same as in the R version), so existing behaviour is unchanged.If running the optimisation with a hierarchical cell type annotation, the
get_weights()method can now return the aggregated weights at any of the annotation levels. For example, if your reference dataset has three levels of annotations (e.g.level_keys = ['major_type', 'subset', 'cell_state']):get_weights(level="major_type")returns weights aggregated over Fibroblasts, Epithelial cells and Immune cells.get_weights(level="subset")returns weights aggregated for Myeloid and Lymphoid in the immune compartment (and for other subsets of Fibroblasts and Epithelial cells if they exist).get_weights(level="subtype")returns weights at the finest resolution, for separating Naive and Memory B cells.Breaking changes
setup_referenceno longer runs QC filtering or HVG selection. It validates and raises instead.setup_referencedropped thecopyparameter.setup_referenceandsetup_spatialreturnAnnDatainstead ofdict.remove_mtwas renamed towarn_mtin both functions.DOT.fit()no longer acceptsmode,ratios_weight, ormax_spot_size. Set these onDOT()instead.max_spot_sizewas renamed tomax_size.DOT.get_weights()returns aDataFrameinstead of a numpy array.kmeans_define_subtypes()returns aDataFrame.Full list in the changelog.
Also fixed
_safe_log2returned different values than R'ssafelog2for zero and negative inputs. This affects the abundance-matching and spatial-coherence loss terms.Test plan
pytest tests/, about 349 tests)run_dot_cli.pyandexample.pyupdated for the new APIl_c