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.
- MultiChat/Analysis/Intra_strength.py +1758 -0
- MultiChat/Analysis/Processing.py +152 -0
- MultiChat/Analysis/__init__.py +2 -0
- MultiChat/Heterogeneous_g_emb/__init__.py +13 -0
- MultiChat/Heterogeneous_g_emb/_settings.py +156 -0
- MultiChat/Heterogeneous_g_emb/_utils.py +143 -0
- MultiChat/Heterogeneous_g_emb/_version.py +3 -0
- MultiChat/Heterogeneous_g_emb/plotting/__init__.py +19 -0
- MultiChat/Heterogeneous_g_emb/plotting/_palettes.py +180 -0
- MultiChat/Heterogeneous_g_emb/plotting/_plot.py +1498 -0
- MultiChat/Heterogeneous_g_emb/plotting/_post_training.py +742 -0
- MultiChat/Heterogeneous_g_emb/plotting/_utils.py +103 -0
- MultiChat/Heterogeneous_g_emb/preprocessing/__init__.py +26 -0
- MultiChat/Heterogeneous_g_emb/preprocessing/_general.py +91 -0
- MultiChat/Heterogeneous_g_emb/preprocessing/_pca.py +182 -0
- MultiChat/Heterogeneous_g_emb/preprocessing/_qc.py +727 -0
- MultiChat/Heterogeneous_g_emb/preprocessing/_utils.py +60 -0
- MultiChat/Heterogeneous_g_emb/preprocessing/_variable_genes.py +82 -0
- MultiChat/Heterogeneous_g_emb/readwrite.py +250 -0
- MultiChat/Heterogeneous_g_emb/tools/__init__.py +23 -0
- MultiChat/Heterogeneous_g_emb/tools/_gene_scores.py +346 -0
- MultiChat/Heterogeneous_g_emb/tools/_general.py +71 -0
- MultiChat/Heterogeneous_g_emb/tools/_integration.py +197 -0
- MultiChat/Heterogeneous_g_emb/tools/_pbg.py +1184 -0
- MultiChat/Heterogeneous_g_emb/tools/_post_training.py +919 -0
- MultiChat/Heterogeneous_g_emb/tools/_umap.py +58 -0
- MultiChat/Heterogeneous_g_emb/tools/_utils.py +253 -0
- MultiChat/Model/Layers.py +116 -0
- MultiChat/Model/__init__.py +3 -0
- MultiChat/Model/model_training.py +166 -0
- MultiChat/Model/modules.py +93 -0
- MultiChat/Model/utilities.py +234 -0
- MultiChat/Plot/Visualization.py +470 -0
- MultiChat/Plot/__init__.py +1 -0
- MultiChat/__init__.py +12 -0
- scmultichat-0.1.0.dist-info/METADATA +156 -0
- scmultichat-0.1.0.dist-info/RECORD +39 -0
- scmultichat-0.1.0.dist-info/WHEEL +5 -0
- scmultichat-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""Predict gene scores based on chromatin accessibility"""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import anndata as ad
|
|
6
|
+
import io
|
|
7
|
+
import pybedtools
|
|
8
|
+
from scipy.sparse import (
|
|
9
|
+
coo_matrix,
|
|
10
|
+
csr_matrix
|
|
11
|
+
)
|
|
12
|
+
import pkgutil
|
|
13
|
+
|
|
14
|
+
from ._utils import _uniquify
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GeneScores:
|
|
18
|
+
"""A class used to represent gene scores
|
|
19
|
+
|
|
20
|
+
Attributes
|
|
21
|
+
----------
|
|
22
|
+
|
|
23
|
+
Methods
|
|
24
|
+
-------
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
def __init__(self,
|
|
28
|
+
adata,
|
|
29
|
+
genome,
|
|
30
|
+
gene_anno=None,
|
|
31
|
+
tss_upstream=1e5,
|
|
32
|
+
tss_downsteam=1e5,
|
|
33
|
+
gb_upstream=5000,
|
|
34
|
+
cutoff_weight=1,
|
|
35
|
+
use_top_pcs=True,
|
|
36
|
+
use_precomputed=True,
|
|
37
|
+
use_gene_weigt=True,
|
|
38
|
+
min_w=1,
|
|
39
|
+
max_w=5):
|
|
40
|
+
"""
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
adata: `Anndata`
|
|
44
|
+
Input anndata
|
|
45
|
+
genome : `str`
|
|
46
|
+
The genome name
|
|
47
|
+
"""
|
|
48
|
+
self.adata = adata
|
|
49
|
+
self.genome = genome
|
|
50
|
+
self.gene_anno = gene_anno
|
|
51
|
+
self.tss_upstream = tss_upstream
|
|
52
|
+
self.tss_downsteam = tss_downsteam
|
|
53
|
+
self.gb_upstream = gb_upstream
|
|
54
|
+
self.cutoff_weight = cutoff_weight
|
|
55
|
+
self.use_top_pcs = use_top_pcs
|
|
56
|
+
self.use_precomputed = use_precomputed
|
|
57
|
+
self.use_gene_weigt = use_gene_weigt
|
|
58
|
+
self.min_w = min_w
|
|
59
|
+
self.max_w = max_w
|
|
60
|
+
|
|
61
|
+
def _read_gene_anno(self):
|
|
62
|
+
"""Read in gene annotation
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
|
|
67
|
+
Returns
|
|
68
|
+
-------
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
assert (self.genome in ['hg19', 'hg38', 'mm9', 'mm10']),\
|
|
72
|
+
"`genome` must be one of ['hg19','hg38','mm9','mm10']"
|
|
73
|
+
|
|
74
|
+
bin_str = pkgutil.get_data('hge',
|
|
75
|
+
f'data/gene_anno/{self.genome}_genes.bed')
|
|
76
|
+
gene_anno = pd.read_csv(io.BytesIO(bin_str),
|
|
77
|
+
encoding='utf8',
|
|
78
|
+
sep='\t',
|
|
79
|
+
header=None,
|
|
80
|
+
names=['chr', 'start', 'end',
|
|
81
|
+
'symbol', 'strand'])
|
|
82
|
+
self.gene_anno = gene_anno
|
|
83
|
+
return self.gene_anno
|
|
84
|
+
|
|
85
|
+
def _extend_tss(self, pbt_gene):
|
|
86
|
+
"""Extend transcription start site in both directions
|
|
87
|
+
|
|
88
|
+
Parameters
|
|
89
|
+
----------
|
|
90
|
+
|
|
91
|
+
Returns
|
|
92
|
+
-------
|
|
93
|
+
|
|
94
|
+
"""
|
|
95
|
+
ext_tss = pbt_gene
|
|
96
|
+
if ext_tss['strand'] == '+':
|
|
97
|
+
ext_tss.start = max(0, ext_tss.start - self.tss_upstream)
|
|
98
|
+
ext_tss.end = max(ext_tss.end, ext_tss.start + self.tss_downsteam)
|
|
99
|
+
else:
|
|
100
|
+
ext_tss.start = max(0, min(ext_tss.start,
|
|
101
|
+
ext_tss.end - self.tss_downsteam))
|
|
102
|
+
ext_tss.end = ext_tss.end + self.tss_upstream
|
|
103
|
+
return ext_tss
|
|
104
|
+
|
|
105
|
+
def _extend_genebody(self, pbt_gene):
|
|
106
|
+
"""Extend gene body upstream
|
|
107
|
+
|
|
108
|
+
Parameters
|
|
109
|
+
----------
|
|
110
|
+
|
|
111
|
+
Returns
|
|
112
|
+
-------
|
|
113
|
+
|
|
114
|
+
"""
|
|
115
|
+
ext_gb = pbt_gene
|
|
116
|
+
if ext_gb['strand'] == '+':
|
|
117
|
+
ext_gb.start = max(0, ext_gb.start - self.gb_upstream)
|
|
118
|
+
else:
|
|
119
|
+
ext_gb.end = ext_gb.end + self.gb_upstream
|
|
120
|
+
return ext_gb
|
|
121
|
+
|
|
122
|
+
def _weight_genes(self):
|
|
123
|
+
"""Weight genes
|
|
124
|
+
|
|
125
|
+
Parameters
|
|
126
|
+
----------
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
|
|
131
|
+
"""
|
|
132
|
+
gene_anno = self.gene_anno
|
|
133
|
+
gene_size = gene_anno['end'] - gene_anno['start']
|
|
134
|
+
w = 1/gene_size
|
|
135
|
+
w_scaled = (self.max_w-self.min_w) * (w-min(w)) / (max(w)-min(w)) \
|
|
136
|
+
+ self.min_w
|
|
137
|
+
return w_scaled
|
|
138
|
+
|
|
139
|
+
def cal_gene_scores(self):
|
|
140
|
+
"""Calculate gene scores
|
|
141
|
+
|
|
142
|
+
Parameters
|
|
143
|
+
----------
|
|
144
|
+
|
|
145
|
+
Returns
|
|
146
|
+
-------
|
|
147
|
+
|
|
148
|
+
"""
|
|
149
|
+
adata = self.adata
|
|
150
|
+
if self.gene_anno is None:
|
|
151
|
+
gene_ann = self._read_gene_anno()
|
|
152
|
+
else:
|
|
153
|
+
gene_ann = self.gene_anno
|
|
154
|
+
|
|
155
|
+
df_gene_ann = gene_ann.copy()
|
|
156
|
+
df_gene_ann.index = _uniquify(df_gene_ann['symbol'].values)
|
|
157
|
+
if self.use_top_pcs:
|
|
158
|
+
mask_p = adata.var['top_pcs']
|
|
159
|
+
else:
|
|
160
|
+
mask_p = pd.Series(True, index=adata.var_names)
|
|
161
|
+
df_peaks = adata.var[mask_p][['chr', 'start', 'end']].copy()
|
|
162
|
+
|
|
163
|
+
if 'gene_scores' not in adata.uns_keys():
|
|
164
|
+
print('Gene scores are being calculated for the first time')
|
|
165
|
+
print('`use_precomputed` has been ignored')
|
|
166
|
+
self.use_precomputed = False
|
|
167
|
+
|
|
168
|
+
if self.use_precomputed:
|
|
169
|
+
print('Using precomputed overlap')
|
|
170
|
+
df_overlap_updated = adata.uns['gene_scores']['overlap'].copy()
|
|
171
|
+
else:
|
|
172
|
+
# add the fifth column
|
|
173
|
+
# so that pybedtool can recognize the sixth column as the strand
|
|
174
|
+
df_gene_ann_for_pbt = df_gene_ann.copy()
|
|
175
|
+
df_gene_ann_for_pbt['score'] = 0
|
|
176
|
+
df_gene_ann_for_pbt = df_gene_ann_for_pbt[['chr', 'start', 'end',
|
|
177
|
+
'symbol', 'score',
|
|
178
|
+
'strand']]
|
|
179
|
+
df_gene_ann_for_pbt['id'] = range(df_gene_ann_for_pbt.shape[0])
|
|
180
|
+
|
|
181
|
+
df_peaks_for_pbt = df_peaks.copy()
|
|
182
|
+
df_peaks_for_pbt['id'] = range(df_peaks_for_pbt.shape[0])
|
|
183
|
+
|
|
184
|
+
pbt_gene_ann = pybedtools.BedTool.from_dataframe(
|
|
185
|
+
df_gene_ann_for_pbt
|
|
186
|
+
)
|
|
187
|
+
pbt_gene_ann_ext = pbt_gene_ann.each(self._extend_tss)
|
|
188
|
+
pbt_gene_gb_ext = pbt_gene_ann.each(self._extend_genebody)
|
|
189
|
+
|
|
190
|
+
pbt_peaks = pybedtools.BedTool.from_dataframe(df_peaks_for_pbt)
|
|
191
|
+
|
|
192
|
+
# peaks overlapping with extended TSS
|
|
193
|
+
pbt_overlap = pbt_peaks.intersect(pbt_gene_ann_ext,
|
|
194
|
+
wa=True,
|
|
195
|
+
wb=True)
|
|
196
|
+
df_overlap = pbt_overlap.to_dataframe(
|
|
197
|
+
names=[x+'_p' for x in df_peaks_for_pbt.columns]
|
|
198
|
+
+ [x+'_g' for x in df_gene_ann_for_pbt.columns])
|
|
199
|
+
# peaks overlapping with gene body
|
|
200
|
+
pbt_overlap2 = pbt_peaks.intersect(pbt_gene_gb_ext,
|
|
201
|
+
wa=True,
|
|
202
|
+
wb=True)
|
|
203
|
+
df_overlap2 = pbt_overlap2.to_dataframe(
|
|
204
|
+
names=[x+'_p' for x in df_peaks_for_pbt.columns]
|
|
205
|
+
+ [x+'_g' for x in df_gene_ann_for_pbt.columns])
|
|
206
|
+
|
|
207
|
+
# add distance and weight for each overlap
|
|
208
|
+
df_overlap_updated = df_overlap.copy()
|
|
209
|
+
df_overlap_updated['dist'] = 0
|
|
210
|
+
|
|
211
|
+
for i, x in enumerate(df_overlap['symbol_g'].unique()):
|
|
212
|
+
# peaks within the extended TSS
|
|
213
|
+
df_overlap_x = \
|
|
214
|
+
df_overlap[df_overlap['symbol_g'] == x].copy()
|
|
215
|
+
# peaks within the gene body
|
|
216
|
+
df_overlap2_x = \
|
|
217
|
+
df_overlap2[df_overlap2['symbol_g'] == x].copy()
|
|
218
|
+
# peaks that are not intersecting with the promoter
|
|
219
|
+
# and gene body of gene x
|
|
220
|
+
id_overlap = df_overlap_x.index[
|
|
221
|
+
~np.isin(df_overlap_x['id_p'], df_overlap2_x['id_p'])]
|
|
222
|
+
mask_x = (df_gene_ann['symbol'] == x)
|
|
223
|
+
range_x = df_gene_ann[mask_x][['start', 'end']].values\
|
|
224
|
+
.flatten()
|
|
225
|
+
if df_overlap_x['strand_g'].iloc[0] == '+':
|
|
226
|
+
df_overlap_updated.loc[id_overlap, 'dist'] = pd.concat(
|
|
227
|
+
[abs(df_overlap_x.loc[id_overlap, 'start_p']
|
|
228
|
+
- (range_x[1])),
|
|
229
|
+
abs(df_overlap_x.loc[id_overlap, 'end_p']
|
|
230
|
+
- max(0, range_x[0]-self.gb_upstream))],
|
|
231
|
+
axis=1, sort=False).min(axis=1)
|
|
232
|
+
else:
|
|
233
|
+
df_overlap_updated.loc[id_overlap, 'dist'] = pd.concat(
|
|
234
|
+
[abs(df_overlap_x.loc[id_overlap, 'start_p']
|
|
235
|
+
- (range_x[1]+self.gb_upstream)),
|
|
236
|
+
abs(df_overlap_x.loc[id_overlap, 'end_p']
|
|
237
|
+
- (range_x[0]))],
|
|
238
|
+
axis=1, sort=False).min(axis=1)
|
|
239
|
+
|
|
240
|
+
n_batch = int(df_gene_ann_for_pbt.shape[0]/5)
|
|
241
|
+
if i % n_batch == 0:
|
|
242
|
+
print(f'Processing: {i/df_gene_ann_for_pbt.shape[0]:.1%}')
|
|
243
|
+
df_overlap_updated['dist'] = df_overlap_updated['dist']\
|
|
244
|
+
.astype(float)
|
|
245
|
+
|
|
246
|
+
adata.uns['gene_scores'] = dict()
|
|
247
|
+
adata.uns['gene_scores']['overlap'] = df_overlap_updated.copy()
|
|
248
|
+
|
|
249
|
+
df_overlap_updated['weight'] = np.exp(
|
|
250
|
+
-(df_overlap_updated['dist'].values/self.gb_upstream))
|
|
251
|
+
mask_w = (df_overlap_updated['weight'] < self.cutoff_weight)
|
|
252
|
+
df_overlap_updated.loc[mask_w, 'weight'] = 0
|
|
253
|
+
# construct genes-by-peaks matrix
|
|
254
|
+
mat_GP = csr_matrix(coo_matrix((df_overlap_updated['weight'],
|
|
255
|
+
(df_overlap_updated['id_g'],
|
|
256
|
+
df_overlap_updated['id_p'])),
|
|
257
|
+
shape=(df_gene_ann.shape[0],
|
|
258
|
+
df_peaks.shape[0])))
|
|
259
|
+
# adata_GP = ad.AnnData(X=csr_matrix(mat_GP),
|
|
260
|
+
# obs=df_gene_ann,
|
|
261
|
+
# var=df_peaks)
|
|
262
|
+
# adata_GP.layers['weight'] = adata_GP.X.copy()
|
|
263
|
+
if self.use_gene_weigt:
|
|
264
|
+
gene_weights = self._weight_genes()
|
|
265
|
+
gene_scores = adata[:, mask_p].X * \
|
|
266
|
+
(mat_GP.T.multiply(gene_weights))
|
|
267
|
+
else:
|
|
268
|
+
gene_scores = adata[:, mask_p].X * mat_GP.T
|
|
269
|
+
adata_CG_atac = ad.AnnData(gene_scores,
|
|
270
|
+
obs=adata.obs.copy(),
|
|
271
|
+
var=df_gene_ann.copy())
|
|
272
|
+
return adata_CG_atac
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def gene_scores(adata,
|
|
276
|
+
genome,
|
|
277
|
+
gene_anno=None,
|
|
278
|
+
tss_upstream=1e5,
|
|
279
|
+
tss_downsteam=1e5,
|
|
280
|
+
gb_upstream=5000,
|
|
281
|
+
cutoff_weight=1,
|
|
282
|
+
use_top_pcs=True,
|
|
283
|
+
use_precomputed=True,
|
|
284
|
+
use_gene_weigt=True,
|
|
285
|
+
min_w=1,
|
|
286
|
+
max_w=5):
|
|
287
|
+
"""Calculate gene scores
|
|
288
|
+
|
|
289
|
+
Parameters
|
|
290
|
+
----------
|
|
291
|
+
adata : AnnData
|
|
292
|
+
Annotated data matrix.
|
|
293
|
+
genome : `str`
|
|
294
|
+
Reference genome. Choose from {'hg19', 'hg38', 'mm9', 'mm10'}
|
|
295
|
+
gene_anno : `pandas.DataFrame`, optional (default: None)
|
|
296
|
+
Dataframe of gene annotation.
|
|
297
|
+
If None, built-in gene annotation will be used depending on `genome`;
|
|
298
|
+
If provided, custom gene annotation will be used instead.
|
|
299
|
+
tss_upstream : `int`, optional (default: 1e5)
|
|
300
|
+
The number of base pairs upstream of TSS
|
|
301
|
+
tss_downsteam : `int`, optional (default: 1e5)
|
|
302
|
+
The number of base pairs downstream of TSS
|
|
303
|
+
gb_upstream : `int`, optional (default: 5000)
|
|
304
|
+
The number of base pairs upstream by which gene body is extended.
|
|
305
|
+
Peaks within the extended gene body are given the weight of 1.
|
|
306
|
+
cutoff_weight : `float`, optional (default: 1)
|
|
307
|
+
Weight cutoff for peaks
|
|
308
|
+
use_top_pcs : `bool`, optional (default: True)
|
|
309
|
+
If True, only peaks associated with top PCs will be used
|
|
310
|
+
use_precomputed : `bool`, optional (default: True)
|
|
311
|
+
If True, overlap bewteen peaks and genes
|
|
312
|
+
(stored in `adata.uns['gene_scores']['overlap']`) will be imported
|
|
313
|
+
use_gene_weigt : `bool`, optional (default: True)
|
|
314
|
+
If True, for each gene, the number of peaks assigned to it
|
|
315
|
+
will be rescaled based on gene size
|
|
316
|
+
min_w : `int`, optional (default: 1)
|
|
317
|
+
The minimum weight for each gene.
|
|
318
|
+
Only valid if `use_gene_weigt` is True
|
|
319
|
+
max_w : `int`, optional (default: 5)
|
|
320
|
+
The maximum weight for each gene.
|
|
321
|
+
Only valid if `use_gene_weigt` is True
|
|
322
|
+
|
|
323
|
+
Returns
|
|
324
|
+
-------
|
|
325
|
+
adata_new: AnnData
|
|
326
|
+
Annotated data matrix.
|
|
327
|
+
Stores #cells x #genes gene score matrix
|
|
328
|
+
|
|
329
|
+
updates `adata` with the following fields.
|
|
330
|
+
overlap: `pandas.DataFrame`, (`adata.uns['gene_scores']['overlap']`)
|
|
331
|
+
Dataframe of overlap between peaks and genes
|
|
332
|
+
"""
|
|
333
|
+
GS = GeneScores(adata,
|
|
334
|
+
genome,
|
|
335
|
+
gene_anno=gene_anno,
|
|
336
|
+
tss_upstream=tss_upstream,
|
|
337
|
+
tss_downsteam=tss_downsteam,
|
|
338
|
+
gb_upstream=gb_upstream,
|
|
339
|
+
cutoff_weight=cutoff_weight,
|
|
340
|
+
use_top_pcs=use_top_pcs,
|
|
341
|
+
use_precomputed=use_precomputed,
|
|
342
|
+
use_gene_weigt=use_gene_weigt,
|
|
343
|
+
min_w=min_w,
|
|
344
|
+
max_w=max_w)
|
|
345
|
+
adata_CG_atac = GS.cal_gene_scores()
|
|
346
|
+
return adata_CG_atac
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""General-purpose tools"""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from sklearn.cluster import KMeans
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def discretize(adata,
|
|
8
|
+
layer=None,
|
|
9
|
+
n_bins=5,
|
|
10
|
+
max_bins=100):
|
|
11
|
+
"""Discretize continous values
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
adata: AnnData
|
|
16
|
+
Annotated data matrix.
|
|
17
|
+
layer: `str`, optional (default: None)
|
|
18
|
+
The layer used to perform discretization
|
|
19
|
+
n_bins: `int`, optional (default: 5)
|
|
20
|
+
The number of bins to produce.
|
|
21
|
+
It must be smaller than `max_bins`.
|
|
22
|
+
max_bins: `int`, optional (default: 100)
|
|
23
|
+
The number of bins used in the initial approximation.
|
|
24
|
+
i.e. the number of bins to cluster.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
updates `adata` with the following fields
|
|
29
|
+
|
|
30
|
+
`.layer['hge']` : `array_like`
|
|
31
|
+
The matrix of discretized values to build HGE(MultiChat) graph.
|
|
32
|
+
`.uns['disc']` : `dict`
|
|
33
|
+
`bin_edges`: The edges of each bin.
|
|
34
|
+
`bin_count`: The number of values in each bin.
|
|
35
|
+
`hist_edges`: The edges of each bin \
|
|
36
|
+
in the initial approximation.
|
|
37
|
+
`hist_count`: The number of values in each bin \
|
|
38
|
+
for the initial approximation.
|
|
39
|
+
"""
|
|
40
|
+
if layer is None:
|
|
41
|
+
X = adata.X
|
|
42
|
+
else:
|
|
43
|
+
X = adata.layers[layer]
|
|
44
|
+
nonzero_cont = X.data
|
|
45
|
+
|
|
46
|
+
hist_count, hist_edges = np.histogram(
|
|
47
|
+
nonzero_cont,
|
|
48
|
+
bins=max_bins,
|
|
49
|
+
density=False)
|
|
50
|
+
hist_centroids = (hist_edges[0:-1] + hist_edges[1:])/2
|
|
51
|
+
|
|
52
|
+
kmeans = KMeans(n_clusters=n_bins, random_state=2021).fit(
|
|
53
|
+
hist_centroids.reshape(-1, 1),
|
|
54
|
+
sample_weight=hist_count)
|
|
55
|
+
cluster_centers = np.sort(kmeans.cluster_centers_.flatten())
|
|
56
|
+
|
|
57
|
+
padding = (hist_edges[-1] - hist_edges[0])/(max_bins*10)
|
|
58
|
+
bin_edges = np.array(
|
|
59
|
+
[hist_edges[0]-padding] +
|
|
60
|
+
list((cluster_centers[0:-1] + cluster_centers[1:])/2) +
|
|
61
|
+
[hist_edges[-1]+padding])
|
|
62
|
+
nonzero_disc = np.digitize(nonzero_cont, bin_edges).reshape(-1,)
|
|
63
|
+
bin_count = np.unique(nonzero_disc, return_counts=True)[1]
|
|
64
|
+
|
|
65
|
+
adata.layers['hge'] = X.copy()
|
|
66
|
+
adata.layers['hge'].data = nonzero_disc
|
|
67
|
+
adata.uns['disc'] = dict()
|
|
68
|
+
adata.uns['disc']['bin_edges'] = bin_edges
|
|
69
|
+
adata.uns['disc']['bin_count'] = bin_count
|
|
70
|
+
adata.uns['disc']['hist_edges'] = hist_edges
|
|
71
|
+
adata.uns['disc']['hist_count'] = hist_count
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Integration across experimental conditions or single cell modalities"""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import anndata as ad
|
|
5
|
+
# from sklearn.metrics.pairwise import pairwise_distances
|
|
6
|
+
from sklearn.utils.extmath import randomized_svd
|
|
7
|
+
from scipy.sparse import csr_matrix, find
|
|
8
|
+
|
|
9
|
+
from ._utils import _knn
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def infer_edges(adata_ref,
|
|
13
|
+
adata_query,
|
|
14
|
+
feature='highly_variable',
|
|
15
|
+
n_components=20,
|
|
16
|
+
random_state=42,
|
|
17
|
+
layer=None,
|
|
18
|
+
k=20,
|
|
19
|
+
metric='euclidean',
|
|
20
|
+
leaf_size=40,
|
|
21
|
+
**kwargs):
|
|
22
|
+
"""Infer edges between reference and query observations
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
adata_ref: `AnnData`
|
|
27
|
+
Annotated reference data matrix.
|
|
28
|
+
adata_query: `AnnData`
|
|
29
|
+
Annotated query data matrix.
|
|
30
|
+
feature: `str`, optional (default: None)
|
|
31
|
+
Feature used for edges inference.
|
|
32
|
+
The data type of `.var[feature]` needs to be `bool`
|
|
33
|
+
n_components: `int`, optional (default: 20)
|
|
34
|
+
The number of components used in `randomized_svd`
|
|
35
|
+
for comparing reference and query observations
|
|
36
|
+
random_state: `int`, optional (default: 42)
|
|
37
|
+
The seed used for truncated randomized SVD
|
|
38
|
+
n_top_edges: `int`, optional (default: None)
|
|
39
|
+
The number of edges to keep
|
|
40
|
+
If specified, `percentile` will be ignored
|
|
41
|
+
percentile: `float`, optional (default: 0.01)
|
|
42
|
+
The percentile of edges to keep
|
|
43
|
+
k: `int`, optional (default: 5)
|
|
44
|
+
The number of nearest neighbors to consider within each dataset
|
|
45
|
+
metric: `str`, optional (default: 'euclidean')
|
|
46
|
+
The metric to use when calculating distance between
|
|
47
|
+
reference and query observations
|
|
48
|
+
layer: `str`, optional (default: None)
|
|
49
|
+
The layer used to perform edge inference
|
|
50
|
+
If None, `.X` will be used.
|
|
51
|
+
kwargs:
|
|
52
|
+
Other keyword arguments are passed down to `randomized_svd()`
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
adata_ref_query: `AnnData`
|
|
57
|
+
Annotated relation matrix betwewn reference and query observations
|
|
58
|
+
Store reference entity as observations and query entity as variables
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
mask_ref = adata_ref.var[feature]
|
|
62
|
+
feature_ref = adata_ref.var_names[mask_ref]
|
|
63
|
+
feature_query = adata_query.var_names
|
|
64
|
+
feature_shared = list(set(feature_ref).intersection(set(feature_query)))
|
|
65
|
+
print(f'#shared features: {len(feature_shared)}')
|
|
66
|
+
if layer is None:
|
|
67
|
+
X_ref = adata_ref[:, feature_shared].X
|
|
68
|
+
X_query = adata_query[:, feature_shared].X
|
|
69
|
+
else:
|
|
70
|
+
X_ref = adata_ref[:, feature_shared].layers[layer]
|
|
71
|
+
X_query = adata_query[:, feature_shared].layers[layer]
|
|
72
|
+
|
|
73
|
+
if any(X_ref.sum(axis=1) == 0) or any(X_query.sum(axis=1) == 0):
|
|
74
|
+
raise ValueError(
|
|
75
|
+
f'Some nodes contain zero expressed {feature} features.\n'
|
|
76
|
+
f'Please try to include more {feature} features.')
|
|
77
|
+
|
|
78
|
+
print('Performing randomized SVD ...')
|
|
79
|
+
mat = X_ref * X_query.T
|
|
80
|
+
U, Sigma, VT = randomized_svd(mat,
|
|
81
|
+
n_components=n_components,
|
|
82
|
+
random_state=random_state,
|
|
83
|
+
**kwargs)
|
|
84
|
+
svd_data = np.vstack((U, VT.T))
|
|
85
|
+
X_svd_ref = svd_data[:U.shape[0], :]
|
|
86
|
+
X_svd_query = svd_data[-VT.shape[1]:, :]
|
|
87
|
+
X_svd_ref = X_svd_ref / (X_svd_ref**2).sum(-1, keepdims=True)**0.5
|
|
88
|
+
X_svd_query = X_svd_query / (X_svd_query**2).sum(-1, keepdims=True)**0.5
|
|
89
|
+
|
|
90
|
+
# print('Searching for neighbors within each dataset ...')
|
|
91
|
+
# knn_conn_ref, knn_dist_ref = _knn(
|
|
92
|
+
# X_ref=X_svd_ref,
|
|
93
|
+
# k=k,
|
|
94
|
+
# leaf_size=leaf_size,
|
|
95
|
+
# metric=metric)
|
|
96
|
+
# knn_conn_query, knn_dist_query = _knn(
|
|
97
|
+
# X_ref=X_svd_query,
|
|
98
|
+
# k=k,
|
|
99
|
+
# leaf_size=leaf_size,
|
|
100
|
+
# metric=metric)
|
|
101
|
+
|
|
102
|
+
print('Searching for mutual nearest neighbors ...')
|
|
103
|
+
knn_conn_ref_query, knn_dist_ref_query = _knn(
|
|
104
|
+
X_ref=X_svd_ref,
|
|
105
|
+
X_query=X_svd_query,
|
|
106
|
+
k=k,
|
|
107
|
+
leaf_size=leaf_size,
|
|
108
|
+
metric=metric)
|
|
109
|
+
knn_conn_query_ref, knn_dist_query_ref = _knn(
|
|
110
|
+
X_ref=X_svd_query,
|
|
111
|
+
X_query=X_svd_ref,
|
|
112
|
+
k=k,
|
|
113
|
+
leaf_size=leaf_size,
|
|
114
|
+
metric=metric)
|
|
115
|
+
|
|
116
|
+
sum_conn_ref_query = knn_conn_ref_query + knn_conn_query_ref.T
|
|
117
|
+
id_x, id_y, values = find(sum_conn_ref_query > 1)
|
|
118
|
+
print(f'{len(id_x)} edges are selected')
|
|
119
|
+
conn_ref_query = csr_matrix(
|
|
120
|
+
(values*1, (id_x, id_y)),
|
|
121
|
+
shape=(knn_conn_ref_query.shape))
|
|
122
|
+
dist_ref_query = csr_matrix(
|
|
123
|
+
(knn_dist_ref_query[id_x, id_y].A.flatten(), (id_x, id_y)),
|
|
124
|
+
shape=(knn_conn_ref_query.shape))
|
|
125
|
+
# it's easier to distinguish zeros (no connection vs zero distance)
|
|
126
|
+
# using similarity scores
|
|
127
|
+
sim_ref_query = csr_matrix(
|
|
128
|
+
(1/(dist_ref_query.data+1), dist_ref_query.nonzero()),
|
|
129
|
+
shape=(dist_ref_query.shape)) # similarity scores
|
|
130
|
+
|
|
131
|
+
# print('Computing similarity scores ...')
|
|
132
|
+
# dist_ref_query = pairwise_distances(X_svd_ref,
|
|
133
|
+
# X_svd_query,
|
|
134
|
+
# metric=metric)
|
|
135
|
+
# sim_ref_query = 1/(1+dist_ref_query)
|
|
136
|
+
# # remove low similarity entries to save memory
|
|
137
|
+
# sim_ref_query = np.where(
|
|
138
|
+
# sim_ref_query < np.percentile(sim_ref_query, pct_keep*100),
|
|
139
|
+
# 0, sim_ref_query)
|
|
140
|
+
# sim_ref_query = csr_matrix(sim_ref_query)
|
|
141
|
+
|
|
142
|
+
adata_ref_query = ad.AnnData(X=sim_ref_query,
|
|
143
|
+
obs=adata_ref.obs,
|
|
144
|
+
var=adata_query.obs)
|
|
145
|
+
adata_ref_query.layers['hge'] = conn_ref_query
|
|
146
|
+
adata_ref_query.obsm['svd'] = X_svd_ref
|
|
147
|
+
# adata_ref_query.obsp['conn'] = knn_conn_ref
|
|
148
|
+
# adata_ref_query.obsp['dist'] = knn_dist_ref
|
|
149
|
+
adata_ref_query.varm['svd'] = X_svd_query
|
|
150
|
+
# adata_ref_query.varp['conn'] = knn_conn_query
|
|
151
|
+
# adata_ref_query.varp['dist'] = knn_dist_query
|
|
152
|
+
return adata_ref_query
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def trim_edges(adata_ref_query,
|
|
156
|
+
cutoff=None,
|
|
157
|
+
n_edges=None):
|
|
158
|
+
"""Trim edges based on the similarity scores
|
|
159
|
+
|
|
160
|
+
Parameters
|
|
161
|
+
----------
|
|
162
|
+
adata_ref_query: `AnnData`
|
|
163
|
+
Annotated relation matrix betwewn reference and query observations.
|
|
164
|
+
n_edges: `int`, optional (default: None)
|
|
165
|
+
The number of edges to keep
|
|
166
|
+
If specified, `percentile` will be ignored
|
|
167
|
+
cutoff: `float`, optional (default: None)
|
|
168
|
+
The distance cutoff.
|
|
169
|
+
If None, it will be decided by `n_top_edges`
|
|
170
|
+
If specified, `n_top_edges` will be ignored
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
updates `adata_ref_query` with the following field.
|
|
175
|
+
`.layers['hge']` : `array_like`
|
|
176
|
+
relation matrix betwewn reference and query observations
|
|
177
|
+
"""
|
|
178
|
+
sim_ref_query = adata_ref_query.X
|
|
179
|
+
if cutoff is None:
|
|
180
|
+
if n_edges is None:
|
|
181
|
+
raise ValueError('"cutoff" or "n_edges" has to be specified')
|
|
182
|
+
else:
|
|
183
|
+
cutoff = \
|
|
184
|
+
np.partition(sim_ref_query.data,
|
|
185
|
+
(sim_ref_query.size-n_edges))[
|
|
186
|
+
sim_ref_query.size-n_edges]
|
|
187
|
+
# cutoff = \
|
|
188
|
+
# np.partition(sim_ref_query.flatten(),
|
|
189
|
+
# (len(sim_ref_query.flatten())-n_edges))[
|
|
190
|
+
# len(sim_ref_query.flatten())-n_edges]
|
|
191
|
+
id_x, id_y, values = find(sim_ref_query > cutoff)
|
|
192
|
+
|
|
193
|
+
print(f'{len(id_x)} edges are selected')
|
|
194
|
+
conn_ref_query = csr_matrix(
|
|
195
|
+
(values*1, (id_x, id_y)),
|
|
196
|
+
shape=(sim_ref_query.shape))
|
|
197
|
+
adata_ref_query.layers['hge'] = conn_ref_query
|