sceps 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.
- sceps/__init__.py +22 -0
- sceps/cli/__init__.py +14 -0
- sceps/cli/aggregate.py +402 -0
- sceps/cli/cluster_neighborhood.py +122 -0
- sceps/cli/corr.py +292 -0
- sceps/cli/run.py +63 -0
- sceps/estimation.py +555 -0
- sceps/neighborhood.py +262 -0
- sceps/scdata.py +296 -0
- sceps/sceps_core.py +364 -0
- sceps/stats_test.py +182 -0
- sceps/utils.py +363 -0
- sceps-0.1.0.dist-info/METADATA +141 -0
- sceps-0.1.0.dist-info/RECORD +18 -0
- sceps-0.1.0.dist-info/WHEEL +5 -0
- sceps-0.1.0.dist-info/entry_points.txt +5 -0
- sceps-0.1.0.dist-info/licenses/LICENSE +21 -0
- sceps-0.1.0.dist-info/top_level.txt +1 -0
sceps/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""scEPS (single-cell Expression exPlainability Statistics).
|
|
2
|
+
|
|
3
|
+
Integrates GWAS and single-cell disease cell atlas data to identify
|
|
4
|
+
disease-associated cell neighborhoods.
|
|
5
|
+
|
|
6
|
+
The Python API lives in :mod:`sceps.sceps_core`::
|
|
7
|
+
|
|
8
|
+
from sceps.sceps_core import *
|
|
9
|
+
|
|
10
|
+
Nothing heavyweight is imported here on purpose: the console scripts in
|
|
11
|
+
``sceps.cli`` import this package first, and pulling scanpy in at this point
|
|
12
|
+
would slow down ``--help`` for every command.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
__version__ = version("sceps")
|
|
19
|
+
except PackageNotFoundError: # running from a source tree without an install
|
|
20
|
+
__version__ = "0.0.0.dev0"
|
|
21
|
+
|
|
22
|
+
__all__ = ["__version__"]
|
sceps/cli/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Command-line entry points for scEPS.
|
|
2
|
+
|
|
3
|
+
Each module here exposes a ``main()`` that is wired to a console script in
|
|
4
|
+
``pyproject.toml``:
|
|
5
|
+
|
|
6
|
+
============================== ==============================
|
|
7
|
+
command module
|
|
8
|
+
============================== ==============================
|
|
9
|
+
``sceps`` :mod:`sceps.cli.run`
|
|
10
|
+
``sceps-cluster-neighborhood`` :mod:`sceps.cli.cluster_neighborhood`
|
|
11
|
+
``sceps-aggregate`` :mod:`sceps.cli.aggregate`
|
|
12
|
+
``sceps-corr`` :mod:`sceps.cli.corr`
|
|
13
|
+
============================== ==============================
|
|
14
|
+
"""
|
sceps/cli/aggregate.py
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import argparse, sys, glob, os
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import numpy as np
|
|
4
|
+
import scipy as sp
|
|
5
|
+
import scipy.stats
|
|
6
|
+
from tqdm import tqdm
|
|
7
|
+
import logging, random
|
|
8
|
+
import scanpy as sc
|
|
9
|
+
|
|
10
|
+
from ..utils import EPS
|
|
11
|
+
from ..stats_test import get_weighted_mean
|
|
12
|
+
|
|
13
|
+
from ..scdata import *
|
|
14
|
+
from sklearn.cluster import MiniBatchKMeans
|
|
15
|
+
import statsmodels.stats.multitest as smm
|
|
16
|
+
|
|
17
|
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
18
|
+
|
|
19
|
+
estimand_col = ['OMEGA_GWAS', 'OMEGA_CONTROL', 'OMEGA_REST', 'OMEGA_OVERALL', 'OMEGA_DIFF']
|
|
20
|
+
info_col = ['NEIGHBORHOOD_SIZE', 'NUM_DONOR', \
|
|
21
|
+
'MEAN_MEAN_EXPR_GWAS', 'MEAN_MEAN_EXPR_CONTROL', 'MEAN_MEAN_EXPR_REST', 'MEAN_MEAN_EXPR_ALL', \
|
|
22
|
+
'MEAN_VAR_EXPR_GWAS', 'MEAN_VAR_EXPR_CONTROL', 'MEAN_VAR_EXPR_REST', 'MEAN_VAR_EXPR_ALL', \
|
|
23
|
+
'VAR_OUTCOME', 'NUM_GENE_GWAS', 'NUM_GENE_CONTROL', 'NUM_GENE_REST']
|
|
24
|
+
|
|
25
|
+
def main():
|
|
26
|
+
|
|
27
|
+
# get command line
|
|
28
|
+
args = get_command_line()
|
|
29
|
+
|
|
30
|
+
# set seed
|
|
31
|
+
random.seed(args.seed)
|
|
32
|
+
np.random.seed(args.seed)
|
|
33
|
+
|
|
34
|
+
# append prefix to the estimand columns if using weighted estimands
|
|
35
|
+
if args.use_sigma:
|
|
36
|
+
for i in range(len(estimand_col)):
|
|
37
|
+
estimand_col[i] = estimand_col[i].replace('OMEGA', 'SIGMA')
|
|
38
|
+
|
|
39
|
+
# load the single-cell data
|
|
40
|
+
logging.info("Loading single-cell data from {}".format(args.adata))
|
|
41
|
+
adata = sc.read_h5ad(args.adata)
|
|
42
|
+
|
|
43
|
+
# If args.cell_id_col not in adata.obs.columns, we use adata.obs.index as replacement
|
|
44
|
+
obs = adata.obs.copy()
|
|
45
|
+
if args.cell_id_col not in obs.columns:
|
|
46
|
+
if (args.cell_id_col is None) or (args.cell_id_col == ''):
|
|
47
|
+
args.cell_id_col = 'sceps.cell_index'
|
|
48
|
+
obs[args.cell_id_col] = obs.index.copy()
|
|
49
|
+
adata.obs = obs
|
|
50
|
+
|
|
51
|
+
# define neighborhood clusters for bootstrap
|
|
52
|
+
if args.neighborhood_clusters != '':
|
|
53
|
+
logging.info("Using pre-computed clusters of cell neighborhoods from {}".format(args.neighborhood_clusters))
|
|
54
|
+
df_neighborhood_cluster = pd.read_table(args.neighborhood_clusters)
|
|
55
|
+
adata = adata[adata.obs[args.cell_id_col].isin(df_neighborhood_cluster[args.cell_id_col])].copy()
|
|
56
|
+
cell2cluster = dict(zip(df_neighborhood_cluster[args.cell_id_col], df_neighborhood_cluster[args.block_bootstrap]))
|
|
57
|
+
adata.obs[args.block_bootstrap] = adata.obs[args.cell_id_col].map(cell2cluster)
|
|
58
|
+
|
|
59
|
+
# aggregate results across batches of sceps runs
|
|
60
|
+
logging.info("Aggregating scEPS results across batches of runs")
|
|
61
|
+
df_sceps = aggregate_sceps_batches(args)
|
|
62
|
+
adata = adata[adata.obs[args.cell_id_col].isin(df_sceps['CELL'])].copy()
|
|
63
|
+
obs = adata.obs.copy()
|
|
64
|
+
|
|
65
|
+
# perform testing at cell type level
|
|
66
|
+
if args.cell_type_col != '':
|
|
67
|
+
all_cell_type_col = args.cell_type_col.split(',')
|
|
68
|
+
for cell_type_col in all_cell_type_col:
|
|
69
|
+
|
|
70
|
+
logging.info("Testing at cell type level {}".format(cell_type_col))
|
|
71
|
+
|
|
72
|
+
# create output file name
|
|
73
|
+
if args.use_sigma == True:
|
|
74
|
+
out_fnm = '{}.{}.sceps.sigma.celltype.txt'.format(args.out, cell_type_col)
|
|
75
|
+
else:
|
|
76
|
+
out_fnm = '{}.{}.sceps.omega.celltype.txt'.format(args.out, cell_type_col)
|
|
77
|
+
|
|
78
|
+
# test all cell types in the cell_type_col column
|
|
79
|
+
df_test_ct = test_all_cell_types(args, cell_type_col, obs, df_sceps)
|
|
80
|
+
|
|
81
|
+
# save results
|
|
82
|
+
df_test_ct.to_csv(out_fnm, sep='\t', index=False, float_format='%.5g')
|
|
83
|
+
else:
|
|
84
|
+
# no cell type column specified, aggregate across all cells
|
|
85
|
+
if args.use_sigma == True:
|
|
86
|
+
out_fnm = '{}.sceps.sigma.celltype.txt'.format(args.out)
|
|
87
|
+
else:
|
|
88
|
+
out_fnm = '{}.sceps.omega.celltype.txt'.format(args.out)
|
|
89
|
+
|
|
90
|
+
# create a temporary cell type column
|
|
91
|
+
obs['scEPS_tmp_cell_type_col'] = 'All'
|
|
92
|
+
df_test_ct = test_all_cell_types(args, 'scEPS_tmp_cell_type_col', obs, df_sceps)
|
|
93
|
+
|
|
94
|
+
# save results
|
|
95
|
+
df_test_ct.to_csv(out_fnm, sep='\t', index=False, float_format='%.5g')
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def aggregate_sceps_batches(args):
|
|
99
|
+
"""
|
|
100
|
+
Aggregate scEPS results across batches of cells
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
# load the list of neighborhoods to use
|
|
104
|
+
df_usenb = None
|
|
105
|
+
if args.use_neighborhoods != '':
|
|
106
|
+
df_usenb = pd.read_table(args.use_neighborhoods, header=None)
|
|
107
|
+
|
|
108
|
+
# load sceps output files
|
|
109
|
+
if args.prefix.endswith('.txt.gz') == False:
|
|
110
|
+
if args.use_sigma == False:
|
|
111
|
+
all_out_ = glob.glob('{}*sceps.omega.txt.gz'.format(args.prefix))
|
|
112
|
+
else:
|
|
113
|
+
all_out_ = glob.glob('{}*sceps.sigma.txt.gz'.format(args.prefix))
|
|
114
|
+
else:
|
|
115
|
+
all_out_ = glob.glob(args.prefix)
|
|
116
|
+
|
|
117
|
+
# get output file name
|
|
118
|
+
if args.use_sigma == False:
|
|
119
|
+
out_fnm = '{}.sceps.omega.txt.gz'.format(args.out)
|
|
120
|
+
else:
|
|
121
|
+
out_fnm = '{}.sceps.sigma.txt.gz'.format(args.out)
|
|
122
|
+
|
|
123
|
+
# exclude files that contains specific string
|
|
124
|
+
all_out = []
|
|
125
|
+
if args.exclude_files_with_str is not None:
|
|
126
|
+
for fnm in all_out_:
|
|
127
|
+
if fnm.find(args.exclude_files_with_str) == -1:
|
|
128
|
+
all_out.append(fnm)
|
|
129
|
+
else:
|
|
130
|
+
all_out = all_out_
|
|
131
|
+
|
|
132
|
+
# aggregate the results
|
|
133
|
+
logging.info('Found {} scEPS score files'.format(len(all_out)))
|
|
134
|
+
df_sceps = []
|
|
135
|
+
for fnm in tqdm(all_out):
|
|
136
|
+
df_sceps_batch = pd.read_table(fnm)
|
|
137
|
+
if df_usenb is not None:
|
|
138
|
+
df_sceps_batch = df_sceps_batch[df_sceps_batch['CELL'].isin(df_usenb[0])].reset_index(drop=True)
|
|
139
|
+
df_sceps.append(df_sceps_batch)
|
|
140
|
+
df_sceps = pd.concat(df_sceps, ignore_index=True)
|
|
141
|
+
|
|
142
|
+
# apply testing at cell neighborhood level
|
|
143
|
+
logging.info("Testing at cell neighborhood level")
|
|
144
|
+
test_cell(df_sceps)
|
|
145
|
+
|
|
146
|
+
# save the aggregated sceps score file
|
|
147
|
+
df_sceps.to_csv(out_fnm, sep='\t', index=False, float_format='%.5g')
|
|
148
|
+
|
|
149
|
+
return df_sceps
|
|
150
|
+
|
|
151
|
+
def test_cell(df_sceps):
|
|
152
|
+
|
|
153
|
+
ncell = df_sceps.shape[0]
|
|
154
|
+
|
|
155
|
+
# iterate through estimand
|
|
156
|
+
for estimand in estimand_col:
|
|
157
|
+
|
|
158
|
+
pval = df_sceps['P_Z_{}'.format(estimand)]
|
|
159
|
+
est_val = df_sceps[estimand].values
|
|
160
|
+
|
|
161
|
+
signif_fdr5 = np.zeros(ncell, dtype=bool)
|
|
162
|
+
signif_fdr10 = np.zeros(ncell, dtype=bool)
|
|
163
|
+
signif_fdr20 = np.zeros(ncell, dtype=bool)
|
|
164
|
+
|
|
165
|
+
signif_fdr5 = (est_val > 0) & (smm.multipletests(pval, alpha=0.05, method='fdr_bh')[0])
|
|
166
|
+
signif_fdr10 = (est_val > 0) & (smm.multipletests(pval, alpha=0.10, method='fdr_bh')[0])
|
|
167
|
+
signif_fdr20 = (est_val > 0) & (smm.multipletests(pval, alpha=0.20, method='fdr_bh')[0])
|
|
168
|
+
|
|
169
|
+
if estimand.find('DIFF') > 0:
|
|
170
|
+
estimand_gwas = estimand.replace('DIFF', 'GWAS')
|
|
171
|
+
signif_fdr5 = signif_fdr5 & (df_sceps[estimand_gwas].values > 0)
|
|
172
|
+
signif_fdr10 = signif_fdr10 & (df_sceps[estimand_gwas].values > 0)
|
|
173
|
+
signif_fdr20 = signif_fdr20 & (df_sceps[estimand_gwas].values > 0)
|
|
174
|
+
|
|
175
|
+
df_sceps['SIGNIF_FDR5_{}'.format(estimand)] = signif_fdr5
|
|
176
|
+
df_sceps['SIGNIF_FDR10_{}'.format(estimand)] = signif_fdr10
|
|
177
|
+
df_sceps['SIGNIF_FDR20_{}'.format(estimand)] = signif_fdr20
|
|
178
|
+
|
|
179
|
+
return df_sceps
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def test_all_cell_types(args, cell_type_col, obs, df_sceps):
|
|
183
|
+
"""
|
|
184
|
+
Test all the cell types
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
# get all cell type
|
|
188
|
+
all_ct = pd.unique(obs[cell_type_col])
|
|
189
|
+
|
|
190
|
+
# perform testing for each cell type
|
|
191
|
+
df_test_out = []
|
|
192
|
+
for ct in tqdm(all_ct):
|
|
193
|
+
|
|
194
|
+
# get test stats
|
|
195
|
+
df_test_out_ct = test_cell_type(args, cell_type_col, obs, ct, df_sceps)
|
|
196
|
+
if df_test_out_ct is None:
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
# add additional information
|
|
200
|
+
df_test_out_ct['CELL_TYPE'] = ct
|
|
201
|
+
if args.add_column is not None:
|
|
202
|
+
if (args.add_column[0] in df_test_out_ct.columns) == False:
|
|
203
|
+
df_test_out_ct[args.add_column[0]] = args.add_column[1]
|
|
204
|
+
|
|
205
|
+
# append to list
|
|
206
|
+
df_test_out.append(df_test_out_ct)
|
|
207
|
+
|
|
208
|
+
# aggregate results
|
|
209
|
+
if len(df_test_out) > 0:
|
|
210
|
+
df_test_out = pd.concat(df_test_out)
|
|
211
|
+
df_test_out = df_test_out.dropna(axis=1, how='all')
|
|
212
|
+
ct_col = df_test_out.pop('CELL_TYPE')
|
|
213
|
+
df_test_out.insert(0, 'CELL_TYPE', ct_col)
|
|
214
|
+
|
|
215
|
+
return df_test_out
|
|
216
|
+
|
|
217
|
+
# return none if nothing to return
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_cell_type(args, cell_type_col, obs, ct, df_sceps):
|
|
222
|
+
"""
|
|
223
|
+
Test a particular cell type
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
# extract cells from the cell type
|
|
227
|
+
ct_cells = obs[obs[cell_type_col]==ct][args.cell_id_col]
|
|
228
|
+
df_sceps_ct = df_sceps[df_sceps['CELL'].isin(ct_cells)].copy()
|
|
229
|
+
|
|
230
|
+
# add block information if using block bootstrap
|
|
231
|
+
if args.block_bootstrap != '':
|
|
232
|
+
cell2block = dict(zip(obs[args.cell_id_col], obs[args.block_bootstrap]))
|
|
233
|
+
df_sceps_ct['BLOCK'] = df_sceps_ct['CELL'].map(cell2block)
|
|
234
|
+
|
|
235
|
+
# check if dataframe empty
|
|
236
|
+
num_cells = df_sceps_ct.shape[0]
|
|
237
|
+
if num_cells == 0:
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
# get summary info
|
|
241
|
+
df_all_test_out = []
|
|
242
|
+
for col in info_col:
|
|
243
|
+
if col in df_sceps_ct.columns:
|
|
244
|
+
df_all_test_out.append(pd.DataFrame({'MEAN_'+col: [np.mean(df_sceps_ct[col])]}))
|
|
245
|
+
|
|
246
|
+
# get bootstrap test statistics
|
|
247
|
+
use_inv_var_wgt = False
|
|
248
|
+
if args.use_inverse_variance_weights == True:
|
|
249
|
+
use_inv_var_wgt = True
|
|
250
|
+
for col in estimand_col:
|
|
251
|
+
test_out = get_bootstrap_test_stats(df_sceps_ct, col,
|
|
252
|
+
nbs=args.num_bootstrap, use_inv_var_wgt=use_inv_var_wgt)
|
|
253
|
+
df_all_test_out.append(pd.DataFrame(test_out))
|
|
254
|
+
df_all_test_out = pd.concat(df_all_test_out, axis=1)
|
|
255
|
+
|
|
256
|
+
# add number of neighborhoods
|
|
257
|
+
df_all_test_out['NUM_CELL'] = num_cells
|
|
258
|
+
|
|
259
|
+
return df_all_test_out
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def get_bootstrap_test_stats(df, col, nbs=1000, use_inv_var_wgt=False, count_signif=True):
|
|
263
|
+
|
|
264
|
+
# prepare bootstrap
|
|
265
|
+
ncell = df.shape[0]
|
|
266
|
+
stats_vec = df[col].values
|
|
267
|
+
se_vec = df['SE_'+col].values
|
|
268
|
+
var_vec = np.square(se_vec)
|
|
269
|
+
inv_var_vec = 1.0 / (var_vec + EPS)
|
|
270
|
+
weight_vec = np.ones(ncell) / ncell
|
|
271
|
+
if use_inv_var_wgt == True:
|
|
272
|
+
weight_vec = inv_var_vec / np.sum(inv_var_vec)
|
|
273
|
+
mean_stats = np.sum(stats_vec * weight_vec)
|
|
274
|
+
all_mean_stats_bs = []
|
|
275
|
+
|
|
276
|
+
# get number of significant cell neighborhood
|
|
277
|
+
if count_signif == True:
|
|
278
|
+
num_signif_fdr5 = np.sum(df['SIGNIF_FDR5_{}'.format(col)])
|
|
279
|
+
num_signif_fdr10 = np.sum(df['SIGNIF_FDR10_{}'.format(col)])
|
|
280
|
+
num_signif_fdr20 = np.sum(df['SIGNIF_FDR20_{}'.format(col)])
|
|
281
|
+
|
|
282
|
+
# standard bootstrap
|
|
283
|
+
if 'BLOCK' not in df.columns:
|
|
284
|
+
all_idx = np.array(range(ncell))
|
|
285
|
+
weight_vec_bs = weight_vec.copy()
|
|
286
|
+
for _ in nbs:
|
|
287
|
+
# bootstrap the cells
|
|
288
|
+
use_idx = np.random.choice(all_idx, size=ncell, replace=True)
|
|
289
|
+
if use_inv_var_wgt == True:
|
|
290
|
+
weight_vec_bs = inv_var_vec[use_idx] / np.sum(inv_var_vec[use_idx])
|
|
291
|
+
all_mean_stats_bs.append(np.sum(stats_vec[use_idx] * weight_vec_bs[use_idx]))
|
|
292
|
+
# block bootstrap
|
|
293
|
+
else:
|
|
294
|
+
block_val = df['BLOCK'].values
|
|
295
|
+
all_block = np.array(pd.unique(df['BLOCK']))
|
|
296
|
+
nblock = all_block.shape[0]
|
|
297
|
+
weight_vec_bs = weight_vec.copy()
|
|
298
|
+
# bootstrap the blocks
|
|
299
|
+
for _ in range(nbs):
|
|
300
|
+
use_block = np.random.choice(all_block, size=nblock, replace=True)
|
|
301
|
+
use_idx = np.concatenate([np.where(block_val == blk)[0] for blk in use_block])
|
|
302
|
+
if use_inv_var_wgt == True:
|
|
303
|
+
weight_vec_bs = inv_var_vec[use_idx] / np.sum(inv_var_vec[use_idx])
|
|
304
|
+
all_mean_stats_bs.append(np.sum(stats_vec[use_idx] * weight_vec_bs[use_idx]))
|
|
305
|
+
|
|
306
|
+
# get test statistics
|
|
307
|
+
se_mean_stats = np.std(all_mean_stats_bs)
|
|
308
|
+
z_mean_stats = mean_stats / (se_mean_stats + EPS)
|
|
309
|
+
p_mean_stats = (1-scipy.stats.norm.cdf(np.fabs(z_mean_stats)))*2.0
|
|
310
|
+
|
|
311
|
+
# create out dict
|
|
312
|
+
if count_signif == True:
|
|
313
|
+
out = {'NUM_SIGNIF_FDR5_{}'.format(col): [num_signif_fdr5],
|
|
314
|
+
'NUM_SIGNIF_FDR10_{}'.format(col): [num_signif_fdr10],
|
|
315
|
+
'NUM_SIGNIF_FDR20_{}'.format(col): [num_signif_fdr20],
|
|
316
|
+
'MEAN_{}'.format(col): [mean_stats],
|
|
317
|
+
'SE_MEAN_{}'.format(col): [se_mean_stats],
|
|
318
|
+
'Z_MEAN_{}'.format(col): [z_mean_stats],
|
|
319
|
+
'P_Z_MEAN_{}'.format(col): [p_mean_stats]}
|
|
320
|
+
else:
|
|
321
|
+
out = {'MEAN_{}'.format(col): [mean_stats],
|
|
322
|
+
'SE_MEAN_{}'.format(col): [se_mean_stats],
|
|
323
|
+
'Z_MEAN_{}'.format(col): [z_mean_stats],
|
|
324
|
+
'P_Z_MEAN_{}'.format(col): [p_mean_stats]}
|
|
325
|
+
|
|
326
|
+
return out
|
|
327
|
+
|
|
328
|
+
def get_command_line():
|
|
329
|
+
|
|
330
|
+
# Create the parser
|
|
331
|
+
parser = argparse.ArgumentParser(description="Summarize the results")
|
|
332
|
+
|
|
333
|
+
# Input regulated command line arguments
|
|
334
|
+
parser.add_argument('--prefix', type=str, required=False,
|
|
335
|
+
help="""Used to specify a regular expression for the file names of the output """ \
|
|
336
|
+
"""for individual cell neighborhoods from step 1.""")
|
|
337
|
+
|
|
338
|
+
parser.add_argument('--adata', type=str, required=False,
|
|
339
|
+
help="""Used to specify the same single-cell data used for obtaining scEPS statistics """ \
|
|
340
|
+
"""at individual cell neighborhood level, with cell ID column specified by the --cell-id-col flag.""")
|
|
341
|
+
|
|
342
|
+
parser.add_argument('--cell-id-col', type=str, required=False, default='',
|
|
343
|
+
help="""Used to specify the name of the column that represents cell IDs in the adata.obs data """ \
|
|
344
|
+
"""frame of the single-cell data. If left empty, scEPS will use what's in adata.obs.index as cell IDs.""")
|
|
345
|
+
|
|
346
|
+
parser.add_argument('--exclude-files-with-str', type=str, default=None, required=False,
|
|
347
|
+
help="""Used to filter out files with specific strings in their file names. This is """ \
|
|
348
|
+
"""primarily used for debugging purposes.""")
|
|
349
|
+
|
|
350
|
+
parser.add_argument('--neighborhood-clusters', type=str, required=False, default='',
|
|
351
|
+
help="""Used to specify the text file from step 2, representing a pre-computed mapping of cell """ \
|
|
352
|
+
"""neighborhoods to approximately independent blocks of cell neighborhoods""")
|
|
353
|
+
|
|
354
|
+
# Testing related command line argument
|
|
355
|
+
parser.add_argument('--cell-type-col', type=str, required=False, default='',
|
|
356
|
+
help="""Used to specify a list of column names (e.g., at different resolutions) in adata.obs """ \
|
|
357
|
+
"""representing cell types. The list of column names need to be separated by commas. The tool will """ \
|
|
358
|
+
"""calculate the average scEPS statistics for each cell type under each cell type column, in separate files. """ \
|
|
359
|
+
"""If this flag is not specified, scEPS will aggregate results across all cell neighborhoods.""")
|
|
360
|
+
|
|
361
|
+
parser.add_argument('--use-neighborhoods', type=str, required=False, default='',
|
|
362
|
+
help="""Used to specify a text file containing a list of cell IDs representing cell neighborhoods. """ \
|
|
363
|
+
"""By default, this is an empty string, and the tool aggregates results from all cell neighborhoods. """ \
|
|
364
|
+
"""If this is non-empty, scEPS will calculate aggregated statistics using only the cell neighborhoods """ \
|
|
365
|
+
"""listed in the text file.""")
|
|
366
|
+
|
|
367
|
+
parser.add_argument('--use-inverse-variance-weights', default=False, required=False, action='store_true',
|
|
368
|
+
help="""If specified (not recommended), the tool will use inverse variance weighted average as """ \
|
|
369
|
+
"""the aggregated statistics for each cell type.""")
|
|
370
|
+
|
|
371
|
+
parser.add_argument('--use-sigma', default=False, required=False, action='store_true',
|
|
372
|
+
help="""If specified, the tool will aggregate the scEPS SIGMA statistics instead of the OMEGA """ \
|
|
373
|
+
"""statistics. If this flag is specified, the --prefix flag should specify file names with """ \
|
|
374
|
+
""".sceps.sigma.txt.gz as suffix.""")
|
|
375
|
+
|
|
376
|
+
parser.add_argument('--block-bootstrap', type=str, required=False, default='sceps.neighborhood_cluster',
|
|
377
|
+
help="""Used to specify the column in that represents approximately independent cell """ \
|
|
378
|
+
"""neighborhood blocks (sceps.neighborhood_cluster by default). If left empty, """ \
|
|
379
|
+
"""the tool will fall back to regular bootstrap across individual (instead of blocks of) """ \
|
|
380
|
+
"""cell neighborhoods.""")
|
|
381
|
+
|
|
382
|
+
parser.add_argument('--num-bootstrap', type=int, required=False, default=1000,
|
|
383
|
+
help="""Used to specify the number of bootstrap samples (1,000 by default).""")
|
|
384
|
+
|
|
385
|
+
parser.add_argument('--seed', type=int, required=False, default=0,
|
|
386
|
+
help="""Used to specify the seed for the random number generator (0 by default).""")
|
|
387
|
+
|
|
388
|
+
# Output related command line argument
|
|
389
|
+
parser.add_argument('--add-column', type=str, required=False, nargs=2, default=None,
|
|
390
|
+
help="""Used to specify the column name and a value for the additional column to add.""")
|
|
391
|
+
|
|
392
|
+
parser.add_argument('--out', type=str, required=False,
|
|
393
|
+
help="""Used to specify the prefix of the output file name.""")
|
|
394
|
+
|
|
395
|
+
# Execute the parse_args() method
|
|
396
|
+
args = parser.parse_args()
|
|
397
|
+
|
|
398
|
+
return args
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
if __name__ == '__main__':
|
|
402
|
+
main()
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import argparse, sys, glob, os
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import numpy as np
|
|
4
|
+
import scipy as sp
|
|
5
|
+
import scipy.stats
|
|
6
|
+
from tqdm import tqdm
|
|
7
|
+
import logging, random
|
|
8
|
+
import scanpy as sc
|
|
9
|
+
import anndata as ad
|
|
10
|
+
from sklearn.cluster import MiniBatchKMeans
|
|
11
|
+
|
|
12
|
+
from ..utils import EPS
|
|
13
|
+
from ..neighborhood import *
|
|
14
|
+
|
|
15
|
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
16
|
+
|
|
17
|
+
def main():
|
|
18
|
+
|
|
19
|
+
# get command line
|
|
20
|
+
args = get_command_line()
|
|
21
|
+
|
|
22
|
+
# set seed
|
|
23
|
+
random.seed(args.seed)
|
|
24
|
+
np.random.seed(args.seed)
|
|
25
|
+
|
|
26
|
+
# define clusters for bootstrap
|
|
27
|
+
logging.info("Loading single-cell data from {}".format(args.adata))
|
|
28
|
+
adata = sc.read_h5ad(args.adata)
|
|
29
|
+
|
|
30
|
+
# If args.cell_id_col not in adata.obs.columns, we use adata.obs.index as replacement
|
|
31
|
+
obs = adata.obs.copy()
|
|
32
|
+
if args.cell_id_col not in obs.columns:
|
|
33
|
+
if args.cell_id_col == '':
|
|
34
|
+
args.cell_id_col = 'sceps.cell_index'
|
|
35
|
+
obs[args.cell_id_col] = obs.index.copy()
|
|
36
|
+
adata.obs = obs
|
|
37
|
+
|
|
38
|
+
# define clusters for bootstrap
|
|
39
|
+
logging.info("Identifying independent groups of cell neighborhoods")
|
|
40
|
+
adata.obs['sceps.neighborhood_cluster'] = get_nbhood_clusters(args, adata)
|
|
41
|
+
|
|
42
|
+
# Create and save a data frame mapping cells to their assigned cluster
|
|
43
|
+
df_out = adata.obs[[args.cell_id_col, 'sceps.neighborhood_cluster']]
|
|
44
|
+
df_out.to_csv(args.out+'.txt.gz', sep='\t', index=False)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_nbhood_clusters(args, adata):
|
|
48
|
+
|
|
49
|
+
# check if neighborhood is already calculated
|
|
50
|
+
has_knn = False
|
|
51
|
+
av = ad.__version__
|
|
52
|
+
if type(av) == str:
|
|
53
|
+
av = version.parse(av)
|
|
54
|
+
if av < version.parse("0.7.2"):
|
|
55
|
+
if "neighbors" in adata.uns:
|
|
56
|
+
if "connectivities" in adata.uns["neighbors"]:
|
|
57
|
+
has_knn = True
|
|
58
|
+
else:
|
|
59
|
+
if "connectivities" in adata.obsp:
|
|
60
|
+
has_knn = True
|
|
61
|
+
|
|
62
|
+
# calculate knn only when necessary
|
|
63
|
+
if has_knn == False:
|
|
64
|
+
sc.pp.neighbors(adata, use_rep=args.neighbors_use_rep)
|
|
65
|
+
|
|
66
|
+
# calculate nam matrix
|
|
67
|
+
adj_mat = get_connectivity(adata)
|
|
68
|
+
trans_mat = get_transition_matrix(adj_mat)
|
|
69
|
+
_, nam_matrix = choose_random_walk_nsteps(args, trans_mat, adata.obs)
|
|
70
|
+
nam_matrix = nam_matrix.values
|
|
71
|
+
nam_matrix = (nam_matrix - nam_matrix.mean(axis=0)) / (nam_matrix.std(axis=0)+EPS)
|
|
72
|
+
|
|
73
|
+
# run kmeans
|
|
74
|
+
kmeans = MiniBatchKMeans(n_clusters=args.num_kmeans_cluster,
|
|
75
|
+
batch_size=int(0.05*adata.shape[0]), random_state=args.seed, n_init='auto')
|
|
76
|
+
|
|
77
|
+
return kmeans.fit(nam_matrix).labels_
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_command_line():
|
|
81
|
+
|
|
82
|
+
# Create the parser
|
|
83
|
+
parser = argparse.ArgumentParser(description="This tool aggregates scEPS statistics across groups of cell neighborhoods.")
|
|
84
|
+
|
|
85
|
+
# Add the arguments
|
|
86
|
+
parser.add_argument('--adata', type=str, required=False,
|
|
87
|
+
help="""Used to specify the input single-cell RNA-seq data in h5ad format.""" \
|
|
88
|
+
"""This should be same single-cell data as analyzed by scEPS. However, the user """ \
|
|
89
|
+
"""may remove adata.X to reduce memory usage, as the clustering tool only requires k-NN """ \
|
|
90
|
+
"""graph for the cells.""")
|
|
91
|
+
|
|
92
|
+
parser.add_argument('--cell-id-col', type=str, required=False, default='',
|
|
93
|
+
help="""Used to specify the name of the column that represents cell IDs in the adata.obs """ \
|
|
94
|
+
"""data frame of the single-cell data. If left empty, scEPS will use what's in """ \
|
|
95
|
+
"""adata.obs.index as cell IDs.""")
|
|
96
|
+
|
|
97
|
+
parser.add_argument('--donor-id-col', type=str, required=False, default='',
|
|
98
|
+
help="""Used to specify the name of the column that represents donor IDs in the adata.obs """ \
|
|
99
|
+
"""data frame of the single-cell data.""")
|
|
100
|
+
|
|
101
|
+
parser.add_argument('--neighbors-use-rep', type=str, required=False, default='X_pca_harmony',
|
|
102
|
+
help="""Used to specify the cell embedding (e.g., PCA, scVI embeddings, etc.) used to """ \
|
|
103
|
+
"""construct the k-NN graph.""")
|
|
104
|
+
|
|
105
|
+
parser.add_argument('--num-kmeans-cluster', type=int, required=False, default=50,
|
|
106
|
+
help="""Used to specify the desired number of clusters (i.e., approximately independent """ \
|
|
107
|
+
"""blocks of cell neighborhoods).""")
|
|
108
|
+
|
|
109
|
+
parser.add_argument('--seed', type=int, required=False, default=0,
|
|
110
|
+
help="""Used to specify the seed for the random number generator. (This is set to 0, by default)""")
|
|
111
|
+
|
|
112
|
+
parser.add_argument('--out', type=str, required=False,
|
|
113
|
+
help="""Used to specify the output file name.""")
|
|
114
|
+
|
|
115
|
+
# Execute the parse_args() method
|
|
116
|
+
args = parser.parse_args()
|
|
117
|
+
|
|
118
|
+
return args
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if __name__ == '__main__':
|
|
122
|
+
main()
|