-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathfinalise_variant_qc.py
More file actions
272 lines (204 loc) · 8.5 KB
/
Copy pathfinalise_variant_qc.py
File metadata and controls
272 lines (204 loc) · 8.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# eam
# 2021-05-13
"""
Finalize variant QC
Actions:
- Apply hard filters
- Apply VQSR filter
- Apply RF filter
- Apply coverage/capture interval filter
usage: finalise_variant_qc.py [-h] [--exome_cohort EXOME_COHORT]
[--write_to_file] [--overwrite]
[--default_ref_genome DEFAULT_REF_GENOME]
optional arguments:
-h, --help show this help message and exit
--exome_cohort EXOME_COHORT
One of <chd_ukbb> or <chd_ddd>
--write_to_file Write output to BGZ-compressed file
--overwrite Overwrite pre-existing data
--default_ref_genome DEFAULT_REF_GENOME
Default reference genome to start Hail
"""
import argparse
import logging
import hail as hl
from utils.data_utils import (get_qc_mt_path,
get_variant_qc_ht_path,
get_vep_annotation_ht,
get_gnomad_genomes_coverage_ht)
from utils.filter import (filter_capture_intervals)
from utils.config import NFS_DIR
# from utils.constants import *
logging.basicConfig(format="%(levelname)s (%(name)s %(lineno)s): %(message)s")
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
# hdfs_dir = HDFS_DIR # set via WES_HDFS_DIR env var
nfs_dir = NFS_DIR
INBREEDING_COEFFICIENT_CUTOFF = -0.3
RF_PROBABILITY_SNV_CUTOFF = 0.2 # TODO: this cutoff could be lower for SNVs (0.1?)
RF_PROBABILITY_INDEL_CUTOFF = 0.2
def load_filtered_mt(exome_cohort: str) -> hl.MatrixTable:
"""Read adj-genotype MT, keep samples passing QC, and drop all annotations."""
mt = hl.read_matrix_table(get_qc_mt_path(dataset=exome_cohort,
part='sample_qc_adj_genotypes',
split=True))
# keep samples passing QC filtering
mt = (mt
.filter_cols(mt.pass_filters)
.select_cols()
.select_rows()
)
return mt
def annotate_variant_info(mt: hl.MatrixTable) -> hl.Table:
"""Annotate rows with variant info fields and return the rows as a HailTable."""
# import variant info fields (vcf info)
variant_info_ht = (get_vep_annotation_ht()
.drop('vep')
)
# Add useful annotation for variant hard filter
ht = (mt
.annotate_rows(inbreeding_coeff=variant_info_ht[mt.row_key].info.InbreedingCoeff,
vqsr_filter=variant_info_ht[mt.row_key].filters,
VQSLOD=variant_info_ht[mt.row_key].info.VQSLOD,
gt_counts=hl.agg.count_where(hl.is_defined(mt.GT)) # expected MT filtered to high-quality GT
)
.rows()
)
return ht
def apply_hard_filters(ht: hl.Table) -> hl.Table:
"""Annotate table with hard-filter flags (fail_inbreeding_coeff, AC0)."""
# 1. Apply variant hard filters
# hard filter expression
variant_hard_filter_expr = {'fail_inbreeding_coeff': ht.inbreeding_coeff < INBREEDING_COEFFICIENT_CUTOFF,
'AC0': ht.gt_counts == 0}
ht = (ht
.annotate(**variant_hard_filter_expr)
)
return ht
def apply_vqsr_filter(ht: hl.Table) -> hl.Table:
"""Annotate table with VQSR filter flag (fail_vqsr)."""
# 2. Apply VQSR filter
ht = (ht
.annotate(fail_vqsr=hl.len(ht.vqsr_filter) != 0)
)
return ht
def apply_rf_filter(ht: hl.Table) -> hl.Table:
"""Import RF result table, join to ht, and annotate with fail_rf flag."""
# 3. Apply RF filter
# import/parse rf final HT
ht_rf = hl.read_table(
get_variant_qc_ht_path(part='rf_result')
)
ht_rf = (ht_rf
.select(rf_probability_tp=ht_rf.rf_probability['TP'],
variant_type=ht_rf.variant_type)
)
ht = (ht
.annotate(**ht_rf[ht.key])
)
ht = (ht
.annotate(fail_rf=hl.case()
.when((ht.rf_probability_tp < RF_PROBABILITY_SNV_CUTOFF) & (ht.variant_type == 'snv'), True)
.when((ht.rf_probability_tp < RF_PROBABILITY_INDEL_CUTOFF) & (ht.variant_type == 'indel'), True)
.default(False)
)
)
return ht
def apply_coverage_and_interval_filters(ht: hl.Table) -> hl.Table:
"""Annotate table with gnomAD genome coverage and capture-interval membership flags."""
# 5. Apply coverage/capture interval filters
## gnomad genome coverage
gnomad_coverage_ht = get_gnomad_genomes_coverage_ht().key_by()
gnomad_coverage_ht = (gnomad_coverage_ht
.annotate(locus=hl.parse_locus(gnomad_coverage_ht.locus, reference_genome='GRCh38'))
.key_by('locus')
)
ht = (ht
.annotate(gnomad_cov_10X=gnomad_coverage_ht[ht.locus].over_10)
)
ht = (ht
.annotate(is_coveraged_gnomad_genomes=ht.gnomad_cov_10X >= 0.9)
)
## defined in capture intervals
# filter to capture intervals (intersect)
ht_defined_intervals = filter_capture_intervals(ht,
capture_intervals=['ssv5_idt_intersect'])
ht = (ht
.annotate(is_defined_capture_intervals=hl.is_defined(ht_defined_intervals[ht.key]))
)
return ht
def summarise_and_checkpoint(ht: hl.Table,
exome_cohort: str,
overwrite: bool,
write_to_file: bool) -> hl.Table:
"""Apply final pass/fail annotation, aggregate filter summary, checkpoint, and optionally export."""
# 6. Summary final variant QC
# final variant qc filter joint expression
final_variant_qc_ann_expr = {
'pass_variant_qc_filters': hl.cond(
~ht.fail_inbreeding_coeff &
~ht.AC0 &
~ht.fail_vqsr &
~ht.fail_rf &
ht.is_coveraged_gnomad_genomes &
ht.is_defined_capture_intervals,
True, False)}
ht = (ht
.annotate(**final_variant_qc_ann_expr)
)
# Counts the number of variants (snv and indels) affected by every filter and add as global field
filter_flags = ['fail_inbreeding_coeff',
'AC0',
'fail_vqsr',
'fail_rf',
'is_coveraged_gnomad_genomes',
'is_defined_capture_intervals',
'pass_variant_qc_filters']
summary_filter_expr = {v: hl.struct(**{f: hl.agg.filter(ht.variant_type == v, hl.agg.counter(ht[f]))
for f in filter_flags})
for v in ['snv', 'indel']
}
ht = ht.annotate_globals(summary_filter=ht.aggregate(summary_filter_expr, _localize=False))
# write HT variant QC final table
output_path = get_variant_qc_ht_path(dataset=exome_cohort,
part='final_qc')
ht = ht.checkpoint(
output_path,
overwrite=overwrite
)
# print filter summary
logger.info(f'Variant QC filter summary: {ht.summary_filter.collect()}')
# export HT to file
if write_to_file:
ht.export(
f'{output_path}.tsv.bgz'
)
return ht
def main(args):
# Start Hail
hl.init(default_reference=args.default_ref_genome)
mt = load_filtered_mt(exome_cohort=args.exome_cohort)
ht = annotate_variant_info(mt=mt)
ht = apply_hard_filters(ht=ht)
ht = apply_vqsr_filter(ht=ht)
ht = apply_rf_filter(ht=ht)
ht = apply_coverage_and_interval_filters(ht=ht)
summarise_and_checkpoint(ht=ht,
exome_cohort=args.exome_cohort,
overwrite=args.overwrite,
write_to_file=args.write_to_file)
# Stop Hail
hl.stop()
print("Finished!")
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--exome_cohort', help="One of <chd_ukbb> or <chd_ddd>",
type=str, default=None)
parser.add_argument('--write_to_file', help='Write output to BGZ-compressed file',
action='store_true')
parser.add_argument('--overwrite', help='Overwrite pre-existing data',
action='store_true')
parser.add_argument('--default_ref_genome', help='Default reference genome to start Hail',
type=str, default='GRCh38')
args = parser.parse_args()
main(args)