2323import logging
2424import argparse
2525import gzip
26+ import random
2627from collections import defaultdict
2728from dataclasses import dataclass
2829
@@ -65,25 +66,37 @@ def __init__(self):
6566 # genomes to retain without needing to pass QC
6667 self .QC_EXCEPTIONS = set (['G015697925' ]) # s__Pinguicoccus supinus representative genome
6768
69+ # maximum number of genomes to select per species
70+ self .MAX_PER_SPECIES = 100
71+
6872 self .log = logging .getLogger ('timestamp' )
6973
70- def parse_metadata_file (self , gtdb_metadata_file : str ) -> dict :
74+ def parse_metadata_file (self , gtdb_metadata_file : str ) -> tuple [ dict , dict ] :
7175 """Parse manual ground truth file."""
7276
7377 genome_data = {}
78+ gtdb_sp_rid = {}
7479 open_file = gzip .open if gtdb_metadata_file .endswith ('.gz' ) else open
7580 with open_file (gtdb_metadata_file , 'rt' ) as f :
7681 header = f .readline ().strip ().split ('\t ' )
82+
7783 gid_idx = header .index ("accession" )
7884 gtdb_taxonomy_idx = header .index ('gtdb_taxonomy' )
7985 ncbi_taxonomy_idx = header .index ('ncbi_taxonomy' )
8086
87+ gtdb_rep_idx = header .index ('gtdb_representative' )
88+
8189 for line in f :
8290 tokens = line .strip ().split ('\t ' )
8391 gid = canonical_gid (tokens [gid_idx ])
8492 genome_data [gid ] = GenomeData (tokens [gtdb_taxonomy_idx ], tokens [ncbi_taxonomy_idx ])
8593
86- return genome_data
94+ is_rep = tokens [gtdb_rep_idx ].lower ().startswith ('t' )
95+ if is_rep :
96+ gtdb_sp = [t .strip () for t in tokens [gtdb_taxonomy_idx ].split (';' )][- 1 ]
97+ gtdb_sp_rid [gtdb_sp ] = gid
98+
99+ return genome_data , gtdb_sp_rid
87100
88101 def parse_taxonomy_file (self , taxonomy_file : str ) -> dict :
89102 """Parse taxonomy file."""
@@ -175,12 +188,33 @@ def run(self, gtdb_bac_metadata_file: str,
175188
176189 # parse genomes from GTDB metadata files
177190 self .log .info ('Parsing GTDB metadata files:' )
178- gtdb_bac_gids = self .parse_metadata_file (gtdb_bac_metadata_file )
179- gtdb_ar_gids = self .parse_metadata_file (gtdb_ar_metadata_file )
191+ gtdb_bac_gids , gtdb_bac_sp_rid = self .parse_metadata_file (gtdb_bac_metadata_file )
192+ gtdb_ar_gids , gtdb_ar_sp_rid = self .parse_metadata_file (gtdb_ar_metadata_file )
180193 gtdb_gids = {** gtdb_bac_gids , ** gtdb_ar_gids }
181- self .log .info (f' - identified { len (gtdb_bac_gids ):,} bacterial genomes' )
182- self .log .info (f' - identified { len (gtdb_ar_gids ):,} archaeal genomes' )
183- self .log .info (f' - identified { len (gtdb_gids ):,} total genomes' )
194+ gtdb_sp_rid = {** gtdb_bac_sp_rid , ** gtdb_ar_sp_rid }
195+ self .log .info (f' - identified { len (gtdb_bac_gids ):,} bacterial genomes from { len (gtdb_bac_sp_rid ):,} species' )
196+ self .log .info (f' - identified { len (gtdb_ar_gids ):,} archaeal genomes from { len (gtdb_ar_sp_rid ):,} species' )
197+ self .log .info (f' - identified { len (gtdb_gids ):,} total genomes from { len (gtdb_sp_rid ):,} species' )
198+
199+ # subsample to maximum number of genomes per species
200+ self .log .info (f'Sampling to { self .MAX_PER_SPECIES } genomes per species:' )
201+
202+ sp_gids = defaultdict (set )
203+ for gid , genome_data in gtdb_gids .items ():
204+ gtdb_sp = [t .strip () for t in genome_data .gtdb_taxonomy .split (';' )][- 1 ]
205+ sp_gids [gtdb_sp ].add (gid )
206+
207+ gtdb_gids_sampled = {}
208+ for sp , gids in sp_gids .items ():
209+ if len (gids ) > self .MAX_PER_SPECIES :
210+ rid = gtdb_sp_rid [sp ]
211+ gids .remove (rid )
212+ gids = [rid ] + random .sample (list (gids ), self .MAX_PER_SPECIES - 1 )
213+
214+ for gid in gids :
215+ gtdb_gids_sampled [gid ] = gtdb_gids [gid ]
216+
217+ self .log .info (f' - retained { len (gtdb_gids_sampled ):,} genomes for training' )
184218
185219 # read NCBI taxonomy for genomes
186220 self .log .info ('Parsing NCBI taxonomy file:' )
@@ -198,7 +232,7 @@ def run(self, gtdb_bac_metadata_file: str,
198232
199233 # combine all training genomes
200234 self .log .info ('Combining all genomes to use for training:' )
201- training_gids = {** gtdb_gids , ** gtdb_reassigned_gids }
235+ training_gids = {** gtdb_gids_sampled , ** gtdb_reassigned_gids }
202236 self .log .info (f' - identified { len (training_gids ):,} total genomes for training' )
203237
204238 # sanity check that all genomes that are a QC expection are accounted for
0 commit comments