Skip to content

Commit 1353bbe

Browse files
author
transfer
committed
resolved conflicts pulling to master from development
2 parents a367fdd + bf2f88c commit 1353bbe

7 files changed

Lines changed: 562 additions & 0 deletions

File tree

CoverageCalculatorPy.py

Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
"""
2+
CoverageCalculatorPy.py
3+
4+
Calculates coverage statistics from depth of coverage file
5+
6+
Aurthor: Christopher Medway
7+
Created: 11th November 2018
8+
Version: 0.0.1
9+
Updated: 11th November 2018
10+
"""
11+
12+
import argparse
13+
import tabix
14+
import os
15+
import logging
16+
17+
18+
def get_args():
19+
"""
20+
uses argparse package to extract command line arguments
21+
"""
22+
23+
# argparse object
24+
parser = argparse.ArgumentParser(
25+
description='accepts a bedfile and GATK depthOfCoverage file and generates coverage metrics')
26+
27+
parser.add_argument('-B', '--bedfile', help='path to bedfile', required=True)
28+
parser.add_argument('-D', '--depthfile', help='path to depth file', required=True)
29+
parser.add_argument('-p', '--padding', help='basepair padding', required=False, default=0)
30+
parser.add_argument('-d', '--depth', help='coverage threshold', required=False, default=100)
31+
parser.add_argument('-o', "--outname", help="output file name", required=False, default="output")
32+
parser.add_argument('-O', '--outdir', help="output file directory", required=False, default="./")
33+
parser.add_argument('-g', '--groupfile', help="file of annotations to group bed intervals", required=False)
34+
35+
args = parser.parse_args()
36+
return args
37+
38+
39+
def main(args):
40+
"""
41+
main function iterates over intervals in given bedfile
42+
"""
43+
44+
# setup logger
45+
logger = logging.getLogger('CoverageCalculatorPy')
46+
logger.setLevel(logging.DEBUG)
47+
handler = logging.StreamHandler()
48+
handler.setLevel(logging.DEBUG)
49+
formatter = logging.Formatter(
50+
'%(levelname)s\t%(asctime)s\t%(name)s\t%(message)s'
51+
)
52+
handler.setFormatter(formatter)
53+
logger.addHandler(handler)
54+
logger.info('running CoverageCalculatorPy.py...')
55+
56+
57+
# make output directory if it doesn't exist
58+
if not os.path.exists(args.outdir):
59+
os.makedirs(args.outdir)
60+
61+
# file handel for coverage output file
62+
covr_outfile = args.outdir + args.outname + ".coverage"
63+
logger.info('coverage information for each interval in given bedfile will be written to: ' + str(covr_outfile))
64+
65+
# remove file if exists
66+
if os.path.exists(covr_outfile):
67+
os.remove(covr_outfile)
68+
69+
# open coverage output file & write header
70+
covfile = open(covr_outfile, 'a+')
71+
covfile.write(
72+
"CHR" +
73+
"\t" +
74+
"START" +
75+
"\t" +
76+
"END" +
77+
"\t" +
78+
"META" +
79+
"\t" +
80+
"AVG_DEPTH" +
81+
"\t" +
82+
"PERC_COVERAGE@" + str(args.depth) + "\n"
83+
)
84+
85+
# variables to store running totals of depth / coverage
86+
# over all intervals in file
87+
mem_depth = 0
88+
mem_meets_depth = 0
89+
mem_length = 0
90+
91+
# setup groups / features variables if this file has been included --groupsfile
92+
if args.groupfile is not None:
93+
logger.info('group file included: ' + str(args.groupfile))
94+
# initialise variables to store depth / coverage over feature
95+
feature = []
96+
feature_length = 0
97+
feature_depth = 0
98+
feature_meets_depth = 0
99+
last_feature = 0
100+
101+
# features made into list
102+
for line in open(args.groupfile):
103+
feature.append(line.rstrip())
104+
105+
# check bedfile and groups are same length - throw error if different
106+
num_ln_grp = len(feature) - 1 # minus one because grp file has a header
107+
num_ln_bed = sum(1 for line in open(args.bedfile))
108+
109+
if num_ln_bed != num_ln_grp:
110+
logger.error('bedfile and groupfile do not have the same number of entries! Groupfile must have a header')
111+
112+
113+
# prepare output file for merged data - this always includes a data for all intervals in .bed
114+
# but can also include groups if the file is given as an argument
115+
mrg_outfile = args.outdir + args.outname + ".totalCoverage"
116+
logger.info('coverage information across all intervals in bedfile will be written to: ' + str(mrg_outfile))
117+
118+
# remove merged file if exists
119+
if os.path.exists(mrg_outfile):
120+
os.remove(mrg_outfile)
121+
122+
# open coverage output file & write header
123+
mrgfile = open(mrg_outfile, 'a+')
124+
125+
mrgfile.write(
126+
"FEATURE" +
127+
"\t" +
128+
"AVG_DEPTH" +
129+
"\t" +
130+
"PERC_COVERAGE@" + str(args.depth) + "\n"
131+
)
132+
133+
134+
# prepare output file for gaps
135+
# file handel
136+
gaps_outfile = args.outdir + args.outname + ".gaps"
137+
logger.info('gaps - intervals not covered at a minimum of ' + str(args.depth) + ' will be written to: ' + str(gaps_outfile))
138+
139+
# remove file if exists
140+
if os.path.exists(gaps_outfile):
141+
os.remove(gaps_outfile)
142+
143+
# open gaps output file & write header
144+
gapsfile = open(gaps_outfile, 'a+')
145+
gapsfile.write("##DEPTH_THRESHOLD=" + str(args.depth) + "\n")
146+
147+
#prepare missing file outfile
148+
# file handel
149+
missing_outfile = args.outdir + args.outname + ".missing"
150+
logger.info('intervals which do not a corresponding depth in ' + args.depthfile + ' will be written to: ' + str(missing_outfile))
151+
152+
# is exists, remove file
153+
if os.path.exists(missing_outfile):
154+
os.remove(missing_outfile)
155+
156+
# open gaps output file & write header
157+
missingfile = open(missing_outfile, 'a+')
158+
159+
160+
# iterate over each line of the bed file
161+
with open(args.bedfile) as bed:
162+
163+
# counter used to access element of the features list that correpondes
164+
# to a bedfile entry. Start on element 1 because 0 contains features header
165+
cnt_bed_ln = 1
166+
167+
for line in bed:
168+
bedlist = line.split()
169+
chr = bedlist[0]
170+
start = int(bedlist[1])
171+
end = int(bedlist[2])
172+
173+
# does bedfile contain 4th column of metadata
174+
if len(bedlist) > 3:
175+
meta = str(bedlist[3])
176+
else:
177+
meta = ""
178+
179+
# function prints coverage for given interval, but also returns stats for aggregation across intervals
180+
depth, meets_depth, length = get_avg_depth(covfile, chr, start, end, meta, args.depthfile, int(args.depth))
181+
182+
# storing coverage info across all intervals in given bedfile
183+
mem_depth = mem_depth + depth
184+
mem_meets_depth = mem_meets_depth + meets_depth
185+
mem_length = mem_length + length
186+
187+
# storing coverage info across features if --groupfile argument included
188+
if args.groupfile is not None:
189+
current_feature = feature[cnt_bed_ln]
190+
191+
if cnt_bed_ln == 1:
192+
last_feature = current_feature
193+
194+
if current_feature != last_feature:
195+
mrgfile.write(
196+
str(feature[cnt_bed_ln - 1] + '_' + feature[0]) +
197+
"\t" +
198+
str(round(feature_depth / feature_length, 0)) +
199+
"\t" +
200+
str(round((feature_meets_depth / feature_length)*100, 1)) + "\n")
201+
202+
feature_depth = 0
203+
feature_meets_depth = 0
204+
feature_length = 0
205+
206+
feature_depth = feature_depth + depth
207+
feature_meets_depth = feature_meets_depth + meets_depth
208+
feature_length = feature_length + length
209+
last_feature = current_feature
210+
211+
# reports gaps for given feature and appends to gaps file
212+
get_gaps(gapsfile, chr, start, end, meta, args.depthfile, int(args.depth))
213+
214+
# reports coordinates which are unavilable (i.e in bed but not in depthfile) and appends to file
215+
report_missing_regions(missingfile, chr, start, end, meta, args.depthfile)
216+
217+
cnt_bed_ln = cnt_bed_ln + 1
218+
219+
# if --groups has been used, last interval in bed will always be the end of a feature - so print
220+
if args.groupfile is not None:
221+
mrgfile.write(
222+
str(feature[cnt_bed_ln - 1] + '_' + feature[0]) +
223+
"\t" +
224+
str(round(feature_depth / feature_length, 0)) +
225+
"\t" +
226+
str(round((feature_meets_depth / feature_length)*100, 1)) + "\n")
227+
228+
# print accumated depth / coverage across entire bedfile
229+
mrgfile.write(
230+
args.outname +
231+
"\t" +
232+
str(round(mem_depth / mem_length, 0)) +
233+
"\t" +
234+
str(round((mem_meets_depth / mem_length) * 100, 1)) + "\n"
235+
)
236+
237+
covfile.close()
238+
239+
240+
241+
242+
def get_bed_lines(depthfile, chr, start, end):
243+
"""
244+
given a genomic interval, extract entries from tabix indexed depth of coverage file
245+
depth of coverage file is generated using GATK3
246+
247+
:param depthfile: depth of coverage file generated in GATK3 and manually tabix indexed (see documentation)
248+
:param chr: chromosome
249+
:param start: start coordinate 0-based
250+
:param end: end coordinate 0-based
251+
:return: records: object containing bed intervals
252+
"""
253+
tb = tabix.open(depthfile)
254+
records = tb.query(chr, start, end)
255+
return records
256+
257+
258+
def get_avg_depth(covfile, chr, start, end, meta, depthfile, depth_threshold):
259+
"""
260+
function to iterate over interval and calculate average coverage & percent of bases meeting given
261+
depth threshold
262+
:param covfile: coverage output filehandle
263+
:param chr: chromosome
264+
:param start: start coordinate 0-based
265+
:param end: end coordinate 0-based
266+
:param meta: meta information from bedfile 4th column
267+
:param depthfile: depth of coverage file generated in GATK3 and manually tabix indexed (see documentation)
268+
:param depth_threshold: minimum depth of coverage for PASS
269+
"""
270+
271+
# get depthfile entry for interval
272+
records = get_bed_lines(depthfile, chr, start, end)
273+
274+
# intialise variables used in forloop
275+
tot_depth = 0
276+
meets_depth = 0 # counts the number of bases meeting min depth requirement
277+
length = (end - start)
278+
279+
for record in records:
280+
depth = int(record[2])
281+
tot_depth = tot_depth + depth # accumaulated depth
282+
if depth >= depth_threshold:
283+
meets_depth = int(meets_depth) + 1
284+
avg_depth = round(tot_depth / length, 0)
285+
perc_coverage = round((meets_depth / length) * 100, 1)
286+
287+
covfile.write(
288+
str(chr) +
289+
"\t" +
290+
str(start) +
291+
"\t" +
292+
str(end) +
293+
"\t" +
294+
str(meta) +
295+
"\t" +
296+
str(avg_depth) +
297+
"\t" +
298+
str(perc_coverage) +
299+
"\n"
300+
)
301+
return tot_depth, meets_depth, length
302+
303+
304+
def get_gaps(gapsfile, chr, start, end, meta, depthfile, depth_threshold):
305+
"""
306+
identifies and prints coordinates, within the given bed interval, that fall below the given
307+
depth threshold
308+
"""
309+
# get depthfile entry for interval
310+
records = get_bed_lines(depthfile, chr, start, end)
311+
312+
# initialise variables used in loop
313+
first_entry = 1
314+
coord = 0
315+
gap_start = 0
316+
317+
for record in records:
318+
319+
coord = int(record[1])
320+
depth = int(record[2])
321+
322+
if first_entry == 1:
323+
gap_start = coord
324+
first_entry = 0
325+
326+
if depth >= depth_threshold:
327+
if coord != gap_start:
328+
# this is the end of a gap and should be printed
329+
gapsfile.write(str(chr) + "\t" + str(gap_start - 1) + "\t" + str(coord - 1) + "\n")
330+
gap_start = coord + 1
331+
else:
332+
# this is not the end of a gap
333+
gap_start = gap_start + 1
334+
335+
# if entire interval is missing
336+
if coord == 0:
337+
print("")
338+
# if interval ended on a gap, print final row
339+
elif coord != gap_start - 1:
340+
gapsfile.write(str(chr) + "\t" + str(gap_start - 1) + "\t" + str(coord) + "\n")
341+
342+
343+
def report_missing_regions(missingfile, chr, start, end, meta, depthfile):
344+
"""
345+
identifies and reports coordinates which are given in the bedfile, but are not included
346+
in the depth of coverage file
347+
"""
348+
# get depthfile entry for interval
349+
records = get_bed_lines(depthfile, chr, start, end)
350+
351+
# initialse variables used in loop
352+
coords_in_depthfile = []
353+
354+
for record in records:
355+
coords_in_depthfile.append(int(record[1]))
356+
357+
coords_in_bed = list(range(start+1, end+1))
358+
missing = sorted(list(set(coords_in_bed) - set(coords_in_depthfile)))
359+
360+
i = 0
361+
while i < len(missing):
362+
if i == 0:
363+
start = missing[i]
364+
else:
365+
if missing[i] - 1 != missing[i - 1]:
366+
missingfile.write(str(chr) + "\t" + str(start - 1) + "\t" + str(missing[i]) + "\t" + str(meta) + "\n")
367+
start = missing[i] + 1
368+
elif i + 1 == len(missing):
369+
missingfile.write(str(chr) + "\t" + str(start - 1) + "\t" + str(missing[i]) + "\t" + str(meta) + "\n")
370+
371+
i = i + 1
372+
373+
374+
if __name__ == '__main__':
375+
args = get_args()
376+
main(args)

0 commit comments

Comments
 (0)