HTSeq 2.1.2__cp313-cp313-macosx_10_15_x86_64.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.
- HTSeq/StepVector.py +629 -0
- HTSeq/StretchVector.py +491 -0
- HTSeq/_HTSeq.cpython-313-darwin.so +0 -0
- HTSeq/_HTSeq_internal.py +85 -0
- HTSeq/_StepVector.cpython-313-darwin.so +0 -0
- HTSeq/__init__.py +1249 -0
- HTSeq/features.py +489 -0
- HTSeq/scripts/__init__.py +0 -0
- HTSeq/scripts/count.py +528 -0
- HTSeq/scripts/count_features/__init__.py +0 -0
- HTSeq/scripts/count_features/count_features_per_file.py +465 -0
- HTSeq/scripts/count_features/reads_io_processor.py +187 -0
- HTSeq/scripts/count_features/reads_stats.py +92 -0
- HTSeq/scripts/count_with_barcodes.py +746 -0
- HTSeq/scripts/qa.py +336 -0
- HTSeq/scripts/utils.py +372 -0
- HTSeq/utils.py +92 -0
- htseq-2.1.2.dist-info/METADATA +813 -0
- htseq-2.1.2.dist-info/RECORD +23 -0
- htseq-2.1.2.dist-info/WHEEL +5 -0
- htseq-2.1.2.dist-info/entry_points.txt +4 -0
- htseq-2.1.2.dist-info/licenses/LICENSE +674 -0
- htseq-2.1.2.dist-info/top_level.txt +1 -0
HTSeq/scripts/qa.py
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
# HTSeq_QA.py
|
|
4
|
+
#
|
|
5
|
+
# (c) Simon Anders, European Molecular Biology Laboratory, 2010
|
|
6
|
+
# released under GNU General Public License
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import os.path
|
|
10
|
+
import argparse
|
|
11
|
+
from itertools import islice
|
|
12
|
+
import numpy as np
|
|
13
|
+
import HTSeq
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import matplotlib
|
|
17
|
+
import matplotlib.pyplot as plt
|
|
18
|
+
from matplotlib.pyplot import Normalize
|
|
19
|
+
except ImportError:
|
|
20
|
+
sys.stderr.write("htseq-qa needs 'matplotlib >= 1.5'")
|
|
21
|
+
raise
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_read_length(readfile, isAlnmntFile):
|
|
25
|
+
readlen = 0
|
|
26
|
+
if isAlnmntFile:
|
|
27
|
+
reads = (a.read for a in readfile)
|
|
28
|
+
else:
|
|
29
|
+
reads = readfile
|
|
30
|
+
for r in islice(reads, 10000):
|
|
31
|
+
if len(r) > readlen:
|
|
32
|
+
readlen = len(r)
|
|
33
|
+
|
|
34
|
+
return readlen
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def compute_quality(
|
|
38
|
+
readfilename,
|
|
39
|
+
file_type,
|
|
40
|
+
nosplit,
|
|
41
|
+
readlen,
|
|
42
|
+
max_qual,
|
|
43
|
+
gamma,
|
|
44
|
+
primary_only=False,
|
|
45
|
+
max_records=-1,
|
|
46
|
+
):
|
|
47
|
+
|
|
48
|
+
if file_type in ("sam", "bam"):
|
|
49
|
+
readfile = HTSeq.BAM_Reader(readfilename)
|
|
50
|
+
isAlnmntFile = True
|
|
51
|
+
elif file_type == "solexa-export":
|
|
52
|
+
readfile = HTSeq.SolexaExportReader(readfilename)
|
|
53
|
+
isAlnmntFile = True
|
|
54
|
+
elif file_type == "fastq":
|
|
55
|
+
readfile = HTSeq.FastqReader(readfilename)
|
|
56
|
+
isAlnmntFile = False
|
|
57
|
+
elif file_type == "solexa-fastq":
|
|
58
|
+
readfile = HTSeq.FastqReader(readfilename, "solexa")
|
|
59
|
+
isAlnmntFile = False
|
|
60
|
+
else:
|
|
61
|
+
raise ValueError('File format not recognized: {:}'.format(file_type))
|
|
62
|
+
|
|
63
|
+
twoColumns = isAlnmntFile and (not nosplit)
|
|
64
|
+
|
|
65
|
+
if readlen is None:
|
|
66
|
+
readlen = get_read_length(readfile, isAlnmntFile)
|
|
67
|
+
|
|
68
|
+
# Initialize count arrays
|
|
69
|
+
base_arr_U = np.zeros((readlen, 5), np.int64)
|
|
70
|
+
qual_arr_U = np.zeros((readlen, max_qual+1), np.int64)
|
|
71
|
+
if twoColumns:
|
|
72
|
+
base_arr_A = np.zeros((readlen, 5), np.int64)
|
|
73
|
+
qual_arr_A = np.zeros((readlen, max_qual+1), np.int64)
|
|
74
|
+
|
|
75
|
+
# Main counting loop
|
|
76
|
+
i = 0
|
|
77
|
+
try:
|
|
78
|
+
for a in readfile:
|
|
79
|
+
if isAlnmntFile:
|
|
80
|
+
r = a.read
|
|
81
|
+
else:
|
|
82
|
+
r = a
|
|
83
|
+
|
|
84
|
+
# Exclude non-primary alignments if requested
|
|
85
|
+
if isAlnmntFile and primary_only:
|
|
86
|
+
if a.aligned and a.not_primary_alignment:
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
if twoColumns and isAlnmntFile and a.aligned:
|
|
90
|
+
r.add_bases_to_count_array(base_arr_A)
|
|
91
|
+
r.add_qual_to_count_array(qual_arr_A)
|
|
92
|
+
else:
|
|
93
|
+
r.add_bases_to_count_array(base_arr_U)
|
|
94
|
+
r.add_qual_to_count_array(qual_arr_U)
|
|
95
|
+
|
|
96
|
+
i += 1
|
|
97
|
+
|
|
98
|
+
if i == max_records:
|
|
99
|
+
break
|
|
100
|
+
|
|
101
|
+
if (i % 200000) == 0:
|
|
102
|
+
if (not isAlnmntFile) or primary_only:
|
|
103
|
+
print(i, "reads processed")
|
|
104
|
+
else:
|
|
105
|
+
print(i, "alignments processed")
|
|
106
|
+
|
|
107
|
+
except:
|
|
108
|
+
sys.stderr.write("Error occured in: %s\n" %
|
|
109
|
+
readfile.get_line_number_string())
|
|
110
|
+
raise
|
|
111
|
+
|
|
112
|
+
if (not isAlnmntFile) or primary_only:
|
|
113
|
+
print(i, "reads processed")
|
|
114
|
+
else:
|
|
115
|
+
print(i, "alignments processed")
|
|
116
|
+
|
|
117
|
+
# Normalize result
|
|
118
|
+
def norm_by_pos(arr):
|
|
119
|
+
arr = np.array(arr, np.float64)
|
|
120
|
+
arr_n = (arr.T / arr.sum(1)).T
|
|
121
|
+
arr_n[arr == 0] = 0
|
|
122
|
+
return arr_n
|
|
123
|
+
|
|
124
|
+
def norm_by_start(arr):
|
|
125
|
+
arr = np.array(arr, np.float64)
|
|
126
|
+
arr_n = (arr.T / arr.sum(1)[0]).T
|
|
127
|
+
arr_n[arr == 0] = 0
|
|
128
|
+
return arr_n
|
|
129
|
+
|
|
130
|
+
result = {
|
|
131
|
+
'isAlnmntFile': isAlnmntFile,
|
|
132
|
+
'readlen': readlen,
|
|
133
|
+
'twoColumns': twoColumns,
|
|
134
|
+
'base_arr_U_n': norm_by_pos(base_arr_U),
|
|
135
|
+
'qual_arr_U_n': norm_by_start(qual_arr_U),
|
|
136
|
+
'nreads_U': base_arr_U[0, :].sum(),
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if twoColumns:
|
|
140
|
+
result['base_arr_A_n'] = norm_by_pos(base_arr_A)
|
|
141
|
+
result['qual_arr_A_n'] = norm_by_start(qual_arr_A)
|
|
142
|
+
result['nreads_A'] = base_arr_A[0, :].sum()
|
|
143
|
+
|
|
144
|
+
return result
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def plot(
|
|
148
|
+
result,
|
|
149
|
+
readfilename,
|
|
150
|
+
outfile,
|
|
151
|
+
max_qual,
|
|
152
|
+
gamma,
|
|
153
|
+
primary_only=False,
|
|
154
|
+
):
|
|
155
|
+
|
|
156
|
+
def plot_bases(arr, ax):
|
|
157
|
+
xg = np.arange(readlen)
|
|
158
|
+
ax.plot(xg, arr[:, 0], marker='.', color='red')
|
|
159
|
+
ax.plot(xg, arr[:, 1], marker='.', color='darkgreen')
|
|
160
|
+
ax.plot(xg, arr[:, 2], marker='.', color='lightgreen')
|
|
161
|
+
ax.plot(xg, arr[:, 3], marker='.', color='orange')
|
|
162
|
+
ax.plot(xg, arr[:, 4], marker='.', color='grey')
|
|
163
|
+
ax.set_xlim(0, readlen-1)
|
|
164
|
+
ax.set_ylim(0, 1)
|
|
165
|
+
ax.text(readlen*.70, .9, "A", color="red")
|
|
166
|
+
ax.text(readlen*.75, .9, "C", color="darkgreen")
|
|
167
|
+
ax.text(readlen*.80, .9, "G", color="lightgreen")
|
|
168
|
+
ax.text(readlen*.85, .9, "T", color="orange")
|
|
169
|
+
ax.text(readlen*.90, .9, "N", color="grey")
|
|
170
|
+
|
|
171
|
+
if outfile is None:
|
|
172
|
+
outfilename = os.path.basename(readfilename) + ".pdf"
|
|
173
|
+
else:
|
|
174
|
+
outfilename = outfile
|
|
175
|
+
|
|
176
|
+
isAlnmntFile = result['isAlnmntFile']
|
|
177
|
+
readlen = result['readlen']
|
|
178
|
+
twoColumns = result['twoColumns']
|
|
179
|
+
|
|
180
|
+
base_arr_U_n = result['base_arr_U_n']
|
|
181
|
+
qual_arr_U_n = result['qual_arr_U_n']
|
|
182
|
+
nreads_U = result['nreads_U']
|
|
183
|
+
|
|
184
|
+
if twoColumns:
|
|
185
|
+
base_arr_A_n = result['base_arr_A_n']
|
|
186
|
+
qual_arr_A_n = result['qual_arr_A_n']
|
|
187
|
+
nreads_A = result['nreads_A']
|
|
188
|
+
|
|
189
|
+
cur_backend = matplotlib.get_backend()
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
matplotlib.use('PDF')
|
|
193
|
+
|
|
194
|
+
fig = plt.figure()
|
|
195
|
+
fig.subplots_adjust(top=.85)
|
|
196
|
+
fig.suptitle(os.path.basename(readfilename), fontweight='bold')
|
|
197
|
+
|
|
198
|
+
if twoColumns:
|
|
199
|
+
|
|
200
|
+
ax = fig.add_subplot(221)
|
|
201
|
+
plot_bases(base_arr_U_n, ax)
|
|
202
|
+
ax.set_ylabel("proportion of base")
|
|
203
|
+
ax.set_title(
|
|
204
|
+
"non-aligned reads\n{:.0%} ({:.4f} million)".format(
|
|
205
|
+
1.0 * nreads_U / (nreads_U+nreads_A),
|
|
206
|
+
1.0 * nreads_U / 1e6,
|
|
207
|
+
))
|
|
208
|
+
|
|
209
|
+
ax2 = fig.add_subplot(222)
|
|
210
|
+
plot_bases(base_arr_A_n, ax2)
|
|
211
|
+
ax2.set_title(
|
|
212
|
+
"{:}\n{:.0%} ({:.4f} million)".format(
|
|
213
|
+
'aligned reads' if primary_only else 'alignments',
|
|
214
|
+
1.0 * nreads_A / (nreads_U+nreads_A),
|
|
215
|
+
1.0 * nreads_A / 1e6,
|
|
216
|
+
))
|
|
217
|
+
|
|
218
|
+
ax3 = fig.add_subplot(223)
|
|
219
|
+
ax3.pcolor(
|
|
220
|
+
qual_arr_U_n.T ** gamma,
|
|
221
|
+
cmap=plt.cm.Greens,
|
|
222
|
+
norm=Normalize(0, 1))
|
|
223
|
+
ax3.set_xlim(0, readlen-1)
|
|
224
|
+
ax3.set_ylim(0, max_qual+1)
|
|
225
|
+
ax3.set_xlabel("position in read")
|
|
226
|
+
ax3.set_ylabel("base-call quality score")
|
|
227
|
+
|
|
228
|
+
ax4 = fig.add_subplot(224)
|
|
229
|
+
ax4.pcolor(
|
|
230
|
+
qual_arr_A_n.T ** gamma,
|
|
231
|
+
cmap=plt.cm.Greens,
|
|
232
|
+
norm=Normalize(0, 1))
|
|
233
|
+
ax4.set_xlim(0, readlen-1)
|
|
234
|
+
ax4.set_ylim(0, max_qual+1)
|
|
235
|
+
ax4.set_xlabel("position in read")
|
|
236
|
+
|
|
237
|
+
else:
|
|
238
|
+
|
|
239
|
+
ax = fig.add_subplot(211)
|
|
240
|
+
plot_bases(base_arr_U_n, ax)
|
|
241
|
+
ax.set_ylabel("proportion of base")
|
|
242
|
+
ax.set_title("{:.3f} million {:}".format(
|
|
243
|
+
1.0 * nreads_U / 1e6,
|
|
244
|
+
'reads' if (not isAlnmntFile) or primary_only else 'alignments',
|
|
245
|
+
))
|
|
246
|
+
|
|
247
|
+
ax2 = fig.add_subplot(212)
|
|
248
|
+
ax2.pcolor(
|
|
249
|
+
qual_arr_U_n.T ** gamma,
|
|
250
|
+
cmap=plt.cm.Greens,
|
|
251
|
+
norm=Normalize(0, 1))
|
|
252
|
+
ax2.set_xlim(0, readlen-1)
|
|
253
|
+
ax2.set_ylim(0, max_qual+1)
|
|
254
|
+
ax2.set_xlabel("position in read")
|
|
255
|
+
ax2.set_ylabel("base-call quality score")
|
|
256
|
+
|
|
257
|
+
fig.savefig(outfilename)
|
|
258
|
+
|
|
259
|
+
finally:
|
|
260
|
+
matplotlib.use(cur_backend)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def main():
|
|
264
|
+
|
|
265
|
+
# **** Parse command line ****
|
|
266
|
+
pa = argparse.ArgumentParser(
|
|
267
|
+
description=
|
|
268
|
+
"This script take a file with high-throughput sequencing reads " +
|
|
269
|
+
"(supported formats: SAM, Solexa _export.txt, FASTQ, Solexa " +
|
|
270
|
+
"_sequence.txt) and performs a simply quality assessment by " +
|
|
271
|
+
"producing plots showing the distribution of called bases and " +
|
|
272
|
+
"base-call quality scores by position within the reads. The " +
|
|
273
|
+
"plots are output as a PDF file.",
|
|
274
|
+
)
|
|
275
|
+
pa.add_argument(
|
|
276
|
+
'readfilename',
|
|
277
|
+
help='The file to count reads in (SAM/BAM or Fastq)',
|
|
278
|
+
)
|
|
279
|
+
pa.add_argument(
|
|
280
|
+
"-t", "--type", type=str, dest="type",
|
|
281
|
+
choices=("sam", "bam", "solexa-export", "fastq", "solexa-fastq"),
|
|
282
|
+
default="sam", help="type of read_file (one of: sam [default], bam, " +
|
|
283
|
+
"solexa-export, fastq, solexa-fastq)")
|
|
284
|
+
pa.add_argument(
|
|
285
|
+
"-o", "--outfile", type=str, dest="outfile",
|
|
286
|
+
help="output filename (default is <read_file>.pdf)")
|
|
287
|
+
pa.add_argument(
|
|
288
|
+
"-r", "--readlength", type=int, dest="readlen",
|
|
289
|
+
help="the maximum read length (when not specified, the script guesses from the file")
|
|
290
|
+
pa.add_argument(
|
|
291
|
+
"-g", "--gamma", type=float, dest="gamma",
|
|
292
|
+
default=0.3,
|
|
293
|
+
help="the gamma factor for the contrast adjustment of the quality score plot")
|
|
294
|
+
pa.add_argument(
|
|
295
|
+
"-n", "--nosplit", action="store_true", dest="nosplit",
|
|
296
|
+
help="do not split reads in unaligned and aligned ones")
|
|
297
|
+
pa.add_argument(
|
|
298
|
+
"-m", "--maxqual", type=int, dest="maxqual", default=41,
|
|
299
|
+
help="the maximum quality score that appears in the data (default: 41)")
|
|
300
|
+
pa.add_argument(
|
|
301
|
+
'--primary-only', action='store_true',
|
|
302
|
+
help="For SAM/BAM input files, ignore alignments that are not primary. " +
|
|
303
|
+
"This only affects 'multimapper' reads that align to several regions " +
|
|
304
|
+
"in the genome. By choosing this option, each read will only count as " +
|
|
305
|
+
"one; without this option, each of its alignments counts as one."
|
|
306
|
+
)
|
|
307
|
+
pa.add_argument(
|
|
308
|
+
'--max-records', type=int, default=-1, dest='max_records',
|
|
309
|
+
help="Limit the analysis to the first N reads/alignments."
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
args = pa.parse_args()
|
|
313
|
+
|
|
314
|
+
result = compute_quality(
|
|
315
|
+
args.readfilename,
|
|
316
|
+
args.type,
|
|
317
|
+
args.nosplit,
|
|
318
|
+
args.readlen,
|
|
319
|
+
args.maxqual,
|
|
320
|
+
args.gamma,
|
|
321
|
+
args.primary_only,
|
|
322
|
+
args.max_records,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
plot(
|
|
326
|
+
result,
|
|
327
|
+
args.readfilename,
|
|
328
|
+
args.outfile,
|
|
329
|
+
args.maxqual,
|
|
330
|
+
args.gamma,
|
|
331
|
+
args.primary_only,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
if __name__ == "__main__":
|
|
336
|
+
main()
|
HTSeq/scripts/utils.py
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class UnknownChrom(Exception):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def my_showwarning(message, category, filename, lineno=None, file=None,
|
|
10
|
+
line=None):
|
|
11
|
+
sys.stderr.write("Warning: %s\n" % message)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def invert_strand(iv):
|
|
15
|
+
iv2 = iv.copy()
|
|
16
|
+
if iv2.strand == "+":
|
|
17
|
+
iv2.strand = "-"
|
|
18
|
+
elif iv2.strand == "-":
|
|
19
|
+
iv2.strand = "+"
|
|
20
|
+
else:
|
|
21
|
+
raise ValueError("Illegal strand")
|
|
22
|
+
return iv2
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _merge_counts(
|
|
26
|
+
results,
|
|
27
|
+
attributes,
|
|
28
|
+
additional_attributes,
|
|
29
|
+
sparse=False,
|
|
30
|
+
dtype=np.float32,
|
|
31
|
+
):
|
|
32
|
+
barcodes = 'cell_barcodes' in results
|
|
33
|
+
|
|
34
|
+
if barcodes:
|
|
35
|
+
cbs = results['cell_barcodes']
|
|
36
|
+
counts = results['counts']
|
|
37
|
+
|
|
38
|
+
feature_attr = sorted(attributes.keys())
|
|
39
|
+
other_features = [
|
|
40
|
+
('__no_feature', 'empty'),
|
|
41
|
+
('__ambiguous', 'ambiguous'),
|
|
42
|
+
('__too_low_aQual', 'lowqual'),
|
|
43
|
+
('__not_aligned', 'notaligned'),
|
|
44
|
+
('__alignment_not_unique', 'nonunique'),
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
fea_names = [fea for fea in feature_attr] + [fea[0] for fea in other_features]
|
|
48
|
+
L = len(fea_names)
|
|
49
|
+
if barcodes:
|
|
50
|
+
n = len(cbs)
|
|
51
|
+
else:
|
|
52
|
+
n = len(results)
|
|
53
|
+
if not sparse:
|
|
54
|
+
table = np.zeros(
|
|
55
|
+
(n, L),
|
|
56
|
+
dtype=dtype,
|
|
57
|
+
)
|
|
58
|
+
else:
|
|
59
|
+
from scipy.sparse import lil_matrix
|
|
60
|
+
table = lil_matrix((n, L), dtype=dtype)
|
|
61
|
+
|
|
62
|
+
if not barcodes:
|
|
63
|
+
fea_ids = [fea for fea in feature_attr] + [fea[1] for fea in other_features]
|
|
64
|
+
for j, r in enumerate(results):
|
|
65
|
+
for i, fn in enumerate(fea_ids):
|
|
66
|
+
if i < len(feature_attr):
|
|
67
|
+
countji = r['counts'][fn]
|
|
68
|
+
else:
|
|
69
|
+
countji = r[fn]
|
|
70
|
+
if countji > 0:
|
|
71
|
+
table[j, i] = countji
|
|
72
|
+
else:
|
|
73
|
+
for j, cb in enumerate(cbs):
|
|
74
|
+
for i, fn in enumerate(fea_names):
|
|
75
|
+
countji = counts[cb][fn]
|
|
76
|
+
if countji > 0:
|
|
77
|
+
table[j, i] = countji
|
|
78
|
+
|
|
79
|
+
if sparse:
|
|
80
|
+
table = table.tocsr()
|
|
81
|
+
|
|
82
|
+
feature_metadata = {
|
|
83
|
+
'id': fea_names,
|
|
84
|
+
}
|
|
85
|
+
for iadd, attr in enumerate(additional_attributes):
|
|
86
|
+
feature_metadata[attr] = [attributes[fn][iadd] for fn in feature_attr]
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
'feature_metadata': feature_metadata,
|
|
90
|
+
'table': table,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _count_results_to_tsv(
|
|
95
|
+
results,
|
|
96
|
+
samples_name,
|
|
97
|
+
attributes,
|
|
98
|
+
additional_attributes,
|
|
99
|
+
output_filename,
|
|
100
|
+
output_delimiter,
|
|
101
|
+
output_append=False,
|
|
102
|
+
add_tsv_header=False
|
|
103
|
+
):
|
|
104
|
+
|
|
105
|
+
barcodes = 'cell_barcodes' in results
|
|
106
|
+
|
|
107
|
+
pad = ['' for attr in additional_attributes]
|
|
108
|
+
|
|
109
|
+
if barcodes:
|
|
110
|
+
cbs = results['cell_barcodes']
|
|
111
|
+
counts = results['counts']
|
|
112
|
+
|
|
113
|
+
# Print or write header
|
|
114
|
+
fields = [''] + pad + cbs
|
|
115
|
+
line = output_delimiter.join(fields)
|
|
116
|
+
if output_filename == '':
|
|
117
|
+
print(line)
|
|
118
|
+
else:
|
|
119
|
+
with open(output_filename, 'w') as f:
|
|
120
|
+
f.write(line)
|
|
121
|
+
f.write('\n')
|
|
122
|
+
|
|
123
|
+
elif add_tsv_header:
|
|
124
|
+
# Write the header.
|
|
125
|
+
# Only get here if we don't have cell barcodes, i.e. this is not called by htseq-count-barcode,
|
|
126
|
+
# and user wants the tsv header
|
|
127
|
+
file_header = output_delimiter.join([''] + pad + samples_name)
|
|
128
|
+
|
|
129
|
+
if output_filename == '':
|
|
130
|
+
print(file_header)
|
|
131
|
+
else:
|
|
132
|
+
# If append to existing file, then open as a
|
|
133
|
+
file_open_opt = 'a' if output_append else 'w'
|
|
134
|
+
|
|
135
|
+
with open(output_filename, file_open_opt) as f:
|
|
136
|
+
f.write(file_header)
|
|
137
|
+
f.write('\n')
|
|
138
|
+
|
|
139
|
+
# Each feature is a row with feature id, additional attrs, and counts
|
|
140
|
+
feature_attr = sorted(attributes.keys())
|
|
141
|
+
for ifn, fn in enumerate(feature_attr):
|
|
142
|
+
if not barcodes:
|
|
143
|
+
fields = [fn] + attributes[fn] + [str(r['counts'][fn]) for r in results]
|
|
144
|
+
else:
|
|
145
|
+
fields = [fn] + attributes[fn] + [str(counts[cb][fn]) for cb in cbs]
|
|
146
|
+
|
|
147
|
+
line = output_delimiter.join(fields)
|
|
148
|
+
if output_filename == '':
|
|
149
|
+
print(line)
|
|
150
|
+
else:
|
|
151
|
+
omode = 'a' if output_append or (ifn > 0) or barcodes or add_tsv_header else 'w'
|
|
152
|
+
with open(output_filename, omode) as f:
|
|
153
|
+
f.write(line)
|
|
154
|
+
f.write('\n')
|
|
155
|
+
|
|
156
|
+
# Add other features (unmapped, etc.)
|
|
157
|
+
other_features = [
|
|
158
|
+
('__no_feature', 'empty'),
|
|
159
|
+
('__ambiguous', 'ambiguous'),
|
|
160
|
+
('__too_low_aQual', 'lowqual'),
|
|
161
|
+
('__not_aligned', 'notaligned'),
|
|
162
|
+
('__alignment_not_unique', 'nonunique'),
|
|
163
|
+
]
|
|
164
|
+
for title, fn in other_features:
|
|
165
|
+
if not barcodes:
|
|
166
|
+
fields = [title] + pad + [str(r[fn]) for r in results]
|
|
167
|
+
else:
|
|
168
|
+
fields = [title] + pad + [str(counts[cb][title]) for cb in cbs]
|
|
169
|
+
line = output_delimiter.join(fields)
|
|
170
|
+
if output_filename == '':
|
|
171
|
+
print(line)
|
|
172
|
+
else:
|
|
173
|
+
with open(output_filename, 'a') as f:
|
|
174
|
+
f.write(line)
|
|
175
|
+
f.write('\n')
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _count_table_to_mtx(
|
|
179
|
+
filename,
|
|
180
|
+
table,
|
|
181
|
+
feature_metadata,
|
|
182
|
+
samples,
|
|
183
|
+
):
|
|
184
|
+
if not str(filename).endswith('.mtx'):
|
|
185
|
+
raise ValueError('Matrix Marker filename should end with ".mtx"')
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
from scipy.io import mmwrite
|
|
189
|
+
except ImportError:
|
|
190
|
+
raise ImportError('Install scipy for mtx support')
|
|
191
|
+
|
|
192
|
+
filename_pfx = str(filename)[:-4]
|
|
193
|
+
filename_feature_meta = filename_pfx+'_features.tsv'
|
|
194
|
+
filename_samples = filename_pfx+'_samples.tsv'
|
|
195
|
+
|
|
196
|
+
# Write main matrix (features as columns)
|
|
197
|
+
mmwrite(
|
|
198
|
+
filename,
|
|
199
|
+
table,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Write input filenames
|
|
203
|
+
with open(filename_samples, 'wt') as fout:
|
|
204
|
+
for fn in samples:
|
|
205
|
+
fout.write(fn+'\n')
|
|
206
|
+
|
|
207
|
+
# Write feature metadata (ids and additional attributes)
|
|
208
|
+
with open(filename_feature_meta, 'wt') as fout:
|
|
209
|
+
nkeys = len(feature_metadata)
|
|
210
|
+
for ik, key in enumerate(feature_metadata):
|
|
211
|
+
if ik != nkeys - 1:
|
|
212
|
+
fout.write(key+'\t')
|
|
213
|
+
else:
|
|
214
|
+
fout.write(key+'\n')
|
|
215
|
+
nfeatures = len(feature_metadata[key])
|
|
216
|
+
for i in range(nfeatures):
|
|
217
|
+
for ik, key in enumerate(feature_metadata):
|
|
218
|
+
if ik != nkeys - 1:
|
|
219
|
+
fout.write(feature_metadata[key][i]+'\t')
|
|
220
|
+
else:
|
|
221
|
+
fout.write(feature_metadata[key][i]+'\n')
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _count_table_to_h5ad(
|
|
225
|
+
filename,
|
|
226
|
+
table,
|
|
227
|
+
feature_metadata,
|
|
228
|
+
samples,
|
|
229
|
+
):
|
|
230
|
+
try:
|
|
231
|
+
import anndata
|
|
232
|
+
except ImportError:
|
|
233
|
+
raise ImportError('Install the anndata package for h5ad support')
|
|
234
|
+
|
|
235
|
+
# If they have anndata, they have scipy and pandas too
|
|
236
|
+
import pandas as pd
|
|
237
|
+
|
|
238
|
+
# We don't have additional attribute (e.g. gene name) for htseq specific features like __no_feature.
|
|
239
|
+
# Hence the trick is to convert the array to series so the value for htseq specific features like __no_feature
|
|
240
|
+
# column is set NaN.
|
|
241
|
+
# See: https://stackoverflow.com/questions/19736080/creating-dataframe-from-a-dictionary-where-entries-have-different-lengths
|
|
242
|
+
feature_metadata = pd.DataFrame(dict([(k, pd.Series(v)) for k, v in feature_metadata.items()]))
|
|
243
|
+
feature_metadata.set_index(feature_metadata.columns[0], inplace=True)
|
|
244
|
+
|
|
245
|
+
adata = anndata.AnnData(
|
|
246
|
+
X=table,
|
|
247
|
+
obs=pd.DataFrame([], index=samples),
|
|
248
|
+
var=feature_metadata,
|
|
249
|
+
)
|
|
250
|
+
adata.write_h5ad(filename)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _count_table_to_loom(
|
|
254
|
+
filename,
|
|
255
|
+
table,
|
|
256
|
+
feature_metadata,
|
|
257
|
+
samples,
|
|
258
|
+
):
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
import loompy
|
|
262
|
+
except ImportError:
|
|
263
|
+
raise ImportError('Install the loompy package for loom support')
|
|
264
|
+
|
|
265
|
+
# Loom uses features as rows...
|
|
266
|
+
layers = {'': table.T}
|
|
267
|
+
row_attrs = feature_metadata
|
|
268
|
+
col_attrs = {'_index': samples}
|
|
269
|
+
loompy.create(
|
|
270
|
+
filename,
|
|
271
|
+
layers=layers,
|
|
272
|
+
row_attrs=row_attrs,
|
|
273
|
+
col_attrs=col_attrs,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _write_output(
|
|
278
|
+
results,
|
|
279
|
+
samples,
|
|
280
|
+
attributes,
|
|
281
|
+
additional_attributes,
|
|
282
|
+
output_filename,
|
|
283
|
+
output_delimiter,
|
|
284
|
+
output_append,
|
|
285
|
+
sparse=False,
|
|
286
|
+
dtype=np.float32,
|
|
287
|
+
add_tsv_header=False
|
|
288
|
+
):
|
|
289
|
+
|
|
290
|
+
"""
|
|
291
|
+
Export the gene counts as tsv/csv, mtx, loom, h5ad files.
|
|
292
|
+
Note, need to update the parameter documentations.
|
|
293
|
+
|
|
294
|
+
Parameters
|
|
295
|
+
----------
|
|
296
|
+
results : list
|
|
297
|
+
List of dictionaries with each element representing the counts for an input BAM file.
|
|
298
|
+
Note, the list is in order of the samples parameter. So the first element in the list corresponds to
|
|
299
|
+
the first file in samples parameter.
|
|
300
|
+
samples : list
|
|
301
|
+
List of input BAM files.
|
|
302
|
+
|
|
303
|
+
"""
|
|
304
|
+
|
|
305
|
+
# Write output to stdout or TSV/CSV
|
|
306
|
+
if output_filename == '':
|
|
307
|
+
_count_results_to_tsv(
|
|
308
|
+
results,
|
|
309
|
+
samples,
|
|
310
|
+
attributes,
|
|
311
|
+
additional_attributes,
|
|
312
|
+
output_filename,
|
|
313
|
+
output_delimiter,
|
|
314
|
+
output_append=False,
|
|
315
|
+
add_tsv_header=add_tsv_header
|
|
316
|
+
)
|
|
317
|
+
return
|
|
318
|
+
|
|
319
|
+
# Get file extension/format
|
|
320
|
+
output_sfx = output_filename.split('.')[-1].lower()
|
|
321
|
+
|
|
322
|
+
if output_sfx in ('csv', 'tsv'):
|
|
323
|
+
_count_results_to_tsv(
|
|
324
|
+
results,
|
|
325
|
+
samples,
|
|
326
|
+
attributes,
|
|
327
|
+
additional_attributes,
|
|
328
|
+
output_filename,
|
|
329
|
+
output_delimiter,
|
|
330
|
+
output_append,
|
|
331
|
+
add_tsv_header=add_tsv_header
|
|
332
|
+
)
|
|
333
|
+
return
|
|
334
|
+
|
|
335
|
+
# Make unified object of counts and feature metadata
|
|
336
|
+
output_dict = _merge_counts(
|
|
337
|
+
results,
|
|
338
|
+
attributes,
|
|
339
|
+
additional_attributes,
|
|
340
|
+
sparse=sparse,
|
|
341
|
+
dtype=dtype,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
if output_sfx == 'mtx':
|
|
345
|
+
_count_table_to_mtx(
|
|
346
|
+
output_filename,
|
|
347
|
+
output_dict['table'],
|
|
348
|
+
output_dict['feature_metadata'],
|
|
349
|
+
samples,
|
|
350
|
+
)
|
|
351
|
+
return
|
|
352
|
+
|
|
353
|
+
if output_sfx == 'loom':
|
|
354
|
+
_count_table_to_loom(
|
|
355
|
+
output_filename,
|
|
356
|
+
output_dict['table'],
|
|
357
|
+
output_dict['feature_metadata'],
|
|
358
|
+
samples,
|
|
359
|
+
)
|
|
360
|
+
return
|
|
361
|
+
|
|
362
|
+
if output_sfx == 'h5ad':
|
|
363
|
+
_count_table_to_h5ad(
|
|
364
|
+
output_filename,
|
|
365
|
+
output_dict['table'],
|
|
366
|
+
output_dict['feature_metadata'],
|
|
367
|
+
samples,
|
|
368
|
+
)
|
|
369
|
+
return
|
|
370
|
+
|
|
371
|
+
raise ValueError(
|
|
372
|
+
f'Format not recognized for output count file: {output_sfx}')
|