scMultiChat 0.1.0__py3-none-any.whl

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 (39) hide show
  1. MultiChat/Analysis/Intra_strength.py +1758 -0
  2. MultiChat/Analysis/Processing.py +152 -0
  3. MultiChat/Analysis/__init__.py +2 -0
  4. MultiChat/Heterogeneous_g_emb/__init__.py +13 -0
  5. MultiChat/Heterogeneous_g_emb/_settings.py +156 -0
  6. MultiChat/Heterogeneous_g_emb/_utils.py +143 -0
  7. MultiChat/Heterogeneous_g_emb/_version.py +3 -0
  8. MultiChat/Heterogeneous_g_emb/plotting/__init__.py +19 -0
  9. MultiChat/Heterogeneous_g_emb/plotting/_palettes.py +180 -0
  10. MultiChat/Heterogeneous_g_emb/plotting/_plot.py +1498 -0
  11. MultiChat/Heterogeneous_g_emb/plotting/_post_training.py +742 -0
  12. MultiChat/Heterogeneous_g_emb/plotting/_utils.py +103 -0
  13. MultiChat/Heterogeneous_g_emb/preprocessing/__init__.py +26 -0
  14. MultiChat/Heterogeneous_g_emb/preprocessing/_general.py +91 -0
  15. MultiChat/Heterogeneous_g_emb/preprocessing/_pca.py +182 -0
  16. MultiChat/Heterogeneous_g_emb/preprocessing/_qc.py +727 -0
  17. MultiChat/Heterogeneous_g_emb/preprocessing/_utils.py +60 -0
  18. MultiChat/Heterogeneous_g_emb/preprocessing/_variable_genes.py +82 -0
  19. MultiChat/Heterogeneous_g_emb/readwrite.py +250 -0
  20. MultiChat/Heterogeneous_g_emb/tools/__init__.py +23 -0
  21. MultiChat/Heterogeneous_g_emb/tools/_gene_scores.py +346 -0
  22. MultiChat/Heterogeneous_g_emb/tools/_general.py +71 -0
  23. MultiChat/Heterogeneous_g_emb/tools/_integration.py +197 -0
  24. MultiChat/Heterogeneous_g_emb/tools/_pbg.py +1184 -0
  25. MultiChat/Heterogeneous_g_emb/tools/_post_training.py +919 -0
  26. MultiChat/Heterogeneous_g_emb/tools/_umap.py +58 -0
  27. MultiChat/Heterogeneous_g_emb/tools/_utils.py +253 -0
  28. MultiChat/Model/Layers.py +116 -0
  29. MultiChat/Model/__init__.py +3 -0
  30. MultiChat/Model/model_training.py +166 -0
  31. MultiChat/Model/modules.py +93 -0
  32. MultiChat/Model/utilities.py +234 -0
  33. MultiChat/Plot/Visualization.py +470 -0
  34. MultiChat/Plot/__init__.py +1 -0
  35. MultiChat/__init__.py +12 -0
  36. scmultichat-0.1.0.dist-info/METADATA +156 -0
  37. scmultichat-0.1.0.dist-info/RECORD +39 -0
  38. scmultichat-0.1.0.dist-info/WHEEL +5 -0
  39. scmultichat-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,919 @@
1
+ """Functions and classes for the analysis after PBG training"""
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+ import anndata as ad
6
+ from scipy.stats import entropy
7
+ from sklearn.neighbors import KDTree
8
+ from scipy.spatial import distance
9
+ # import faiss
10
+
11
+ from ._utils import _gini, _get_fdr
12
+
13
+
14
+ def softmax(adata_ref,
15
+ adata_query,
16
+ T=0.5,
17
+ n_top=None,
18
+ percentile=0):
19
+ """Softmax-based transformation
20
+
21
+ This will transform query data to reference-comparable data
22
+
23
+ Parameters
24
+ ----------
25
+ adata_ref: `AnnData`
26
+ Reference anndata.
27
+ adata_query: `list`
28
+ Query anndata objects
29
+ T: `float`
30
+ Temperature parameter.
31
+ It controls the output probability distribution.
32
+ When T goes to inf, it becomes a discrete uniform distribution,
33
+ each query becomes the average of reference;
34
+ When T goes to zero, softargmax converges to arg max,
35
+ each query is approximately the best of reference.
36
+ cutoff: `float`
37
+ The cutoff used to filter out low-probability reference entities
38
+ Returns
39
+ -------
40
+ updates `adata_query` with the following field.
41
+ softmax: `array_like` (`.layers['softmax']`)
42
+ Store #observations × #dimensions softmax transformed data matrix.
43
+ """
44
+
45
+ scores_ref_query = np.matmul(adata_ref.X, adata_query.X.T)
46
+ # avoid overflow encountered
47
+ scores_ref_query = scores_ref_query - scores_ref_query.max()
48
+ scores_softmax = np.exp(scores_ref_query/T) / \
49
+ (np.exp(scores_ref_query/T).sum(axis=0))[None, :]
50
+ if n_top is None:
51
+ thresh = np.percentile(scores_softmax, q=percentile, axis=0)
52
+ else:
53
+ thresh = (np.sort(scores_softmax, axis=0)[::-1, :])[n_top-1, ]
54
+ mask = scores_softmax < thresh[None, :]
55
+ scores_softmax[mask] = 0
56
+ # rescale to make scores add up to 1
57
+ scores_softmax = scores_softmax/scores_softmax.sum(axis=0, keepdims=1)
58
+ X_query = np.dot(scores_softmax.T, adata_ref.X)
59
+ adata_query.layers['softmax'] = X_query
60
+
61
+
62
+ class HgeEmbed:
63
+ """A class used to represent post-training embedding analyis
64
+
65
+ Attributes
66
+ ----------
67
+
68
+ Methods
69
+ -------
70
+
71
+ """
72
+
73
+ def __init__(self,
74
+ adata_ref,
75
+ list_adata_query,
76
+ T=0.5,
77
+ list_T=None,
78
+ percentile=50,
79
+ n_top=None,
80
+ list_percentile=None,
81
+ use_precomputed=True,
82
+ ):
83
+ """
84
+ Parameters
85
+ ----------
86
+ adata_ref: `AnnData`
87
+ Reference anndata.
88
+ list_adata_query: `list`
89
+ A list query anndata objects
90
+ T: `float`
91
+ Temperature parameter shared by all query adata objects.
92
+ It controls the output probability distribution.
93
+ when T goes to inf, it becomes a discrete uniform distribution,
94
+ each query becomes the average of reference;
95
+ when T goes to zero, softargmax converges to arg max,
96
+ each query is approximately the best of reference.
97
+ list_T: `list`, (default: None)
98
+ A list of temperature parameters.
99
+ It should correspond to each of query data.
100
+ Once it's specified, it will override `T`.
101
+ cutoff: `float`, (default: None)
102
+ The cutoff used to filter out low-probability reference entities
103
+ list_cutoff: `list`, (default: None)
104
+ A list of cutoff values.
105
+ It should correspond to each of query data.
106
+ Once it's specified, it will override `cutoff`.
107
+ """
108
+ assert isinstance(list_adata_query, list), \
109
+ "`list_adata_query` must be list"
110
+ if list_T is not None:
111
+ assert isinstance(list_T, list), \
112
+ "`list_T` must be list"
113
+ self.adata_ref = adata_ref
114
+ self.list_adata_query = list_adata_query
115
+ self.T = T
116
+ self.list_T = list_T
117
+ self.percentile = percentile
118
+ self.n_top = n_top
119
+ self.list_percentile = list_percentile
120
+ self.use_precomputed = use_precomputed
121
+
122
+ def embed(self):
123
+ """Embed a list of query datasets along with reference dataset
124
+ into the same space
125
+
126
+ Returns
127
+ -------
128
+ adata_all: `AnnData`
129
+ Store #entities × #dimensions.
130
+ """
131
+ adata_ref = self.adata_ref
132
+ list_adata_query = self.list_adata_query
133
+ use_precomputed = self.use_precomputed
134
+ T = self.T
135
+ list_T = self.list_T
136
+ n_top = self.n_top
137
+ percentile = self.percentile
138
+ list_percentile = self.list_percentile
139
+ X_all = adata_ref.X.copy()
140
+ # obs_all = pd.DataFrame(
141
+ # data=['ref']*adata_ref.shape[0],
142
+ # index=adata_ref.obs.index,
143
+ # columns=['id_dataset'])
144
+ obs_all = adata_ref.obs.copy()
145
+ obs_all['id_dataset'] = ['ref']*adata_ref.shape[0]
146
+ for i, adata_query in enumerate(list_adata_query):
147
+ if list_T is not None:
148
+ param_T = list_T[i]
149
+ else:
150
+ param_T = T
151
+ if list_percentile is not None:
152
+ param_percentile = list_percentile[i]
153
+ else:
154
+ param_percentile = percentile
155
+ if use_precomputed:
156
+ if 'softmax' in adata_query.layers.keys():
157
+ print(f'Reading in precomputed softmax-transformed matrix '
158
+ f'for query data {i};')
159
+ else:
160
+ print(f'No softmax-transformed matrix exists '
161
+ f'for query data {i}')
162
+ print("Performing softmax transformation;")
163
+ softmax(
164
+ adata_ref,
165
+ adata_query,
166
+ T=param_T,
167
+ percentile=param_percentile,
168
+ n_top=n_top,
169
+ )
170
+ else:
171
+ print(f'Performing softmax transformation '
172
+ f'for query data {i};')
173
+ softmax(
174
+ adata_ref,
175
+ adata_query,
176
+ T=param_T,
177
+ percentile=param_percentile,
178
+ n_top=n_top,
179
+ )
180
+ X_all = np.vstack((X_all, adata_query.layers['softmax']))
181
+ # obs_all = obs_all.append(
182
+ # pd.DataFrame(
183
+ # data=[f'query_{i}']*adata_query.shape[0],
184
+ # index=adata_query.obs.index,
185
+ # columns=['id_dataset'])
186
+ # )
187
+ obs_query = adata_query.obs.copy()
188
+ obs_query['id_dataset'] = [f'query_{i}']*adata_query.shape[0]
189
+ obs_all = pd.concat(
190
+ [obs_all, obs_query], ignore_index=False)
191
+ adata_all = ad.AnnData(X=X_all,
192
+ obs=obs_all)
193
+ return adata_all
194
+
195
+
196
+ def embed(adata_ref,
197
+ list_adata_query,
198
+ T=0.5,
199
+ list_T=None,
200
+ percentile=0,
201
+ n_top=None,
202
+ list_percentile=None,
203
+ use_precomputed=False):
204
+ """Embed a list of query datasets along with reference dataset
205
+ into the same space
206
+
207
+ Parameters
208
+ ----------
209
+ adata_ref: `AnnData`
210
+ Reference anndata.
211
+ list_adata_query: `list`
212
+ A list query anndata objects
213
+ T: `float`
214
+ Temperature parameter shared by all query adata objects.
215
+ It controls the output probability distribution.
216
+ when T goes to inf, it becomes a discrete uniform distribution,
217
+ each query becomes the average of reference;
218
+ when T goes to zero, softargmax converges to arg max,
219
+ each query is approximately the best of reference.
220
+ list_T: `list`, (default: None)
221
+ A list of temperature parameters.
222
+ It should correspond to each of query data.
223
+ Once it's specified, it will override `T`.
224
+
225
+ Returns
226
+ -------
227
+ adata_all: `AnnData`
228
+ Store #entities × #dimensions.
229
+ updates `adata_query` with the following field.
230
+ softmax: `array_like` (`.layers['softmax']`)
231
+ Store #observations × #dimensions softmax transformed data matrix.
232
+ """
233
+ SE = HgeEmbed(adata_ref,
234
+ list_adata_query,
235
+ T=T,
236
+ list_T=list_T,
237
+ percentile=percentile,
238
+ n_top=n_top,
239
+ list_percentile=list_percentile,
240
+ use_precomputed=use_precomputed)
241
+ adata_all = SE.embed()
242
+ return adata_all
243
+
244
+ def _compare_entities(adata_ref,
245
+ adata_query,
246
+ n_top_cells=50,
247
+ T=1):
248
+ """Compare the embeddings of two entities by calculating
249
+
250
+ the following values between reference and query entities:
251
+
252
+ - dot product
253
+ - normalized dot product
254
+ - softmax probability
255
+
256
+ and the following metrics for each query entity:
257
+
258
+ - max (The average maximum dot product of top-rank reference entities,
259
+ based on normalized dot product)
260
+ - std (standard deviation of reference entities,
261
+ based on dot product)
262
+ - gini (Gini coefficients of reference entities,
263
+ based on softmax probability)
264
+ - entropy (The entropy of reference entities,
265
+ based on softmax probability)
266
+
267
+ Parameters
268
+ ----------
269
+ adata_ref: `AnnData`
270
+ Reference entity anndata.
271
+ adata_query: `AnnData`
272
+ Query entity anndata.
273
+ n_top_cells: `int`, optional (default: 50)
274
+ The number of cells to consider when calculating the metric 'max'
275
+ T: `float`
276
+ Temperature parameter for softmax.
277
+ It controls the output probability distribution.
278
+ When T goes to inf, it becomes a discrete uniform distribution,
279
+ each query becomes the average of reference;
280
+ When T goes to zero, softargmax converges to arg max,
281
+ each query is approximately the best of reference.
282
+
283
+ Returns
284
+ -------
285
+ adata_cmp: `AnnData`
286
+ Store reference entity as observations and query entity as variables
287
+ """
288
+ X_ref = adata_ref.X
289
+ X_query = adata_query.X
290
+ X_cmp = np.matmul(X_ref, X_query.T)
291
+ adata_cmp = ad.AnnData(X=X_cmp,
292
+ obs=adata_ref.obs,
293
+ var=adata_query.obs)
294
+ adata_cmp.layers['norm'] = X_cmp \
295
+ - np.log(np.exp(X_cmp).mean(axis=0)).reshape(1, -1)
296
+ X_sorted_normed = np.sort(adata_cmp.layers['norm'], axis=0)
297
+ adata_cmp.layers['softmax'] = np.exp(X_cmp/T) \
298
+ / np.exp(X_cmp/T).sum(axis=0).reshape(1, -1)
299
+ adata_cmp.var['max'] = \
300
+ np.clip(np.sort(adata_cmp.layers['norm'], axis=0)[-n_top_cells:, ],
301
+ a_min=0,
302
+ a_max=None).mean(axis=0)
303
+ adata_cmp.var['maxmin'] = (X_sorted_normed[-n_top_cells:, ]-X_sorted_normed[:n_top_cells,]).mean(axis=0)
304
+ adata_cmp.var['std'] = np.std(X_cmp, axis=0, ddof=1)
305
+ adata_cmp.var['gini'] = np.array([_gini(adata_cmp.layers['softmax'][:, i])
306
+ for i in np.arange(X_cmp.shape[1])])
307
+ adata_cmp.var['entropy'] = entropy(adata_cmp.layers['softmax'])
308
+ return adata_cmp
309
+
310
+ def compare_entities(adata_ref,
311
+ adata_query,
312
+ adata_query_null = None,
313
+ n_top_cells=50,
314
+ T=1,
315
+ ):
316
+ """Compare the embeddings of two entities by calculating
317
+
318
+ the following values between reference and query entities:
319
+
320
+ - dot product
321
+ - normalized dot product
322
+ - softmax probability
323
+
324
+ and the following metrics for each query entity:
325
+
326
+ - max (The average maximum dot product of top-rank reference entities,
327
+ based on normalized dot product)
328
+ - std (standard deviation of reference entities,
329
+ based on dot product)
330
+ - gini (Gini coefficients of reference entities,
331
+ based on softmax probability)
332
+ - entropy (The entropy of reference entities,
333
+ based on softmax probability)
334
+
335
+ Optionally produces FDR of metrics when null query embedding is provided.
336
+ Metric score of null embedding will be used as the null disbribution of the
337
+ metrics.
338
+
339
+ Parameters
340
+ ----------
341
+ adata_ref: `AnnData`
342
+ Reference entity anndata.
343
+ adata_query: `AnnData`
344
+ Query entity anndata.
345
+ adata_query_null: `AnnData`, optional (default: None)
346
+ Null query entity anndata generated by random graph.
347
+ FDR of each metric is calculated using the metrics calcualted
348
+ from the embedding of these null nodes as the null distribution.
349
+ n_top_cells: `int`, optional (default: 50)
350
+ The number of cells to consider when calculating the metric 'max'
351
+ T: `float`
352
+ Temperature parameter for softmax.
353
+ It controls the output probability distribution.
354
+ When T goes to inf, it becomes a discrete uniform distribution,
355
+ each query becomes the average of reference;
356
+ When T goes to zero, softargmax converges to arg max,
357
+ each query is approximately the best of reference.
358
+
359
+ Returns
360
+ -------
361
+ adata_cmp: `AnnData`
362
+ Store reference entity as observations and query entity as variables
363
+ """
364
+ adata_cmp = _compare_entities(adata_ref, adata_query, n_top_cells = n_top_cells, T = T)
365
+ if adata_query_null is None:
366
+ return(adata_cmp)
367
+
368
+ adata_cmp_null = _compare_entities(adata_ref, adata_query_null, n_top_cells = n_top_cells, T = T)
369
+ for metric in ['max', 'std', 'gini', 'entropy']:
370
+ if metric == 'entropy':
371
+ # for entropy, we need the p_value in the opposite direction (lower value if significant)
372
+ p_val, fdr = _get_fdr(-adata_cmp.var[metric], -adata_cmp_null.var[metric])
373
+ else:
374
+ p_val, fdr = _get_fdr(adata_cmp.var[metric], adata_cmp_null.var[metric])
375
+ adata_cmp.var[f"{metric}_p"] = p_val
376
+ adata_cmp.var[f"{metric}_fdr"] = fdr
377
+ return adata_cmp
378
+
379
+
380
+ def query(adata,
381
+ obsm='X_umap',
382
+ layer=None,
383
+ metric='euclidean',
384
+ anno_filter=None,
385
+ filters=None,
386
+ entity=None,
387
+ pin=None,
388
+ k=20,
389
+ use_radius=False,
390
+ r=None,
391
+ **kwargs
392
+ ):
393
+ """Query the "database" of entites
394
+
395
+ Parameters
396
+ ----------
397
+ adata : `AnnData`
398
+ Anndata object to query.
399
+ obsm : `str`, optional (default: "X_umap")
400
+ The multi-dimensional annotation to use for calculating the distance.
401
+ layer : `str`, optional (default: None)
402
+ The layer to use for calculating the distance.
403
+ metric : `str`, optional (default: "euclidean")
404
+ The distance metric to use.
405
+ More metrics can be found at "`DistanceMetric class
406
+ <https://scikit-learn.org/stable/modules/generated/sklearn.neighbors.DistanceMetric.html>`__"
407
+ anno_filter : `str`, optional (default: None)
408
+ The annotation of filter to use.
409
+ It should be one of ``adata.obs_keys()``
410
+ filters : `list`, optional (default: None)
411
+ The filters to use.
412
+ It should be a list of values in ``adata.obs[anno_filter]``
413
+ entity : `list`, optional (default: None)
414
+ Query entity. It needs to be in ``adata.obs_names()``
415
+ k : `int`, optional (default: 20)
416
+ The number of nearest neighbors to return.
417
+ Only valid if ``use_radius`` is False
418
+ use_radius : `bool`, optional (default: False)
419
+ If True, query for neighbors within a given radius
420
+ r: `float`, optional (default: None)
421
+ Distance within which neighbors are returned.
422
+ If None, it will be estimated based the range of the space.
423
+ **kwargs: `dict`, optional
424
+ Extra arguments to ``sklearn.neighbors.KDTree``.
425
+
426
+ Returns
427
+ -------
428
+ updates `adata` with the following fields.
429
+
430
+ params: `dict`, (`adata.uns['query']['params']`)
431
+ Parameters used for the query
432
+ output: `pandas.DataFrame`, (`adata.uns['query']['output']`)
433
+ Query result.
434
+ """
435
+ if sum(list(map(lambda x: x is None,
436
+ [entity, pin]))) == 2:
437
+ raise ValueError("One of `entity` and `pin` must be specified")
438
+ if sum(list(map(lambda x: x is not None,
439
+ [entity, pin]))) == 2:
440
+ print("`entity` will be ignored.")
441
+ if entity is not None:
442
+ entity = np.array(entity).flatten()
443
+
444
+ if sum(list(map(lambda x: x is not None,
445
+ [layer, obsm]))) == 2:
446
+ raise ValueError("Only one of `layer` and `obsm` can be used")
447
+ elif obsm is not None:
448
+ X = adata.obsm[obsm].copy()
449
+ if pin is None:
450
+ pin = adata[entity, :].obsm[obsm].copy()
451
+ elif layer is not None:
452
+ X = adata.layers[layer].copy()
453
+ if pin is None:
454
+ pin = adata[entity, :].layers[layer].copy()
455
+ else:
456
+ X = adata.X.copy()
457
+ if pin is None:
458
+ pin = adata[entity, :].X.copy()
459
+ pin = np.reshape(np.array(pin), [-1, X.shape[1]])
460
+
461
+ if use_radius:
462
+ kdt = KDTree(X, metric=metric, **kwargs)
463
+ if r is None:
464
+ r = np.mean(X.max(axis=0) - X.min(axis=0))/5
465
+ ind, dist = kdt.query_radius(pin,
466
+ r=r,
467
+ sort_results=True,
468
+ return_distance=True)
469
+ df_output = pd.DataFrame()
470
+ for ii in np.arange(pin.shape[0]):
471
+ df_output_ii = adata.obs.iloc[ind[ii], ].copy()
472
+ df_output_ii['distance'] = dist[ii]
473
+ if entity is not None:
474
+ df_output_ii['query'] = entity[ii]
475
+ else:
476
+ df_output_ii['query'] = ii
477
+ df_output = pd.concat(
478
+ [df_output, df_output_ii], ignore_index=False)
479
+ if anno_filter is not None:
480
+ if anno_filter in adata.obs_keys():
481
+ if filters is None:
482
+ filters = df_output[anno_filter].unique().tolist()
483
+ df_output.query(f'{anno_filter} == @filters', inplace=True)
484
+ else:
485
+ raise ValueError(f'could not find {anno_filter}')
486
+ df_output = df_output.sort_values(by='distance')
487
+ else:
488
+ # assert (metric in ['euclidean', 'dot_product']),\
489
+ # "`metric` must be one of ['euclidean','dot_product']"
490
+ if anno_filter is not None:
491
+ if anno_filter in adata.obs_keys():
492
+ if filters is None:
493
+ filters = adata.obs[anno_filter].unique().tolist()
494
+ ids_filters = \
495
+ np.where(np.isin(adata.obs[anno_filter], filters))[0]
496
+ else:
497
+ raise ValueError(f'could not find {anno_filter}')
498
+ else:
499
+ ids_filters = np.arange(X.shape[0])
500
+ kdt = KDTree(X[ids_filters, :], metric=metric, **kwargs)
501
+ dist, ind = kdt.query(pin,
502
+ k=k,
503
+ sort_results=True,
504
+ return_distance=True)
505
+
506
+ # use faiss
507
+ # X = X.astype('float32')
508
+ # if metric == 'euclidean':
509
+ # faiss_index = faiss.IndexFlatL2(X.shape[1]) # build the index
510
+ # faiss_index.add(X[ids_filters, :])
511
+ # dist, ind = faiss_index.search(pin, k)
512
+ # if metric == 'dot_product':
513
+ # faiss_index = faiss.IndexFlatIP(X.shape[1]) # build the index
514
+ # faiss_index.add(X[ids_filters, :])
515
+ # sim, ind = faiss_index.search(pin, k)
516
+ # dist = -sim
517
+
518
+ df_output = pd.DataFrame()
519
+ for ii in np.arange(pin.shape[0]):
520
+ df_output_ii = \
521
+ adata.obs.iloc[ids_filters, ].iloc[ind[ii, ], ].copy()
522
+ df_output_ii['distance'] = dist[ii, ]
523
+ if entity is not None:
524
+ df_output_ii['query'] = entity[ii]
525
+ else:
526
+ df_output_ii['query'] = ii
527
+ df_output = pd.concat(
528
+ [df_output, df_output_ii], ignore_index=False)
529
+ df_output = df_output.sort_values(by='distance')
530
+
531
+ adata.uns['query'] = dict()
532
+ adata.uns['query']['params'] = {'obsm': obsm,
533
+ 'layer': layer,
534
+ 'entity': entity,
535
+ 'pin': pin,
536
+ 'k': k,
537
+ 'use_radius': use_radius,
538
+ 'r': r}
539
+ adata.uns['query']['output'] = df_output.copy()
540
+ return df_output
541
+
542
+
543
+ def find_master_regulators(adata_all,
544
+ list_tf_motif=None,
545
+ list_tf_gene=None,
546
+ metric='euclidean',
547
+ anno_filter='entity_anno',
548
+ filter_gene='gene',
549
+ metrics_gene=None,
550
+ metrics_motif=None,
551
+ cutoff_gene_max=1.5,
552
+ cutoff_gene_gini=0.3,
553
+ cutoff_gene_std=None,
554
+ cutoff_gene_entropy=None,
555
+ cutoff_motif_max=1.5,
556
+ cutoff_motif_gini=0.3,
557
+ cutoff_motif_std=None,
558
+ cutoff_motif_entropy=None,
559
+ ):
560
+ """Find all the master regulators
561
+
562
+ Parameters
563
+ ----------
564
+ adata_all : `AnnData`
565
+ Anndata object storing MultiChat embedding of all entities.
566
+ list_tf_motif : `list`
567
+ A list of TF motifs. They should match TF motifs in `list_tf_gene`.
568
+ list_tf_gene : `list`
569
+ A list TF genes. They should match TF motifs in `list_tf_motif`.
570
+ metric : `str`, optional (default: "euclidean")
571
+ The distance metric to use. It can be ‘braycurtis’, ‘canberra’,
572
+ ‘chebyshev’, ‘cityblock’, ‘correlation’, ‘cosine’, ‘dice’, ‘euclidean’,
573
+ ‘hamming’, ‘jaccard’, ‘jensenshannon’, ‘kulsinski’, ‘mahalanobis’,
574
+ ‘matching’, ‘minkowski’, ‘rogerstanimoto’, ‘russellrao’, ‘seuclidean’,
575
+ ‘sokalmichener’, ‘sokalsneath’, ‘sqeuclidean’, ‘wminkowski’, ‘yule’.
576
+ anno_filter : `str`, optional (default: None)
577
+ The annotation of filter to use.
578
+ It should be one of ``adata.obs_keys()``
579
+ filter_gene : `str`, optional (default: None)
580
+ The filter for gene.
581
+ It should be in ``adata.obs[anno_filter]``
582
+ metrics_gene : `pandas.DataFrame`, optional (default: None)
583
+ HGE metrics for genes.
584
+ metrics_motif : `pandas.DataFrame`, optional (default: None)
585
+ HGE metrics for motifs.
586
+ cutoff_gene_max, cutoff_motif_max: `float`
587
+ cutoff of HGE metric `max value` for genes and motifs
588
+ cutoff_gene_gini, cutoff_motif_gini: `float`
589
+ cutoff of HGE metric `Gini index` for genes and motifs
590
+ cutoff_gene_gini, cutoff_motif_gini: `float`
591
+ cutoff of HGE metric `Gini index` for genes and motifs
592
+ cutoff_gene_std, cutoff_motif_std: `float`
593
+ cutoff of HGE metric `standard deviation` for genes and motifs
594
+ cutoff_gene_entropy, cutoff_motif_entropy: `float`
595
+ cutoff of HGE metric `entropy` for genes and motifs
596
+
597
+ Returns
598
+ -------
599
+ df_MR: `pandas.DataFrame`
600
+ Dataframe of master regulators
601
+ """
602
+ if sum(list(map(lambda x: x is None,
603
+ [list_tf_motif, list_tf_gene]))) > 0:
604
+ return "Please specify both `list_tf_motif` and `list_tf_gene`"
605
+
606
+ assert isinstance(list_tf_motif, list), \
607
+ "`list_tf_motif` must be list"
608
+ assert isinstance(list_tf_gene, list), \
609
+ "`list_tf_gene` must be list"
610
+ assert len(list_tf_motif) == len(list_tf_gene), \
611
+ "`list_tf_motif` and `list_tf_gene` must have the same length"
612
+ assert len(list_tf_motif) == len(set(list_tf_motif)), \
613
+ "Duplicates are found in `list_tf_motif`"
614
+
615
+ genes = adata_all[adata_all.obs[anno_filter] == filter_gene].\
616
+ obs_names.tolist().copy()
617
+ # Master Regulator
618
+ df_MR = pd.DataFrame(list(zip(list_tf_motif, list_tf_gene)),
619
+ columns=['motif', 'gene'])
620
+
621
+ if metrics_motif is not None:
622
+ print('Adding motif metrics ...')
623
+ assert isinstance(metrics_motif, pd.DataFrame), \
624
+ "`metrics_motif` must be pd.DataFrame"
625
+ df_metrics_motif = metrics_motif.loc[list_tf_motif, ].copy()
626
+ df_metrics_motif.columns = df_metrics_motif.columns + '_motif'
627
+ df_MR = df_MR.merge(df_metrics_motif,
628
+ how='left',
629
+ left_on='motif',
630
+ right_index=True)
631
+
632
+ if metrics_gene is not None:
633
+ print('Adding gene metrics ...')
634
+ assert isinstance(metrics_gene, pd.DataFrame), \
635
+ "`metrics_gene` must be pd.DataFrame"
636
+ df_metrics_gene = metrics_gene.loc[list_tf_gene, ].copy()
637
+ df_metrics_gene.index = list_tf_motif # avoid duplicate genes
638
+ df_metrics_gene.columns = df_metrics_gene.columns + '_gene'
639
+ df_MR = df_MR.merge(df_metrics_gene,
640
+ how='left',
641
+ left_on='motif',
642
+ right_index=True)
643
+ print('Computing distances between TF motifs and genes ...')
644
+ dist_MG = distance.cdist(adata_all[df_MR['motif'], ].X,
645
+ adata_all[genes, ].X,
646
+ metric=metric)
647
+ dist_MG = pd.DataFrame(dist_MG,
648
+ index=df_MR['motif'].tolist(),
649
+ columns=genes)
650
+ df_MR.insert(2, 'rank', -1)
651
+ df_MR.insert(3, 'dist', -1)
652
+ for i in np.arange(df_MR.shape[0]):
653
+ x_motif = df_MR['motif'].iloc[i]
654
+ x_gene = df_MR['gene'].iloc[i]
655
+ df_MR.loc[i, 'rank'] = dist_MG.loc[x_motif, ].rank()[x_gene]
656
+ df_MR.loc[i, 'dist'] = dist_MG.loc[x_motif, x_gene]
657
+
658
+ if metrics_gene is not None:
659
+ print('filtering master regulators based on gene metrics:')
660
+ if cutoff_gene_entropy is not None:
661
+ print('entropy')
662
+ df_MR = df_MR[df_MR['entropy_gene'] > cutoff_gene_entropy]
663
+ if cutoff_gene_gini is not None:
664
+ print('Gini index')
665
+ df_MR = df_MR[df_MR['gini_gene'] > cutoff_gene_gini]
666
+ if cutoff_gene_max is not None:
667
+ print('max')
668
+ df_MR = df_MR[df_MR['max_gene'] > cutoff_gene_max]
669
+ if cutoff_gene_std is not None:
670
+ print('standard deviation')
671
+ df_MR = df_MR[df_MR['std_gene'] > cutoff_gene_std]
672
+ if metrics_motif is not None:
673
+ print('filtering master regulators based on motif metrics:')
674
+ if cutoff_motif_entropy is not None:
675
+ print('entropy')
676
+ df_MR = df_MR[df_MR['entropy_motif'] > cutoff_motif_entropy]
677
+ if cutoff_motif_gini is not None:
678
+ print('Gini index')
679
+ df_MR = df_MR[df_MR['gini_motif'] > cutoff_motif_gini]
680
+ if cutoff_motif_max is not None:
681
+ print('max')
682
+ df_MR = df_MR[df_MR['max_motif'] > cutoff_motif_max]
683
+ if cutoff_motif_std is not None:
684
+ print('standard deviation')
685
+ df_MR = df_MR[df_MR['std_motif'] > cutoff_motif_std]
686
+ df_MR = df_MR.sort_values(by='rank', ignore_index=True)
687
+ return df_MR
688
+
689
+
690
+ def find_target_genes(adata_all,
691
+ adata_PM,
692
+ list_tf_motif=None,
693
+ list_tf_gene=None,
694
+ adata_CP=None,
695
+ metric='euclidean',
696
+ anno_filter='entity_anno',
697
+ filter_peak='peak',
698
+ filter_gene='gene',
699
+ n_genes=200,
700
+ cutoff_gene=None,
701
+ cutoff_peak=1000,
702
+ use_precomputed=True,
703
+ ):
704
+ """For a given TF, infer its target genes
705
+
706
+ Parameters
707
+ ----------
708
+ adata_all : `AnnData`
709
+ Anndata object storing HGE embedding of all entities.
710
+ adata_PM : `AnnData`
711
+ Peaks-by-motifs anndata object.
712
+ list_tf_motif : `list`
713
+ A list of TF motifs. They should match TF motifs in `list_tf_gene`.
714
+ list_tf_gene : `list`
715
+ A list TF genes. They should match TF motifs in `list_tf_motif`.
716
+ adata_CP : `AnnData`, optional (default: None)
717
+ When ``use_precomputed`` is True, it can be set None
718
+ metric : `str`, optional (default: "euclidean")
719
+ The distance metric to use. It can be ‘braycurtis’, ‘canberra’,
720
+ ‘chebyshev’, ‘cityblock’, ‘correlation’, ‘cosine’, ‘dice’, ‘euclidean’,
721
+ ‘hamming’, ‘jaccard’, ‘jensenshannon’, ‘kulsinski’, ‘mahalanobis’,
722
+ ‘matching’, ‘minkowski’, ‘rogerstanimoto’, ‘russellrao’, ‘seuclidean’,
723
+ ‘sokalmichener’, ‘sokalsneath’, ‘sqeuclidean’, ‘wminkowski’, ‘yule’.
724
+ anno_filter : `str`, optional (default: None)
725
+ The annotation of filter to use.
726
+ It should be one of ``adata.obs_keys()``
727
+ filter_gene : `str`, optional (default: None)
728
+ The filter for gene.
729
+ It should be in ``adata.obs[anno_filter]``
730
+ filter_peak : `str`, optional (default: None)
731
+ The filter for peak.
732
+ It should be in ``adata.obs[anno_filter]``
733
+ n_genes : `int`, optional (default: 200)
734
+ The number of neighbor genes to consider initially
735
+ around TF gene or TF motif
736
+ cutoff_gene : `float`, optional (default: None)
737
+ Cutoff of "average_rank"
738
+ cutoff_peak : `int`, optional (default: 1000)
739
+ Cutoff for peaks-associated ranks, including
740
+ "rank_peak_to_gene" and "rank_peak_to_TFmotif".
741
+ use_precomputed : `bool`, optional (default: True)
742
+ Distances calculated between genes, peaks, and motifs
743
+ (stored in `adata.uns['tf_targets']`) will be imported
744
+
745
+ Returns
746
+ -------
747
+ dict_tf_targets : `dict`
748
+ Target genes for each TF.
749
+
750
+ updates `adata` with the following fields.
751
+
752
+ tf_targets: `dict`, (`adata.uns['tf_targets']`)
753
+ Distances calculated between genes, peaks, and motifs
754
+ """
755
+ if sum(list(map(lambda x: x is None,
756
+ [list_tf_motif, list_tf_gene]))) > 0:
757
+ return "Please specify both `list_tf_motif` and `list_tf_gene`"
758
+
759
+ assert isinstance(list_tf_motif, list), \
760
+ "`list_tf_motif` must be list"
761
+ assert isinstance(list_tf_gene, list), \
762
+ "`list_tf_gene` must be list"
763
+ assert len(list_tf_motif) == len(list_tf_gene), \
764
+ "`list_tf_motif` and `list_tf_gene` must have the same length"
765
+
766
+ def isin(a, b):
767
+ return np.array([item in b for item in a])
768
+
769
+ print('Preprocessing ...')
770
+ if use_precomputed and 'tf_targets' in adata_all.uns_keys():
771
+ print('importing precomputed variables ...')
772
+ genes = adata_all.uns['tf_targets']['genes']
773
+ peaks = adata_all.uns['tf_targets']['peaks']
774
+ peaks_in_genes = adata_all.uns['tf_targets']['peaks_in_genes']
775
+ dist_PG = adata_all.uns['tf_targets']['dist_PG']
776
+ overlap_PG = adata_all.uns['tf_targets']['overlap']
777
+ else:
778
+ assert (adata_CP is not None), \
779
+ '`adata_CP` needs to be specified '\
780
+ 'when no precomputed variable is stored'
781
+ if 'gene_scores' not in adata_CP.uns_keys():
782
+ print('Please run "si.tl.gene_scores(adata_CP)" first.')
783
+ else:
784
+ overlap_PG = adata_CP.uns['gene_scores']['overlap'].copy()
785
+ overlap_PG['peak'] = \
786
+ overlap_PG[['chr_p', 'start_p', 'end_p']].apply(
787
+ lambda row: '_'.join(row.values.astype(str)), axis=1)
788
+ tuples = list(zip(overlap_PG['symbol_g'], overlap_PG['peak']))
789
+ multi_indices = pd.MultiIndex.from_tuples(
790
+ tuples, names=["gene", "peak"])
791
+ overlap_PG.index = multi_indices
792
+
793
+ genes = adata_all[adata_all.obs[anno_filter] == filter_gene].\
794
+ obs_names.tolist().copy()
795
+ peaks = adata_all[adata_all.obs[anno_filter] == filter_peak].\
796
+ obs_names.tolist().copy()
797
+ peaks_in_genes = list(set(overlap_PG['peak']))
798
+
799
+ print(f'#genes: {len(genes)}')
800
+ print(f'#peaks: {len(peaks)}')
801
+ print(f'#genes-associated peaks: {len(peaks_in_genes)}')
802
+ print('computing distances between genes '
803
+ 'and genes-associated peaks ...')
804
+ dist_PG = distance.cdist(
805
+ adata_all[peaks_in_genes, ].X,
806
+ adata_all[genes, ].X,
807
+ metric=metric,
808
+ )
809
+ dist_PG = pd.DataFrame(dist_PG, index=peaks_in_genes, columns=[genes])
810
+ print("Saving variables into `.uns['tf_targets']` ...")
811
+ adata_all.uns['tf_targets'] = dict()
812
+ adata_all.uns['tf_targets']['overlap'] = overlap_PG
813
+ adata_all.uns['tf_targets']['dist_PG'] = dist_PG
814
+ adata_all.uns['tf_targets']['peaks_in_genes'] = peaks_in_genes
815
+ adata_all.uns['tf_targets']['genes'] = genes
816
+ adata_all.uns['tf_targets']['peaks'] = peaks
817
+ adata_all.uns['tf_targets']['peaks_in_genes'] = peaks_in_genes
818
+
819
+ dict_tf_targets = dict()
820
+ for tf_motif, tf_gene in zip(list_tf_motif, list_tf_gene):
821
+
822
+ print(f'searching for target genes of {tf_motif}')
823
+ motif_peaks = adata_PM.obs_names[adata_PM[:, tf_motif].X.nonzero()[0]]
824
+ motif_genes = list(
825
+ set(overlap_PG[isin(overlap_PG['peak'], motif_peaks)]['symbol_g'])
826
+ .intersection(genes))
827
+
828
+ # rank of the distances from genes to tf_motif
829
+ dist_GM_motif = distance.cdist(adata_all[genes, ].X,
830
+ adata_all[tf_motif, ].X,
831
+ metric=metric)
832
+ dist_GM_motif = pd.DataFrame(dist_GM_motif,
833
+ index=genes,
834
+ columns=[tf_motif])
835
+ rank_GM_motif = dist_GM_motif.rank(axis=0)
836
+
837
+ # rank of the distances from genes to tf_gene
838
+ dist_GG_motif = distance.cdist(adata_all[genes, ].X,
839
+ adata_all[tf_gene, ].X,
840
+ metric=metric)
841
+ dist_GG_motif = pd.DataFrame(dist_GG_motif,
842
+ index=genes,
843
+ columns=[tf_gene])
844
+ rank_GG_motif = dist_GG_motif.rank(axis=0)
845
+
846
+ # rank of the distances from peaks to tf_motif
847
+ dist_PM_motif = distance.cdist(
848
+ adata_all[peaks_in_genes, ].X,
849
+ adata_all[tf_motif, ].X,
850
+ metric=metric)
851
+ dist_PM_motif = pd.DataFrame(dist_PM_motif,
852
+ index=peaks_in_genes,
853
+ columns=[tf_motif])
854
+ rank_PM_motif = dist_PM_motif.rank(axis=0)
855
+
856
+ # rank of the distances from peaks to candidate genes
857
+ cand_genes = \
858
+ dist_GG_motif[tf_gene].nsmallest(n_genes).index.tolist()\
859
+ + dist_GM_motif[tf_motif].nsmallest(n_genes).index.tolist()
860
+ print(f'#candinate genes is {len(cand_genes)}')
861
+ print('removing duplicate genes ...')
862
+ print('removing genes that do not contain TF motif ...')
863
+ cand_genes = list(set(cand_genes).intersection(set(motif_genes)))
864
+ print(f'#candinate genes is {len(cand_genes)}')
865
+ dist_PG_motif = distance.cdist(
866
+ adata_all[peaks_in_genes, ].X,
867
+ adata_all[cand_genes, ].X,
868
+ metric=metric
869
+ )
870
+ dist_PG_motif = pd.DataFrame(dist_PG_motif,
871
+ index=peaks_in_genes,
872
+ columns=cand_genes)
873
+ rank_PG_motif = dist_PG_motif.rank(axis=0)
874
+
875
+ df_tf_targets = pd.DataFrame(index=cand_genes)
876
+ df_tf_targets['average_rank'] = -1
877
+ df_tf_targets['has_motif'] = 'no'
878
+ df_tf_targets['rank_gene_to_TFmotif'] = -1
879
+ df_tf_targets['rank_gene_to_TFgene'] = -1
880
+ df_tf_targets['rank_peak_to_TFmotif'] = -1
881
+ df_tf_targets['rank_peak2_to_TFmotif'] = -1
882
+ df_tf_targets['rank_peak_to_gene'] = -1
883
+ df_tf_targets['rank_peak2_to_gene'] = -1
884
+ for i, g in enumerate(cand_genes):
885
+ g_peaks = list(set(overlap_PG.loc[[g]]['peak']))
886
+ g_motif_peaks = list(set(g_peaks).intersection(motif_peaks))
887
+ if len(g_motif_peaks) > 0:
888
+ df_tf_targets.loc[g, 'has_motif'] = 'yes'
889
+ df_tf_targets.loc[g, 'rank_gene_to_TFmotif'] = \
890
+ rank_GM_motif[tf_motif][g]
891
+ df_tf_targets.loc[g, 'rank_gene_to_TFgene'] = \
892
+ rank_GG_motif[tf_gene][g]
893
+ df_tf_targets.loc[g, 'rank_peak_to_TFmotif'] = \
894
+ rank_PM_motif.loc[g_peaks, tf_motif].min()
895
+ df_tf_targets.loc[g, 'rank_peak2_to_TFmotif'] = \
896
+ rank_PM_motif.loc[g_motif_peaks, tf_motif].min()
897
+ df_tf_targets.loc[g, 'rank_peak_to_gene'] = \
898
+ rank_PG_motif.loc[g_peaks, g].min()
899
+ df_tf_targets.loc[g, 'rank_peak2_to_gene'] = \
900
+ rank_PG_motif.loc[g_peaks, g].min()
901
+ if i % int(len(cand_genes)/5) == 0:
902
+ print(f'completed: {i/len(cand_genes):.1%}')
903
+ df_tf_targets['average_rank'] = \
904
+ df_tf_targets[['rank_gene_to_TFmotif',
905
+ 'rank_gene_to_TFgene']].mean(axis=1)
906
+ if cutoff_peak is not None:
907
+ print('Pruning candidate genes based on nearby peaks ...')
908
+ df_tf_targets = df_tf_targets[
909
+ (df_tf_targets[[
910
+ 'rank_peak_to_TFmotif',
911
+ 'rank_peak_to_gene']]
912
+ < cutoff_peak).sum(axis=1) > 0]
913
+ if cutoff_gene is not None:
914
+ print('Pruning candidate genes based on average rank ...')
915
+ df_tf_targets = df_tf_targets[
916
+ df_tf_targets['average_rank'] < cutoff_gene]
917
+ dict_tf_targets[tf_motif] = \
918
+ df_tf_targets.sort_values(by='average_rank').copy()
919
+ return dict_tf_targets