SpatialQuery 0.0.5__tar.gz → 0.0.6__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 (35) hide show
  1. {spatialquery-0.0.5/SpatialQuery.egg-info → spatialquery-0.0.6}/PKG-INFO +15 -2
  2. {spatialquery-0.0.5 → spatialquery-0.0.6}/README.md +14 -1
  3. {spatialquery-0.0.5 → spatialquery-0.0.6}/SpatialQuery/__init__.py +3 -1
  4. {spatialquery-0.0.5 → spatialquery-0.0.6}/SpatialQuery/plotting.py +82 -1
  5. {spatialquery-0.0.5 → spatialquery-0.0.6}/SpatialQuery/spatial_gene_covarying.py +1 -0
  6. {spatialquery-0.0.5 → spatialquery-0.0.6}/SpatialQuery/spatial_query.py +216 -3
  7. {spatialquery-0.0.5 → spatialquery-0.0.6}/SpatialQuery/spatial_query_multiple_fov.py +452 -159
  8. {spatialquery-0.0.5 → spatialquery-0.0.6}/SpatialQuery/spatial_utils.py +301 -3
  9. {spatialquery-0.0.5 → spatialquery-0.0.6/SpatialQuery.egg-info}/PKG-INFO +15 -2
  10. {spatialquery-0.0.5 → spatialquery-0.0.6}/pyproject.toml +1 -1
  11. {spatialquery-0.0.5 → spatialquery-0.0.6}/LICENSE +0 -0
  12. {spatialquery-0.0.5 → spatialquery-0.0.6}/MANIFEST.in +0 -0
  13. {spatialquery-0.0.5 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/__init__.py +0 -0
  14. {spatialquery-0.0.5 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/core_methods.py +0 -0
  15. {spatialquery-0.0.5 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/cpp_src/EliasFano.h +0 -0
  16. {spatialquery-0.0.5 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/cpp_src/QueryScore.cpp +0 -0
  17. {spatialquery-0.0.5 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/cpp_src/QueryScore.h +0 -0
  18. {spatialquery-0.0.5 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/cpp_src/Serialization.h +0 -0
  19. {spatialquery-0.0.5 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/cpp_src/__init__.py +0 -0
  20. {spatialquery-0.0.5 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/cpp_src/const.h +0 -0
  21. {spatialquery-0.0.5 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/cpp_src/eliasFano.cpp +0 -0
  22. {spatialquery-0.0.5 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/cpp_src/fp_growth.cpp +0 -0
  23. {spatialquery-0.0.5 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/cpp_src/fp_growth.h +0 -0
  24. {spatialquery-0.0.5 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/cpp_src/serialization.cpp +0 -0
  25. {spatialquery-0.0.5 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/cpp_src/typedef.h +0 -0
  26. {spatialquery-0.0.5 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/cpp_src/utils.cpp +0 -0
  27. {spatialquery-0.0.5 → spatialquery-0.0.6}/SpatialQuery/scfind4sp/cpp_src/utils.h +0 -0
  28. {spatialquery-0.0.5 → spatialquery-0.0.6}/SpatialQuery/spatial_differential_pattern.py +0 -0
  29. {spatialquery-0.0.5 → spatialquery-0.0.6}/SpatialQuery/utils.py +0 -0
  30. {spatialquery-0.0.5 → spatialquery-0.0.6}/SpatialQuery.egg-info/SOURCES.txt +0 -0
  31. {spatialquery-0.0.5 → spatialquery-0.0.6}/SpatialQuery.egg-info/dependency_links.txt +0 -0
  32. {spatialquery-0.0.5 → spatialquery-0.0.6}/SpatialQuery.egg-info/requires.txt +0 -0
  33. {spatialquery-0.0.5 → spatialquery-0.0.6}/SpatialQuery.egg-info/top_level.txt +0 -0
  34. {spatialquery-0.0.5 → spatialquery-0.0.6}/setup.cfg +0 -0
  35. {spatialquery-0.0.5 → spatialquery-0.0.6}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SpatialQuery
3
- Version: 0.0.5
3
+ Version: 0.0.6
4
4
  Summary: Spatial query tools for analyzing spatial transcriptomics data
5
5
  Author-email: Shaokun An <shan12@bwh.harvard.edu>
6
6
  License-Expression: MIT
@@ -35,6 +35,10 @@ Requires-Dist: sphinx-copybutton; extra == "docs"
35
35
  Requires-Dist: myst-parser; extra == "docs"
36
36
  Dynamic: license-file
37
37
 
38
+ <p align="center">
39
+ <img src="https://raw.githubusercontent.com/ShaokunAn/Spatial-Query/main/docs/_static/img/logo.png" alt="SpatialQuery" width="480">
40
+ </p>
41
+
38
42
  # SpatialQuery
39
43
 
40
44
  SpatialQuery is a Python package for systematic spatial analysis of single-cell resolution spatial omics data. It provides a unified framework to discover, quantify, and compare recurring spatial cell type patterns — termed *motifs* — across single or multiple fields of view (FOVs).
@@ -47,6 +51,7 @@ Key capabilities include:
47
51
 
48
52
  - **Spatial motif discovery**: Identify frequent cell type patterns in local neighborhoods
49
53
  - **Motif enrichment analysis**: Statistically test whether motifs occur more than expected
54
+ - **Motif composition**: Quantify the relative abundance of each motif cell type within motif-positive neighborhoods
50
55
  - **Differential motif analysis**: Compare spatial compositions across conditions
51
56
  - **Motif-associated differential expression**: Find DE genes linked to specific motifs
52
57
  - **Cross-cell gene-gene covariation**: Detect spatially dependent intercellular gene correlations
@@ -58,13 +63,15 @@ Key capabilities include:
58
63
  pip install SpatialQuery
59
64
  ```
60
65
 
66
+ > **Note:** Installation typically takes ~5 minutes depending on your environment.
67
+
61
68
  ## Documentation
62
69
 
63
70
  Full documentation, tutorials, and API reference: **[https://spatialquery.readthedocs.io/en/latest/](https://spatialquery.readthedocs.io/en/latest/)**
64
71
 
65
72
  ## Reproducibility
66
73
 
67
- The following tutorials reproduce the main analyses presented in our manuscript:
74
+ The following tutorials reproduce the main analyses presented in our manuscript. Each tutorial takes approximately 5–20 minutes to run depending on dataset size and hardware.
68
75
 
69
76
  - [Mouse organogenesis — single-FOV analysis (seqFISH)](https://spatialquery.readthedocs.io/en/latest/tutorials/tutorial_1.html)
70
77
  - [Kidney disease atlas — multi-condition analysis (MERFISH)](https://spatialquery.readthedocs.io/en/latest/tutorials/tutorial_2.html)
@@ -90,6 +97,12 @@ spm = spatial_query_multi(adatas=adatas, datasets=datasets,
90
97
  enrich_motif = spm.motif_enrichment_dist(ct="T_cell", dataset="healthy", max_dist=10, min_support=0.5)
91
98
  ```
92
99
 
100
+ ## Citation
101
+
102
+ If you use SpatialQuery in your research, please cite:
103
+
104
+ > An, S. et al. [SpatialQuery: scalable discovery and molecular characterization of multicellular motifs from spatial omics data](https://www.biorxiv.org/content/10.64898/2026.04.22.720136v1). *bioRxiv* (2026).
105
+
93
106
  ## License
94
107
 
95
108
  MIT
@@ -1,3 +1,7 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/ShaokunAn/Spatial-Query/main/docs/_static/img/logo.png" alt="SpatialQuery" width="480">
3
+ </p>
4
+
1
5
  # SpatialQuery
2
6
 
3
7
  SpatialQuery is a Python package for systematic spatial analysis of single-cell resolution spatial omics data. It provides a unified framework to discover, quantify, and compare recurring spatial cell type patterns — termed *motifs* — across single or multiple fields of view (FOVs).
@@ -10,6 +14,7 @@ Key capabilities include:
10
14
 
11
15
  - **Spatial motif discovery**: Identify frequent cell type patterns in local neighborhoods
12
16
  - **Motif enrichment analysis**: Statistically test whether motifs occur more than expected
17
+ - **Motif composition**: Quantify the relative abundance of each motif cell type within motif-positive neighborhoods
13
18
  - **Differential motif analysis**: Compare spatial compositions across conditions
14
19
  - **Motif-associated differential expression**: Find DE genes linked to specific motifs
15
20
  - **Cross-cell gene-gene covariation**: Detect spatially dependent intercellular gene correlations
@@ -21,13 +26,15 @@ Key capabilities include:
21
26
  pip install SpatialQuery
22
27
  ```
23
28
 
29
+ > **Note:** Installation typically takes ~5 minutes depending on your environment.
30
+
24
31
  ## Documentation
25
32
 
26
33
  Full documentation, tutorials, and API reference: **[https://spatialquery.readthedocs.io/en/latest/](https://spatialquery.readthedocs.io/en/latest/)**
27
34
 
28
35
  ## Reproducibility
29
36
 
30
- The following tutorials reproduce the main analyses presented in our manuscript:
37
+ The following tutorials reproduce the main analyses presented in our manuscript. Each tutorial takes approximately 5–20 minutes to run depending on dataset size and hardware.
31
38
 
32
39
  - [Mouse organogenesis — single-FOV analysis (seqFISH)](https://spatialquery.readthedocs.io/en/latest/tutorials/tutorial_1.html)
33
40
  - [Kidney disease atlas — multi-condition analysis (MERFISH)](https://spatialquery.readthedocs.io/en/latest/tutorials/tutorial_2.html)
@@ -53,6 +60,12 @@ spm = spatial_query_multi(adatas=adatas, datasets=datasets,
53
60
  enrich_motif = spm.motif_enrichment_dist(ct="T_cell", dataset="healthy", max_dist=10, min_support=0.5)
54
61
  ```
55
62
 
63
+ ## Citation
64
+
65
+ If you use SpatialQuery in your research, please cite:
66
+
67
+ > An, S. et al. [SpatialQuery: scalable discovery and molecular characterization of multicellular motifs from spatial omics data](https://www.biorxiv.org/content/10.64898/2026.04.22.720136v1). *bioRxiv* (2026).
68
+
56
69
  ## License
57
70
 
58
71
  MIT
@@ -7,7 +7,8 @@ from .plotting import (
7
7
  plot_motif_grid,
8
8
  plot_motif_rand,
9
9
  plot_motif_celltype,
10
- plot_all_center_motif
10
+ plot_all_center_motif,
11
+ plot_motif_composition
11
12
  )
12
13
 
13
14
  __all__ = [
@@ -22,5 +23,6 @@ __all__ = [
22
23
  'plot_motif_rand',
23
24
  'plot_motif_celltype',
24
25
  'plot_all_center_motif',
26
+ 'plot_motif_composition',
25
27
  'interactive_motif'
26
28
  ]
@@ -670,7 +670,88 @@ def plot_motif_enrichment_heatmap(enrich_df: pd.DataFrame,
670
670
  plt.savefig(save_path, dpi=300, bbox_inches='tight')
671
671
 
672
672
  plt.show()
673
-
673
+
674
+
675
+ def plot_motif_composition(comp_df: pd.DataFrame,
676
+ motif: Optional[List[str]] = None,
677
+ bins: int = 25,
678
+ colors: Optional[List] = None,
679
+ figsize: tuple = (6, 5),
680
+ title: Optional[str] = None,
681
+ save_path: Optional[str] = None):
682
+ """
683
+ Plot the distribution of each motif cell type's proportion across motif-positive center cells.
684
+
685
+ One semi-transparent histogram per motif cell type, on fixed bins over [0, 1]; the legend
686
+ reports the median and interquartile range (IQR) of each type's proportion.
687
+
688
+ Parameters
689
+ ----------
690
+ comp_df : pd.DataFrame
691
+ Output from motif_composition_dist or motif_composition_knn.
692
+ motif : List[str], optional
693
+ Motif to plot. May be omitted when comp_df holds a single motif.
694
+ bins : int, default=25
695
+ Number of equal-width bins on [0, 1].
696
+ colors : List, optional
697
+ One color per motif cell type, in sorted cell type order. Defaults to the tab10 palette.
698
+ figsize : tuple, default=(6, 5)
699
+ Figure size.
700
+ title : str, optional
701
+ Figure title. Defaults to the center cell type and the motif.
702
+ save_path : str, optional
703
+ Path to save the figure. If None, the figure will not be saved.
704
+
705
+ Returns
706
+ -------
707
+ None
708
+ Displays a histogram of motif cell type proportions.
709
+ """
710
+ if len(comp_df) == 0:
711
+ print("No motif-positive center cells to plot.")
712
+ return
713
+
714
+ keys = comp_df['motifs'].map(spatial_utils.canonical_motif)
715
+ if motif is None:
716
+ if keys.nunique() > 1:
717
+ raise ValueError("comp_df holds several motifs; specify which one to plot with motif.")
718
+ motif_key = keys.iloc[0]
719
+ else:
720
+ motif_key = spatial_utils.canonical_motif([motif] if isinstance(motif, str) else motif)
721
+ df = comp_df[keys == motif_key]
722
+ if len(df) == 0:
723
+ raise ValueError(f"Motif {list(motif_key)} is not in comp_df.")
724
+
725
+ cell_types = list(motif_key)
726
+ if colors is None:
727
+ colors = [plt.cm.tab10(i % 10) for i in range(len(cell_types))]
728
+ bin_edges = np.linspace(0, 1, bins + 1)
729
+
730
+ fig, ax = plt.subplots(figsize=figsize)
731
+ for cell_type, color in zip(cell_types, colors):
732
+ values = df.loc[df['cell_type'] == cell_type, 'proportion'].to_numpy(dtype=float)
733
+ q25, median, q75 = np.percentile(values, [25, 50, 75])
734
+ ax.hist(values, bins=bin_edges, color=color, alpha=0.5,
735
+ label=f'{cell_type}\n(median={median:.2f}, IQR={q75 - q25:.2f})')
736
+
737
+ ax.set_xlim(-0.03, 1.03)
738
+ ax.set_xlabel('Relative abundance among motif-associated cells', fontsize=12)
739
+ ax.set_ylabel('Number of motif-positive anchors', fontsize=12)
740
+ ax.spines[['top', 'right']].set_visible(False)
741
+ ax.legend(frameon=False, fontsize=10, loc='best')
742
+
743
+ if title is None:
744
+ centers = df['center'].unique()
745
+ center = f'{centers[0]} | ' if len(centers) == 1 else ''
746
+ title = f'{center}{" + ".join(cell_types)}'
747
+ ax.set_title(title, fontsize=13)
748
+ plt.tight_layout()
749
+
750
+ if save_path is not None:
751
+ plt.savefig(save_path, dpi=300, bbox_inches='tight')
752
+
753
+ plt.show()
754
+
674
755
 
675
756
  def plot_differential_pattern_heatmap(
676
757
  diff_motif: dict,
@@ -4395,6 +4395,7 @@ def compute_gene_gene_correlation_by_type_binary_multi_fov(
4395
4395
  if len(non_center_types) == 1:
4396
4396
  print(f"Only one non-center cell type in motif: {non_center_types}. Using compute_gene_gene_correlation_binary method.")
4397
4397
  result = compute_gene_gene_correlation_binary_multi_fov(
4398
+ sq_objs=sq_objs,
4398
4399
  ct=ct,
4399
4400
  motif=motif,
4400
4401
  dataset=dataset,
@@ -157,6 +157,7 @@ class spatial_query:
157
157
  k: int = 30,
158
158
  min_support: float = 0.5,
159
159
  max_dist: float = 20,
160
+ if_max: bool = True,
160
161
  ) -> pd.DataFrame:
161
162
  """
162
163
  Find frequent patterns within the KNNs of certain cell type.
@@ -171,6 +172,9 @@ class spatial_query:
171
172
  Threshold of frequency to consider a pattern as a frequent pattern.
172
173
  max_dist : float, default=20
173
174
  Maximum distance for considering a cell as a neighbor.
175
+ if_max : bool, default=True
176
+ Return only maximal patterns. Set to False to return every frequent pattern,
177
+ which nested motif enrichment needs to look up sub-pattern supports.
174
178
 
175
179
  Returns
176
180
  -------
@@ -190,7 +194,7 @@ class spatial_query:
190
194
  k=k,
191
195
  min_support=min_support,
192
196
  max_dist=max_dist,
193
- if_max=True
197
+ if_max=if_max
194
198
  )
195
199
 
196
200
  return fp
@@ -200,6 +204,7 @@ class spatial_query:
200
204
  max_dist: float = 20,
201
205
  min_size: int = 0,
202
206
  min_support: float = 0.5,
207
+ if_max: bool = True,
203
208
  ):
204
209
  """
205
210
  Find frequent patterns within the radius of certain cell type.
@@ -214,6 +219,9 @@ class spatial_query:
214
219
  Minimum neighborhood size for each point to consider.
215
220
  min_support : float, default=0.5
216
221
  Threshold of frequency to consider a pattern as a frequent pattern.
222
+ if_max : bool, default=True
223
+ Return only maximal patterns. Set to False to return every frequent pattern,
224
+ which nested motif enrichment needs to look up sub-pattern supports.
217
225
 
218
226
  Returns
219
227
  -------
@@ -233,7 +241,8 @@ class spatial_query:
233
241
  max_dist=max_dist,
234
242
  min_size=min_size,
235
243
  min_support=min_support,
236
- cinds=cinds
244
+ cinds=cinds,
245
+ if_max=if_max
237
246
  )
238
247
 
239
248
  return fp
@@ -244,7 +253,8 @@ class spatial_query:
244
253
  k: int = 30,
245
254
  min_support: float = 0.5,
246
255
  max_dist: float = 20,
247
- return_cellID: bool = False
256
+ return_cellID: bool = False,
257
+ mode: str = 'maximal'
248
258
  ) -> pd.DataFrame:
249
259
  """
250
260
  Perform motif enrichment analysis using k-nearest neighbors (KNN).
@@ -265,6 +275,16 @@ class spatial_query:
265
275
  return_cellID : bool, default=False
266
276
  Indicate whether return cell IDs for each frequent pattern within the neighborhood of grid points.
267
277
  By defaults do not return cell ID.
278
+ mode : {'maximal', 'nested'}, default='maximal'
279
+ How the motifs to test are chosen when motifs=None. Ignored when motifs is given.
280
+
281
+ 'maximal'
282
+ Test the maximal patterns only.
283
+ 'nested'
284
+ Test the maximal patterns, then descend into the sub-patterns of any that
285
+ are not significant, stopping a branch as soon as it becomes significant or
286
+ reaches length 1. Finds cores that a maximal pattern hides when one abundant
287
+ cell type dilutes it. Multiple-testing correction covers everything tested.
268
288
 
269
289
  Returns
270
290
  -------
@@ -285,6 +305,27 @@ class spatial_query:
285
305
  if ct not in self.labels.unique():
286
306
  raise ValueError(f"Found no {ct} in {self.label_key}!")
287
307
 
308
+ if mode not in ('nested', 'maximal'):
309
+ raise ValueError(f"mode must be 'nested' or 'maximal', got {mode!r}")
310
+
311
+ # Nested descent only decides WHICH motifs get tested; each batch is scored by this
312
+ # same method, so the statistics are identical to the maximal mode's.
313
+ if mode == 'nested' and motifs is None:
314
+ fp_all = self.find_fp_knn(ct=ct, k=k, min_support=min_support,
315
+ max_dist=max_dist, if_max=False)
316
+ if len(fp_all) == 0:
317
+ return spatial_utils.apply_fdr_correction(pd.DataFrame())
318
+ # Cell IDs are DataFrame columns here, so they travel with the rows and need
319
+ # no separate merging.
320
+ return spatial_utils.nested_motif_descent(
321
+ test_motifs=lambda ms: self.motif_enrichment_knn(
322
+ ct=ct, motifs=ms, k=k, min_support=min_support, max_dist=max_dist,
323
+ return_cellID=return_cellID, mode='maximal'),
324
+ maximal_motifs=spatial_utils.find_maximal_patterns(fp_all)['itemsets'],
325
+ support_of={spatial_utils.canonical_motif(i): s
326
+ for i, s in zip(fp_all['itemsets'], fp_all['support'])},
327
+ )
328
+
288
329
  dists, idxs = self.kd_tree.query(self.spatial_pos,
289
330
  k=k + 1, workers=-1
290
331
  ) # use k+1 to find the knn except for the points themselves
@@ -418,6 +459,7 @@ class spatial_query:
418
459
  min_size: int = 0,
419
460
  min_support: float = 0.5,
420
461
  return_cellID: bool = False,
462
+ mode: str = 'maximal',
421
463
  ) -> DataFrame:
422
464
  """
423
465
  Perform motif enrichment analysis within a specified radius-based neighborhood.
@@ -438,6 +480,16 @@ class spatial_query:
438
480
  return_cellID : bool, default=False
439
481
  Indicate whether return cell IDs for each motif within the neighborhood of central cell type.
440
482
  By defaults do not return cell ID.
483
+ mode : {'maximal', 'nested'}, default='maximal'
484
+ How the motifs to test are chosen when motifs=None. Ignored when motifs is given.
485
+
486
+ 'maximal'
487
+ Test the maximal patterns only.
488
+ 'nested'
489
+ Test the maximal patterns, then descend into the sub-patterns of any that
490
+ are not significant, stopping a branch as soon as it becomes significant or
491
+ reaches length 1. Finds cores that a maximal pattern hides when one abundant
492
+ cell type dilutes it. Multiple-testing correction covers everything tested.
441
493
 
442
494
  Returns
443
495
  -------
@@ -458,6 +510,28 @@ class spatial_query:
458
510
  if ct not in self.labels.unique():
459
511
  raise ValueError(f"Found no {ct} in {self.label_key}!")
460
512
 
513
+ if mode not in ('nested', 'maximal'):
514
+ raise ValueError(f"mode must be 'nested' or 'maximal', got {mode!r}")
515
+
516
+ # Nested descent only decides WHICH motifs get tested; each batch is scored by this
517
+ # same method, so the statistics are identical to the maximal mode's.
518
+ if mode == 'nested' and motifs is None:
519
+ fp_all = self.find_fp_dist(ct=ct, max_dist=max_dist, min_size=min_size,
520
+ min_support=min_support, if_max=False)
521
+ if len(fp_all) == 0:
522
+ return spatial_utils.apply_fdr_correction(pd.DataFrame())
523
+ # Cell IDs are DataFrame columns here, so they travel with the rows and need
524
+ # no separate merging.
525
+ return spatial_utils.nested_motif_descent(
526
+ test_motifs=lambda ms: self.motif_enrichment_dist(
527
+ ct=ct, motifs=ms, max_dist=max_dist, min_size=min_size,
528
+ min_support=min_support, return_cellID=return_cellID,
529
+ mode='maximal'),
530
+ maximal_motifs=spatial_utils.find_maximal_patterns(fp_all)['itemsets'],
531
+ support_of={spatial_utils.canonical_motif(i): s
532
+ for i, s in zip(fp_all['itemsets'], fp_all['support'])},
533
+ )
534
+
461
535
  out = []
462
536
  if motifs is None:
463
537
  fp = self.find_fp_dist(ct=ct,
@@ -576,6 +650,109 @@ class spatial_query:
576
650
  out_pd = out_pd.sort_values(by='adj-pval', ignore_index=True)
577
651
  return out_pd
578
652
 
653
+ def motif_composition_dist(self,
654
+ ct: str,
655
+ motifs: Union[str, List[str], List[List[str]]],
656
+ max_dist: float = 20,
657
+ ) -> pd.DataFrame:
658
+ """
659
+ Compute the composition of motif cell types in the radius-based neighborhoods of center cells.
660
+
661
+ Uses the same neighborhoods as motif_enrichment_dist, but performs no enrichment test.
662
+ Only center cells whose neighborhood contains every cell type of the motif (motif-positive
663
+ centers) are kept. For each of them, the proportion of a motif cell type is its number of
664
+ cells divided by the total number of motif-type cells in that neighborhood.
665
+
666
+ Parameters
667
+ ----------
668
+ ct : str
669
+ Cell type as the center cells.
670
+ motifs : str or List[str] or List[List[str]]
671
+ Motifs to analyze, e.g. the 'motifs' column of motif_enrichment_dist output.
672
+ max_dist : float, default=20
673
+ Maximum distance for considering a cell as a neighbor.
674
+
675
+ Returns
676
+ -------
677
+ pd.DataFrame
678
+ Long-format table with one row per (motif-positive center cell, motif cell type):
679
+ - center: center cell type name
680
+ - motifs: sorted list of cell types in the motif
681
+ - cell_type: motif cell type this row describes
682
+ - n_cells: number of cells of cell_type in the neighborhood
683
+ - n_motif_cells: number of motif-type cells in the neighborhood
684
+ - proportion: n_cells / n_motif_cells
685
+ """
686
+ if ct not in self.labels.unique():
687
+ raise ValueError(f"Found no {ct} in {self.label_key}!")
688
+ motifs = spatial_utils.normalize_motif_list(motifs, self.labels.unique(), self.label_key)
689
+
690
+ label_encoder = LabelEncoder()
691
+ int_labels = label_encoder.fit_transform(np.array(self.labels))
692
+ cinds = np.where(self.labels == ct)[0]
693
+ counts = spatial_utils.anchor_neighbor_counts_dist(
694
+ self.kd_tree, self.spatial_pos, cinds, int_labels,
695
+ len(label_encoder.classes_), max_dist)
696
+
697
+ out = [spatial_utils.motif_composition_table(
698
+ ct, motif, counts[:, label_encoder.transform(np.array(motif))]) for motif in motifs]
699
+ if len(out) == 0:
700
+ return pd.DataFrame(columns=spatial_utils.COMPOSITION_COLUMNS)
701
+ return pd.concat(out, ignore_index=True)
702
+
703
+ def motif_composition_knn(self,
704
+ ct: str,
705
+ motifs: Union[str, List[str], List[List[str]]],
706
+ k: int = 30,
707
+ max_dist: float = 20,
708
+ ) -> pd.DataFrame:
709
+ """
710
+ Compute the composition of motif cell types in the k-nearest-neighbor neighborhoods of center cells.
711
+
712
+ Uses the same neighborhoods as motif_enrichment_knn, but performs no enrichment test.
713
+ Only center cells whose neighborhood contains every cell type of the motif (motif-positive
714
+ centers) are kept. For each of them, the proportion of a motif cell type is its number of
715
+ cells divided by the total number of motif-type cells in that neighborhood.
716
+
717
+ Parameters
718
+ ----------
719
+ ct : str
720
+ Cell type of the center cell.
721
+ motifs : str or List[str] or List[List[str]]
722
+ Motifs to analyze, e.g. the 'motifs' column of motif_enrichment_knn output.
723
+ k : int, default=30
724
+ Number of nearest neighbors to consider.
725
+ max_dist : float, default=20
726
+ Maximum distance for neighbors.
727
+
728
+ Returns
729
+ -------
730
+ pd.DataFrame
731
+ Long-format table with one row per (motif-positive center cell, motif cell type):
732
+ - center: center cell type name
733
+ - motifs: sorted list of cell types in the motif
734
+ - cell_type: motif cell type this row describes
735
+ - n_cells: number of cells of cell_type in the neighborhood
736
+ - n_motif_cells: number of motif-type cells in the neighborhood
737
+ - proportion: n_cells / n_motif_cells
738
+ """
739
+ if ct not in self.labels.unique():
740
+ raise ValueError(f"Found no {ct} in {self.label_key}!")
741
+ motifs = spatial_utils.normalize_motif_list(motifs, self.labels.unique(), self.label_key)
742
+
743
+ label_encoder = LabelEncoder()
744
+ int_labels = label_encoder.fit_transform(np.array(self.labels))
745
+ cinds = np.where(self.labels == ct)[0]
746
+ counts = spatial_utils.anchor_neighbor_counts_knn(
747
+ self.kd_tree, self.spatial_pos, cinds, int_labels,
748
+ len(label_encoder.classes_), k, max_dist)
749
+
750
+ out = [spatial_utils.motif_composition_table(
751
+ ct, motif, counts[:, label_encoder.transform(np.array(motif))]) for motif in motifs]
752
+ if len(out) == 0:
753
+ return pd.DataFrame(columns=spatial_utils.COMPOSITION_COLUMNS)
754
+ return pd.concat(out, ignore_index=True)
755
+
579
756
 
580
757
  def find_patterns_grid(self,
581
758
  max_dist: float = 20,
@@ -1404,6 +1581,42 @@ class spatial_query:
1404
1581
  """
1405
1582
  return plotting.plot_motif_enrichment_heatmap(enrich_df=enrich_df, figsize=figsize,
1406
1583
  save_path=save_path, title=title, cmap=cmap)
1584
+
1585
+ def plot_motif_composition(self,
1586
+ comp_df: pd.DataFrame,
1587
+ motif: Optional[List[str]] = None,
1588
+ bins: int = 25,
1589
+ colors: Optional[List] = None,
1590
+ figsize: tuple = (6, 5),
1591
+ title: Optional[str] = None,
1592
+ save_path: Optional[str] = None):
1593
+ """
1594
+ Plot the distribution of each motif cell type's proportion across motif-positive center cells.
1595
+
1596
+ Parameters
1597
+ ----------
1598
+ comp_df : pd.DataFrame
1599
+ Output DataFrame from motif_composition_dist or motif_composition_knn.
1600
+ motif : List[str], optional
1601
+ Motif to plot. May be omitted when comp_df holds a single motif.
1602
+ bins : int, default=25
1603
+ Number of equal-width bins on [0, 1].
1604
+ colors : List, optional
1605
+ One color per motif cell type, in sorted cell type order. Defaults to the tab10 palette.
1606
+ figsize : tuple, default=(6, 5)
1607
+ Figure size.
1608
+ title : str, optional
1609
+ Figure title. Defaults to the center cell type and the motif.
1610
+ save_path : str, optional
1611
+ Path to save the figure. If None, the figure will not be saved.
1612
+
1613
+ Returns
1614
+ -------
1615
+ None
1616
+ Displays a histogram of motif cell type proportions, with median and IQR in the legend.
1617
+ """
1618
+ return plotting.plot_motif_composition(comp_df=comp_df, motif=motif, bins=bins, colors=colors,
1619
+ figsize=figsize, title=title, save_path=save_path)
1407
1620
 
1408
1621
  def plot_gene_pair_heatmap(self,
1409
1622
  gene_pair_df: pd.DataFrame,