mixalime 2.29.4__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.
- mixalime/__init__.py +35 -0
- mixalime/combine.py +466 -0
- mixalime/create.py +368 -0
- mixalime/data/Lato-Medium.ttf +0 -0
- mixalime/data/Lato-Regular.ttf +0 -0
- mixalime/data/OFL.txt +94 -0
- mixalime/data/dnase_k562.tar.gz +0 -0
- mixalime/diff.py +729 -0
- mixalime/export.py +655 -0
- mixalime/fit.py +227 -0
- mixalime/main.py +1163 -0
- mixalime/models.py +126 -0
- mixalime/plot.py +603 -0
- mixalime/tests.py +365 -0
- mixalime/utils.py +162 -0
- mixalime-2.29.4.dist-info/METADATA +241 -0
- mixalime-2.29.4.dist-info/RECORD +20 -0
- mixalime-2.29.4.dist-info/WHEEL +5 -0
- mixalime-2.29.4.dist-info/entry_points.txt +2 -0
- mixalime-2.29.4.dist-info/top_level.txt +1 -0
mixalime/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
__version__ = '2.29.4'
|
|
2
|
+
import importlib
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
__min_reqs__ = [
|
|
6
|
+
'pip>=23.0',
|
|
7
|
+
'typer>=0.6.1',
|
|
8
|
+
'numpy>=1.21.0',
|
|
9
|
+
'jax<0.9',
|
|
10
|
+
'jaxlib<0.9',
|
|
11
|
+
'matplotlib>=3.5.1',
|
|
12
|
+
'pandas>=1.4.1',
|
|
13
|
+
'scipy<1.16',
|
|
14
|
+
'statsmodels>=0.13.2',
|
|
15
|
+
'betanegbinfit>=1.10.1',
|
|
16
|
+
'datatable>=1.0.0' ,
|
|
17
|
+
'dill>=0.3.7',
|
|
18
|
+
'rich>=12.6.0',
|
|
19
|
+
'portion>=2.3.0',
|
|
20
|
+
'pysam>=0.22.0'
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
def versiontuple(v):
|
|
24
|
+
return tuple(map(int, (v.split("."))))
|
|
25
|
+
|
|
26
|
+
def check_packages():
|
|
27
|
+
for req in __min_reqs__:
|
|
28
|
+
try:
|
|
29
|
+
module, ver = req.split(' @').split('>=')
|
|
30
|
+
ver = versiontuple(ver)
|
|
31
|
+
v = versiontuple(importlib.import_module(module).__version__)
|
|
32
|
+
except (AttributeError, ValueError):
|
|
33
|
+
continue
|
|
34
|
+
if v < ver:
|
|
35
|
+
raise ImportError(f'Version of the {module} package should be at least {ver} (found: {v}).')
|
mixalime/combine.py
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from scipy import stats as st
|
|
3
|
+
import numpy as np
|
|
4
|
+
import dill
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from .utils import get_init_file, openers, select_filenames
|
|
8
|
+
from multiprocessing import cpu_count, Pool, Manager
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
from functools import partial
|
|
11
|
+
from itertools import islice
|
|
12
|
+
from statsmodels.stats import multitest
|
|
13
|
+
import logging
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def combine_p_values_logit(pvalues):
|
|
17
|
+
pvalues = np.array(list(filter(lambda x: x < 1, pvalues)))
|
|
18
|
+
k = len(pvalues)
|
|
19
|
+
if k == 0:
|
|
20
|
+
return 1.0
|
|
21
|
+
elif k == 1:
|
|
22
|
+
return float(pvalues[0])
|
|
23
|
+
# statistic = float(-sum(map(log, pvalues)) + sum(map(lambda x: log1p(-x), pvalues)))
|
|
24
|
+
statistic = -np.log(pvalues).sum() + np.log1p(-pvalues).sum()
|
|
25
|
+
nu = np.int_(5 * k + 4)
|
|
26
|
+
approx_factor = np.sqrt(np.int_(3) * nu / (np.int_(k) * np.square(np.pi) * (nu - np.int_(2))))
|
|
27
|
+
pval = st.distributions.t.sf(statistic * approx_factor, nu)
|
|
28
|
+
if pval == 0:
|
|
29
|
+
statistic = -2 * np.log(pvalues).sum()
|
|
30
|
+
# statistic = float(-2 * sum(map(log, pvalues)))
|
|
31
|
+
pval = st.distributions.chi2.sf(statistic, 2 * k)
|
|
32
|
+
return pval
|
|
33
|
+
|
|
34
|
+
def combine_es(es, pvalues, uniform_weights=False):
|
|
35
|
+
pvalues = np.array(list(map(float, pvalues)))
|
|
36
|
+
es = np.array(es)
|
|
37
|
+
weights = -np.log10(pvalues)
|
|
38
|
+
inds = np.isfinite(weights)
|
|
39
|
+
if not inds.sum():
|
|
40
|
+
return np.mean(es)
|
|
41
|
+
weights = weights[inds]
|
|
42
|
+
if uniform_weights:
|
|
43
|
+
weights[:] = 1.0
|
|
44
|
+
es = es[inds]
|
|
45
|
+
s = weights.sum()
|
|
46
|
+
if s == 0:
|
|
47
|
+
weights = 1 / len(weights)
|
|
48
|
+
else:
|
|
49
|
+
weights /= s
|
|
50
|
+
return np.sum(weights * es)
|
|
51
|
+
|
|
52
|
+
def estimate_min_coverage(test, es_cutoff: float = 1, pval_cutoff: float = 0.5):
|
|
53
|
+
res = defaultdict(dict)
|
|
54
|
+
for allele in test:
|
|
55
|
+
for bad in test[allele]:
|
|
56
|
+
it = test[allele][bad]
|
|
57
|
+
counts = np.array(list(it.keys()))
|
|
58
|
+
coverage = counts.sum(axis=1)
|
|
59
|
+
stop = False
|
|
60
|
+
for cov in np.unique(coverage):
|
|
61
|
+
ind = coverage == cov
|
|
62
|
+
pairs = counts[ind]
|
|
63
|
+
for t in pairs:
|
|
64
|
+
pvalue, es = it[tuple(t)]
|
|
65
|
+
if (es > es_cutoff) and (pvalue < pval_cutoff):
|
|
66
|
+
stop = True
|
|
67
|
+
break
|
|
68
|
+
if stop:
|
|
69
|
+
break
|
|
70
|
+
res[allele][bad] = cov if stop else float('inf')
|
|
71
|
+
return res
|
|
72
|
+
|
|
73
|
+
def combine_stats(inds, snvs, stats, groups, min_cnt_sum=20, uniform_weights=False):
|
|
74
|
+
pvalues = list()
|
|
75
|
+
es = list()
|
|
76
|
+
ks = list()
|
|
77
|
+
for i in inds:
|
|
78
|
+
k, lt = snvs[i]
|
|
79
|
+
lt = lt[1:]
|
|
80
|
+
if groups:
|
|
81
|
+
lt = filter(lambda x: x[0] in groups, lt)
|
|
82
|
+
lt = [t[1:] for t in lt]
|
|
83
|
+
if not lt or not max(sum(t[:-1]) >= min_cnt_sum[t[-1]] for t in lt):
|
|
84
|
+
ref = alt = ref_es = alt_es = np.nan
|
|
85
|
+
k = None
|
|
86
|
+
else:
|
|
87
|
+
ref_pvals, ref_es = zip(*[stats['ref'][t[-1]][t[:-1]] for t in lt])
|
|
88
|
+
alt_pvals, alt_es = zip(*[stats['alt'][t[-1]][t[:-1]] for t in lt])
|
|
89
|
+
ref = combine_p_values_logit(ref_pvals)
|
|
90
|
+
alt = combine_p_values_logit(alt_pvals)
|
|
91
|
+
ref_es = combine_es(ref_es, ref_pvals, uniform_weights=uniform_weights)
|
|
92
|
+
alt_es = combine_es(alt_es, alt_pvals, uniform_weights=uniform_weights)
|
|
93
|
+
pvalues.append((ref, alt))
|
|
94
|
+
es.append((ref_es, alt_es))
|
|
95
|
+
ks.append(k)
|
|
96
|
+
return pvalues, es, ks
|
|
97
|
+
|
|
98
|
+
def batched(iterable, n):
|
|
99
|
+
it = iter(iterable)
|
|
100
|
+
while (batch := tuple(islice(it, n))):
|
|
101
|
+
yield batch
|
|
102
|
+
|
|
103
|
+
def combine(name: str, group_files=None, alpha=0.05, min_cnt_sum=20, adaptive_min_cover=False, adaptive_es=1.0, adaptive_pval=0.05,
|
|
104
|
+
uniform_weights=False, filter_id=None, filter_chr=None, subname=None, n_jobs=1, save_to_file=True):
|
|
105
|
+
if filter_chr is not None:
|
|
106
|
+
filter_chr = re.compile(filter_chr)
|
|
107
|
+
if filter_id is not None:
|
|
108
|
+
filter_id = re.compile(filter_id)
|
|
109
|
+
n_jobs = cpu_count() - 1 if n_jobs == -1 else n_jobs
|
|
110
|
+
filename = get_init_file(name)
|
|
111
|
+
compressor = filename.split('.')[-1]
|
|
112
|
+
open = openers[compressor]
|
|
113
|
+
with open(filename, 'rb') as f:
|
|
114
|
+
init = dill.load(f)
|
|
115
|
+
snvs = init['snvs']
|
|
116
|
+
scorefiles = init['scorefiles']
|
|
117
|
+
del init
|
|
118
|
+
if group_files is None:
|
|
119
|
+
group_files = list()
|
|
120
|
+
else:
|
|
121
|
+
group_files = select_filenames(group_files, scorefiles)
|
|
122
|
+
if not group_files:
|
|
123
|
+
raise SyntaxError('No files found for given pattern(s).')
|
|
124
|
+
filename = f'{name}.test.{compressor}'
|
|
125
|
+
with open(filename, 'rb') as f:
|
|
126
|
+
stats = dill.load(f)
|
|
127
|
+
groups = set()
|
|
128
|
+
for file in group_files:
|
|
129
|
+
try:
|
|
130
|
+
i = scorefiles.index(file)
|
|
131
|
+
groups.add(i)
|
|
132
|
+
except ValueError:
|
|
133
|
+
logging.error(f'Unknown scorefile {file}')
|
|
134
|
+
if adaptive_min_cover:
|
|
135
|
+
adaptive_coverage = estimate_min_coverage(stats, es_cutoff=adaptive_es, pval_cutoff=adaptive_pval)
|
|
136
|
+
min_coverage = {bad: min(adaptive_coverage['ref'][bad], adaptive_coverage['alt'][bad]) for bad in adaptive_coverage['ref']}
|
|
137
|
+
else:
|
|
138
|
+
adaptive_coverage = None
|
|
139
|
+
min_coverage = {bad: min_cnt_sum for bad in stats['ref']}
|
|
140
|
+
del scorefiles
|
|
141
|
+
ref_comb_pvals = list()
|
|
142
|
+
alt_comb_pvals = list()
|
|
143
|
+
ref_comb_es = list()
|
|
144
|
+
alt_comb_es = list()
|
|
145
|
+
comb_names = list()
|
|
146
|
+
its = snvs.items()
|
|
147
|
+
del snvs
|
|
148
|
+
its = list(its)
|
|
149
|
+
if filter_id:
|
|
150
|
+
its = list(filter(lambda x: x[1][0][1] and filter_id.match(x[1][0][1]), its))
|
|
151
|
+
if filter_chr:
|
|
152
|
+
its = list(filter(lambda x: filter_chr.match(x[0]), its))
|
|
153
|
+
with Manager() as manager:
|
|
154
|
+
if n_jobs > 1:
|
|
155
|
+
its = manager.list(its)
|
|
156
|
+
with Pool(n_jobs) as p:
|
|
157
|
+
f = partial(combine_stats, snvs=its, stats=stats, groups=groups, min_cnt_sum=min_coverage,
|
|
158
|
+
uniform_weights=uniform_weights)
|
|
159
|
+
if n_jobs > 1:
|
|
160
|
+
sz = int(np.ceil(len(its) / n_jobs))
|
|
161
|
+
inds = batched(range(len(its)), sz)
|
|
162
|
+
iterate = p.map(f, inds, chunksize=1)
|
|
163
|
+
else:
|
|
164
|
+
iterate = map(f, [list(range(len(its)))])
|
|
165
|
+
for pvals, es, ks in iterate:
|
|
166
|
+
for (ref, alt), (ref_es, alt_es), k in zip(pvals, es, ks):
|
|
167
|
+
if k is None:
|
|
168
|
+
continue
|
|
169
|
+
ref_comb_pvals.append(ref)
|
|
170
|
+
alt_comb_pvals.append(alt)
|
|
171
|
+
ref_comb_es.append(ref_es)
|
|
172
|
+
alt_comb_es.append(alt_es)
|
|
173
|
+
comb_names.append(k)
|
|
174
|
+
ref_comb_pvals = np.array(ref_comb_pvals)
|
|
175
|
+
inds = ref_comb_pvals == 0.0
|
|
176
|
+
if np.any(inds):
|
|
177
|
+
ref_comb_pvals[inds] = ref_comb_pvals[~inds].min()
|
|
178
|
+
alt_comb_pvals = np.array(alt_comb_pvals)
|
|
179
|
+
inds = alt_comb_pvals == 0.0
|
|
180
|
+
if np.any(inds):
|
|
181
|
+
alt_comb_pvals[inds] = alt_comb_pvals[~inds].min()
|
|
182
|
+
_, ref_fdr_pvals, _, _ = multitest.multipletests(ref_comb_pvals, alpha=alpha, method='fdr_bh')
|
|
183
|
+
_, alt_fdr_pvals, _, _ = multitest.multipletests(alt_comb_pvals, alpha=alpha, method='fdr_bh')
|
|
184
|
+
res = dict()
|
|
185
|
+
for i in range(len(comb_names)):
|
|
186
|
+
|
|
187
|
+
res[comb_names[i]] = ((ref_comb_pvals[i], alt_comb_pvals[i]),
|
|
188
|
+
(ref_comb_es[i], alt_comb_es[i]),
|
|
189
|
+
(ref_fdr_pvals[i], alt_fdr_pvals[i]))
|
|
190
|
+
res = {'groups': groups, 'snvs': res}
|
|
191
|
+
res = {subname: res}
|
|
192
|
+
if os.path.isfile(f'{name}.comb.{compressor}'):
|
|
193
|
+
with open(f'{name}.comb.{compressor}', 'rb') as f:
|
|
194
|
+
print('eba')
|
|
195
|
+
r = dill.load(f)
|
|
196
|
+
print('gat')
|
|
197
|
+
for t in r:
|
|
198
|
+
if t not in res:
|
|
199
|
+
res[t] = r[t]
|
|
200
|
+
if save_to_file:
|
|
201
|
+
with open(f'{name}.comb.{compressor}', 'wb') as f:
|
|
202
|
+
dill.dump(res, f)
|
|
203
|
+
return res, adaptive_coverage
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def polymbine_stats(inds, snvs, stats_list, groups_list, min_cnt_sums_list, uniform_weights=False):
|
|
207
|
+
"""
|
|
208
|
+
Worker function for polymbine.
|
|
209
|
+
Aggregates statistics across multiple projects for the given indices of SNVs.
|
|
210
|
+
"""
|
|
211
|
+
pvalues = list()
|
|
212
|
+
es = list()
|
|
213
|
+
ks = list()
|
|
214
|
+
|
|
215
|
+
for i in inds:
|
|
216
|
+
k, lt = snvs[i]
|
|
217
|
+
# lt[0] is metadata (id, ref, alt), lt[1:] are replicates
|
|
218
|
+
# Replicates in polymbine have an extra element at the end: project_index
|
|
219
|
+
replicates = lt[1:]
|
|
220
|
+
|
|
221
|
+
valid_pvals_ref = []
|
|
222
|
+
valid_es_ref = []
|
|
223
|
+
valid_pvals_alt = []
|
|
224
|
+
valid_es_alt = []
|
|
225
|
+
|
|
226
|
+
has_valid_data = False
|
|
227
|
+
|
|
228
|
+
for t in replicates:
|
|
229
|
+
# t structure: (file_index, ref_cnt, alt_cnt, ..., bad, project_index)
|
|
230
|
+
p_idx = t[-1]
|
|
231
|
+
bad = t[-2]
|
|
232
|
+
counts = t[:-2] # (file_index, ref, alt, ...)
|
|
233
|
+
file_ind = counts[0]
|
|
234
|
+
cnt_signature = counts[1:] # (ref, alt) usually
|
|
235
|
+
|
|
236
|
+
# Check if this file is in the allowed groups for this specific project
|
|
237
|
+
if groups_list[p_idx] and file_ind not in groups_list[p_idx]:
|
|
238
|
+
continue
|
|
239
|
+
|
|
240
|
+
# Check coverage using the project-specific threshold
|
|
241
|
+
min_cov = min_cnt_sums_list[p_idx].get(bad, 20)
|
|
242
|
+
if sum(cnt_signature) < min_cov:
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
# Retrieve stats from the specific project's dictionary
|
|
246
|
+
# stats structure: stats_list[p_idx]['ref'][bad][(ref, alt)] -> (pval, es)
|
|
247
|
+
try:
|
|
248
|
+
# The original code expects count tuple without file_index for lookup
|
|
249
|
+
lookup_counts = cnt_signature
|
|
250
|
+
|
|
251
|
+
# Get Reference Allele Stats
|
|
252
|
+
r_pval, r_es = stats_list[p_idx]['ref'][bad][lookup_counts]
|
|
253
|
+
valid_pvals_ref.append(r_pval)
|
|
254
|
+
valid_es_ref.append(r_es)
|
|
255
|
+
|
|
256
|
+
# Get Alternative Allele Stats
|
|
257
|
+
a_pval, a_es = stats_list[p_idx]['alt'][bad][lookup_counts]
|
|
258
|
+
valid_pvals_alt.append(a_pval)
|
|
259
|
+
valid_es_alt.append(a_es)
|
|
260
|
+
|
|
261
|
+
has_valid_data = True
|
|
262
|
+
except KeyError:
|
|
263
|
+
# Should not happen if data integrity is maintained, but safe to skip
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
if not has_valid_data:
|
|
267
|
+
ref = alt = ref_es = alt_es = np.nan
|
|
268
|
+
k = None
|
|
269
|
+
else:
|
|
270
|
+
ref = combine_p_values_logit(valid_pvals_ref)
|
|
271
|
+
alt = combine_p_values_logit(valid_pvals_alt)
|
|
272
|
+
ref_es = combine_es(valid_es_ref, valid_pvals_ref, uniform_weights=uniform_weights)
|
|
273
|
+
alt_es = combine_es(valid_es_alt, valid_pvals_alt, uniform_weights=uniform_weights)
|
|
274
|
+
|
|
275
|
+
pvalues.append((ref, alt))
|
|
276
|
+
es.append((ref_es, alt_es))
|
|
277
|
+
ks.append(k)
|
|
278
|
+
|
|
279
|
+
return pvalues, es, ks
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def polymbine(output_name: str, names: list[str], group_files=None, alpha=0.05, min_cnt_sum=20,
|
|
283
|
+
adaptive_min_cover=False, adaptive_es=1.0, adaptive_pval=0.05,
|
|
284
|
+
uniform_weights=False, filter_id=None, filter_chr=None, subname=None,
|
|
285
|
+
n_jobs=1, save_to_file=True):
|
|
286
|
+
if filter_chr is not None:
|
|
287
|
+
filter_chr = re.compile(filter_chr)
|
|
288
|
+
if filter_id is not None:
|
|
289
|
+
filter_id = re.compile(filter_id)
|
|
290
|
+
n_jobs = cpu_count() - 1 if n_jobs == -1 else n_jobs
|
|
291
|
+
|
|
292
|
+
# Lists to store per-project data
|
|
293
|
+
all_stats = []
|
|
294
|
+
all_groups = []
|
|
295
|
+
all_min_coverage = []
|
|
296
|
+
adaptive_coverage = None
|
|
297
|
+
|
|
298
|
+
# Unified dictionary to store merged SNVs
|
|
299
|
+
# Key: (chr, pos, etc.) -> Value: [metadata, (data_p1...), (data_p2...), ...]
|
|
300
|
+
combined_snvs = defaultdict(lambda: [None])
|
|
301
|
+
|
|
302
|
+
# 1. Load data from all projects
|
|
303
|
+
for p_idx, name in enumerate(names):
|
|
304
|
+
filename = get_init_file(name)
|
|
305
|
+
if not filename:
|
|
306
|
+
raise FileNotFoundError(f"Init file not found for project: {name}")
|
|
307
|
+
|
|
308
|
+
compressor = filename.split('.')[-1]
|
|
309
|
+
opener = openers[compressor]
|
|
310
|
+
|
|
311
|
+
with opener(filename, 'rb') as f:
|
|
312
|
+
init = dill.load(f)
|
|
313
|
+
snvs = init['snvs']
|
|
314
|
+
scorefiles = init['scorefiles']
|
|
315
|
+
del init
|
|
316
|
+
|
|
317
|
+
# Determine groups for this project
|
|
318
|
+
current_groups = set()
|
|
319
|
+
if group_files:
|
|
320
|
+
if len(group_files) == len(names):
|
|
321
|
+
g_files = [group_files[p_idx]]
|
|
322
|
+
else:
|
|
323
|
+
g_files = group_files
|
|
324
|
+
|
|
325
|
+
selected_files = select_filenames(g_files, scorefiles)
|
|
326
|
+
for file in selected_files:
|
|
327
|
+
try:
|
|
328
|
+
i = scorefiles.index(file)
|
|
329
|
+
current_groups.add(i)
|
|
330
|
+
except ValueError:
|
|
331
|
+
continue
|
|
332
|
+
else:
|
|
333
|
+
# If no groups specified, all files in the project are valid,
|
|
334
|
+
# but usually empty set implies "all" in existing logic (or explicit logic handles it).
|
|
335
|
+
# Looking at combine_stats: "if groups: lt = filter(...)".
|
|
336
|
+
# So empty set means "don't filter", i.e., use all.
|
|
337
|
+
pass
|
|
338
|
+
|
|
339
|
+
all_groups.append(current_groups)
|
|
340
|
+
|
|
341
|
+
# Load stats
|
|
342
|
+
filename_test = f'{name}.test.{compressor}'
|
|
343
|
+
with opener(filename_test, 'rb') as f:
|
|
344
|
+
stats = dill.load(f)
|
|
345
|
+
|
|
346
|
+
# Calculate coverage thresholds
|
|
347
|
+
if adaptive_min_cover:
|
|
348
|
+
adaptive_coverage = estimate_min_coverage(stats, es_cutoff=adaptive_es, pval_cutoff=adaptive_pval)
|
|
349
|
+
min_coverage = {bad: min(adaptive_coverage['ref'][bad], adaptive_coverage['alt'][bad]) for bad in adaptive_coverage['ref']}
|
|
350
|
+
else:
|
|
351
|
+
min_coverage = {bad: min_cnt_sum for bad in stats['ref']}
|
|
352
|
+
|
|
353
|
+
all_stats.append(stats)
|
|
354
|
+
all_min_coverage.append(min_coverage)
|
|
355
|
+
del scorefiles
|
|
356
|
+
|
|
357
|
+
# Merge SNVs
|
|
358
|
+
# We append the project index to the tuple so the worker knows which stats to use
|
|
359
|
+
for k, v in snvs.items():
|
|
360
|
+
if combined_snvs[k][0] is None:
|
|
361
|
+
combined_snvs[k][0] = v[0] # Copy metadata (chr, id, etc) from the first project that has it
|
|
362
|
+
|
|
363
|
+
# v[1:] are the data tuples: (file_ind, ref, alt, ..., bad)
|
|
364
|
+
# We append p_idx to make it: (file_ind, ref, alt, ..., bad, p_idx)
|
|
365
|
+
for replicate in v[1:]:
|
|
366
|
+
combined_snvs[k].append(replicate + (p_idx,))
|
|
367
|
+
|
|
368
|
+
del snvs
|
|
369
|
+
|
|
370
|
+
# 2. Filter SNVs
|
|
371
|
+
its = combined_snvs.items()
|
|
372
|
+
del combined_snvs
|
|
373
|
+
its = list(its)
|
|
374
|
+
|
|
375
|
+
if filter_id:
|
|
376
|
+
its = list(filter(lambda x: x[1][0][1] and filter_id.match(x[1][0][1]), its))
|
|
377
|
+
if filter_chr:
|
|
378
|
+
its = list(filter(lambda x: filter_chr.match(x[0]), its))
|
|
379
|
+
|
|
380
|
+
# 3. Parallel Processing
|
|
381
|
+
ref_comb_pvals = list()
|
|
382
|
+
alt_comb_pvals = list()
|
|
383
|
+
ref_comb_es = list()
|
|
384
|
+
alt_comb_es = list()
|
|
385
|
+
comb_names = list()
|
|
386
|
+
|
|
387
|
+
with Manager() as manager:
|
|
388
|
+
if n_jobs > 1:
|
|
389
|
+
its = manager.list(its)
|
|
390
|
+
# We treat stats as read-only. Passing large dicts via Manager is slow.
|
|
391
|
+
# Passing them as regular arguments to partial relies on pickle.
|
|
392
|
+
# If stats are huge, this might be memory intensive, but standard for this pattern.
|
|
393
|
+
|
|
394
|
+
with Pool(n_jobs) as p:
|
|
395
|
+
f = partial(polymbine_stats, snvs=its, stats_list=all_stats,
|
|
396
|
+
groups_list=all_groups, min_cnt_sums_list=all_min_coverage,
|
|
397
|
+
uniform_weights=uniform_weights)
|
|
398
|
+
|
|
399
|
+
if n_jobs > 1:
|
|
400
|
+
sz = int(np.ceil(len(its) / n_jobs))
|
|
401
|
+
inds_gen = batched(range(len(its)), sz)
|
|
402
|
+
iterate = p.map(f, inds_gen, chunksize=1)
|
|
403
|
+
else:
|
|
404
|
+
iterate = map(f, [list(range(len(its)))])
|
|
405
|
+
|
|
406
|
+
for pvals, es, ks in iterate:
|
|
407
|
+
for (ref, alt), (ref_es, alt_es), k in zip(pvals, es, ks):
|
|
408
|
+
if k is None:
|
|
409
|
+
continue
|
|
410
|
+
ref_comb_pvals.append(ref)
|
|
411
|
+
alt_comb_pvals.append(alt)
|
|
412
|
+
ref_comb_es.append(ref_es)
|
|
413
|
+
alt_comb_es.append(alt_es)
|
|
414
|
+
comb_names.append(k)
|
|
415
|
+
|
|
416
|
+
# 4. Post-processing (FDR & Saving)
|
|
417
|
+
# The logic below is identical to combine()
|
|
418
|
+
ref_comb_pvals = np.array(ref_comb_pvals)
|
|
419
|
+
inds = ref_comb_pvals == 0.0
|
|
420
|
+
if np.any(inds):
|
|
421
|
+
ref_comb_pvals[inds] = ref_comb_pvals[~inds].min() if np.any(~inds) else 1.0
|
|
422
|
+
|
|
423
|
+
alt_comb_pvals = np.array(alt_comb_pvals)
|
|
424
|
+
inds = alt_comb_pvals == 0.0
|
|
425
|
+
if np.any(inds):
|
|
426
|
+
alt_comb_pvals[inds] = alt_comb_pvals[~inds].min() if np.any(~inds) else 1.0
|
|
427
|
+
|
|
428
|
+
# Handle empty results case
|
|
429
|
+
if len(ref_comb_pvals) > 0:
|
|
430
|
+
_, ref_fdr_pvals, _, _ = multitest.multipletests(ref_comb_pvals, alpha=alpha, method='fdr_bh')
|
|
431
|
+
_, alt_fdr_pvals, _, _ = multitest.multipletests(alt_comb_pvals, alpha=alpha, method='fdr_bh')
|
|
432
|
+
else:
|
|
433
|
+
ref_fdr_pvals = []
|
|
434
|
+
alt_fdr_pvals = []
|
|
435
|
+
|
|
436
|
+
res = dict()
|
|
437
|
+
for i in range(len(comb_names)):
|
|
438
|
+
res[comb_names[i]] = ((ref_comb_pvals[i], alt_comb_pvals[i]),
|
|
439
|
+
(ref_comb_es[i], alt_comb_es[i]),
|
|
440
|
+
(ref_fdr_pvals[i], alt_fdr_pvals[i]))
|
|
441
|
+
|
|
442
|
+
# Structure result
|
|
443
|
+
# We store the Union of all valid groups used across projects
|
|
444
|
+
# Note: groups here is slightly ambiguous in merged context, we store list of sets
|
|
445
|
+
res = {'groups': all_groups, 'snvs': res}
|
|
446
|
+
res = {subname: res}
|
|
447
|
+
|
|
448
|
+
# Saving logic
|
|
449
|
+
# compressor = 'gz' # Default or derived from last file
|
|
450
|
+
out_filename = f'{output_name}.comb.{compressor}'
|
|
451
|
+
|
|
452
|
+
if os.path.isfile(out_filename):
|
|
453
|
+
try:
|
|
454
|
+
with openers[compressor](out_filename, 'rb') as f:
|
|
455
|
+
r = dill.load(f)
|
|
456
|
+
for t in r:
|
|
457
|
+
if t not in res:
|
|
458
|
+
res[t] = r[t]
|
|
459
|
+
except Exception as e:
|
|
460
|
+
logging.warning(f"Could not merge with existing file {out_filename}: {e}")
|
|
461
|
+
|
|
462
|
+
if save_to_file:
|
|
463
|
+
with openers[compressor](out_filename, 'wb') as f:
|
|
464
|
+
dill.dump(res, f)
|
|
465
|
+
|
|
466
|
+
return res, adaptive_coverage
|