22
33import argparse
44import csv
5+ import logging
56import os
67import re
78import math
1011import pandas as pd
1112from 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
4755def 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
193275if __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