Skip to content

Commit 1fdebad

Browse files
authored
Merge pull request #167 from EBI-Metagenomics/bugfix/write-gff-missing-contigs-when-user-provided-proteins
Bugfix/write gff missing contigs when user provided proteins
2 parents 7584428 + af13c89 commit 1fdebad

15 files changed

Lines changed: 354 additions & 85 deletions

File tree

.editorconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ indent_size = 2
1919
indent_size = 2
2020

2121
[*.nf]
22-
indent_size = 2
22+
indent_size = 4

bin/contig_taxonomic_assign.py

Lines changed: 125 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import argparse
44
import csv
5+
import logging
56
import os
67
import re
78
import math
@@ -10,38 +11,45 @@
1011
import pandas as pd
1112
from ete3 import NCBITaxa
1213

14+
logging.basicConfig(
15+
level=logging.INFO,
16+
format="%(asctime)s %(levelname)s %(message)s",
17+
datefmt="%Y-%m-%d %H:%M:%S",
18+
)
19+
1320

1421
# Some taxa are discontinued, we should exclude them (https://github.com/EBI-Metagenomics/emg-viral-pipeline/issues/113):
15-
EXCLUDE_TAXA = ["Allolevivirus",
16-
"Autographivirinae",
17-
"Buttersvirus",
18-
"Caudovirales",
19-
"Chungbukvirus",
20-
"Incheonvirus",
21-
"Leviviridae",
22-
"Levivirus",
23-
"Mandarivirus",
24-
"Pbi1virus",
25-
"Phicbkvirus",
26-
"Radnorvirus",
27-
"Sitaravirus",
28-
"Vidavervirus",
29-
"Myoviridae",
30-
"Siphoviridae",
31-
"Podoviridae",
32-
"Viunavirus",
33-
"Orthohepevirus",
34-
"Klosneuvirus",
35-
"Hendrixvirus",
36-
"Rubulavirus",
37-
"Avulavirus",
38-
"Catovirus",
39-
"Nucleorhabdovirus",
40-
"Viunavirus",
41-
"Gammalipothrixvirus",
42-
"Peduovirinae",
43-
"Sedoreovirinae"
44-
]
22+
EXCLUDE_TAXA = [
23+
"Allolevivirus",
24+
"Autographivirinae",
25+
"Buttersvirus",
26+
"Caudovirales",
27+
"Chungbukvirus",
28+
"Incheonvirus",
29+
"Leviviridae",
30+
"Levivirus",
31+
"Mandarivirus",
32+
"Pbi1virus",
33+
"Phicbkvirus",
34+
"Radnorvirus",
35+
"Sitaravirus",
36+
"Vidavervirus",
37+
"Myoviridae",
38+
"Siphoviridae",
39+
"Podoviridae",
40+
"Viunavirus",
41+
"Orthohepevirus",
42+
"Klosneuvirus",
43+
"Hendrixvirus",
44+
"Rubulavirus",
45+
"Avulavirus",
46+
"Catovirus",
47+
"Nucleorhabdovirus",
48+
"Viunavirus",
49+
"Gammalipothrixvirus",
50+
"Peduovirinae",
51+
"Sedoreovirinae",
52+
]
4553

4654

4755
def main(args):
@@ -62,18 +70,40 @@ def main(args):
6270
for name, *_, avg_cds, std_cds, _, mult_factor in csv_reader
6371
}
6472

65-
file_header = ["contig_ID", "superkingdom", "kingdom", "phylum", "subphylum", "class", "order", "suborder", "family", "subfamily", "genus"]
73+
file_header = [
74+
"contig_ID",
75+
"superkingdom",
76+
"kingdom",
77+
"phylum",
78+
"subphylum",
79+
"class",
80+
"order",
81+
"suborder",
82+
"family",
83+
"subfamily",
84+
"genus",
85+
]
6686

6787
exclude_deprecated_taxa = False
6888
if args.version4:
6989
exclude_deprecated_taxa = True
7090

71-
output_gen = contig_tax(input_df, args.ncbi_db, args.tax_thres, factor_dict, file_header, exclude_deprecated_taxa)
91+
output_gen = contig_tax(
92+
input_df,
93+
args.ncbi_db,
94+
args.tax_thres,
95+
factor_dict,
96+
file_header,
97+
exclude_deprecated_taxa,
98+
)
7299

73-
print(args.input_file)
100+
logging.info(f"Processing input file: {args.input_file}")
101+
logging.info(
102+
f"Settings: tax_thres={args.tax_thres:.2f}, version4={args.version4}, exclude_deprecated={exclude_deprecated_taxa}"
103+
)
74104

75105
out_file = re.split(r"\.[a-z]+$", os.path.basename(args.input_file))[0]
76-
106+
77107
if not os.path.exists(args.outdir):
78108
os.mkdir(args.outdir)
79109
with open(
@@ -85,44 +115,71 @@ def main(args):
85115
tsv_writer.writerow(item)
86116

87117

88-
def contig_tax(annot_df, ncbi_db, tax_thres, taxon_factor_dict, output_taxa_order, exclude_deprecated_taxa=False):
118+
def contig_tax(
119+
annot_df,
120+
ncbi_db,
121+
tax_thres,
122+
taxon_factor_dict,
123+
output_taxa_order,
124+
exclude_deprecated_taxa=False,
125+
):
89126
"""This function takes the annotation table generated by viral_contig_maps.py and generates a table that
90127
provides the taxonomic lineage of each viral contig, based on the corresponding ViPhOG annotations"""
91128

92129
ncbi = NCBITaxa(dbfile=ncbi_db)
93130
viphog_rank = ["genus", "subfamily", "family", "order"]
94131
contig_set = set(annot_df["Contig"])
95132

133+
logging.info(f"Assigning taxonomy for {len(contig_set)} contigs")
134+
135+
assigned = 0
136+
unassigned_no_hits = 0
137+
unassigned_below_thres = 0
138+
96139
for contig in contig_set:
97140
contig_lineage = []
98141
contig_df = annot_df[annot_df["Contig"] == contig]
99142
total_prot = len(contig_df)
100143
annot_prot = sum(contig_df["Best_hit"] != "No hit")
101144
if annot_prot == 0:
145+
logging.debug(f"Contig {contig}: no ViPhOG hits ({total_prot} proteins) - skipping")
146+
unassigned_no_hits += 1
102147
contig_lineage.extend([""] * len(output_taxa_order[1:]))
103148
else:
149+
logging.debug(f"Contig {contig}: {annot_prot}/{total_prot} proteins with ViPhOG hits")
104150
contig_hits = contig_df[pd.notnull(contig_df["Label"])]["Label"].values
105151
taxid_list = []
106152
for item in contig_hits:
107153
if len(ncbi.get_name_translator([item])):
108154
taxid_list.append(ncbi.get_name_translator([item])[item][0])
109155
else:
110-
print(f'No {item} found in NCBI db')
111-
156+
logging.warning(f"Taxon label '{item}' not found in NCBI db (contig {contig})")
157+
112158
hit_lineages = []
113159
for item in taxid_list:
114160
lineage_dict = {}
115161
try:
116162
for x, y in ncbi.get_rank(ncbi.get_lineage(item)).items():
117163
if y in viphog_rank:
118164
taxa_to_check = ncbi.get_taxid_translator([x])[x]
119-
if exclude_deprecated_taxa and taxa_to_check not in EXCLUDE_TAXA:
165+
# Check is some taxa should be excluded
166+
if exclude_deprecated_taxa:
167+
if taxa_to_check in EXCLUDE_TAXA:
168+
logging.info(f"Taxon '{taxa_to_check}' is deprecated and excluded (contig {contig})")
169+
else:
170+
lineage_dict[y] = taxa_to_check
171+
else:
120172
lineage_dict[y] = taxa_to_check
121173
if lineage_dict:
122174
hit_lineages.append(lineage_dict)
123175
except ValueError:
124-
print(f'Can not return lineage for {item}')
176+
logging.warning(f"Cannot retrieve lineage for taxid {item} (contig {contig})")
125177
pass
178+
179+
if not hit_lineages:
180+
logging.debug(f"Contig {contig}: no valid lineages resolved from {len(taxid_list)} hits")
181+
182+
contig_assigned = False
126183
for rank in output_taxa_order[::-1][:-1]:
127184
taxon_list = [item.get(rank) for item in hit_lineages]
128185
total_hits = sum(pd.notnull(taxon_list))
@@ -142,14 +199,21 @@ def contig_tax(annot_df, ncbi_db, tax_thres, taxon_factor_dict, output_taxa_orde
142199
if hit_taxon in taxon_factor_dict.keys()
143200
else 1
144201
)
145-
if prop_hits < tax_thres * taxon_factor:
146-
hit_bound = math.ceil(tax_thres * taxon_factor * total_hits)
202+
effective_thres = tax_thres * taxon_factor
203+
logging.debug(
204+
f"Contig {contig} | rank={rank} | taxon={hit_taxon} | prop={prop_hits:.2f} | thres={effective_thres:.2f} (factor={taxon_factor:.2f})"
205+
)
206+
if prop_hits < effective_thres:
207+
hit_bound = math.ceil(effective_thres * total_hits)
147208
hit_diff = hit_bound - hit_count
148209
under_thres.append((hit_taxon, hit_diff))
149210
else:
150211
over_thres.append((hit_taxon, prop_hits))
151212
if len(over_thres) == 0:
152213
best_under = sorted(under_thres, key=lambda x: x[1])[0]
214+
logging.debug(
215+
f"Contig {contig} | rank={rank}: all below threshold, best candidate is '{best_under[0]}'"
216+
)
153217
contig_lineage.append(best_under[0])
154218
else:
155219
sorted_over_thres = [
@@ -176,22 +240,39 @@ def contig_tax(annot_df, ncbi_db, tax_thres, taxon_factor_dict, output_taxa_orde
176240
taxon_lineage_list = [
177241
taxon_lineage_dict.get(item, "")
178242
for item in output_taxa_order[
179-
1:output_taxa_order.index(rank)+1
243+
1 : output_taxa_order.index(rank) + 1
180244
]
181245
]
246+
logging.info(f"Contig {contig} assigned at rank '{rank}' via taxon '{taxon}'")
247+
contig_assigned = True
182248
break
183249
else:
250+
logging.debug(
251+
f"Contig {contig} | rank={rank}: candidates {sorted_over_thres} all failed CDS size filter"
252+
)
184253
contig_lineage.append("")
185254
continue
186255
contig_lineage.reverse()
187256
contig_lineage = taxon_lineage_list + contig_lineage
188257
break
258+
259+
if annot_prot > 0:
260+
if contig_assigned:
261+
assigned += 1
262+
else:
263+
logging.debug(f"Contig {contig}: had hits but could not be assigned (below threshold or no valid lineage)")
264+
unassigned_below_thres += 1
265+
189266
contig_lineage = [contig] + contig_lineage
190267
yield contig_lineage
191268

269+
total = len(contig_set)
270+
logging.info(
271+
f"Summary: {assigned}/{total} contigs assigned | {unassigned_no_hits} no ViPhOG hits | {unassigned_below_thres} hits but unassigned"
272+
)
273+
192274

193275
if __name__ == "__main__":
194-
195276
parser = argparse.ArgumentParser(
196277
description="Generate tabular file with taxonomic assignment of viral contigs based on ViPhOG annotations"
197278
)
@@ -232,7 +313,7 @@ def contig_tax(annot_df, ncbi_db, tax_thres, taxon_factor_dict, output_taxa_orde
232313
"--version4",
233314
dest="version4",
234315
help="Flag for whether to use version 4 or not. This has implications for taxa that are ignored in version 4.",
235-
action="store_true"
316+
action="store_true",
236317
)
237318
args = parser.parse_args()
238319

0 commit comments

Comments
 (0)