Skip to content

Commit db39e84

Browse files
gregcaporasoclaudeOddant1
authored
BUG: tabulate-seqs shows every taxonomy in the collection and respects merge_method (#371)
# Description <!-- REQUIRED: Briefly describe this PR, or link the issue it closes. --> Since the refactor in #369, tabulate-seqs rendered only the first member of the --i-taxonomy collection, and it built rows from every sequence regardless of --p-merge-method (only the length statistics used the merged set). Each taxonomy now gets its own Taxon/Confidence columns, prefixed with its collection key when keys are given ('gtdb Taxon', 'silva Taxon'); unkeyed collections keep plain headers for one taxonomy and are numbered ('Taxonomy 1 Taxon', ...) for several. Rows are limited to the features selected by merge_method. Found by @gregcaporaso while comparing GTDB and SILVA taxonomies of the gut-to-soil ASVs; he asked for the fix on a branch. Claude wrote the change and the six new tests, which read the table JSON embedded in index.html (the existing tests check page text and did not catch either regression). This also led to the finding and fixing (by @Oddant1) of rachis-org/rachis#974. # AI Disclosure <!-- REQUIRED: Check exactly one option. --> - [ ] NO AI USED. - [X] AI USED. # AI Usage Details AI detected this issue through some work that I was doing that utilized this. Code, commit message, and tests initially written by Claude. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Anthony Simard <als872@nau.edu> Co-authored-by: Oddant1 <oddant1@hotmail.com>
1 parent 9c210db commit db39e84

2 files changed

Lines changed: 122 additions & 4 deletions

File tree

‎q2_feature_table/_summarize/_visualizer.py‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,27 @@ def tabulate_seqs(
101101
seq_df = pd.DataFrame.from_dict(sequences, orient='index')
102102
if taxonomy is not None:
103103
if isinstance(taxonomy, pd.DataFrame):
104-
seq_df = pd.concat([seq_df, taxonomy], axis=1)
105-
else:
106-
taxonomy = next(iter(taxonomy.values()))
107-
seq_df = pd.concat([seq_df, taxonomy], axis=1)
104+
taxonomy = {'0': taxonomy}
105+
keys = list(taxonomy)
106+
# unlabelled collections are keyed '0', '1', ... by the framework
107+
labelled = keys != [str(i) for i in range(len(keys))]
108+
for key, member in taxonomy.items():
109+
# 'Taxon: gtdb'; unlabelled: plain for one, 'Taxon: 1', ... for
110+
# several
111+
if labelled:
112+
label = key
113+
elif len(keys) > 1:
114+
label = str(int(key) + 1)
115+
else:
116+
label = None
117+
118+
if label is not None:
119+
member = member.rename(columns=lambda c: f'{c}: {label}')
120+
seq_df = pd.concat([seq_df, member], axis=1)
108121
if metadata is not None:
109122
seq_df = pd.concat([seq_df, metadata_df], axis=1)
123+
# show only the features selected by merge_method
124+
seq_df = seq_df.loc[[i for i in seq_df.index if i in display_sequences]]
110125
seq_df.index.name = 'Feature ID'
111126
seq_df = seq_df[
112127
[col for col in seq_df.columns if col != 'seq'] + ['seq']

‎q2_feature_table/tests/test_summarize.py‎

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,109 @@ def test_optional_input_strict_fail(self):
468468
taxonomy=taxonomy)
469469
# Did not error out, this is a problem
470470

471+
def _rendered_table(self, output_dir):
472+
with open(os.path.join(output_dir, 'index.html')) as fh:
473+
html = fh.read()
474+
data = re.search(
475+
r'<script id="data" type="application/json">(.*?)</script>',
476+
html, re.S).group(1)
477+
table = json.loads(data)
478+
columns = [c[0] for c in table['columns']]
479+
rows = {r[columns.index('Feature ID')]: dict(zip(columns, r))
480+
for r in table['data']}
481+
return columns, rows
482+
483+
def _seqs_and_taxonomies(self):
484+
seqs = DNAIterator(skbio.DNA(a, metadata=b) for a, b in (
485+
('ACGT', {'id': 'seq1'}),
486+
('AAAA', {'id': 'seq2'}),
487+
('CCCC', {'id': 'seq3'})))
488+
index = ['seq1', 'seq2', 'seq3']
489+
tax1 = pd.DataFrame([('a;b;c', '0.9'), ('a;b;d', '0.8'),
490+
('a;e', '0.7')],
491+
index=index, columns=['Taxon', 'Confidence'])
492+
tax2 = pd.DataFrame([('x;y;z', '1.0'), ('x;y;w', '0.6'),
493+
('x', '0.5')],
494+
index=index, columns=['Taxon', 'Confidence'])
495+
return seqs, tax1, tax2
496+
497+
def test_multiple_taxonomies_labelled(self):
498+
seqs, tax1, tax2 = self._seqs_and_taxonomies()
499+
with tempfile.TemporaryDirectory() as output_dir:
500+
tabulate_seqs(output_dir, seqs,
501+
taxonomy={'gtdb': tax1, 'silva': tax2})
502+
columns, rows = self._rendered_table(output_dir)
503+
for col in ('Taxon: gtdb', 'Confidence: gtdb',
504+
'Taxon: silva', 'Confidence: silva'):
505+
self.assertIn(col, columns)
506+
self.assertNotIn('Taxon', columns)
507+
self.assertEqual(rows['seq2']['Taxon: gtdb'], 'a;b;d')
508+
self.assertEqual(rows['seq2']['Taxon: silva'], 'x;y;w')
509+
510+
def test_multiple_taxonomies_unlabelled(self):
511+
seqs, tax1, tax2 = self._seqs_and_taxonomies()
512+
with tempfile.TemporaryDirectory() as output_dir:
513+
tabulate_seqs(output_dir, seqs, taxonomy={'0': tax1, '1': tax2})
514+
columns, rows = self._rendered_table(output_dir)
515+
self.assertIn('Taxon: 1', columns)
516+
self.assertIn('Taxon: 2', columns)
517+
self.assertEqual(rows['seq3']['Taxon: 1'], 'a;e')
518+
self.assertEqual(rows['seq3']['Taxon: 2'], 'x')
519+
520+
def test_single_taxonomy_labelled(self):
521+
seqs, tax1, _ = self._seqs_and_taxonomies()
522+
with tempfile.TemporaryDirectory() as output_dir:
523+
tabulate_seqs(output_dir, seqs, taxonomy={'gtdb': tax1})
524+
columns, rows = self._rendered_table(output_dir)
525+
self.assertIn('Taxon: gtdb', columns)
526+
self.assertNotIn('Taxon', columns)
527+
self.assertEqual(rows['seq1']['Taxon: gtdb'], 'a;b;c')
528+
529+
def test_single_taxonomy_unlabelled(self):
530+
seqs, tax1, _ = self._seqs_and_taxonomies()
531+
with tempfile.TemporaryDirectory() as output_dir:
532+
tabulate_seqs(output_dir, seqs, taxonomy={'0': tax1})
533+
columns, rows = self._rendered_table(output_dir)
534+
self.assertIn('Taxon', columns)
535+
self.assertIn('Confidence', columns)
536+
self.assertEqual(rows['seq1']['Taxon'], 'a;b;c')
537+
538+
def test_single_taxonomy_unlabelled_result_collection(self):
539+
# the action receives a ResultCollection, whose iteration differs
540+
# from a dict's
541+
seqs, tax1, _ = self._seqs_and_taxonomies()
542+
with tempfile.TemporaryDirectory() as output_dir:
543+
tabulate_seqs(output_dir, seqs,
544+
taxonomy=qiime2.ResultCollection({'0': tax1}))
545+
columns, rows = self._rendered_table(output_dir)
546+
self.assertIn('Taxon', columns)
547+
self.assertNotIn('Taxon: 0', columns)
548+
self.assertEqual(rows['seq1']['Taxon'], 'a;b;c')
549+
550+
def test_rows_intersect(self):
551+
seqs, tax1, tax2 = self._seqs_and_taxonomies()
552+
metadata = qiime2.Metadata(pd.DataFrame(
553+
{'frequency': [10.0, 20.0]},
554+
index=pd.Index(['seq1', 'seq3'], name='feature id')))
555+
with tempfile.TemporaryDirectory() as output_dir:
556+
tabulate_seqs(output_dir, seqs, metadata=metadata,
557+
taxonomy={'gtdb': tax1, 'silva': tax2},
558+
merge_method='intersect')
559+
_, rows = self._rendered_table(output_dir)
560+
self.assertEqual(set(rows), {'seq1', 'seq3'})
561+
self.assertEqual(rows['seq3']['frequency'], 20.0)
562+
563+
def test_rows_union(self):
564+
seqs, tax1, _ = self._seqs_and_taxonomies()
565+
metadata = qiime2.Metadata(pd.DataFrame(
566+
{'frequency': [10.0, 30.0]},
567+
index=pd.Index(['seq1', 'seq4'], name='feature id')))
568+
with tempfile.TemporaryDirectory() as output_dir:
569+
tabulate_seqs(output_dir, seqs, metadata=metadata,
570+
taxonomy={'gtdb': tax1}, merge_method='union')
571+
_, rows = self._rendered_table(output_dir)
572+
self.assertEqual(set(rows), {'seq1', 'seq2', 'seq3', 'seq4'})
573+
471574

472575
class _SummarizeTests(TestCase):
473576
def test_basic(self):

0 commit comments

Comments
 (0)